Move paragraph widow and orphan prevention into flow (#4767)
@ -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,
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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<Frame> = lines
|
||||
lines
|
||||
.iter()
|
||||
.map(|line| commit(engine, p, line, width, region.y, shrink))
|
||||
.collect::<SourceResult<_>>()?;
|
||||
|
||||
// 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::<SourceResult<_>>()
|
||||
.map(Fragment::frames)
|
||||
}
|
||||
|
@ -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<Linebreaks>,
|
||||
/// 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),
|
||||
})
|
||||
|
BIN
tests/ref/flow-widow-forced.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 967 B After Width: | Height: | Size: 965 B |
BIN
tests/ref/issue-1445-widow-orphan-unnecessary-skip.png
Normal file
After Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 713 B After Width: | Height: | Size: 704 B |
@ -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)))
|
||||
|
@ -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),
|
||||
|
@ -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 ---
|
||||
|