diff options
| author | Laurenz <laurmaedje@gmail.com> | 2023-07-02 19:59:52 +0200 |
|---|---|---|
| committer | Laurenz <laurmaedje@gmail.com> | 2023-07-02 20:07:43 +0200 |
| commit | ebfdb1dafa430786db10dad2ef7d5467c1bdbed1 (patch) | |
| tree | 2bbc24ddb4124c4bb14dec0e536129d4de37b056 /crates/typst-library/src/layout/flow.rs | |
| parent | 3ab19185093d7709f824b95b979060ce125389d8 (diff) | |
Move everything into `crates/` directory
Diffstat (limited to 'crates/typst-library/src/layout/flow.rs')
| -rw-r--r-- | crates/typst-library/src/layout/flow.rs | 583 |
1 files changed, 583 insertions, 0 deletions
diff --git a/crates/typst-library/src/layout/flow.rs b/crates/typst-library/src/layout/flow.rs new file mode 100644 index 00000000..accd092a --- /dev/null +++ b/crates/typst-library/src/layout/flow.rs @@ -0,0 +1,583 @@ +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<Content>, +} + +impl Layout for FlowElem { + #[tracing::instrument(name = "FlowElem::layout", skip_all)] + fn layout( + &self, + vt: &mut Vt, + styles: StyleChain, + regions: Regions, + ) -> SourceResult<Fragment> { + 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::<VElem>() { + layouter.layout_spacing(vt, elem, styles)?; + } else if let Some(elem) = child.to::<ParElem>() { + layouter.layout_par(vt, elem, styles)?; + } else if child.is::<LineElem>() + || child.is::<RectElem>() + || child.is::<SquareElem>() + || child.is::<EllipseElem>() + || child.is::<CircleElem>() + || child.is::<ImageElem>() + || child.is::<PolygonElem>() + || child.is::<PathElem>() + { + let layoutable = child.with::<dyn Layout>().unwrap(); + layouter.layout_single(vt, layoutable, styles)?; + } else if child.is::<MetaElem>() { + 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 child.can::<dyn Layout>() { + layouter.layout_multiple(vt, child, styles)?; + } else if child.is::<ColbreakElem>() { + if !layouter.regions.backlog.is_empty() || layouter.regions.last.is_some() + { + layouter.finish_region()?; + } + } else { + bail!(child.span(), "unexpected flow child"); + } + } + + layouter.finish() + } +} + +/// 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<bool>, + /// 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<FlowItem>, + /// Whether we have any footnotes in the current region. + has_footnotes: bool, + /// Footnote configuration. + footnote_config: FootnoteConfig, + /// Finished frames for previous regions. + finished: Vec<Frame>, +} + +/// 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<Align>, sticky: bool, movable: bool }, + /// An absolutely placed frame. + Placed(Frame), + /// 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![], + 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()?; + 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 into multiple regions. + fn layout_multiple( + &mut self, + vt: &mut Vt, + block: &Content, + styles: StyleChain, + ) -> SourceResult<()> { + // Placed elements that are out of flow produce placed items which + // aren't aligned later. + if let Some(placed) = block.to::<PlaceElem>() { + if placed.out_of_flow(styles) { + let frame = block.layout(vt, styles, self.regions)?.into_frame(); + self.layout_item(vt, FlowItem::Placed(frame))?; + return Ok(()); + } + } else if self.regions.is_full() { + // Skip directly if region is already full. + self.finish_region()?; + } + + // How to align the block. + let aligns = if let Some(align) = block.to::<AlignElem>() { + align.alignment(styles) + } else if let Some((_, local)) = block.to_styled() { + AlignElem::alignment_in(styles.chain(local)) + } else { + AlignElem::alignment_in(styles) + } + .resolve(styles); + + // Temporarily delegerate rootness to the columns. + let is_root = self.root; + if is_root && block.is::<ColumnsElem>() { + self.root = false; + self.regions.root = true; + } + + // Layout the block itself. + let sticky = BlockElem::sticky_in(styles); + let fragment = block.layout(vt, styles, self.regions)?; + let mut notes = Vec::new(); + + 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()?; + } + + self.layout_item( + vt, + FlowItem::Frame { frame, aligns, sticky, movable: false }, + )?; + } + + if self.root && !self.handle_footnotes(vt, &mut notes, false, false)? { + self.finish_region()?; + self.handle_footnotes(vt, &mut notes, false, true)?; + } + + 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, 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 size = frame.size(); + if !self.regions.size.y.fits(size.y) && !self.regions.in_last() { + self.finish_region()?; + } + + self.regions.size.y -= size.y; + 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()?; + self.items.extend(item); + self.regions.size.y -= size.y; + self.handle_footnotes(vt, &mut notes, true, true)?; + } + return Ok(()); + } + } + FlowItem::Placed(_) => {} + FlowItem::Footnote(_) => {} + } + + self.items.push(item); + Ok(()) + } + + /// Finish the frame for one region. + fn finish_region(&mut self) -> 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 first_footnote = true; + for item in &self.items { + match item { + FlowItem::Absolute(v, _) => used.y += *v, + FlowItem::Fractional(v) => fr += *v, + FlowItem::Frame { frame, .. } => { + let size = frame.size(); + used.y += size.y; + used.x.set_max(size.x); + } + FlowItem::Placed(_) => {} + FlowItem::Footnote(frame) => { + let size = frame.size(); + footnote_height += size.y; + if !first_footnote { + footnote_height += self.footnote_config.gap; + } + first_footnote = false; + used.x.set_max(size.x); + } + } + } + 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 offset = Abs::zero(); + let mut ruler = Align::Top; + let mut footnote_offset = size.y - footnote_height; + + // 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::Footnote(frame) => { + let pos = Point::with_y(footnote_offset); + footnote_offset += frame.height() + self.footnote_config.gap; + output.push_frame(pos, frame); + } + FlowItem::Placed(frame) => { + output.push_frame(Point::zero(), frame); + } + } + } + + // Advance to the next region. + self.finished.push(output); + self.regions.next(); + self.initial = self.regions.size; + self.has_footnotes = false; + Ok(()) + } + + /// Finish layouting and return the resulting fragment. + fn finish(mut self) -> SourceResult<Fragment> { + if self.expand.y { + while !self.regions.backlog.is_empty() { + self.finish_region()?; + } + } + + self.finish_region()?; + Ok(Fragment::frames(self.finished)) + } +} + +impl FlowLayouter<'_> { + /// Processes all footnotes in the frame. + #[tracing::instrument(skip_all)] + fn handle_footnotes( + &mut self, + vt: &mut Vt, + notes: &mut Vec<FootnoteElem>, + movable: bool, + force: bool, + ) -> SourceResult<bool> { + 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()?; + 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<FootnoteElem>, 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::<FootnoteElem>() else { continue }; + notes.push(footnote.clone()); + } + _ => {} + } + } +} |
