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::introspection::{Locator, SplitLocator, Tag, TagElem};
|
||||||
use crate::layout::{
|
use crate::layout::{
|
||||||
Abs, AlignElem, Axes, BlockElem, ColbreakElem, FixedAlignment, FlushElem, Fr,
|
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::model::{FootnoteElem, FootnoteEntry, ParElem};
|
||||||
use crate::realize::StyleVec;
|
use crate::realize::StyleVec;
|
||||||
|
use crate::text::TextElem;
|
||||||
use crate::utils::Numeric;
|
use crate::utils::Numeric;
|
||||||
|
|
||||||
/// Arranges spacing, paragraphs and block-level elements into a flow.
|
/// Arranges spacing, paragraphs and block-level elements into a flow.
|
||||||
@ -278,6 +280,7 @@ impl<'a, 'e> FlowLayouter<'a, 'e> {
|
|||||||
// Fetch properties.
|
// Fetch properties.
|
||||||
let align = AlignElem::alignment_in(styles).resolve(styles);
|
let align = AlignElem::alignment_in(styles).resolve(styles);
|
||||||
let leading = ParElem::leading_in(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,
|
// Layout the paragraph into lines. This only depends on the base size,
|
||||||
// not on the Y position.
|
// 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.
|
// Layout the lines.
|
||||||
for (i, mut frame) in lines.into_iter().enumerate() {
|
for (i, mut frame) in lines.into_iter().enumerate() {
|
||||||
if i > 0 {
|
if i > 0 {
|
||||||
self.handle_item(FlowItem::Absolute(leading, true))?;
|
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.drain_tag(&mut frame);
|
||||||
self.handle_item(FlowItem::Frame {
|
self.handle_item(FlowItem::Frame {
|
||||||
frame,
|
frame,
|
||||||
|
@ -128,11 +128,11 @@ pub fn collect<'a>(
|
|||||||
let mut iter = children.chain(styles).peekable();
|
let mut iter = children.chain(styles).peekable();
|
||||||
let mut locator = locator.split();
|
let mut locator = locator.split();
|
||||||
|
|
||||||
|
let outer_dir = TextElem::dir_in(*styles);
|
||||||
let first_line_indent = ParElem::first_line_indent_in(*styles);
|
let first_line_indent = ParElem::first_line_indent_in(*styles);
|
||||||
if !first_line_indent.is_zero()
|
if !first_line_indent.is_zero()
|
||||||
&& consecutive
|
&& consecutive
|
||||||
&& AlignElem::alignment_in(*styles).resolve(*styles).x
|
&& AlignElem::alignment_in(*styles).resolve(*styles).x == outer_dir.start().into()
|
||||||
== TextElem::dir_in(*styles).start().into()
|
|
||||||
{
|
{
|
||||||
collector.push_item(Item::Absolute(first_line_indent.resolve(*styles), false));
|
collector.push_item(Item::Absolute(first_line_indent.resolve(*styles), false));
|
||||||
collector.spans.push(1, Span::detached());
|
collector.spans.push(1, Span::detached());
|
||||||
@ -144,8 +144,6 @@ pub fn collect<'a>(
|
|||||||
collector.spans.push(1, Span::detached());
|
collector.spans.push(1, Span::detached());
|
||||||
}
|
}
|
||||||
|
|
||||||
let outer_dir = TextElem::dir_in(*styles);
|
|
||||||
|
|
||||||
while let Some((child, styles)) = iter.next() {
|
while let Some((child, styles)) = iter.next() {
|
||||||
let prev_len = collector.full.len();
|
let prev_len = collector.full.len();
|
||||||
|
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::layout::{Abs, Frame, Point};
|
|
||||||
use crate::utils::Numeric;
|
use crate::utils::Numeric;
|
||||||
|
|
||||||
/// Turns the selected lines into frames.
|
/// Turns the selected lines into frames.
|
||||||
@ -26,38 +25,9 @@ pub fn finalize(
|
|||||||
|
|
||||||
// Stack the lines into one frame per region.
|
// Stack the lines into one frame per region.
|
||||||
let shrink = ParElem::shrink_in(styles);
|
let shrink = ParElem::shrink_in(styles);
|
||||||
let mut frames: Vec<Frame> = lines
|
lines
|
||||||
.iter()
|
.iter()
|
||||||
.map(|line| commit(engine, p, line, width, region.y, shrink))
|
.map(|line| commit(engine, p, line, width, region.y, shrink))
|
||||||
.collect::<SourceResult<_>>()?;
|
.collect::<SourceResult<_>>()
|
||||||
|
.map(Fragment::frames)
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
|
@ -43,8 +43,6 @@ pub struct Preparation<'a> {
|
|||||||
pub cjk_latin_spacing: bool,
|
pub cjk_latin_spacing: bool,
|
||||||
/// Whether font fallback is enabled for this paragraph.
|
/// Whether font fallback is enabled for this paragraph.
|
||||||
pub fallback: bool,
|
pub fallback: bool,
|
||||||
/// The leading of the paragraph.
|
|
||||||
pub leading: Abs,
|
|
||||||
/// How to determine line breaks.
|
/// How to determine line breaks.
|
||||||
pub linebreaks: Smart<Linebreaks>,
|
pub linebreaks: Smart<Linebreaks>,
|
||||||
/// The text size.
|
/// The text size.
|
||||||
@ -136,7 +134,6 @@ pub fn prepare<'a>(
|
|||||||
hang: ParElem::hanging_indent_in(styles),
|
hang: ParElem::hanging_indent_in(styles),
|
||||||
cjk_latin_spacing,
|
cjk_latin_spacing,
|
||||||
fallback: TextElem::fallback_in(styles),
|
fallback: TextElem::fallback_in(styles),
|
||||||
leading: ParElem::leading_in(styles),
|
|
||||||
linebreaks: ParElem::linebreaks_in(styles),
|
linebreaks: ParElem::linebreaks_in(styles),
|
||||||
size: TextElem::size_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.
|
// All three lines go to the next page.
|
||||||
#set text(olive)
|
#set text(olive)
|
||||||
#lorem(10)
|
#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 ---
|
--- grid-header-lack-of-space ---
|
||||||
// Test lack of space for header + text.
|
// Test lack of space for header + text.
|
||||||
#set page(height: 9em)
|
#set page(height: 8em)
|
||||||
|
|
||||||
#table(
|
#table(
|
||||||
rows: (auto, 2.5em, auto, auto, 10em),
|
rows: (auto, 2.5em, auto, auto, 10em),
|
||||||
|
@ -163,11 +163,10 @@ Ref @fn
|
|||||||
--- issue-multiple-footnote-in-one-line ---
|
--- issue-multiple-footnote-in-one-line ---
|
||||||
// Test that the logic that keeps footnote entry together with
|
// Test that the logic that keeps footnote entry together with
|
||||||
// their markers also works for multiple footnotes in a single
|
// their markers also works for multiple footnotes in a single
|
||||||
// line or frame (here, there are two lines, but they are one
|
// line.
|
||||||
// unit due to orphan prevention).
|
|
||||||
#set page(height: 100pt)
|
#set page(height: 100pt)
|
||||||
#v(40pt)
|
#v(50pt)
|
||||||
A #footnote[a] \
|
A #footnote[a]
|
||||||
B #footnote[b]
|
B #footnote[b]
|
||||||
|
|
||||||
--- issue-1433-footnote-in-list ---
|
--- issue-1433-footnote-in-list ---
|
||||||
|