Fix flow bugs

This commit is contained in:
Laurenz 2023-02-10 10:58:19 +01:00
parent 924a27bc37
commit ebe919220d
16 changed files with 161 additions and 74 deletions

View File

@ -9,7 +9,7 @@ use crate::prelude::*;
/// the contents of boxes. /// the contents of boxes.
#[capable(Layout)] #[capable(Layout)]
#[derive(Hash)] #[derive(Hash)]
pub struct FlowNode(pub StyleVec<Content>, pub bool); pub struct FlowNode(pub StyleVec<Content>);
#[node] #[node]
impl FlowNode {} impl FlowNode {}
@ -21,12 +21,12 @@ impl Layout for FlowNode {
styles: StyleChain, styles: StyleChain,
regions: Regions, regions: Regions,
) -> SourceResult<Fragment> { ) -> SourceResult<Fragment> {
let mut layouter = FlowLayouter::new(regions, self.1); let mut layouter = FlowLayouter::new(regions);
for (child, map) in self.0.iter() { for (child, map) in self.0.iter() {
let styles = styles.chain(&map); let styles = styles.chain(&map);
if let Some(&node) = child.to::<VNode>() { if let Some(&node) = child.to::<VNode>() {
layouter.layout_spacing(node.amount, styles); layouter.layout_spacing(node, styles);
} else if let Some(node) = child.to::<ParNode>() { } else if let Some(node) = child.to::<ParNode>() {
let barrier = Style::Barrier(child.id()); let barrier = Style::Barrier(child.id());
let styles = styles.chain_one(&barrier); let styles = styles.chain_one(&barrier);
@ -34,7 +34,7 @@ impl Layout for FlowNode {
} else if child.has::<dyn Layout>() { } else if child.has::<dyn Layout>() {
layouter.layout_block(vt, child, styles)?; layouter.layout_block(vt, child, styles)?;
} else if child.is::<ColbreakNode>() { } else if child.is::<ColbreakNode>() {
layouter.finish_region(false); layouter.finish_region();
} else { } else {
panic!("unexpected flow child: {child:?}"); panic!("unexpected flow child: {child:?}");
} }
@ -53,8 +53,6 @@ impl Debug for FlowNode {
/// Performs flow layout. /// Performs flow layout.
struct FlowLayouter<'a> { struct FlowLayouter<'a> {
/// Whether this is a root page-level flow.
root: bool,
/// The regions to layout children into. /// The regions to layout children into.
regions: Regions<'a>, regions: Regions<'a>,
/// Whether the flow should expand to fill the region. /// Whether the flow should expand to fill the region.
@ -73,13 +71,11 @@ struct FlowLayouter<'a> {
/// A prepared item in a flow layout. /// A prepared item in a flow layout.
#[derive(Debug)] #[derive(Debug)]
enum FlowItem { enum FlowItem {
/// Absolute spacing between other items. /// Spacing between other items and whether it is weak.
Absolute(Abs), Absolute(Abs, bool),
/// Leading between paragraph lines.
Leading(Abs),
/// Fractional spacing between other items. /// Fractional spacing between other items.
Fractional(Fr), Fractional(Fr),
/// A frame for a layouted block and how to align it. /// A frame for a layouted block, how to align it, and whether it is sticky.
Frame(Frame, Axes<Align>, bool), Frame(Frame, Axes<Align>, bool),
/// An absolutely placed frame. /// An absolutely placed frame.
Placed(Frame), Placed(Frame),
@ -87,7 +83,7 @@ enum FlowItem {
impl<'a> FlowLayouter<'a> { impl<'a> FlowLayouter<'a> {
/// Create a new flow layouter. /// Create a new flow layouter.
fn new(mut regions: Regions<'a>, root: bool) -> Self { fn new(mut regions: Regions<'a>) -> Self {
let expand = regions.expand; let expand = regions.expand;
let full = regions.first; let full = regions.first;
@ -95,7 +91,6 @@ impl<'a> FlowLayouter<'a> {
regions.expand.y = false; regions.expand.y = false;
Self { Self {
root,
regions, regions,
expand, expand,
full, full,
@ -106,11 +101,12 @@ impl<'a> FlowLayouter<'a> {
} }
/// Layout vertical spacing. /// Layout vertical spacing.
fn layout_spacing(&mut self, spacing: Spacing, styles: StyleChain) { fn layout_spacing(&mut self, node: VNode, styles: StyleChain) {
self.layout_item(match spacing { self.layout_item(match node.amount {
Spacing::Relative(v) => { Spacing::Relative(v) => FlowItem::Absolute(
FlowItem::Absolute(v.resolve(styles).relative_to(self.full.y)) v.resolve(styles).relative_to(self.full.y),
} node.weakness > 0,
),
Spacing::Fractional(v) => FlowItem::Fractional(v), Spacing::Fractional(v) => FlowItem::Fractional(v),
}); });
} }
@ -125,25 +121,42 @@ impl<'a> FlowLayouter<'a> {
let aligns = styles.get(AlignNode::ALIGNS).resolve(styles); let aligns = styles.get(AlignNode::ALIGNS).resolve(styles);
let leading = styles.get(ParNode::LEADING); let leading = styles.get(ParNode::LEADING);
let consecutive = self.last_was_par; let consecutive = self.last_was_par;
let fragment = par.layout( let frames = par
vt, .layout(
styles, vt,
consecutive, styles,
self.regions.first.x, consecutive,
self.regions.base, self.regions.first.x,
self.regions.expand.x, self.regions.base,
)?; self.regions.expand.x,
)?
.into_frames();
let len = fragment.len(); let mut sticky = self.items.len();
for (i, frame) in fragment.into_iter().enumerate() { for (i, item) in self.items.iter().enumerate().rev() {
match *item {
FlowItem::Absolute(_, _) => {}
FlowItem::Frame(.., true) => sticky = i,
_ => break,
}
}
if let [first, ..] = frames.as_slice() {
if !self.regions.first.y.fits(first.height()) && !self.regions.in_last() {
let carry: Vec<_> = self.items.drain(sticky..).collect();
self.finish_region();
for item in carry {
self.layout_item(item);
}
}
}
for (i, frame) in frames.into_iter().enumerate() {
if i > 0 { if i > 0 {
self.layout_item(FlowItem::Leading(leading)); self.layout_item(FlowItem::Absolute(leading, true));
} }
// Prevent widows and orphans. self.layout_item(FlowItem::Frame(frame, aligns, false));
let border = (i == 0 && len >= 2) || i + 2 == len;
let sticky = self.root && !frame.is_empty() && border;
self.layout_item(FlowItem::Frame(frame, aligns, sticky));
} }
self.last_was_par = true; self.last_was_par = true;
@ -186,15 +199,12 @@ impl<'a> FlowLayouter<'a> {
/// Layout a finished frame. /// Layout a finished frame.
fn layout_item(&mut self, item: FlowItem) { fn layout_item(&mut self, item: FlowItem) {
match item { match item {
FlowItem::Absolute(v) | FlowItem::Leading(v) => self.regions.first.y -= v, FlowItem::Absolute(v, _) => self.regions.first.y -= v,
FlowItem::Fractional(_) => {} FlowItem::Fractional(_) => {}
FlowItem::Frame(ref frame, ..) => { FlowItem::Frame(ref frame, ..) => {
let size = frame.size(); let size = frame.size();
if !self.regions.first.y.fits(size.y) if !self.regions.first.y.fits(size.y) && !self.regions.in_last() {
&& !self.regions.in_last() self.finish_region();
&& self.items.iter().any(|item| !matches!(item, FlowItem::Leading(_)))
{
self.finish_region(true);
} }
self.regions.first.y -= size.y; self.regions.first.y -= size.y;
@ -206,34 +216,22 @@ impl<'a> FlowLayouter<'a> {
} }
/// Finish the frame for one region. /// Finish the frame for one region.
fn finish_region(&mut self, something_follows: bool) { fn finish_region(&mut self) {
let mut end = self.items.len(); // Trim weak spacing.
if something_follows { while self
for (i, item) in self.items.iter().enumerate().rev() { .items
match *item { .last()
FlowItem::Absolute(_) .map_or(false, |item| matches!(item, FlowItem::Absolute(_, true)))
| FlowItem::Leading(_) {
| FlowItem::Fractional(_) => {}
FlowItem::Frame(.., true) => end = i,
_ => break,
}
}
if end == 0 {
return;
}
}
let carry: Vec<_> = self.items.drain(end..).collect();
while let Some(FlowItem::Leading(_)) = self.items.last() {
self.items.pop(); self.items.pop();
} }
// Determine the used size.
let mut fr = Fr::zero(); let mut fr = Fr::zero();
let mut used = Size::zero(); let mut used = Size::zero();
for item in &self.items { for item in &self.items {
match *item { match *item {
FlowItem::Absolute(v) | FlowItem::Leading(v) => used.y += v, FlowItem::Absolute(v, _) => used.y += v,
FlowItem::Fractional(v) => fr += v, FlowItem::Fractional(v) => fr += v,
FlowItem::Frame(ref frame, ..) => { FlowItem::Frame(ref frame, ..) => {
let size = frame.size(); let size = frame.size();
@ -247,6 +245,7 @@ impl<'a> FlowLayouter<'a> {
// Determine the size of the flow in this region depending on whether // Determine the size of the flow in this region depending on whether
// the region expands. // the region expands.
let mut size = self.expand.select(self.full, used); let mut size = self.expand.select(self.full, used);
size.y.set_min(self.full.y);
// Account for fractional spacing in the size calculation. // Account for fractional spacing in the size calculation.
let remaining = self.full.y - used.y; let remaining = self.full.y - used.y;
@ -262,7 +261,7 @@ impl<'a> FlowLayouter<'a> {
// Place all frames. // Place all frames.
for item in self.items.drain(..) { for item in self.items.drain(..) {
match item { match item {
FlowItem::Absolute(v) | FlowItem::Leading(v) => { FlowItem::Absolute(v, _) => {
offset += v; offset += v;
} }
FlowItem::Fractional(v) => { FlowItem::Fractional(v) => {
@ -286,21 +285,17 @@ impl<'a> FlowLayouter<'a> {
self.finished.push(output); self.finished.push(output);
self.regions.next(); self.regions.next();
self.full = self.regions.first; self.full = self.regions.first;
for item in carry {
self.layout_item(item);
}
} }
/// Finish layouting and return the resulting fragment. /// Finish layouting and return the resulting fragment.
fn finish(mut self) -> Fragment { fn finish(mut self) -> Fragment {
if self.expand.y { if self.expand.y {
while !self.regions.backlog.is_empty() { while !self.regions.backlog.is_empty() {
self.finish_region(false); self.finish_region();
} }
} }
self.finish_region(false); self.finish_region();
Fragment::frames(self.finished) Fragment::frames(self.finished)
} }
} }

View File

@ -148,7 +148,7 @@ impl Layout for Content {
pub trait Inline: Layout {} pub trait Inline: Layout {}
/// A sequence of regions to layout into. /// A sequence of regions to layout into.
#[derive(Debug, Copy, Clone, Hash)] #[derive(Copy, Clone, Hash)]
pub struct Regions<'a> { pub struct Regions<'a> {
/// The (remaining) size of the first region. /// The (remaining) size of the first region.
pub first: Size, pub first: Size,
@ -247,6 +247,26 @@ impl<'a> Regions<'a> {
} }
} }
impl Debug for Regions<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str("Regions ")?;
let mut list = f.debug_list();
let mut prev = self.first.y;
list.entry(&self.first);
for &height in self.backlog {
list.entry(&Size::new(self.first.x, height));
prev = height;
}
if let Some(last) = self.last {
if last != prev {
list.entry(&Size::new(self.first.x, last));
}
list.entry(&(..));
}
list.finish()
}
}
/// Realize into a node that is capable of root-level layout. /// Realize into a node that is capable of root-level layout.
fn realize_root<'a>( fn realize_root<'a>(
vt: &mut Vt, vt: &mut Vt,
@ -280,7 +300,7 @@ fn realize_block<'a>(
builder.accept(content, styles)?; builder.accept(content, styles)?;
builder.interrupt_par()?; builder.interrupt_par()?;
let (children, shared) = builder.flow.0.finish(); let (children, shared) = builder.flow.0.finish();
Ok((FlowNode(children, false).pack(), shared)) Ok((FlowNode(children).pack(), shared))
} }
/// Builds a document or a flow node from content. /// Builds a document or a flow node from content.
@ -468,7 +488,7 @@ impl<'a, 'v, 't> Builder<'a, 'v, 't> {
let (flow, shared) = mem::take(&mut self.flow).0.finish(); let (flow, shared) = mem::take(&mut self.flow).0.finish();
let styles = let styles =
if shared == StyleChain::default() { styles.unwrap() } else { shared }; if shared == StyleChain::default() { styles.unwrap() } else { shared };
let page = PageNode(FlowNode(flow, true).pack()).pack(); let page = PageNode(FlowNode(flow).pack()).pack();
let stored = self.scratch.content.alloc(page); let stored = self.scratch.content.alloc(page);
self.accept(stored, styles)?; self.accept(stored, styles)?;
} }

View File

@ -1127,11 +1127,36 @@ fn finalize(
} }
// Stack the lines into one frame per region. // Stack the lines into one frame per region.
lines let mut frames: Vec<Frame> = lines
.iter() .iter()
.map(|line| commit(vt, p, line, base, width)) .map(|line| commit(vt, p, line, base, width))
.collect::<SourceResult<_>>() .collect::<SourceResult<_>>()?;
.map(Fragment::frames)
// Prevent orphans.
let leading = p.styles.get(ParNode::LEADING);
if frames.len() >= 2 && !frames[1].is_empty() {
let second = frames.remove(1);
let first = &mut frames[0];
merge(first, second, leading);
}
// Prevent widows.
let len = frames.len();
if len >= 2 && !frames[len - 2].is_empty() {
let second = frames.pop().unwrap();
let first = frames.last_mut().unwrap();
merge(first, second, leading);
}
Ok(Fragment::frames(frames))
}
/// Merge two line frames
fn merge(first: &mut Frame, second: Frame, leading: Abs) {
let offset = first.height() + leading;
let total = offset + second.height();
first.push_frame(Point::with_y(offset), second);
first.size_mut().y = total;
} }
/// Commit to a line and build its frame. /// Commit to a line and build its frame.

View File

@ -347,6 +347,7 @@ impl Frame {
impl Debug for Frame { impl Debug for Frame {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str("Frame ")?;
f.debug_list() f.debug_list()
.entries(self.elements.iter().map(|(_, element)| element)) .entries(self.elements.iter().map(|(_, element)| element))
.finish() .finish()

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
tests/ref/bugs/flow-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

BIN
tests/ref/bugs/flow-2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

BIN
tests/ref/bugs/flow-3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@ -0,0 +1,12 @@
// The well-known columns bug.
---
#set page(height: 70pt)
Hallo
#columns(2)[
= A
Text
= B
Text
]

11
tests/typ/bugs/flow-1.typ Normal file
View File

@ -0,0 +1,11 @@
// In this bug, the first line of the second paragraph was on its page alone an
// the rest moved down. The reason was that the second block resulted in
// overlarge frames because the region wasn't finished properly.
---
#set page(height: 70pt)
#block[This file tests a bug where an almost empty page occurs.]
#block[
The text in this second block was torn apart and split up for
some reason beyond my knowledge.
]

10
tests/typ/bugs/flow-2.typ Normal file
View File

@ -0,0 +1,10 @@
// In this bug, the first part of the paragraph moved down to the second page
// because trailing leading wasn't trimmed, resulting in an overlarge frame.
---
#set page(height: 60pt)
#v(19pt)
#block[
But, soft! what light through yonder window breaks?
It is the east, and Juliet is the sun.
]

12
tests/typ/bugs/flow-3.typ Normal file
View File

@ -0,0 +1,12 @@
// In this bug, there was a bit of space below the heading because weak spacing
// directly before a layout-induced column or page break wasn't trimmed.
---
#set page(height: 60pt)
#rect(inset: 0pt, columns(2)[
Text
#v(12pt)
Hi
#v(10pt, weak: true)
At column break.
])

View File

@ -1,4 +1,4 @@
// Test that a heading doesn't become an orphan. // Test that lines and headings doesn't become orphan.
--- ---
#set page(height: 100pt) #set page(height: 100pt)

View File

@ -22,6 +22,7 @@
#align(bottom)[ #align(bottom)[
Bottom \ Bottom \
Bottom \ Bottom \
#v(0pt)
Top Top
] ]
], ],