diff options
Diffstat (limited to 'crates/typst-library/src/layout/flow.rs')
| -rw-r--r-- | crates/typst-library/src/layout/flow.rs | 714 |
1 files changed, 0 insertions, 714 deletions
diff --git a/crates/typst-library/src/layout/flow.rs b/crates/typst-library/src/layout/flow.rs deleted file mode 100644 index 17a39254..00000000 --- a/crates/typst-library/src/layout/flow.rs +++ /dev/null @@ -1,714 +0,0 @@ -use std::mem; - -use comemo::Prehashed; - -use crate::layout::{ - 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. -#[elem(Layout)] -pub struct FlowElem { - /// The children that will be arranges into a flow. - #[variadic] - pub children: Vec<Prehashed<Content>>, -} - -impl Layout for FlowElem { - #[tracing::instrument(name = "FlowElem::layout", skip_all)] - fn layout( - &self, - vt: &mut Vt, - styles: StyleChain, - regions: Regions, - ) -> SourceResult<Fragment> { - if !regions.size.x.is_finite() && regions.expand.x { - bail!(error!(self.span(), "cannot expand into infinite width")); - } - if !regions.size.y.is_finite() && regions.expand.y { - bail!(error!(self.span(), "cannot expand into infinite height")); - } - let mut layouter = FlowLayouter::new(regions, styles); - - for mut child in self.children().iter().map(|c| &**c) { - 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::soft(Size::zero()); - frame.meta(styles, true); - layouter.items.push(FlowItem::Frame { - frame, - align: Axes::splat(FixedAlign::Start), - sticky: true, - movable: false, - }); - } else if let Some(placed) = child.to::<PlaceElem>() { - layouter.layout_placed(vt, placed, styles)?; - } 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(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<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>, - /// A queue of floats. - pending_floats: 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, align: Axes<FixedAlign>, sticky: bool, movable: bool }, - /// An absolutely placed frame. - Placed { - frame: Frame, - x_align: FixedAlign, - y_align: Smart<Option<FixedAlign>>, - delta: Axes<Rel<Abs>>, - 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 align = 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, align, 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 align = 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, align, 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(FixedAlign::Center, |align| { - align.x().unwrap_or_default().resolve(styles) - }); - let y_align = alignment.map(|align| align.y().map(VAlign::fix)); - 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::<ColumnsElem>() { - 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 align = 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); - - // 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, align, 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 { FixedAlign::End } else { FixedAlign::Start }; - *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(FixedAlign::End)) { - 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(FixedAlign::Start)) => { - float_top_height += frame.height() - } - Smart::Custom(Some(FixedAlign::End)) => { - 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 + float_top_height + float_bottom_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::soft(size); - let mut ruler = FixedAlign::Start; - 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, align, .. } => { - ruler = ruler.max(align.y); - let x = align.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(FixedAlign::Start)) => { - let y = float_top_offset; - float_top_offset += frame.height(); - y - } - Smart::Custom(Some(FixedAlign::End)) => { - 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_map(size, Rel::relative_to).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<Fragment> { - 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<FootnoteElem>, - ) -> 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<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 checkpoint = vt.locator.clone(); - 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(); - } - - // Undo Vt modifications. - *vt.locator = checkpoint; - - 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<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.location() == content.location()) => - { - let Some(footnote) = content.to::<FootnoteElem>() else { continue }; - notes.push(footnote.clone()); - } - _ => {} - } - } -} |
