diff --git a/crates/typst/src/layout/flow.rs b/crates/typst/src/layout/flow.rs index a1f1402e..e15159e1 100644 --- a/crates/typst/src/layout/flow.rs +++ b/crates/typst/src/layout/flow.rs @@ -14,10 +14,12 @@ use crate::foundations::{ use crate::introspection::{Locator, SplitLocator, Tag, TagElem}; use crate::layout::{ Abs, AlignElem, Axes, BlockElem, ColbreakElem, FixedAlignment, FlushElem, Fr, - Fragment, Frame, FrameItem, PlaceElem, Point, Regions, Rel, Size, Spacing, VElem, + Fragment, Frame, FrameItem, PlaceElem, Point, Ratio, Regions, Rel, Size, Spacing, + VElem, }; use crate::model::{FootnoteElem, FootnoteEntry, ParElem}; use crate::realize::StyleVec; +use crate::text::TextElem; use crate::utils::Numeric; /// Arranges spacing, paragraphs and block-level elements into a flow. @@ -278,6 +280,7 @@ impl<'a, 'e> FlowLayouter<'a, 'e> { // Fetch properties. let align = AlignElem::alignment_in(styles).resolve(styles); let leading = ParElem::leading_in(styles); + let costs = TextElem::costs_in(styles); // Layout the paragraph into lines. This only depends on the base size, // not on the Y position. @@ -305,12 +308,51 @@ impl<'a, 'e> FlowLayouter<'a, 'e> { } } + // Determine whether to prevent widow and orphans. + let len = lines.len(); + let prevent_orphans = + costs.orphan() > Ratio::zero() && len >= 2 && !lines[1].is_empty(); + let prevent_widows = + costs.widow() > Ratio::zero() && len >= 2 && !lines[len - 2].is_empty(); + let prevent_all = len == 3 && prevent_orphans && prevent_widows; + + // Store the heights of lines at the edges because we'll potentially + // need these later when `lines` is already moved. + let height_at = |i| lines.get(i).map(Frame::height).unwrap_or_default(); + let front_1 = height_at(0); + let front_2 = height_at(1); + let back_2 = height_at(len.saturating_sub(2)); + let back_1 = height_at(len.saturating_sub(1)); + // Layout the lines. for (i, mut frame) in lines.into_iter().enumerate() { if i > 0 { self.handle_item(FlowItem::Absolute(leading, true))?; } + // To prevent widows and orphans, we require enough space for + // - all lines if it's just three + // - the first two lines if we're at the first line + // - the last two lines if we're at the second to last line + let needed = if prevent_all && i == 0 { + front_1 + leading + front_2 + leading + back_1 + } else if prevent_orphans && i == 0 { + front_1 + leading + front_2 + } else if prevent_widows && i >= 2 && i + 2 == len { + back_2 + leading + back_1 + } else { + frame.height() + }; + + // If the line(s) don't fit into this region, but they do fit into + // the next, then advance. + if !self.regions.in_last() + && !self.regions.size.y.fits(needed) + && self.regions.iter().nth(1).is_some_and(|region| region.y.fits(needed)) + { + self.finish_region(false)?; + } + self.drain_tag(&mut frame); self.handle_item(FlowItem::Frame { frame, diff --git a/crates/typst/src/layout/inline/collect.rs b/crates/typst/src/layout/inline/collect.rs index b6a847f5..5021dc55 100644 --- a/crates/typst/src/layout/inline/collect.rs +++ b/crates/typst/src/layout/inline/collect.rs @@ -128,11 +128,11 @@ pub fn collect<'a>( let mut iter = children.chain(styles).peekable(); let mut locator = locator.split(); + let outer_dir = TextElem::dir_in(*styles); let first_line_indent = ParElem::first_line_indent_in(*styles); if !first_line_indent.is_zero() && consecutive - && AlignElem::alignment_in(*styles).resolve(*styles).x - == TextElem::dir_in(*styles).start().into() + && AlignElem::alignment_in(*styles).resolve(*styles).x == outer_dir.start().into() { collector.push_item(Item::Absolute(first_line_indent.resolve(*styles), false)); collector.spans.push(1, Span::detached()); @@ -144,8 +144,6 @@ pub fn collect<'a>( collector.spans.push(1, Span::detached()); } - let outer_dir = TextElem::dir_in(*styles); - while let Some((child, styles)) = iter.next() { let prev_len = collector.full.len(); diff --git a/crates/typst/src/layout/inline/finalize.rs b/crates/typst/src/layout/inline/finalize.rs index c8ba4729..03493af5 100644 --- a/crates/typst/src/layout/inline/finalize.rs +++ b/crates/typst/src/layout/inline/finalize.rs @@ -1,5 +1,4 @@ use super::*; -use crate::layout::{Abs, Frame, Point}; use crate::utils::Numeric; /// Turns the selected lines into frames. @@ -26,38 +25,9 @@ pub fn finalize( // Stack the lines into one frame per region. let shrink = ParElem::shrink_in(styles); - let mut frames: Vec = lines + lines .iter() .map(|line| commit(engine, p, line, width, region.y, shrink)) - .collect::>()?; - - // Positive ratios enable prevention, while zero and negative ratios disable - // it. - if p.costs.orphan().get() > 0.0 { - // Prevent orphans. - if frames.len() >= 2 && !frames[1].is_empty() { - let second = frames.remove(1); - let first = &mut frames[0]; - merge(first, second, p.leading); - } - } - if p.costs.widow().get() > 0.0 { - // 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, p.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; + .collect::>() + .map(Fragment::frames) } diff --git a/crates/typst/src/layout/inline/prepare.rs b/crates/typst/src/layout/inline/prepare.rs index 59682b2c..9e73af66 100644 --- a/crates/typst/src/layout/inline/prepare.rs +++ b/crates/typst/src/layout/inline/prepare.rs @@ -43,8 +43,6 @@ pub struct Preparation<'a> { pub cjk_latin_spacing: bool, /// Whether font fallback is enabled for this paragraph. pub fallback: bool, - /// The leading of the paragraph. - pub leading: Abs, /// How to determine line breaks. pub linebreaks: Smart, /// The text size. @@ -136,7 +134,6 @@ pub fn prepare<'a>( hang: ParElem::hanging_indent_in(styles), cjk_latin_spacing, fallback: TextElem::fallback_in(styles), - leading: ParElem::leading_in(styles), linebreaks: ParElem::linebreaks_in(styles), size: TextElem::size_in(styles), }) diff --git a/tests/ref/flow-widow-forced.png b/tests/ref/flow-widow-forced.png new file mode 100644 index 00000000..98a953af Binary files /dev/null and b/tests/ref/flow-widow-forced.png differ diff --git a/tests/ref/grid-header-and-footer-lack-of-space.png b/tests/ref/grid-header-and-footer-lack-of-space.png index 78705776..303c6f31 100644 Binary files a/tests/ref/grid-header-and-footer-lack-of-space.png and b/tests/ref/grid-header-and-footer-lack-of-space.png differ diff --git a/tests/ref/grid-header-lack-of-space.png b/tests/ref/grid-header-lack-of-space.png index 4d2b483f..8b222174 100644 Binary files a/tests/ref/grid-header-lack-of-space.png and b/tests/ref/grid-header-lack-of-space.png differ diff --git a/tests/ref/grid-rowspan-split-9.png b/tests/ref/grid-rowspan-split-9.png index 5346be71..8d878c28 100644 Binary files a/tests/ref/grid-rowspan-split-9.png and b/tests/ref/grid-rowspan-split-9.png differ diff --git a/tests/ref/issue-1445-widow-orphan-unnecessary-skip.png b/tests/ref/issue-1445-widow-orphan-unnecessary-skip.png new file mode 100644 index 00000000..7cd7888d Binary files /dev/null and b/tests/ref/issue-1445-widow-orphan-unnecessary-skip.png differ diff --git a/tests/ref/issue-multiple-footnote-in-one-line.png b/tests/ref/issue-multiple-footnote-in-one-line.png index 1d8c017d..cdb83af2 100644 Binary files a/tests/ref/issue-multiple-footnote-in-one-line.png and b/tests/ref/issue-multiple-footnote-in-one-line.png differ diff --git a/tests/suite/layout/flow/orphan.typ b/tests/suite/layout/flow/orphan.typ index 70eac731..bd938d96 100644 --- a/tests/suite/layout/flow/orphan.typ +++ b/tests/suite/layout/flow/orphan.typ @@ -29,3 +29,14 @@ This is the start and it goes on. // All three lines go to the next page. #set text(olive) #lorem(10) + +--- flow-widow-forced --- +// Ensure that a widow is allowed when the three lines don't all fit. +#set page(height: 50pt) +#lorem(10) + +--- issue-1445-widow-orphan-unnecessary-skip --- +// Ensure that widow/orphan prevention doesn't unnecessarily move things +// to another page. +#set page(width: 16cm) +#block(height: 30pt, fill: aqua, columns(2, lorem(19))) diff --git a/tests/suite/layout/grid/headers.typ b/tests/suite/layout/grid/headers.typ index c9c95e13..c3b92997 100644 --- a/tests/suite/layout/grid/headers.typ +++ b/tests/suite/layout/grid/headers.typ @@ -220,7 +220,7 @@ --- grid-header-lack-of-space --- // Test lack of space for header + text. -#set page(height: 9em) +#set page(height: 8em) #table( rows: (auto, 2.5em, auto, auto, 10em), diff --git a/tests/suite/model/footnote.typ b/tests/suite/model/footnote.typ index d72ca25a..99372551 100644 --- a/tests/suite/model/footnote.typ +++ b/tests/suite/model/footnote.typ @@ -163,11 +163,10 @@ Ref @fn --- issue-multiple-footnote-in-one-line --- // Test that the logic that keeps footnote entry together with // their markers also works for multiple footnotes in a single -// line or frame (here, there are two lines, but they are one -// unit due to orphan prevention). +// line. #set page(height: 100pt) -#v(40pt) -A #footnote[a] \ +#v(50pt) +A #footnote[a] B #footnote[b] --- issue-1433-footnote-in-list ---