use std::cell::{LazyCell, RefCell}; use std::fmt::{self, Debug, Formatter}; use std::hash::Hash; use bumpalo::boxed::Box as BumpBox; use bumpalo::Bump; use comemo::{Track, Tracked, TrackedMut}; use typst_library::diag::{bail, SourceResult}; use typst_library::engine::{Engine, Route, Sink, Traced}; use typst_library::foundations::{Packed, Resolve, Smart, StyleChain}; use typst_library::introspection::{ Introspector, Location, Locator, LocatorLink, SplitLocator, Tag, TagElem, }; use typst_library::layout::{ Abs, AlignElem, Alignment, Axes, BlockElem, ColbreakElem, FixedAlignment, FlushElem, Fr, Fragment, Frame, PagebreakElem, PlaceElem, PlacementScope, Ratio, Region, Regions, Rel, Size, Sizing, Spacing, VElem, }; use typst_library::model::ParElem; use typst_library::routines::{Pair, Routines}; use typst_library::text::TextElem; use typst_library::World; use super::{layout_multi_block, layout_single_block}; /// Collects all elements of the flow into prepared children. These are much /// simpler to handle than the raw elements. #[typst_macros::time] pub fn collect<'a>( engine: &mut Engine, bump: &'a Bump, children: &[Pair<'a>], locator: Locator<'a>, base: Size, expand: bool, ) -> SourceResult>> { Collector { engine, bump, children, locator: locator.split(), base, expand, output: Vec::with_capacity(children.len()), last_was_par: false, } .run() } /// State for collection. struct Collector<'a, 'x, 'y> { engine: &'x mut Engine<'y>, bump: &'a Bump, children: &'x [Pair<'a>], base: Size, expand: bool, locator: SplitLocator<'a>, output: Vec>, last_was_par: bool, } impl<'a> Collector<'a, '_, '_> { /// Perform the collection. fn run(mut self) -> SourceResult>> { for &(child, styles) in self.children { if let Some(elem) = child.to_packed::() { self.output.push(Child::Tag(&elem.tag)); } else if let Some(elem) = child.to_packed::() { self.v(elem, styles); } else if let Some(elem) = child.to_packed::() { self.par(elem, styles)?; } else if let Some(elem) = child.to_packed::() { self.block(elem, styles); } else if let Some(elem) = child.to_packed::() { self.place(elem, styles)?; } else if child.is::() { self.output.push(Child::Flush); } else if let Some(elem) = child.to_packed::() { self.output.push(Child::Break(elem.weak(styles))); } else if child.is::() { bail!( child.span(), "pagebreaks are not allowed inside of containers"; hint: "try using a `#colbreak()` instead", ); } else { bail!(child.span(), "{} is not allowed here", child.func().name()); } } Ok(self.output) } /// Collect vertical spacing into a relative or fractional child. fn v(&mut self, elem: &'a Packed, styles: StyleChain<'a>) { self.output.push(match elem.amount { Spacing::Rel(rel) => Child::Rel(rel.resolve(styles), elem.weak(styles) as u8), Spacing::Fr(fr) => Child::Fr(fr), }); } /// Collect a paragraph into [`LineChild`]ren. This already performs line /// layout since it is not dependent on the concrete regions. fn par( &mut self, elem: &'a Packed, styles: StyleChain<'a>, ) -> SourceResult<()> { let align = AlignElem::alignment_in(styles).resolve(styles); let leading = ParElem::leading_in(styles); let spacing = ParElem::spacing_in(styles); let costs = TextElem::costs_in(styles); let lines = crate::layout_inline( self.engine, &elem.children, self.locator.next(&elem.span()), styles, self.last_was_par, self.base, self.expand, )? .into_frames(); self.output.push(Child::Rel(spacing.into(), 4)); // Determine whether to prevent widow and orphans. let len = lines.len(); let prevent_orphans = costs.orphan() > Ratio::zero() && len >= 2 && !lines[1].is_empty(); let prevent_widows = costs.widow() > Ratio::zero() && len >= 2 && !lines[len - 2].is_empty(); let prevent_all = len == 3 && prevent_orphans && prevent_widows; // Store the heights of lines at the edges because we'll potentially // need these later when `lines` is already moved. let height_at = |i| lines.get(i).map(Frame::height).unwrap_or_default(); let front_1 = height_at(0); let front_2 = height_at(1); let back_2 = height_at(len.saturating_sub(2)); let back_1 = height_at(len.saturating_sub(1)); for (i, frame) in lines.into_iter().enumerate() { if i > 0 { self.output.push(Child::Rel(leading.into(), 5)); } // To prevent widows and orphans, we require enough space for // - all lines if it's just three // - the first two lines if we're at the first line // - the last two lines if we're at the second to last line let need = if prevent_all && i == 0 { front_1 + leading + front_2 + leading + back_1 } else if prevent_orphans && i == 0 { front_1 + leading + front_2 } else if prevent_widows && i >= 2 && i + 2 == len { back_2 + leading + back_1 } else { frame.height() }; self.output .push(Child::Line(self.boxed(LineChild { frame, align, need }))); } self.output.push(Child::Rel(spacing.into(), 4)); self.last_was_par = true; Ok(()) } /// Collect a block into a [`SingleChild`] or [`MultiChild`] depending on /// whether it is breakable. fn block(&mut self, elem: &'a Packed, styles: StyleChain<'a>) { let locator = self.locator.next(&elem.span()); let align = AlignElem::alignment_in(styles).resolve(styles); let alone = self.children.len() == 1; let sticky = elem.sticky(styles); let breakable = elem.breakable(styles); let fr = match elem.height(styles) { Sizing::Fr(fr) => Some(fr), _ => None, }; let fallback = LazyCell::new(|| ParElem::spacing_in(styles)); let spacing = |amount| match amount { Smart::Auto => Child::Rel((*fallback).into(), 4), Smart::Custom(Spacing::Rel(rel)) => Child::Rel(rel.resolve(styles), 3), Smart::Custom(Spacing::Fr(fr)) => Child::Fr(fr), }; self.output.push(spacing(elem.above(styles))); if !breakable || fr.is_some() { self.output.push(Child::Single(self.boxed(SingleChild { align, sticky, alone, fr, elem, styles, locator, cell: CachedCell::new(), }))); } else { self.output.push(Child::Multi(self.boxed(MultiChild { align, sticky, alone, elem, styles, locator, cell: CachedCell::new(), }))); }; self.output.push(spacing(elem.below(styles))); self.last_was_par = false; } /// Collects a placed element into a [`PlacedChild`]. fn place( &mut self, elem: &'a Packed, styles: StyleChain<'a>, ) -> SourceResult<()> { let alignment = elem.alignment(styles); let align_x = alignment.map_or(FixedAlignment::Center, |align| { align.x().unwrap_or_default().resolve(styles) }); let align_y = alignment.map(|align| align.y().map(|y| y.resolve(styles))); let scope = elem.scope(styles); let float = elem.float(styles); match (float, align_y) { (true, Smart::Custom(None | Some(FixedAlignment::Center))) => bail!( elem.span(), "vertical floating placement must be `auto`, `top`, or `bottom`" ), (false, Smart::Auto) => bail!( elem.span(), "automatic positioning is only available for floating placement"; hint: "you can enable floating placement with `place(float: true, ..)`" ), _ => {} } if !float && scope == PlacementScope::Parent { bail!( elem.span(), "parent-scoped positioning is currently only available for floating placement"; hint: "you can enable floating placement with `place(float: true, ..)`" ); } let locator = self.locator.next(&elem.span()); let clearance = elem.clearance(styles); let delta = Axes::new(elem.dx(styles), elem.dy(styles)).resolve(styles); self.output.push(Child::Placed(self.boxed(PlacedChild { align_x, align_y, scope, float, clearance, delta, elem, styles, locator, alignment, cell: CachedCell::new(), }))); Ok(()) } /// Wraps a value in a bump-allocated box to reduce its footprint in the /// [`Child`] enum. fn boxed(&self, value: T) -> BumpBox<'a, T> { BumpBox::new_in(value, self.bump) } } /// A prepared child in flow layout. /// /// The larger variants are bump-boxed to keep the enum size down. #[derive(Debug)] pub enum Child<'a> { /// An introspection tag. Tag(&'a Tag), /// Relative spacing with a specific weakness level. Rel(Rel, u8), /// Fractional spacing. Fr(Fr), /// An already layouted line of a paragraph. Line(BumpBox<'a, LineChild>), /// An unbreakable block. Single(BumpBox<'a, SingleChild<'a>>), /// A breakable block. Multi(BumpBox<'a, MultiChild<'a>>), /// An absolutely or floatingly placed element. Placed(BumpBox<'a, PlacedChild<'a>>), /// A place flush. Flush, /// An explicit column break. Break(bool), } /// A child that encapsulates a layouted line of a paragraph. #[derive(Debug)] pub struct LineChild { pub frame: Frame, pub align: Axes, pub need: Abs, } /// A child that encapsulates a prepared unbreakable block. #[derive(Debug)] pub struct SingleChild<'a> { pub align: Axes, pub sticky: bool, pub alone: bool, pub fr: Option, elem: &'a Packed, styles: StyleChain<'a>, locator: Locator<'a>, cell: CachedCell>, } impl SingleChild<'_> { /// Build the child's frame given the region's base size. pub fn layout(&self, engine: &mut Engine, region: Region) -> SourceResult { self.cell.get_or_init(region, |mut region| { // Vertical expansion is only kept if this block is the only child. region.expand.y &= self.alone; layout_single_impl( engine.routines, engine.world, engine.introspector, engine.traced, TrackedMut::reborrow_mut(&mut engine.sink), engine.route.track(), self.elem, self.locator.track(), self.styles, region, ) }) } } /// The cached, internal implementation of [`SingleChild::layout`]. #[comemo::memoize] #[allow(clippy::too_many_arguments)] fn layout_single_impl( routines: &Routines, world: Tracked, introspector: Tracked, traced: Tracked, sink: TrackedMut, route: Tracked, elem: &Packed, locator: Tracked, styles: StyleChain, region: Region, ) -> SourceResult { let link = LocatorLink::new(locator); let locator = Locator::link(&link); let mut engine = Engine { routines, world, introspector, traced, sink, route: Route::extend(route), }; layout_single_block(elem, &mut engine, locator, styles, region) .map(|frame| frame.post_processed(styles)) } /// A child that encapsulates a prepared breakable block. #[derive(Debug)] pub struct MultiChild<'a> { pub align: Axes, pub sticky: bool, alone: bool, elem: &'a Packed, styles: StyleChain<'a>, locator: Locator<'a>, cell: CachedCell>, } impl<'a> MultiChild<'a> { /// Build the child's frames given regions. pub fn layout<'b>( &'b self, engine: &mut Engine, regions: Regions, ) -> SourceResult<(Frame, Option>)> { let fragment = self.layout_full(engine, regions)?; // Extract the first frame. let mut frames = fragment.into_iter(); let frame = frames.next().unwrap(); // If there's more, return a `spill`. let mut spill = None; if frames.next().is_some() { spill = Some(MultiSpill { multi: self, full: regions.full, first: regions.size.y, backlog: vec![], min_backlog_len: regions.backlog.len(), }); } Ok((frame, spill)) } /// The shared internal implementation of [`Self::layout`] and /// [`MultiSpill::layout`]. fn layout_full( &self, engine: &mut Engine, regions: Regions, ) -> SourceResult { self.cell.get_or_init(regions, |mut regions| { // Vertical expansion is only kept if this block is the only child. regions.expand.y &= self.alone; layout_multi_impl( engine.routines, engine.world, engine.introspector, engine.traced, TrackedMut::reborrow_mut(&mut engine.sink), engine.route.track(), self.elem, self.locator.track(), self.styles, regions, ) }) } } /// The cached, internal implementation of [`MultiChild::layout_full`]. #[comemo::memoize] #[allow(clippy::too_many_arguments)] fn layout_multi_impl( routines: &Routines, world: Tracked, introspector: Tracked, traced: Tracked, sink: TrackedMut, route: Tracked, elem: &Packed, locator: Tracked, styles: StyleChain, regions: Regions, ) -> SourceResult { let link = LocatorLink::new(locator); let locator = Locator::link(&link); let mut engine = Engine { routines, world, introspector, traced, sink, route: Route::extend(route), }; layout_multi_block(elem, &mut engine, locator, styles, regions).map(|mut fragment| { for frame in &mut fragment { frame.post_process(styles); } fragment }) } /// The spilled remains of a `MultiChild` that broke across two regions. #[derive(Debug, Clone)] pub struct MultiSpill<'a, 'b> { multi: &'b MultiChild<'a>, first: Abs, full: Abs, backlog: Vec, min_backlog_len: usize, } impl MultiSpill<'_, '_> { /// Build the spill's frames given regions. pub fn layout( mut self, engine: &mut Engine, regions: Regions, ) -> SourceResult<(Frame, Option)> { // The first region becomes unchangable and committed to our backlog. self.backlog.push(regions.size.y); // The remaining regions are ephemeral and may be replaced. let mut backlog: Vec<_> = self.backlog.iter().chain(regions.backlog).copied().collect(); // Remove unnecessary backlog items to prevent it from growing // unnecessarily, changing the region's hash. while backlog.len() > self.min_backlog_len && backlog.last().copied() == regions.last { backlog.pop(); } // Build the pod with the merged regions. let pod = Regions { size: Size::new(regions.size.x, self.first), expand: regions.expand, full: self.full, backlog: &backlog, last: regions.last, }; // Extract the not-yet-processed frames. let mut frames = self .multi .layout_full(engine, pod)? .into_iter() .skip(self.backlog.len()); // Ensure that the backlog never shrinks, so that unwrapping below is at // least fairly safe. Note that the whole region juggling here is // fundamentally not ideal: It is a compatibility layer between the old // (all regions provided upfront) & new (each region provided on-demand, // like an iterator) layout model. This approach is not 100% correct, as // in the old model later regions could have an effect on earlier // frames, but it's the best we can do for now, until the multi // layouters are refactored to the new model. self.min_backlog_len = self.min_backlog_len.max(backlog.len()); // Save the first frame. let frame = frames.next().unwrap(); // If there's more, return a `spill`. let mut spill = None; if frames.next().is_some() { spill = Some(self); } Ok((frame, spill)) } /// The alignment of the breakable block. pub fn align(&self) -> Axes { self.multi.align } } /// A child that encapsulates a prepared placed element. #[derive(Debug)] pub struct PlacedChild<'a> { pub align_x: FixedAlignment, pub align_y: Smart>, pub scope: PlacementScope, pub float: bool, pub clearance: Abs, pub delta: Axes>, elem: &'a Packed, styles: StyleChain<'a>, locator: Locator<'a>, alignment: Smart, cell: CachedCell>, } impl PlacedChild<'_> { /// Build the child's frame given the region's base size. pub fn layout(&self, engine: &mut Engine, base: Size) -> SourceResult { self.cell.get_or_init(base, |base| { let align = self.alignment.unwrap_or_else(|| Alignment::CENTER); let aligned = AlignElem::set_alignment(align).wrap(); let mut frame = crate::layout_frame( engine, &self.elem.body, self.locator.relayout(), self.styles.chain(&aligned), Region::new(base, Axes::splat(false)), )?; if self.float { frame.set_parent(self.elem.location().unwrap()); } Ok(frame.post_processed(self.styles)) }) } /// The element's location. pub fn location(&self) -> Location { self.elem.location().unwrap() } } /// Wraps a parameterized computation and caches its latest output. /// /// - When the computation is performed multiple times consecutively with the /// same argument, reuses the cache. /// - When the argument changes, the new output is cached. #[derive(Clone)] struct CachedCell(RefCell>); impl CachedCell { /// Create an empty cached cell. fn new() -> Self { Self(RefCell::new(None)) } /// Perform the computation `f` with caching. fn get_or_init(&self, input: I, f: F) -> T where I: Hash, T: Clone, F: FnOnce(I) -> T, { let input_hash = typst_utils::hash128(&input); let mut slot = self.0.borrow_mut(); if let Some((hash, output)) = &*slot { if *hash == input_hash { return output.clone(); } } let output = f(input); *slot = Some((input_hash, output.clone())); output } } impl Default for CachedCell { fn default() -> Self { Self::new() } } impl Debug for CachedCell { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.pad("CachedCell(..)") } }