Fix flow bugs
@ -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
|
||||||
|
.layout(
|
||||||
vt,
|
vt,
|
||||||
styles,
|
styles,
|
||||||
consecutive,
|
consecutive,
|
||||||
self.regions.first.x,
|
self.regions.first.x,
|
||||||
self.regions.base,
|
self.regions.base,
|
||||||
self.regions.expand.x,
|
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() {
|
||||||
if i > 0 {
|
match *item {
|
||||||
self.layout_item(FlowItem::Leading(leading));
|
FlowItem::Absolute(_, _) => {}
|
||||||
|
FlowItem::Frame(.., true) => sticky = i,
|
||||||
|
_ => break,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prevent widows and orphans.
|
if let [first, ..] = frames.as_slice() {
|
||||||
let border = (i == 0 && len >= 2) || i + 2 == len;
|
if !self.regions.first.y.fits(first.height()) && !self.regions.in_last() {
|
||||||
let sticky = self.root && !frame.is_empty() && border;
|
let carry: Vec<_> = self.items.drain(sticky..).collect();
|
||||||
self.layout_item(FlowItem::Frame(frame, aligns, sticky));
|
self.finish_region();
|
||||||
|
for item in carry {
|
||||||
|
self.layout_item(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i, frame) in frames.into_iter().enumerate() {
|
||||||
|
if i > 0 {
|
||||||
|
self.layout_item(FlowItem::Absolute(leading, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
self.layout_item(FlowItem::Frame(frame, aligns, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)?;
|
||||||
}
|
}
|
||||||
|
@ -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.
|
||||||
|
@ -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()
|
||||||
|
BIN
tests/ref/bugs/columns-1.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
tests/ref/bugs/flow-1.png
Normal file
After Width: | Height: | Size: 9.5 KiB |
BIN
tests/ref/bugs/flow-2.png
Normal file
After Width: | Height: | Size: 5.5 KiB |
BIN
tests/ref/bugs/flow-3.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
12
tests/typ/bugs/columns-1.typ
Normal 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
@ -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
@ -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
@ -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.
|
||||||
|
])
|
@ -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)
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
#align(bottom)[
|
#align(bottom)[
|
||||||
Bottom \
|
Bottom \
|
||||||
Bottom \
|
Bottom \
|
||||||
|
#v(0pt)
|
||||||
Top
|
Top
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
|