Clean up flow a bit (#4505)

This commit is contained in:
Laurenz 2024-07-05 14:38:05 +02:00 committed by GitHub
parent b847cccba4
commit 906de589ce
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -47,51 +47,7 @@ impl Packed<FlowElem> {
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment> {
if !regions.size.x.is_finite() && regions.expand.x {
bail!(self.span(), "cannot expand into infinite width");
}
if !regions.size.y.is_finite() && regions.expand.y {
bail!(self.span(), "cannot expand into infinite height");
}
// Check whether we have just a single multiple-layoutable element. In
// that case, we do not set `expand.y` to `false`, but rather keep it at
// its original value (since that element can take the full space).
//
// Consider the following code: `block(height: 5cm, pad(10pt, align(bottom, ..)))`
// Thanks to the code below, the expansion will be passed all the way
// through the block & pad and reach the innermost flow, so that things
// are properly bottom-aligned.
let mut alone = false;
if let [child] = self.children().elements() {
alone = child.is::<BlockElem>();
}
let mut layouter = FlowLayouter::new(locator, styles, regions, alone);
for (child, styles) in self.children().chain(&styles) {
if let Some(elem) = child.to_packed::<TagElem>() {
layouter.layout_tag(elem);
} else if child.is::<FlushElem>() {
layouter.flush(engine)?;
} else if let Some(elem) = child.to_packed::<VElem>() {
layouter.layout_spacing(engine, elem, styles)?;
} else if let Some(elem) = child.to_packed::<ParElem>() {
layouter.layout_par(engine, elem, styles)?;
} else if let Some(elem) = child.to_packed::<BlockElem>() {
layouter.layout_block(engine, elem, styles)?;
} else if let Some(placed) = child.to_packed::<PlaceElem>() {
layouter.layout_placed(engine, placed, styles)?;
} else if child.is::<ColbreakElem>() {
if !layouter.regions.backlog.is_empty() || layouter.regions.last.is_some()
{
layouter.finish_region(engine, true)?;
}
} else {
bail!(child.span(), "unexpected flow child");
}
}
layouter.finish(engine)
FlowLayouter::new(engine, self, locator, &styles, regions).layout()
}
}
@ -103,13 +59,17 @@ impl Debug for FlowElem {
}
/// Performs flow layout.
struct FlowLayouter<'a> {
struct FlowLayouter<'a, 'e> {
/// The engine.
engine: &'a mut Engine<'e>,
/// The children that will be arranged into a flow.
flow: &'a Packed<FlowElem>,
/// Whether this is the root flow.
root: bool,
/// Provides unique locations to the flow's children.
locator: SplitLocator<'a>,
/// The shared styles.
styles: StyleChain<'a>,
styles: &'a StyleChain<'a>,
/// The regions to layout children into.
regions: Regions<'a>,
/// Whether the flow should expand to fill the region.
@ -124,7 +84,7 @@ struct FlowLayouter<'a> {
/// Spacing and layouted blocks for the current region.
items: Vec<FlowItem>,
/// A queue of tags that will be attached to the next frame.
pending_tags: Vec<Tag>,
pending_tags: Vec<&'a Tag>,
/// A queue of floating elements.
pending_floats: Vec<FlowItem>,
/// Whether we have any footnotes in the current region.
@ -157,18 +117,27 @@ enum FlowItem {
align: Axes<FixedAlignment>,
/// Whether the frame sticks to the item after it (for orphan prevention).
sticky: bool,
/// Whether the frame is movable; that is, kept together with its footnotes.
/// Whether the frame is movable; that is, kept together with its
/// footnotes.
///
/// This is true for frames created by paragraphs and [`LayoutSingle`] elements.
/// This is true for frames created by paragraphs and
/// [`BlockElem::single_layouter`] elements.
movable: bool,
},
/// An absolutely placed frame.
Placed {
/// The layouted content.
frame: Frame,
/// Where to place the content horizontally.
x_align: FixedAlignment,
/// Where to place the content vertically.
y_align: Smart<Option<FixedAlignment>>,
/// A translation to apply to the content.
delta: Axes<Rel<Abs>>,
/// Whether the content floats --- i.e. collides with in-flow content.
float: bool,
/// The amount of space that needs to be kept between the placed content
/// and in-flow content. Only relevant if `float` is `true`.
clearance: Abs,
},
/// A footnote frame (can also be the separator).
@ -193,24 +162,41 @@ impl FlowItem {
}
}
impl<'a> FlowLayouter<'a> {
impl<'a, 'e> FlowLayouter<'a, 'e> {
/// Create a new flow layouter.
fn new(
engine: &'a mut Engine<'e>,
flow: &'a Packed<FlowElem>,
locator: Locator<'a>,
styles: StyleChain<'a>,
styles: &'a StyleChain<'a>,
mut regions: Regions<'a>,
alone: bool,
) -> Self {
let expand = regions.expand;
let root = std::mem::replace(&mut regions.root, false);
// Check whether we have just a single multiple-layoutable element. In
// that case, we do not set `expand.y` to `false`, but rather keep it at
// its original value (since that element can take the full space).
//
// Consider the following code: `block(height: 5cm, pad(10pt,
// align(bottom, ..)))`. Thanks to the code below, the expansion will be
// passed all the way through the block & pad and reach the innermost
// flow, so that things are properly bottom-aligned.
let mut alone = false;
if let [child] = flow.children.elements() {
alone = child.is::<BlockElem>();
}
// Disable vertical expansion when there are multiple or not directly
// layoutable children.
let expand = regions.expand;
if !alone {
regions.expand.y = false;
}
// The children aren't root.
let root = std::mem::replace(&mut regions.root, false);
Self {
engine,
flow,
root,
locator: locator.split(),
styles,
@ -223,52 +209,84 @@ impl<'a> FlowLayouter<'a> {
pending_floats: vec![],
has_footnotes: false,
footnote_config: FootnoteConfig {
separator: FootnoteEntry::separator_in(styles),
clearance: FootnoteEntry::clearance_in(styles),
gap: FootnoteEntry::gap_in(styles),
separator: FootnoteEntry::separator_in(*styles),
clearance: FootnoteEntry::clearance_in(*styles),
gap: FootnoteEntry::gap_in(*styles),
},
finished: vec![],
}
}
/// Layout the flow.
fn layout(mut self) -> SourceResult<Fragment> {
for (child, styles) in self.flow.children.chain(self.styles) {
if let Some(elem) = child.to_packed::<TagElem>() {
self.handle_tag(elem);
} else if let Some(elem) = child.to_packed::<VElem>() {
self.handle_v(elem, styles)?;
} else if let Some(elem) = child.to_packed::<ColbreakElem>() {
self.handle_colbreak(elem)?;
} else if let Some(elem) = child.to_packed::<ParElem>() {
self.handle_par(elem, styles)?;
} else if let Some(elem) = child.to_packed::<BlockElem>() {
self.handle_block(elem, styles)?;
} else if let Some(elem) = child.to_packed::<PlaceElem>() {
self.handle_place(elem, styles)?;
} else if let Some(elem) = child.to_packed::<FlushElem>() {
self.handle_flush(elem)?;
} else {
bail!(child.span(), "unexpected flow child");
}
}
self.finish()
}
/// Place explicit metadata into the flow.
fn layout_tag(&mut self, elem: &Packed<TagElem>) {
self.pending_tags.push(elem.tag.clone());
fn handle_tag(&mut self, elem: &'a Packed<TagElem>) {
self.pending_tags.push(&elem.tag);
}
/// Layout vertical spacing.
fn layout_spacing(
&mut self,
engine: &mut Engine,
v: &Packed<VElem>,
styles: StyleChain,
) -> SourceResult<()> {
self.layout_item(
engine,
match v.amount() {
fn handle_v(&mut self, v: &'a Packed<VElem>, styles: StyleChain) -> SourceResult<()> {
self.handle_item(match v.amount {
Spacing::Rel(rel) => FlowItem::Absolute(
// Resolve the spacing relative to the current base height.
rel.resolve(styles).relative_to(self.initial.y),
v.weakness(styles) > 0,
),
Spacing::Fr(fr) => FlowItem::Fractional(*fr),
},
)
Spacing::Fr(fr) => FlowItem::Fractional(fr),
})
}
/// Layout a column break.
fn handle_colbreak(&mut self, _: &'a Packed<ColbreakElem>) -> SourceResult<()> {
// If there is still an available region, skip to it.
// TODO: Turn this into a region abstraction.
if !self.regions.backlog.is_empty() || self.regions.last.is_some() {
self.finish_region(true)?;
}
Ok(())
}
/// Layout a paragraph.
fn layout_par(
fn handle_par(
&mut self,
engine: &mut Engine,
par: &Packed<ParElem>,
par: &'a Packed<ParElem>,
styles: StyleChain,
) -> SourceResult<()> {
// Fetch properties.
let align = AlignElem::alignment_in(styles).resolve(styles);
let leading = ParElem::leading_in(styles);
// Layout the paragraph into lines. This only depends on the base size,
// not on the Y position.
let consecutive = self.last_was_par;
let locator = self.locator.next(&par.span());
let lines = par
.layout(
engine,
self.locator.next(&par.span()),
self.engine,
locator,
styles,
consecutive,
self.regions.base(),
@ -280,39 +298,26 @@ impl<'a> FlowLayouter<'a> {
// previous sticky frame to the next region (if available)
if let Some(first) = lines.first() {
while !self.regions.size.y.fits(first.height()) && !self.regions.in_last() {
let mut sticky = self.items.len();
for (i, item) in self.items.iter().enumerate().rev() {
match *item {
FlowItem::Absolute(_, _) => {}
FlowItem::Frame { sticky: true, .. } => sticky = i,
_ => break,
}
}
let carry: Vec<_> = self.items.drain(sticky..).collect();
self.finish_region(engine, false)?;
let in_last = self.regions.in_last();
for item in carry {
self.layout_item(engine, item)?;
}
let in_last = self.finish_region_with_migration()?;
if in_last {
break;
}
}
}
// Layout the lines.
for (i, mut frame) in lines.into_iter().enumerate() {
if i > 0 {
self.layout_item(engine, FlowItem::Absolute(leading, true))?;
self.handle_item(FlowItem::Absolute(leading, true))?;
}
self.drain_tag(&mut frame);
self.layout_item(
engine,
FlowItem::Frame { frame, align, sticky: false, movable: true },
)?;
self.handle_item(FlowItem::Frame {
frame,
align,
sticky: false,
movable: true,
})?;
}
self.last_was_par = true;
@ -320,56 +325,54 @@ impl<'a> FlowLayouter<'a> {
}
/// Layout into multiple regions.
fn layout_block(
fn handle_block(
&mut self,
engine: &mut Engine,
block: &'a Packed<BlockElem>,
styles: StyleChain<'a>,
) -> SourceResult<()> {
// Temporarily delegate rootness to the columns.
// Fetch properties.
let sticky = block.sticky(styles);
let align = AlignElem::alignment_in(styles).resolve(styles);
// If the block is "rootable" it may host footnotes. In that case, we
// defer rootness to it temporarily. We disable our own rootness to
// prevent duplicate footnotes.
let is_root = self.root;
if is_root && block.rootable(styles) {
self.root = false;
self.regions.root = true;
}
if self.regions.is_full() {
// Skip directly if region is already full.
self.finish_region(engine, false)?;
if self.regions.is_full() {
self.finish_region(false)?;
}
// Layout the block itself.
let sticky = block.sticky(styles);
let fragment = block.layout(
engine,
self.engine,
self.locator.next(&block.span()),
styles,
self.regions,
)?;
// How to align the block.
let align = AlignElem::alignment_in(styles).resolve(styles);
let mut notes = Vec::new();
for (i, mut frame) in fragment.into_iter().enumerate() {
// Find footnotes in the frame.
if self.root {
find_footnotes(&mut notes, &frame);
collect_footnotes(&mut notes, &frame);
}
if i > 0 {
self.finish_region(engine, false)?;
self.finish_region(false)?;
}
self.drain_tag(&mut frame);
frame.post_process(styles);
self.layout_item(
engine,
FlowItem::Frame { frame, align, sticky, movable: false },
)?;
self.handle_item(FlowItem::Frame { frame, align, sticky, movable: false })?;
}
self.try_handle_footnotes(engine, notes)?;
self.try_handle_footnotes(notes)?;
self.root = is_root;
self.regions.root = false;
@ -379,50 +382,56 @@ impl<'a> FlowLayouter<'a> {
}
/// Layout a placed element.
fn layout_placed(
fn handle_place(
&mut self,
engine: &mut Engine,
placed: &Packed<PlaceElem>,
placed: &'a Packed<PlaceElem>,
styles: StyleChain,
) -> SourceResult<()> {
// Fetch properties.
let float = placed.float(styles);
let clearance = placed.clearance(styles);
let alignment = placed.alignment(styles);
let delta = Axes::new(placed.dx(styles), placed.dy(styles)).resolve(styles);
let x_align = alignment.map_or(FixedAlignment::Center, |align| {
align.x().unwrap_or_default().resolve(styles)
});
let y_align = alignment.map(|align| align.y().map(|y| y.resolve(styles)));
let mut frame = placed
.layout(
engine,
self.engine,
self.locator.next(&placed.span()),
styles,
self.regions.base(),
)?
.into_frame();
frame.post_process(styles);
let item = FlowItem::Placed { frame, x_align, y_align, delta, float, clearance };
self.layout_item(engine, item)
self.handle_item(FlowItem::Placed {
frame,
x_align,
y_align,
delta,
float,
clearance,
})
}
/// Attach currently pending metadata to the frame.
fn drain_tag(&mut self, frame: &mut Frame) {
if !self.pending_tags.is_empty() && !frame.is_empty() {
frame.prepend_multiple(
self.pending_tags
.drain(..)
.map(|tag| (Point::zero(), FrameItem::Tag(tag))),
);
/// Lays out all floating elements before continuing with other content.
fn handle_flush(&mut self, _: &'a Packed<FlushElem>) -> SourceResult<()> {
for item in std::mem::take(&mut self.pending_floats) {
self.handle_item(item)?;
}
while !self.pending_floats.is_empty() {
self.finish_region(false)?;
}
Ok(())
}
/// Layout a finished frame.
fn layout_item(
&mut self,
engine: &mut Engine,
mut item: FlowItem,
) -> SourceResult<()> {
fn handle_item(&mut self, mut item: FlowItem) -> SourceResult<()> {
match item {
FlowItem::Absolute(v, weak) => {
if weak
@ -439,24 +448,24 @@ impl<'a> FlowLayouter<'a> {
FlowItem::Frame { ref frame, movable, .. } => {
let height = frame.height();
while !self.regions.size.y.fits(height) && !self.regions.in_last() {
self.finish_region(engine, false)?;
self.finish_region(false)?;
}
let in_last = self.regions.in_last();
self.regions.size.y -= height;
if self.root && movable {
let mut notes = Vec::new();
find_footnotes(&mut notes, frame);
collect_footnotes(&mut notes, frame);
self.items.push(item);
// When we are already in_last, we can directly force the
// footnotes.
if !self.handle_footnotes(engine, &mut notes, true, in_last)? {
if !self.handle_footnotes(&mut notes, true, in_last)? {
let item = self.items.pop();
self.finish_region(engine, false)?;
self.finish_region(false)?;
self.items.extend(item);
self.regions.size.y -= height;
self.handle_footnotes(engine, &mut notes, true, true)?;
self.handle_footnotes(&mut notes, true, true)?;
}
return Ok(());
}
@ -504,8 +513,8 @@ impl<'a> FlowLayouter<'a> {
// Find footnotes in the frame.
if self.root {
let mut notes = vec![];
find_footnotes(&mut notes, frame);
self.try_handle_footnotes(engine, notes)?;
collect_footnotes(&mut notes, frame);
self.try_handle_footnotes(notes)?;
}
}
FlowItem::Footnote(_) => {}
@ -515,12 +524,49 @@ impl<'a> FlowLayouter<'a> {
Ok(())
}
/// Attach currently pending metadata to the frame.
fn drain_tag(&mut self, frame: &mut Frame) {
if !self.pending_tags.is_empty() && !frame.is_empty() {
frame.prepend_multiple(
self.pending_tags
.drain(..)
.map(|tag| (Point::zero(), FrameItem::Tag(tag.clone()))),
);
}
}
/// Finisht the region, migrating all sticky items to the next one.
///
/// Returns whether we migrated into a last region.
fn finish_region_with_migration(&mut self) -> SourceResult<bool> {
// Find the suffix of sticky items.
let mut sticky = self.items.len();
for (i, item) in self.items.iter().enumerate().rev() {
match *item {
FlowItem::Absolute(_, _) => {}
FlowItem::Frame { sticky: true, .. } => sticky = i,
_ => break,
}
}
let carry: Vec<_> = self.items.drain(sticky..).collect();
self.finish_region(false)?;
let in_last = self.regions.in_last();
for item in carry {
self.handle_item(item)?;
}
Ok(in_last)
}
/// Finish the frame for one region.
///
/// Set `force` to `true` to allow creating a frame for out-of-flow elements
/// only (this is used to force the creation of a frame in case the
/// remaining elements are all out-of-flow).
fn finish_region(&mut self, engine: &mut Engine, force: bool) -> SourceResult<()> {
fn finish_region(&mut self, force: bool) -> SourceResult<()> {
// Early return if we don't have any relevant items.
if !force
&& !self.items.is_empty()
&& self.items.iter().all(FlowItem::is_out_of_flow)
@ -585,6 +631,13 @@ impl<'a> FlowLayouter<'a> {
size.y = self.initial.y;
}
if !self.regions.size.x.is_finite() && self.expand.x {
bail!(self.flow.span(), "cannot expand into infinite width");
}
if !self.regions.size.y.is_finite() && self.expand.y {
bail!(self.flow.span(), "cannot expand into infinite height");
}
let mut output = Frame::soft(size);
let mut ruler = FixedAlignment::Start;
let mut float_top_offset = Abs::zero();
@ -653,7 +706,9 @@ impl<'a> FlowLayouter<'a> {
if force && !self.pending_tags.is_empty() {
let pos = Point::with_y(offset);
output.push_multiple(
self.pending_tags.drain(..).map(|tag| (pos, FrameItem::Tag(tag))),
self.pending_tags
.drain(..)
.map(|tag| (pos, FrameItem::Tag(tag.clone()))),
);
}
@ -665,62 +720,42 @@ impl<'a> FlowLayouter<'a> {
// Try to place floats into the next region.
for item in std::mem::take(&mut self.pending_floats) {
self.layout_item(engine, item)?;
}
Ok(())
}
/// Lays out all floating elements before continuing with other content.
fn flush(&mut self, engine: &mut Engine) -> SourceResult<()> {
for item in std::mem::take(&mut self.pending_floats) {
self.layout_item(engine, item)?;
}
while !self.pending_floats.is_empty() {
self.finish_region(engine, false)?;
self.handle_item(item)?;
}
Ok(())
}
/// Finish layouting and return the resulting fragment.
fn finish(mut self, engine: &mut Engine) -> SourceResult<Fragment> {
fn finish(mut self) -> SourceResult<Fragment> {
if self.expand.y {
while !self.regions.backlog.is_empty() {
self.finish_region(engine, true)?;
self.finish_region(true)?;
}
}
self.finish_region(engine, true)?;
self.finish_region(true)?;
while !self.items.is_empty() {
self.finish_region(engine, true)?;
self.finish_region(true)?;
}
Ok(Fragment::frames(self.finished))
}
}
impl FlowLayouter<'_> {
/// Tries to process all footnotes in the frame, placing them
/// in the next region if they could not be placed in the current
/// one.
fn try_handle_footnotes(
&mut self,
engine: &mut Engine,
mut notes: Vec<Packed<FootnoteElem>>,
) -> SourceResult<()> {
// When we are already in_last, we can directly force the
// footnotes.
if self.root
&& !self.handle_footnotes(
engine,
&mut notes,
false,
self.regions.in_last(),
)?
&& !self.handle_footnotes(&mut notes, false, self.regions.in_last())?
{
self.finish_region(engine, false)?;
self.handle_footnotes(engine, &mut notes, false, true)?;
self.finish_region(false)?;
self.handle_footnotes(&mut notes, false, true)?;
}
Ok(())
}
@ -731,7 +766,6 @@ impl FlowLayouter<'_> {
/// regions.
fn handle_footnotes(
&mut self,
engine: &mut Engine,
notes: &mut Vec<Packed<FootnoteElem>>,
movable: bool,
force: bool,
@ -750,16 +784,16 @@ impl FlowLayouter<'_> {
}
if !self.has_footnotes {
self.layout_footnote_separator(engine)?;
self.layout_footnote_separator()?;
}
self.regions.size.y -= self.footnote_config.gap;
let frames = FootnoteEntry::new(notes[k].clone())
.pack()
.layout(
engine,
self.engine,
Locator::synthesize(notes[k].location().unwrap()),
self.styles,
*self.styles,
self.regions.with_root(false),
)?
.into_frames();
@ -780,10 +814,10 @@ impl FlowLayouter<'_> {
let prev = notes.len();
for (i, frame) in frames.into_iter().enumerate() {
find_footnotes(notes, &frame);
collect_footnotes(notes, &frame);
if i > 0 {
self.finish_region(engine, false)?;
self.layout_footnote_separator(engine)?;
self.finish_region(false)?;
self.layout_footnote_separator()?;
self.regions.size.y -= self.footnote_config.gap;
}
self.regions.size.y -= frame.height();
@ -804,14 +838,14 @@ impl FlowLayouter<'_> {
}
/// Layout and save the footnote separator, typically a line.
fn layout_footnote_separator(&mut self, engine: &mut Engine) -> SourceResult<()> {
fn layout_footnote_separator(&mut self) -> SourceResult<()> {
let expand = Axes::new(self.regions.expand.x, false);
let pod = Regions::one(self.regions.base(), expand);
let separator = &self.footnote_config.separator;
// FIXME: Shouldn't use `root()` here.
let mut frame = separator
.layout(engine, Locator::root(), self.styles, pod)?
.layout(self.engine, Locator::root(), *self.styles, pod)?
.into_frame();
frame.size_mut().y += self.footnote_config.clearance;
frame.translate(Point::with_y(self.footnote_config.clearance));
@ -824,11 +858,11 @@ impl FlowLayouter<'_> {
}
}
/// Finds all footnotes in the frame.
fn find_footnotes(notes: &mut Vec<Packed<FootnoteElem>>, frame: &Frame) {
/// Collect all footnotes in a frame.
fn collect_footnotes(notes: &mut Vec<Packed<FootnoteElem>>, frame: &Frame) {
for (_, item) in frame.items() {
match item {
FrameItem::Group(group) => find_footnotes(notes, &group.frame),
FrameItem::Group(group) => collect_footnotes(notes, &group.frame),
FrameItem::Tag(tag)
if !notes.iter().any(|note| note.location() == tag.elem.location()) =>
{