summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--crates/typst/src/layout/flow.rs51
-rw-r--r--tests/ref/layout/columns.pngbin107087 -> 95706 bytes
-rw-r--r--tests/ref/layout/out-of-flow-in-block.pngbin0 -> 16297 bytes
-rw-r--r--tests/typ/layout/columns.typ7
-rw-r--r--tests/typ/layout/out-of-flow-in-block.typ61
5 files changed, 107 insertions, 12 deletions
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::<ColbreakElem>() {
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<Fragment> {
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<FootnoteElem>,
) -> 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
--- a/tests/ref/layout/columns.png
+++ b/tests/ref/layout/columns.png
Binary files 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
--- /dev/null
+++ b/tests/ref/layout/out-of-flow-in-block.png
Binary files 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)
+]