diff --git a/crates/typst/src/layout/flow.rs b/crates/typst/src/layout/flow.rs index b4e7afd1..8983ccda 100644 --- a/crates/typst/src/layout/flow.rs +++ b/crates/typst/src/layout/flow.rs @@ -82,7 +82,7 @@ impl Layout for FlowElem { } else if child.is::() { if !layouter.regions.backlog.is_empty() || layouter.regions.last.is_some() { - layouter.finish_region(engine)?; + layouter.finish_region(engine, true)?; } } else { bail!(child.span(), "unexpected flow child"); @@ -160,6 +160,19 @@ impl FlowItem { Self::Frame { frame, .. } | Self::Footnote(frame) => frame.height(), } } + + /// Whether this item is out-of-flow. + /// + /// Out-of-flow items are guaranteed to have a [`Size::zero()`]. + fn is_out_of_flow(&self) -> bool { + match self { + Self::Placed { float: false, .. } => true, + Self::Frame { frame, .. } => { + frame.items().all(|(_, item)| matches!(item, FrameItem::Meta(..))) + } + _ => false, + } + } } impl<'a> FlowLayouter<'a> { @@ -243,7 +256,7 @@ impl<'a> FlowLayouter<'a> { if let Some(first) = lines.first() { if !self.regions.size.y.fits(first.height()) && !self.regions.in_last() { let carry: Vec<_> = self.items.drain(sticky..).collect(); - self.finish_region(engine)?; + self.finish_region(engine, false)?; for item in carry { self.layout_item(engine, item)?; } @@ -323,7 +336,7 @@ impl<'a> FlowLayouter<'a> { if self.regions.is_full() { // Skip directly if region is already full. - self.finish_region(engine)?; + self.finish_region(engine, false)?; } // How to align the block. @@ -347,7 +360,7 @@ impl<'a> FlowLayouter<'a> { } if i > 0 { - self.finish_region(engine)?; + self.finish_region(engine, false)?; } let item = FlowItem::Frame { frame, align, sticky, movable: false }; @@ -386,7 +399,7 @@ impl<'a> FlowLayouter<'a> { FlowItem::Frame { ref frame, movable, .. } => { let height = frame.height(); if !self.regions.size.y.fits(height) && !self.regions.in_last() { - self.finish_region(engine)?; + self.finish_region(engine, false)?; } self.regions.size.y -= height; @@ -396,7 +409,7 @@ impl<'a> FlowLayouter<'a> { self.items.push(item); if !self.handle_footnotes(engine, &mut notes, true, false)? { let item = self.items.pop(); - self.finish_region(engine)?; + self.finish_region(engine, false)?; self.items.extend(item); self.regions.size.y -= height; self.handle_footnotes(engine, &mut notes, true, true)?; @@ -454,7 +467,21 @@ impl<'a> FlowLayouter<'a> { } /// Finish the frame for one region. - fn finish_region(&mut self, engine: &mut Engine) -> SourceResult<()> { + /// + /// Set `force` to `true` to allow creating a frame for out-of-flow elements + /// only (this is used to force the creation of a frame in case the + /// remaining elements are all out-of-flow). + fn finish_region(&mut self, engine: &mut Engine, force: bool) -> SourceResult<()> { + if !force + && !self.items.is_empty() + && self.items.iter().all(FlowItem::is_out_of_flow) + { + self.finished.push(Frame::soft(self.initial)); + self.regions.next(); + self.initial = self.regions.size; + return Ok(()); + } + // Trim weak spacing. while self .items @@ -591,13 +618,13 @@ impl<'a> FlowLayouter<'a> { fn finish(mut self, engine: &mut Engine) -> SourceResult { if self.expand.y { while !self.regions.backlog.is_empty() { - self.finish_region(engine)?; + self.finish_region(engine, true)?; } } - self.finish_region(engine)?; + self.finish_region(engine, true)?; while !self.items.is_empty() { - self.finish_region(engine)?; + self.finish_region(engine, true)?; } Ok(Fragment::frames(self.finished)) @@ -611,7 +638,7 @@ impl FlowLayouter<'_> { mut notes: Vec, ) -> SourceResult<()> { if self.root && !self.handle_footnotes(engine, &mut notes, false, false)? { - self.finish_region(engine)?; + self.finish_region(engine, false)?; self.handle_footnotes(engine, &mut notes, false, true)?; } Ok(()) @@ -673,7 +700,7 @@ impl FlowLayouter<'_> { for (i, frame) in frames.into_iter().enumerate() { find_footnotes(notes, &frame); if i > 0 { - self.finish_region(engine)?; + self.finish_region(engine, false)?; self.layout_footnote_separator(engine)?; self.regions.size.y -= self.footnote_config.gap; } diff --git a/tests/ref/layout/columns.png b/tests/ref/layout/columns.png index 51fd5b2c..38912f1b 100644 Binary files a/tests/ref/layout/columns.png and b/tests/ref/layout/columns.png differ diff --git a/tests/ref/layout/out-of-flow-in-block.png b/tests/ref/layout/out-of-flow-in-block.png new file mode 100644 index 00000000..97637145 Binary files /dev/null and b/tests/ref/layout/out-of-flow-in-block.png differ diff --git a/tests/typ/layout/columns.typ b/tests/typ/layout/columns.typ index 32060ab4..ecf636e7 100644 --- a/tests/typ/layout/columns.typ +++ b/tests/typ/layout/columns.typ @@ -103,3 +103,10 @@ This is a normal page. Very normal. // Test a page with zero columns. // Error: 49-50 number must be positive #set page(height: auto, width: 7.05cm, columns: 0) + +--- +// Test colbreak after only out-of-flow elements. +#set page(width: 7.05cm, columns: 2) +#place[OOF] +#colbreak() +In flow. diff --git a/tests/typ/layout/out-of-flow-in-block.typ b/tests/typ/layout/out-of-flow-in-block.typ new file mode 100644 index 00000000..2461aa5d --- /dev/null +++ b/tests/typ/layout/out-of-flow-in-block.typ @@ -0,0 +1,61 @@ +// Test out-of-flow items (place, counter updates, etc.) at the +// beginning of a block not creating a frame just for them. + +--- +// No item in the first region. +#set page(height: 5cm, margin: 1cm) +No item in the first region. +#block(breakable: true, stroke: 1pt, inset: 0.5cm)[ + #rect(height: 2cm, fill: gray) +] + +--- +// Counter update in the first region. +#set page(height: 5cm, margin: 1cm) +Counter update. +#block(breakable: true, stroke: 1pt, inset: 0.5cm)[ + #counter("dummy").step() + #rect(height: 2cm, fill: gray) +] + +--- +// Placed item in the first region. +#set page(height: 5cm, margin: 1cm) +Placed item in the first region. +#block(breakable: true, above: 1cm, stroke: 1pt, inset: 0.5cm)[ + #place(dx: -0.5cm, dy: -0.75cm, box(width: 200%)[OOF]) + #rect(height: 2cm, fill: gray) +] + +--- +// In-flow item with size zero in the first region. +#set page(height: 5cm, margin: 1cm) +In-flow, zero-sized item. +#block(breakable: true, stroke: 1pt, inset: 0.5cm)[ + #set block(spacing: 0pt) + #line(length: 0pt) + #rect(height: 2cm, fill: gray) + #line(length: 100%) +] + +--- +// Counter update and placed item in the first region. +#set page(height: 5cm, margin: 1cm) +Counter update + place. +#block(breakable: true, above: 1cm, stroke: 1pt, inset: 0.5cm)[ + #counter("dummy").step() + #place(dx: -0.5cm, dy: -0.75cm, box([OOF])) + #rect(height: 2cm, fill: gray) +] + +--- +// Mix-and-match all the previous ones. +#set page(height: 5cm, margin: 1cm) +Mix-and-match all the previous tests. +#block(breakable: true, above: 1cm, stroke: 1pt, inset: 0.5cm)[ + #counter("dummy").step() + #place(dx: -0.5cm, dy: -0.75cm, box(width: 200%)[OOF]) + #line(length: 100%) + #place(dy: -0.8em)[OOF] + #rect(height: 2cm, fill: gray) +]