use std::mem; use super::{ AlignElem, BlockElem, ColbreakElem, ColumnsElem, ParElem, PlaceElem, Spacing, VElem, }; use crate::meta::{FootnoteElem, FootnoteEntry}; use crate::prelude::*; use crate::visualize::{ CircleElem, EllipseElem, ImageElem, LineElem, PathElem, PolygonElem, RectElem, SquareElem, }; /// Arranges spacing, paragraphs and block-level elements into a flow. /// /// This element is responsible for layouting both the top-level content flow /// and the contents of boxes. /// /// Display: Flow /// Category: layout #[element(Layout)] pub struct FlowElem { /// The children that will be arranges into a flow. #[variadic] pub children: Vec, } impl Layout for FlowElem { #[tracing::instrument(name = "FlowElem::layout", skip_all)] fn layout( &self, vt: &mut Vt, styles: StyleChain, regions: Regions, ) -> SourceResult { let mut layouter = FlowLayouter::new(regions, styles); for mut child in &self.children() { let outer = styles; let mut styles = styles; if let Some((elem, map)) = child.to_styled() { child = elem; styles = outer.chain(map); } if let Some(elem) = child.to::() { layouter.layout_spacing(vt, elem, styles)?; } else if let Some(elem) = child.to::() { layouter.layout_par(vt, elem, styles)?; } else if child.is::() || child.is::() || child.is::() || child.is::() || child.is::() || child.is::() || child.is::() || child.is::() { let layoutable = child.with::().unwrap(); layouter.layout_single(vt, layoutable, styles)?; } else if child.is::() { let mut frame = Frame::new(Size::zero()); frame.meta(styles, true); layouter.items.push(FlowItem::Frame { frame, aligns: Axes::new(Align::Top, Align::Left), sticky: true, movable: false, }); } else if let Some(placed) = child.to::() { layouter.layout_placed(vt, placed, styles)?; } else if child.can::() { layouter.layout_multiple(vt, child, styles)?; } else if child.is::() { if !layouter.regions.backlog.is_empty() || layouter.regions.last.is_some() { layouter.finish_region(vt)?; } } else { bail!(child.span(), "unexpected flow child"); } } layouter.finish(vt) } } /// Performs flow layout. struct FlowLayouter<'a> { /// Whether this is the root flow. root: bool, /// The regions to layout children into. regions: Regions<'a>, /// The shared styles. styles: StyleChain<'a>, /// Whether the flow should expand to fill the region. expand: Axes, /// The initial size of `regions.size` that was available before we started /// subtracting. initial: Size, /// Whether the last block was a paragraph. last_was_par: bool, /// Spacing and layouted blocks for the current region. items: Vec, /// A queue of floats. pending_floats: Vec, /// Whether we have any footnotes in the current region. has_footnotes: bool, /// Footnote configuration. footnote_config: FootnoteConfig, /// Finished frames for previous regions. finished: Vec, } /// Cached footnote configuration. struct FootnoteConfig { separator: Content, clearance: Abs, gap: Abs, } /// A prepared item in a flow layout. #[derive(Debug)] enum FlowItem { /// Spacing between other items and whether it is weak. Absolute(Abs, bool), /// Fractional spacing between other items. Fractional(Fr), /// A frame for a layouted block, how to align it, whether it sticks to the /// item after it (for orphan prevention), and whether it is movable /// (to keep it together with its footnotes). Frame { frame: Frame, aligns: Axes, sticky: bool, movable: bool }, /// An absolutely placed frame. Placed { frame: Frame, x_align: Align, y_align: Smart>, delta: Axes>, float: bool, clearance: Abs, }, /// A footnote frame (can also be the separator). Footnote(Frame), } impl FlowItem { /// The inherent height of the item. fn height(&self) -> Abs { match self { Self::Absolute(v, _) => *v, Self::Fractional(_) | Self::Placed { .. } => Abs::zero(), Self::Frame { frame, .. } | Self::Footnote(frame) => frame.height(), } } } impl<'a> FlowLayouter<'a> { /// Create a new flow layouter. fn new(mut regions: Regions<'a>, styles: StyleChain<'a>) -> Self { let expand = regions.expand; // Disable vertical expansion & root for children. regions.expand.y = false; let root = mem::replace(&mut regions.root, false); Self { root, regions, styles, expand, initial: regions.size, last_was_par: false, items: vec![], pending_floats: vec![], has_footnotes: false, footnote_config: FootnoteConfig { separator: FootnoteEntry::separator_in(styles), clearance: FootnoteEntry::clearance_in(styles), gap: FootnoteEntry::gap_in(styles), }, finished: vec![], } } /// Layout vertical spacing. #[tracing::instrument(name = "FlowLayouter::layout_spacing", skip_all)] fn layout_spacing( &mut self, vt: &mut Vt, v: &VElem, styles: StyleChain, ) -> SourceResult<()> { self.layout_item( vt, 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), }, ) } /// Layout a paragraph. #[tracing::instrument(name = "FlowLayouter::layout_par", skip_all)] fn layout_par( &mut self, vt: &mut Vt, par: &ParElem, styles: StyleChain, ) -> SourceResult<()> { let aligns = AlignElem::alignment_in(styles).resolve(styles); let leading = ParElem::leading_in(styles); let consecutive = self.last_was_par; let lines = par .layout(vt, styles, consecutive, self.regions.base(), self.regions.expand.x)? .into_frames(); 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, } } if let Some(first) = lines.first() { if !self.regions.size.y.fits(first.height()) && !self.regions.in_last() { let carry: Vec<_> = self.items.drain(sticky..).collect(); self.finish_region(vt)?; for item in carry { self.layout_item(vt, item)?; } } } for (i, frame) in lines.into_iter().enumerate() { if i > 0 { self.layout_item(vt, FlowItem::Absolute(leading, true))?; } self.layout_item( vt, FlowItem::Frame { frame, aligns, sticky: false, movable: true }, )?; } self.last_was_par = true; Ok(()) } /// Layout into a single region. #[tracing::instrument(name = "FlowLayouter::layout_single", skip_all)] fn layout_single( &mut self, vt: &mut Vt, content: &dyn Layout, styles: StyleChain, ) -> SourceResult<()> { let aligns = AlignElem::alignment_in(styles).resolve(styles); let sticky = BlockElem::sticky_in(styles); let pod = Regions::one(self.regions.base(), Axes::splat(false)); let frame = content.layout(vt, styles, pod)?.into_frame(); self.layout_item(vt, FlowItem::Frame { frame, aligns, sticky, movable: true })?; self.last_was_par = false; Ok(()) } /// Layout a placed element. fn layout_placed( &mut self, vt: &mut Vt, placed: &PlaceElem, styles: StyleChain, ) -> SourceResult<()> { 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(Align::Center, |aligns| { aligns.x.unwrap_or(GenAlign::Start).resolve(styles) }); let y_align = alignment.map(|align| align.y.resolve(styles)); let frame = placed.layout(vt, styles, self.regions)?.into_frame(); let item = FlowItem::Placed { frame, x_align, y_align, delta, float, clearance }; self.layout_item(vt, item) } /// Layout into multiple regions. fn layout_multiple( &mut self, vt: &mut Vt, block: &Content, styles: StyleChain, ) -> SourceResult<()> { // Temporarily delegerate rootness to the columns. let is_root = self.root; if is_root && block.is::() { self.root = false; self.regions.root = true; } let mut notes = Vec::new(); if self.regions.is_full() { // Skip directly if region is already full. self.finish_region(vt)?; } // How to align the block. let aligns = if let Some(align) = block.to::() { align.alignment(styles) } else if let Some((_, local)) = block.to_styled() { AlignElem::alignment_in(styles.chain(local)) } else { AlignElem::alignment_in(styles) } .resolve(styles); // Layout the block itself. let sticky = BlockElem::sticky_in(styles); let fragment = block.layout(vt, styles, self.regions)?; for (i, frame) in fragment.into_iter().enumerate() { // Find footnotes in the frame. if self.root { find_footnotes(&mut notes, &frame); } if i > 0 { self.finish_region(vt)?; } let item = FlowItem::Frame { frame, aligns, sticky, movable: false }; self.layout_item(vt, item)?; } self.try_handle_footnotes(vt, notes)?; self.root = is_root; self.regions.root = false; self.last_was_par = false; Ok(()) } /// Layout a finished frame. #[tracing::instrument(name = "FlowLayouter::layout_item", skip_all)] fn layout_item(&mut self, vt: &mut Vt, mut item: FlowItem) -> SourceResult<()> { match item { FlowItem::Absolute(v, weak) => { if weak && !self .items .iter() .any(|item| matches!(item, FlowItem::Frame { .. })) { return Ok(()); } self.regions.size.y -= v } FlowItem::Fractional(_) => {} FlowItem::Frame { ref frame, movable, .. } => { let height = frame.height(); if !self.regions.size.y.fits(height) && !self.regions.in_last() { self.finish_region(vt)?; } self.regions.size.y -= height; if self.root && movable { let mut notes = Vec::new(); find_footnotes(&mut notes, frame); self.items.push(item); if !self.handle_footnotes(vt, &mut notes, true, false)? { let item = self.items.pop(); self.finish_region(vt)?; self.items.extend(item); self.regions.size.y -= height; self.handle_footnotes(vt, &mut notes, true, true)?; } return Ok(()); } } FlowItem::Placed { float: false, .. } => {} FlowItem::Placed { ref mut frame, ref mut y_align, float: true, clearance, .. } => { // If the float doesn't fit, queue it for the next region. if !self.regions.size.y.fits(frame.height() + clearance) && !self.regions.in_last() { self.pending_floats.push(item); return Ok(()); } // Select the closer placement, top or bottom. if y_align.is_auto() { let ratio = (self.regions.size.y - (frame.height() + clearance) / 2.0) / self.regions.full; let better_align = if ratio <= 0.5 { Align::Bottom } else { Align::Top }; *y_align = Smart::Custom(Some(better_align)); } // Add some clearance so that the float doesn't touch the main // content. frame.size_mut().y += clearance; if *y_align == Smart::Custom(Some(Align::Bottom)) { frame.translate(Point::with_y(clearance)); } self.regions.size.y -= frame.height(); // Find footnotes in the frame. if self.root { let mut notes = vec![]; find_footnotes(&mut notes, frame); self.try_handle_footnotes(vt, notes)?; } } FlowItem::Footnote(_) => {} } self.items.push(item); Ok(()) } /// Finish the frame for one region. fn finish_region(&mut self, vt: &mut Vt) -> SourceResult<()> { // Trim weak spacing. while self .items .last() .map_or(false, |item| matches!(item, FlowItem::Absolute(_, true))) { self.items.pop(); } // Determine the used size. let mut fr = Fr::zero(); let mut used = Size::zero(); let mut footnote_height = Abs::zero(); let mut float_top_height = Abs::zero(); let mut float_bottom_height = Abs::zero(); let mut first_footnote = true; for item in &self.items { match item { FlowItem::Absolute(v, _) => used.y += *v, FlowItem::Fractional(v) => fr += *v, FlowItem::Frame { frame, .. } => { used.y += frame.height(); used.x.set_max(frame.width()); } FlowItem::Placed { float: false, .. } => {} FlowItem::Placed { frame, float: true, y_align, .. } => match y_align { Smart::Custom(Some(Align::Top)) => float_top_height += frame.height(), Smart::Custom(Some(Align::Bottom)) => { float_bottom_height += frame.height() } _ => {} }, FlowItem::Footnote(frame) => { footnote_height += frame.height(); if !first_footnote { footnote_height += self.footnote_config.gap; } first_footnote = false; used.x.set_max(frame.width()); } } } used.y += footnote_height; // Determine the size of the flow in this region depending on whether // the region expands. Also account for fractional spacing and // footnotes. let mut size = self.expand.select(self.initial, used).min(self.initial); if (fr.get() > 0.0 || self.has_footnotes) && self.initial.y.is_finite() { size.y = self.initial.y; } let mut output = Frame::new(size); let mut ruler = Align::Top; let mut float_top_offset = Abs::zero(); let mut offset = float_top_height; let mut float_bottom_offset = Abs::zero(); let mut footnote_offset = Abs::zero(); // Place all frames. for item in self.items.drain(..) { match item { FlowItem::Absolute(v, _) => { offset += v; } FlowItem::Fractional(v) => { let remaining = self.initial.y - used.y; offset += v.share(fr, remaining); } FlowItem::Frame { frame, aligns, .. } => { ruler = ruler.max(aligns.y); let x = aligns.x.position(size.x - frame.width()); let y = offset + ruler.position(size.y - used.y); let pos = Point::new(x, y); offset += frame.height(); output.push_frame(pos, frame); } FlowItem::Placed { frame, x_align, y_align, delta, float, .. } => { let x = x_align.position(size.x - frame.width()); let y = if float { match y_align { Smart::Custom(Some(Align::Top)) => { let y = float_top_offset; float_top_offset += frame.height(); y } Smart::Custom(Some(Align::Bottom)) => { let y = size.y - footnote_height - float_bottom_height + float_bottom_offset; float_bottom_offset += frame.height(); y } _ => unreachable!("float must be y aligned"), } } else { match y_align { Smart::Custom(Some(align)) => { align.position(size.y - frame.height()) } _ => offset + ruler.position(size.y - used.y), } }; let pos = Point::new(x, y) + delta.zip(size).map(|(d, s)| d.relative_to(s)).to_point(); output.push_frame(pos, frame); } FlowItem::Footnote(frame) => { let y = size.y - footnote_height + footnote_offset; footnote_offset += frame.height() + self.footnote_config.gap; output.push_frame(Point::with_y(y), frame); } } } // Advance to the next region. self.finished.push(output); self.regions.next(); self.initial = self.regions.size; self.has_footnotes = false; // Try to place floats. for item in mem::take(&mut self.pending_floats) { self.layout_item(vt, item)?; } Ok(()) } /// Finish layouting and return the resulting fragment. fn finish(mut self, vt: &mut Vt) -> SourceResult { if self.expand.y { while !self.regions.backlog.is_empty() { self.finish_region(vt)?; } } self.finish_region(vt)?; while !self.items.is_empty() { self.finish_region(vt)?; } Ok(Fragment::frames(self.finished)) } } impl FlowLayouter<'_> { fn try_handle_footnotes( &mut self, vt: &mut Vt, mut notes: Vec, ) -> SourceResult<()> { if self.root && !self.handle_footnotes(vt, &mut notes, false, false)? { self.finish_region(vt)?; self.handle_footnotes(vt, &mut notes, false, true)?; } Ok(()) } /// Processes all footnotes in the frame. #[tracing::instrument(skip_all)] fn handle_footnotes( &mut self, vt: &mut Vt, notes: &mut Vec, movable: bool, force: bool, ) -> SourceResult { let items_len = self.items.len(); let notes_len = notes.len(); // Process footnotes one at a time. let mut k = 0; while k < notes.len() { if notes[k].is_ref() { k += 1; continue; } if !self.has_footnotes { self.layout_footnote_separator(vt)?; } self.regions.size.y -= self.footnote_config.gap; let frames = FootnoteEntry::new(notes[k].clone()) .pack() .layout(vt, self.styles, self.regions.with_root(false))? .into_frames(); // If the entries didn't fit, abort (to keep footnote and entry // together). if !force && (k == 0 || movable) && frames.first().map_or(false, Frame::is_empty) { // Remove existing footnotes attempts because we need to // move the item to the next page. notes.truncate(notes_len); // Undo region modifications. for item in self.items.drain(items_len..) { self.regions.size.y -= item.height(); } return Ok(false); } let prev = notes.len(); for (i, frame) in frames.into_iter().enumerate() { find_footnotes(notes, &frame); if i > 0 { self.finish_region(vt)?; self.layout_footnote_separator(vt)?; self.regions.size.y -= self.footnote_config.gap; } self.regions.size.y -= frame.height(); self.items.push(FlowItem::Footnote(frame)); } k += 1; // Process the nested notes before dealing with further top-level // notes. let nested = notes.len() - prev; if nested > 0 { notes[k..].rotate_right(nested); } } Ok(true) } /// Layout and save the footnote separator, typically a line. #[tracing::instrument(skip_all)] fn layout_footnote_separator(&mut self, vt: &mut Vt) -> SourceResult<()> { let expand = Axes::new(self.regions.expand.x, false); let pod = Regions::one(self.regions.base(), expand); let separator = &self.footnote_config.separator; let mut frame = separator.layout(vt, self.styles, pod)?.into_frame(); frame.size_mut().y += self.footnote_config.clearance; frame.translate(Point::with_y(self.footnote_config.clearance)); self.has_footnotes = true; self.regions.size.y -= frame.height(); self.items.push(FlowItem::Footnote(frame)); Ok(()) } } /// Finds all footnotes in the frame. #[tracing::instrument(skip_all)] fn find_footnotes(notes: &mut Vec, frame: &Frame) { for (_, item) in frame.items() { match item { FrameItem::Group(group) => find_footnotes(notes, &group.frame), FrameItem::Meta(Meta::Elem(content), _) if !notes.iter().any(|note| note.0.location() == content.location()) => { let Some(footnote) = content.to::() else { continue }; notes.push(footnote.clone()); } _ => {} } } }