Merge 23ef479a730057529773eb8190805e5c48169a84 into 9b09146a6b5e936966ed7ee73bce9dd2df3810ae

This commit is contained in:
efeyakinci 2025-05-06 21:18:15 +00:00 committed by GitHub
commit ffac381888
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 103 additions and 33 deletions

View File

@ -14,7 +14,7 @@ use typst_library::introspection::{
use typst_library::layout::{
Abs, AlignElem, Alignment, Axes, BlockElem, ColbreakElem, FixedAlignment, FlushElem,
Fr, Fragment, Frame, PagebreakElem, PlaceElem, PlacementScope, Ratio, Region,
Regions, Rel, Size, Sizing, Spacing, VElem,
Regions, Rel, Size, Sizing, Spacing, Sticky, VElem,
};
use typst_library::model::ParElem;
use typst_library::routines::{Pair, Routines};
@ -375,7 +375,7 @@ pub struct LineChild {
#[derive(Debug)]
pub struct SingleChild<'a> {
pub align: Axes<FixedAlignment>,
pub sticky: bool,
pub sticky: Option<Sticky>,
pub alone: bool,
pub fr: Option<Fr>,
elem: &'a Packed<BlockElem>,
@ -441,7 +441,7 @@ fn layout_single_impl(
#[derive(Debug)]
pub struct MultiChild<'a> {
pub align: Axes<FixedAlignment>,
pub sticky: bool,
pub sticky: Option<Sticky>,
alone: bool,
elem: &'a Packed<BlockElem>,
styles: StyleChain<'a>,

View File

@ -1,6 +1,7 @@
use typst_library::introspection::Tag;
use typst_library::layout::{
Abs, Axes, FixedAlignment, Fr, Frame, FrameItem, Point, Region, Regions, Rel, Size,
Sticky,
};
use typst_utils::Numeric;
@ -244,7 +245,7 @@ impl<'a, 'b> Distributor<'a, 'b, '_, '_, '_> {
return Err(Stop::Finish(false));
}
self.frame(line.frame.clone(), line.align, false, false)
self.frame(line.frame.clone(), line.align, None, false)
}
/// Processes an unbreakable block.
@ -307,7 +308,7 @@ impl<'a, 'b> Distributor<'a, 'b, '_, '_, '_> {
// Lay out the spilled remains.
let align = spill.align();
let (frame, spill) = spill.layout(self.composer.engine, self.regions)?;
self.frame(frame, align, false, true)?;
self.frame(frame, align, None, true)?;
// If there's still more, save it into the `spill` and finish the
// region.
@ -324,10 +325,28 @@ impl<'a, 'b> Distributor<'a, 'b, '_, '_, '_> {
&mut self,
frame: Frame,
align: Axes<FixedAlignment>,
sticky: bool,
sticky: Option<Sticky>,
breakable: bool,
) -> FlowResult<()> {
if sticky {
// Check if the successor frame has sticky: above
let has_sticky_successor = self
.composer
.work
.children
.iter()
.skip(1)
.find_map(|child| match child {
Child::Single(single) => Some(
single.sticky.as_ref().map(|s| s.is_sticky_above()).unwrap_or(false),
),
Child::Multi(multi) => Some(
multi.sticky.as_ref().map(|s| s.is_sticky_above()).unwrap_or(false),
),
_ => None,
})
.unwrap_or(false);
let mut stick_to_successor = || {
// If the frame is sticky and we haven't remembered a preceding
// sticky element, make a checkpoint which we can restore should we
// end on this sticky element.
@ -350,12 +369,22 @@ impl<'a, 'b> Distributor<'a, 'b, '_, '_, '_> {
{
self.sticky = Some(self.snapshot());
}
} else if !frame.is_empty() {
// If the frame isn't sticky, we can forget a previous snapshot. We
// interrupt a group of sticky blocks, if there was one, so we reset
// the saved stickable check for the next group of sticky blocks.
self.sticky = None;
self.stickable = None;
};
match sticky {
Some(Sticky::Below) => {
stick_to_successor();
}
_ if has_sticky_successor => {
stick_to_successor();
}
_ => {
// Only clear the snapshot if this frame isn't empty
if !frame.is_empty() {
self.sticky = None;
self.stickable = None;
}
}
}
// Handle footnotes.
@ -443,7 +472,13 @@ impl<'a, 'b> Distributor<'a, 'b, '_, '_, '_> {
// the flow, restore the saved checkpoint to move the sticky
// suffix to the next region.
if let Some(snapshot) = self.sticky.take() {
self.restore(snapshot)
// Only restore snapshot if there's no spill
// If a sticky breakable element can be spilled to the following page,
// it's fine to put some of it on this page, since the next page will still
// have the next element on the same page as *some* of the spilled content.
if self.composer.work.spill.is_none() {
self.restore(snapshot)
}
}
}

View File

@ -1,7 +1,7 @@
use crate::diag::{bail, SourceResult};
use crate::engine::Engine;
use crate::foundations::{
cast, elem, Args, AutoValue, Construct, Content, NativeElement, Packed, Smart,
cast, elem, Args, AutoValue, Cast, Construct, Content, NativeElement, Packed, Smart,
StyleChain, Value,
};
use crate::introspection::Locator;
@ -179,6 +179,27 @@ pub enum InlineItem {
Frame(Frame),
}
/// Defines how a block sticks to adjacent content.
#[derive(Debug, Cast, Copy, Clone, Eq, PartialEq, Hash)]
pub enum Sticky {
/// Block sticks to the content below it.
Below,
/// Makes the block above stick to this block.
Above,
/// Makes the block above and below stick to this block.
Both,
}
impl Sticky {
pub fn is_sticky_above(self) -> bool {
matches!(self, Self::Above | Self::Both)
}
pub fn is_sticky_below(self) -> bool {
matches!(self, Self::Below | Self::Both)
}
}
/// A block-level container.
///
/// Such a container can be used to separate content, size it, and give it a
@ -338,23 +359,29 @@ pub struct BlockElem {
#[default(false)]
pub clip: bool,
/// Whether this block must stick to the following one, with no break in
/// between.
/// If and how this block must stick to the blocks surrounding it, with no
/// break in between.
///
/// This is, by default, set on heading blocks to prevent orphaned headings
/// at the bottom of the page.
/// This is, by default, set to "below" on heading blocks to prevent
/// orphaned headings at the bottom of the page.
///
/// ```example
/// >>> #set page(height: 140pt)
/// // Disable stickiness of headings.
/// #show heading: set block(sticky: false)
/// #show heading: set block(sticky: none)
/// #lorem(20)
///
/// = Chapter
/// #lorem(10)
///
/// #table(
/// columns: 2,
/// [A], [B],
/// [C], [D],
/// )
/// #block(sticky: "above")[The above table shows that...]
/// ```
#[default(false)]
pub sticky: bool,
pub sticky: Option<Sticky>,
/// The contents of the block.
#[positional]

View File

@ -13,7 +13,9 @@ use crate::html::{attr, tag, HtmlElem};
use crate::introspection::{
Count, Counter, CounterUpdate, Locatable, Locator, LocatorLink,
};
use crate::layout::{Abs, Axes, BlockBody, BlockElem, Em, HElem, Length, Region, Sides};
use crate::layout::{
Abs, Axes, BlockBody, BlockElem, Em, HElem, Length, Region, Sides, Sticky,
};
use crate::model::{Numbering, Outlinable, Refable, Supplement};
use crate::text::{FontWeight, LocalName, SpaceElem, TextElem, TextSize};
@ -323,7 +325,7 @@ impl ShowSet for Packed<HeadingElem> {
out.set(TextElem::set_weight(FontWeight::BOLD));
out.set(BlockElem::set_above(Smart::Custom(above.into())));
out.set(BlockElem::set_below(Smart::Custom(below.into())));
out.set(BlockElem::set_sticky(true));
out.set(BlockElem::set_sticky(Some(Sticky::Below)));
out
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -155,17 +155,17 @@ Paragraph
--- block-sticky ---
#set page(height: 100pt)
#lines(3)
#block(sticky: true)[D]
#block(sticky: true)[E]
#block(sticky: "below")[D]
#block(sticky: "below")[E]
F
--- block-sticky-alone ---
#set page(height: 50pt)
#block(sticky: true)[A]
#block(sticky: "below")[A]
--- block-sticky-many ---
#set page(height: 80pt)
#set block(sticky: true)
#set block(breakable: false, sticky: "below")
#block[A]
#block[B]
#block[C]
@ -176,16 +176,22 @@ E
--- block-sticky-colbreak ---
A
#block(sticky: true)[B]
#block(sticky: "below")[B]
#colbreak()
C
--- block-sticky-breakable ---
// Ensure that sticky blocks are still breakable.
#set page(height: 60pt)
#block(sticky: true, lines(4))
#block(sticky: "below", lines(4))
E
--- block-sticky-above ---
#set page(height: 50pt)
#block(height: 15pt)[A]
#block(height: 5pt, breakable: false)[B]
#block(height: 5pt, sticky: "above")[C]
--- box-clip-rect ---
// Test box clipping with a rectangle
Hello #box(width: 1em, height: 1em, clip: false)[#rect(width: 3em, height: 3em, fill: red)]
@ -291,7 +297,7 @@ Paragraph
#set page(height: 3cm)
#v(1.6cm)
#block(height: 2cm, breakable: true)[
#block(sticky: true)[*A*]
#block(sticky: "below")[*A*]
b
]
@ -300,7 +306,7 @@ Paragraph
#set page(height: 3cm)
#v(2cm)
#block(sticky: true)[*A*]
#block(sticky: "below")[*A*]
b
@ -308,7 +314,7 @@ b
#set page(height: 3cm)
#v(2cm, weak: true)
#block(sticky: true)[*A*]
#block(sticky: "below")[*A*]
b