use typst_library::introspection::Tag; use typst_library::layout::{ Abs, Axes, FixedAlignment, Fr, Frame, FrameItem, Point, Region, Regions, Rel, Size, }; use typst_utils::Numeric; use super::{ Child, Composer, FlowResult, LineChild, MultiChild, MultiSpill, PlacedChild, SingleChild, Stop, Work, }; /// Distributes as many children as fit from `composer.work` into the first /// region and returns the resulting frame. pub fn distribute(composer: &mut Composer, regions: Regions) -> FlowResult { let mut distributor = Distributor { composer, regions, items: vec![], sticky: None, stickable: None, }; let init = distributor.snapshot(); let forced = match distributor.run() { Ok(()) => distributor.composer.work.done(), Err(Stop::Finish(forced)) => forced, Err(err) => return Err(err), }; let region = Region::new(regions.size, regions.expand); distributor.finalize(region, init, forced) } /// State for distribution. /// /// See [Composer] regarding lifetimes. struct Distributor<'a, 'b, 'x, 'y, 'z> { /// The composer that is used to handle insertions. composer: &'z mut Composer<'a, 'b, 'x, 'y>, /// Regions which are continuously shrunk as new items are added. regions: Regions<'z>, /// Already laid out items, not yet aligned. items: Vec>, /// A snapshot which can be restored to migrate a suffix of sticky blocks to /// the next region. sticky: Option>, /// Whether the current group of consecutive sticky blocks are still sticky /// and may migrate with the attached frame. This is `None` while we aren't /// processing sticky blocks. On the first sticky block, this will become /// `Some(true)` if migrating sticky blocks as usual would make a /// difference - this is given by `regions.may_progress()`. Otherwise, it /// is set to `Some(false)`, which is usually the case when the first /// sticky block in the group is at the very top of the page (then, /// migrating it would just lead us back to the top of the page, leading /// to an infinite loop). In that case, all sticky blocks of the group are /// also disabled, until this is reset to `None` on the first non-sticky /// frame we find. /// /// While this behavior of disabling stickiness of sticky blocks at the /// very top of the page may seem non-ideal, it is only problematic (that /// is, may lead to orphaned sticky blocks / headings) if the combination /// of 'sticky blocks + attached frame' doesn't fit in one page, in which /// case there is nothing Typst can do to improve the situation, as sticky /// blocks are supposed to always be in the same page as the subsequent /// frame, but that is impossible in that case, which is thus pathological. stickable: Option, } /// A snapshot of the distribution state. struct DistributionSnapshot<'a, 'b> { work: Work<'a, 'b>, items: usize, } /// A laid out item in a distribution. enum Item<'a, 'b> { /// An introspection tag. Tag(&'a Tag), /// Absolute spacing and its weakness level. Abs(Abs, u8), /// Fractional spacing or a fractional block. Fr(Fr, Option<&'b SingleChild<'a>>), /// A frame for a laid out line or block. Frame(Frame, Axes), /// A frame for an absolutely (not floatingly) placed child. Placed(Frame, &'b PlacedChild<'a>), } impl Item<'_, '_> { /// Whether this item should be migrated to the next region if the region /// consists solely of such items. fn migratable(&self) -> bool { match self { Self::Tag(_) => true, Self::Frame(frame, _) => { frame.size().is_zero() && frame.items().all(|(_, item)| { matches!(item, FrameItem::Link(_, _) | FrameItem::Tag(_)) }) } Self::Placed(_, placed) => !placed.float, _ => false, } } } impl<'a, 'b> Distributor<'a, 'b, '_, '_, '_> { /// Distributes content into the region. fn run(&mut self) -> FlowResult<()> { // First, handle spill of a breakable block. if let Some(spill) = self.composer.work.spill.take() { self.multi_spill(spill)?; } // If spill are taken care of, process children until no space is left // or no children are left. while let Some(child) = self.composer.work.head() { self.child(child)?; self.composer.work.advance(); } Ok(()) } /// Processes a single child. /// /// - Returns `Ok(())` if the child was successfully processed. /// - Returns `Err(Stop::Finish)` if a region break should be triggered. /// - Returns `Err(Stop::Relayout(_))` if the region needs to be relayouted /// due to an insertion (float/footnote). /// - Returns `Err(Stop::Error(_))` if there was a fatal error. fn child(&mut self, child: &'b Child<'a>) -> FlowResult<()> { match child { Child::Tag(tag) => self.tag(tag), Child::Rel(amount, weakness) => self.rel(*amount, *weakness), Child::Fr(fr) => self.fr(*fr), Child::Line(line) => self.line(line)?, Child::Single(single) => self.single(single)?, Child::Multi(multi) => self.multi(multi)?, Child::Placed(placed) => self.placed(placed)?, Child::Flush => self.flush()?, Child::Break(weak) => self.break_(*weak)?, } Ok(()) } /// Processes a tag. fn tag(&mut self, tag: &'a Tag) { self.composer.work.tags.push(tag); } /// Generate items for pending tags. fn flush_tags(&mut self) { if !self.composer.work.tags.is_empty() { let tags = &mut self.composer.work.tags; self.items.extend(tags.iter().copied().map(Item::Tag)); tags.clear(); } } /// Processes relative spacing. fn rel(&mut self, amount: Rel, weakness: u8) { let amount = amount.relative_to(self.regions.base().y); if weakness > 0 && !self.keep_spacing(amount, weakness) { return; } self.regions.size.y -= amount; self.items.push(Item::Abs(amount, weakness)); } /// Processes fractional spacing. fn fr(&mut self, fr: Fr) { self.trim_spacing(); self.items.push(Item::Fr(fr, None)); } /// Decides whether to keep weak spacing based on previous items. If there /// is a preceding weak spacing, it might be patched in place. fn keep_spacing(&mut self, amount: Abs, weakness: u8) -> bool { for item in self.items.iter_mut().rev() { match *item { Item::Abs(prev_amount, prev_weakness @ 1..) => { if weakness <= prev_weakness && (weakness < prev_weakness || amount > prev_amount) { self.regions.size.y -= amount - prev_amount; *item = Item::Abs(amount, weakness); } return false; } Item::Tag(_) | Item::Abs(..) | Item::Placed(..) => {} Item::Fr(.., None) => return false, Item::Frame(..) | Item::Fr(.., Some(_)) => return true, } } false } /// Trims trailing weak spacing from the items. fn trim_spacing(&mut self) { for (i, item) in self.items.iter().enumerate().rev() { match *item { Item::Abs(amount, 1..) => { self.regions.size.y += amount; self.items.remove(i); break; } Item::Tag(_) | Item::Abs(..) | Item::Placed(..) => {} Item::Frame(..) | Item::Fr(..) => break, } } } /// The amount of trailing weak spacing. fn weak_spacing(&mut self) -> Abs { for item in self.items.iter().rev() { match *item { Item::Abs(amount, 1..) => return amount, Item::Tag(_) | Item::Abs(..) | Item::Placed(..) => {} Item::Frame(..) | Item::Fr(..) => break, } } Abs::zero() } /// Processes a line of a paragraph. fn line(&mut self, line: &'b LineChild) -> FlowResult<()> { // If the line doesn't fit and a followup region may improve things, // finish the region. if !self.regions.size.y.fits(line.frame.height()) && self.regions.may_progress() { return Err(Stop::Finish(false)); } // If the line's need, which includes its own height and that of // following lines grouped by widow/orphan prevention, does not fit into // the current region, but does fit into the next region, finish the // region. if !self.regions.size.y.fits(line.need) && self .regions .iter() .nth(1) .is_some_and(|region| region.y.fits(line.need)) { return Err(Stop::Finish(false)); } self.frame(line.frame.clone(), line.align, false, false) } /// Processes an unbreakable block. fn single(&mut self, single: &'b SingleChild<'a>) -> FlowResult<()> { // Lay out the block. let frame = single.layout( self.composer.engine, Region::new(self.regions.base(), self.regions.expand), )?; // Handle fractionally sized blocks. if let Some(fr) = single.fr { self.composer .footnotes(&self.regions, &frame, Abs::zero(), false, true)?; self.flush_tags(); self.items.push(Item::Fr(fr, Some(single))); return Ok(()); } // If the block doesn't fit and a followup region may improve things, // finish the region. if !self.regions.size.y.fits(frame.height()) && self.regions.may_progress() { return Err(Stop::Finish(false)); } self.frame(frame, single.align, single.sticky, false) } /// Processes a breakable block. fn multi(&mut self, multi: &'b MultiChild<'a>) -> FlowResult<()> { // Skip directly if the region is already (over)full. `line` and // `single` implicitly do this through their `fits` checks. if self.regions.is_full() { return Err(Stop::Finish(false)); } // Lay out the block. let (frame, spill) = multi.layout(self.composer.engine, self.regions)?; self.frame(frame, multi.align, multi.sticky, true)?; // If the block didn't fully fit into the current region, save it into // the `spill` and finish the region. if let Some(spill) = spill { self.composer.work.spill = Some(spill); self.composer.work.advance(); return Err(Stop::Finish(false)); } Ok(()) } /// Processes spillover from a breakable block. fn multi_spill(&mut self, spill: MultiSpill<'a, 'b>) -> FlowResult<()> { // Skip directly if the region is already (over)full. if self.regions.is_full() { self.composer.work.spill = Some(spill); return Err(Stop::Finish(false)); } // Lay out the spilled remains. let align = spill.align(); let (frame, spill) = spill.layout(self.composer.engine, self.regions)?; self.frame(frame, align, false, true)?; // If there's still more, save it into the `spill` and finish the // region. if let Some(spill) = spill { self.composer.work.spill = Some(spill); return Err(Stop::Finish(false)); } Ok(()) } /// Processes an in-flow frame, generated from a line or block. fn frame( &mut self, frame: Frame, align: Axes, sticky: bool, breakable: bool, ) -> FlowResult<()> { if sticky { // If the frame is sticky and we haven't remembered a preceding // sticky element, make a checkpoint which we can restore should we // end on this sticky element. // // The first sticky block within consecutive sticky blocks // determines whether this group of sticky blocks has stickiness // disabled or not. // // The criteria used here is: if migrating this group of sticky // blocks together with the "attached" block can't improve the lack // of space, since we're at the start of the region, then we don't // do so, and stickiness is disabled (at least, for this region). // Otherwise, migration is allowed. // // Note that, since the whole region is checked, this ensures sticky // blocks at the top of a block - but not necessarily of the page - // can still be migrated. if self.sticky.is_none() && *self.stickable.get_or_insert_with(|| self.regions.may_progress()) { self.sticky = Some(self.snapshot()); } } else if !frame.is_empty() { // If the frame isn't sticky, we can forget a previous snapshot. We // interrupt a group of sticky blocks, if there was one, so we reset // the saved stickable check for the next group of sticky blocks. self.sticky = None; self.stickable = None; } // Handle footnotes. self.composer.footnotes( &self.regions, &frame, frame.height(), breakable, true, )?; // Push an item for the frame. self.regions.size.y -= frame.height(); self.flush_tags(); self.items.push(Item::Frame(frame, align)); Ok(()) } /// Processes an absolutely or floatingly placed child. fn placed(&mut self, placed: &'b PlacedChild<'a>) -> FlowResult<()> { if placed.float { // If the element is floatingly placed, let the composer handle it. // It might require relayout because the area available for // distribution shrinks. We make the spacing occupied by weak // spacing temporarily available again because it can collapse if it // ends up at a break due to the float. let weak_spacing = self.weak_spacing(); self.regions.size.y += weak_spacing; self.composer.float( placed, &self.regions, self.items.iter().any(|item| matches!(item, Item::Frame(..))), true, )?; self.regions.size.y -= weak_spacing; } else { let frame = placed.layout(self.composer.engine, self.regions.base())?; self.composer .footnotes(&self.regions, &frame, Abs::zero(), true, true)?; self.flush_tags(); self.items.push(Item::Placed(frame, placed)); } Ok(()) } /// Processes a float flush. fn flush(&mut self) -> FlowResult<()> { // If there are still pending floats, finish the region instead of // adding more content to it. if !self.composer.work.floats.is_empty() { return Err(Stop::Finish(false)); } Ok(()) } /// Processes a column break. fn break_(&mut self, weak: bool) -> FlowResult<()> { // If there is a region to break into, break into it. if (!weak || !self.items.is_empty()) && (!self.regions.backlog.is_empty() || self.regions.last.is_some()) { self.composer.work.advance(); return Err(Stop::Finish(true)); } Ok(()) } /// Arranges the produced items into an output frame. /// /// This performs alignment and resolves fractional spacing and blocks. fn finalize( mut self, region: Region, init: DistributionSnapshot<'a, 'b>, forced: bool, ) -> FlowResult { if forced { // If this is the very end of the flow, flush pending tags. self.flush_tags(); } else if !self.items.is_empty() && self.items.iter().all(Item::migratable) { // Restore the initial state of all items are migratable. self.restore(init); } else { // If we ended on a sticky block, but are not yet at the end of // the flow, restore the saved checkpoint to move the sticky // suffix to the next region. if let Some(snapshot) = self.sticky.take() { self.restore(snapshot) } } self.trim_spacing(); let mut frs = Fr::zero(); let mut used = Size::zero(); let mut has_fr_child = false; // Determine the amount of used space and the sum of fractionals. for item in &self.items { match item { Item::Abs(v, _) => used.y += *v, Item::Fr(v, child) => { frs += *v; has_fr_child |= child.is_some(); } Item::Frame(frame, _) => { used.y += frame.height(); used.x.set_max(frame.width()); } Item::Tag(_) | Item::Placed(..) => {} } } // When we have fractional spacing, occupy the remaining space with it. let mut fr_space = Abs::zero(); if frs.get() > 0.0 && region.size.y.is_finite() { fr_space = region.size.y - used.y; used.y = region.size.y; } // Lay out fractionally sized blocks. let mut fr_frames = vec![]; if has_fr_child { for item in &self.items { let Item::Fr(v, Some(single)) = item else { continue }; let length = v.share(frs, fr_space); let pod = Region::new(Size::new(region.size.x, length), region.expand); let frame = single.layout(self.composer.engine, pod)?; used.x.set_max(frame.width()); fr_frames.push(frame); } } // Also consider the width of insertions for alignment. if !region.expand.x { used.x.set_max(self.composer.insertion_width()); } // Determine the region's size. let size = region.expand.select(region.size, used.min(region.size)); let free = size.y - used.y; let mut output = Frame::soft(size); let mut ruler = FixedAlignment::Start; let mut offset = Abs::zero(); let mut fr_frames = fr_frames.into_iter(); // Position all items. for item in self.items { match item { Item::Tag(tag) => { let y = offset + ruler.position(free); let pos = Point::with_y(y); output.push(pos, FrameItem::Tag(tag.clone())); } Item::Abs(v, _) => { offset += v; } Item::Fr(v, single) => { let length = v.share(frs, fr_space); if let Some(single) = single { let frame = fr_frames.next().unwrap(); let x = single.align.x.position(size.x - frame.width()); let pos = Point::new(x, offset); output.push_frame(pos, frame); } offset += length; } Item::Frame(frame, align) => { ruler = ruler.max(align.y); let x = align.x.position(size.x - frame.width()); let y = offset + ruler.position(free); let pos = Point::new(x, y); offset += frame.height(); output.push_frame(pos, frame); } Item::Placed(frame, placed) => { let x = placed.align_x.position(size.x - frame.width()); let y = match placed.align_y.unwrap_or_default() { Some(align) => align.position(size.y - frame.height()), _ => offset + ruler.position(free), }; let pos = Point::new(x, y) + placed.delta.zip_map(size, Rel::relative_to).to_point(); output.push_frame(pos, frame); } } } Ok(output) } /// Create a snapshot of the work and items. fn snapshot(&self) -> DistributionSnapshot<'a, 'b> { DistributionSnapshot { work: self.composer.work.clone(), items: self.items.len(), } } /// Restore a snapshot of the work and items. fn restore(&mut self, snapshot: DistributionSnapshot<'a, 'b>) { *self.composer.work = snapshot.work; self.items.truncate(snapshot.items); } }