diff --git a/crates/typst-layout/src/flow/block.rs b/crates/typst-layout/src/flow/block.rs index d6cfe3a9e..141c4d374 100644 --- a/crates/typst-layout/src/flow/block.rs +++ b/crates/typst-layout/src/flow/block.rs @@ -206,13 +206,11 @@ pub fn layout_multi_block( let has_inset = !inset.is_zero(); let is_explicit = matches!(body, None | Some(BlockBody::Content(_))); - // Skip filling/stroking the first frame if it is empty and a non-empty - // one follows. + // Skip filling, stroking and labeling the first frame if it is empty and + // a non-empty one follows. let mut skip_first = false; if let [first, rest @ ..] = fragment.as_slice() { - skip_first = has_fill_or_stroke - && first.is_empty() - && rest.iter().any(|frame| !frame.is_empty()); + skip_first = first.is_empty() && rest.iter().any(|frame| !frame.is_empty()); } // Post-process to apply insets, clipping, fills, and strokes. @@ -244,7 +242,8 @@ pub fn layout_multi_block( // Assign label to each frame in the fragment. if let Some(label) = elem.label() { - for frame in fragment.iter_mut() { + // Skip empty orphan frames, as a label would make them non-empty. + for frame in fragment.iter_mut().skip(if skip_first { 1 } else { 0 }) { frame.label(label); } } diff --git a/crates/typst-layout/src/flow/collect.rs b/crates/typst-layout/src/flow/collect.rs index 76268b590..c52327e88 100644 --- a/crates/typst-layout/src/flow/collect.rs +++ b/crates/typst-layout/src/flow/collect.rs @@ -459,6 +459,7 @@ impl<'a> MultiChild<'a> { regions: Regions, ) -> SourceResult<(Frame, Option>)> { let fragment = self.layout_full(engine, regions)?; + let exist_non_empty_frame = fragment.iter().any(|f| !f.is_empty()); // Extract the first frame. let mut frames = fragment.into_iter(); @@ -468,6 +469,7 @@ impl<'a> MultiChild<'a> { let mut spill = None; if frames.next().is_some() { spill = Some(MultiSpill { + exist_non_empty_frame, multi: self, full: regions.full, first: regions.size.y, @@ -539,6 +541,7 @@ fn layout_multi_impl( /// The spilled remains of a `MultiChild` that broke across two regions. #[derive(Debug, Clone)] pub struct MultiSpill<'a, 'b> { + pub(super) exist_non_empty_frame: bool, multi: &'b MultiChild<'a>, first: Abs, full: Abs, diff --git a/crates/typst-layout/src/flow/distribute.rs b/crates/typst-layout/src/flow/distribute.rs index f504d22e7..d12b1ff68 100644 --- a/crates/typst-layout/src/flow/distribute.rs +++ b/crates/typst-layout/src/flow/distribute.rs @@ -283,6 +283,13 @@ impl<'a, 'b> Distributor<'a, 'b, '_, '_, '_> { // Lay out the block. let (frame, spill) = multi.layout(self.composer.engine, self.regions)?; + if frame.is_empty() && spill.as_ref().is_some_and(|s| s.exist_non_empty_frame) { + // If the first frame is empty, but there are non-empty frames in + // the spill, the whole child should be put in the next region to + // avoid any invisible orphans at the end of this region. + return Err(Stop::Finish(false)); + } + self.frame(frame, multi.align, multi.sticky, true)?; // If the block didn't fully fit into the current region, save it into diff --git a/tests/ref/block-multiple-pages-empty.png b/tests/ref/block-multiple-pages-empty.png new file mode 100644 index 000000000..44f9df91b Binary files /dev/null and b/tests/ref/block-multiple-pages-empty.png differ diff --git a/tests/ref/grid-header-containing-rowspan.png b/tests/ref/grid-header-containing-rowspan.png index 0436748c9..94ee40396 100644 Binary files a/tests/ref/grid-header-containing-rowspan.png and b/tests/ref/grid-header-containing-rowspan.png differ diff --git a/tests/ref/grid-header-orphan-prevention.png b/tests/ref/grid-header-orphan-prevention.png index 691817314..2241efe1a 100644 Binary files a/tests/ref/grid-header-orphan-prevention.png and b/tests/ref/grid-header-orphan-prevention.png differ diff --git a/tests/ref/issue-2914-block-fill-skip-nested.png b/tests/ref/issue-2914-block-fill-skip-nested.png new file mode 100644 index 000000000..0dcbd6d85 Binary files /dev/null and b/tests/ref/issue-2914-block-fill-skip-nested.png differ diff --git a/tests/ref/issue-2914-block-height-cut-off.png b/tests/ref/issue-2914-block-height-cut-off.png new file mode 100644 index 000000000..559af35de Binary files /dev/null and b/tests/ref/issue-2914-block-height-cut-off.png differ diff --git a/tests/ref/issue-6125-block-place-width-limited.png b/tests/ref/issue-6125-block-place-width-limited.png new file mode 100644 index 000000000..28c564570 Binary files /dev/null and b/tests/ref/issue-6125-block-place-width-limited.png differ diff --git a/tests/ref/issue-6304-block-skip-label.png b/tests/ref/issue-6304-block-skip-label.png new file mode 100644 index 000000000..089cb7d41 Binary files /dev/null and b/tests/ref/issue-6304-block-skip-label.png differ diff --git a/tests/ref/locate-migrated-breakable.png b/tests/ref/locate-migrated-breakable.png new file mode 100644 index 000000000..2ee414e45 Binary files /dev/null and b/tests/ref/locate-migrated-breakable.png differ diff --git a/tests/suite/introspection/locate.typ b/tests/suite/introspection/locate.typ index 18611dded..420c8b92a 100644 --- a/tests/suite/introspection/locate.typ +++ b/tests/suite/introspection/locate.typ @@ -72,6 +72,18 @@ B #pagebreak(weak: true) #metadata(none) +--- locate-migrated-breakable --- +// Ensure that when a breakable element fully migrates to the next page without +// orphan frames, its position correctly reflects that. +#set page(height: 40pt) +A +#block[B] + +#context test( + locate().position(), + (page: 2, x: 10pt, y: 10pt), +) + --- issue-4029-locate-after-spacing --- #set page(margin: 10pt) #show heading: it => v(40pt) + it diff --git a/tests/suite/layout/container.typ b/tests/suite/layout/container.typ index 489c88925..2942bc34a 100644 --- a/tests/suite/layout/container.typ +++ b/tests/suite/layout/container.typ @@ -64,6 +64,12 @@ First! is the sun. ] +--- block-multiple-pages-empty --- +#set page(height: 60pt) +A +#block(height: 30pt) +B + --- block-box-fill --- #set page(height: 100pt) #let words = lorem(18).split() @@ -287,6 +293,37 @@ Paragraph #block(width: 100%, fill: red, box("a box")) #block(width: 100%, fill: red, [#box("a box") #box()]) +--- issue-2914-block-height-cut-off --- +// Ensure that breaking a block doesn't shrink its height. +#set page(height: 65pt) +#set block(fill: aqua, width: 25pt, height: 25pt, inset: 5pt) + +#block[A] +#block[B] + +--- issue-2914-block-fill-skip-nested --- +// Ensure that fill and stroke are skipped for an empty frame with a nested block. +#set page(height: 50pt) +A +#block(fill: aqua, stroke: blue, inset: 5pt, width: 100%, block[B]) + +--- issue-6304-block-skip-label --- +// Ensure that labeling is skipped for an empty orphan frame. +#set page(height: 60pt) +A +#block(sticky: true)[B] +#block[C]