diff --git a/crates/typst/src/layout/flow.rs b/crates/typst/src/layout/flow.rs index cdf6034d5..a1f1402e4 100644 --- a/crates/typst/src/layout/flow.rs +++ b/crates/typst/src/layout/flow.rs @@ -47,51 +47,7 @@ impl Packed { styles: StyleChain, regions: Regions, ) -> SourceResult { - 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::(); - } - - let mut layouter = FlowLayouter::new(locator, styles, regions, alone); - for (child, styles) in self.children().chain(&styles) { - if let Some(elem) = child.to_packed::() { - layouter.layout_tag(elem); - } else if child.is::() { - layouter.flush(engine)?; - } else if let Some(elem) = child.to_packed::() { - layouter.layout_spacing(engine, elem, styles)?; - } else if let Some(elem) = child.to_packed::() { - layouter.layout_par(engine, elem, styles)?; - } else if let Some(elem) = child.to_packed::() { - layouter.layout_block(engine, elem, styles)?; - } else if let Some(placed) = child.to_packed::() { - layouter.layout_placed(engine, placed, styles)?; - } else if child.is::() { - 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, /// 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, /// A queue of tags that will be attached to the next frame. - pending_tags: Vec, + pending_tags: Vec<&'a Tag>, /// A queue of floating elements. pending_floats: Vec, /// Whether we have any footnotes in the current region. @@ -157,18 +117,27 @@ enum FlowItem { align: Axes, /// 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>, + /// A translation to apply to the content. delta: Axes>, + /// 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, 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::(); + } // 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 { + for (child, styles) in self.flow.children.chain(self.styles) { + if let Some(elem) = child.to_packed::() { + self.handle_tag(elem); + } else if let Some(elem) = child.to_packed::() { + self.handle_v(elem, styles)?; + } else if let Some(elem) = child.to_packed::() { + self.handle_colbreak(elem)?; + } else if let Some(elem) = child.to_packed::() { + self.handle_par(elem, styles)?; + } else if let Some(elem) = child.to_packed::() { + self.handle_block(elem, styles)?; + } else if let Some(elem) = child.to_packed::() { + self.handle_place(elem, styles)?; + } else if let Some(elem) = child.to_packed::() { + 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) { - self.pending_tags.push(elem.tag.clone()); + fn handle_tag(&mut self, elem: &'a Packed) { + self.pending_tags.push(&elem.tag); } /// Layout vertical spacing. - fn layout_spacing( - &mut self, - engine: &mut Engine, - v: &Packed, - styles: StyleChain, - ) -> SourceResult<()> { - self.layout_item( - engine, - match v.amount() { - Spacing::Rel(rel) => FlowItem::Absolute( - rel.resolve(styles).relative_to(self.initial.y), - v.weakness(styles) > 0, - ), - Spacing::Fr(fr) => FlowItem::Fractional(*fr), - }, - ) + fn handle_v(&mut self, v: &'a Packed, 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), + }) + } + + /// Layout a column break. + fn handle_colbreak(&mut self, _: &'a Packed) -> 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, + par: &'a Packed, 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, 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; } + // Skip directly if region is already full. if self.regions.is_full() { - // Skip directly if region is already full. - self.finish_region(engine, false)?; + 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, + placed: &'a Packed, 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) -> 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 { + // 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 { + fn finish(mut self) -> SourceResult { 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>, ) -> 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>, 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>, frame: &Frame) { +/// Collect all footnotes in a frame. +fn collect_footnotes(notes: &mut Vec>, 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()) => {