diff --git a/crates/typst/src/foundations/selector.rs b/crates/typst/src/foundations/selector.rs index 3a9ab3082..575baa134 100644 --- a/crates/typst/src/foundations/selector.rs +++ b/crates/typst/src/foundations/selector.rs @@ -10,7 +10,7 @@ use crate::foundations::{ cast, func, repr, scope, ty, CastInfo, Content, Context, Dict, Element, FromValue, Func, Label, Reflect, Regex, Repr, Str, StyleChain, Type, Value, }; -use crate::introspection::{Introspector, Locatable, Location}; +use crate::introspection::{Introspector, Locatable, Location, Unqueriable}; use crate::symbols::Symbol; /// A helper macro to create a field selector used in [`Selector::Elem`] @@ -339,7 +339,7 @@ impl FromValue for LocatableSelector { fn validate(selector: &Selector) -> StrResult<()> { match selector { Selector::Elem(elem, _) => { - if !elem.can::() { + if !elem.can::() || elem.can::() { Err(eco_format!("{} is not locatable", elem.name()))? } } diff --git a/crates/typst/src/introspection/counter.rs b/crates/typst/src/introspection/counter.rs index ba126e180..38da6363e 100644 --- a/crates/typst/src/introspection/counter.rs +++ b/crates/typst/src/introspection/counter.rs @@ -12,7 +12,7 @@ use crate::foundations::{ Element, Func, IntoValue, Label, LocatableSelector, NativeElement, Packed, Repr, Selector, Show, Smart, Str, StyleChain, Value, }; -use crate::introspection::{Introspector, Locatable, Location}; +use crate::introspection::{Introspector, Locatable, Location, Tag}; use crate::layout::{Frame, FrameItem, PageElem}; use crate::math::EquationElem; use crate::model::{FigureElem, FootnoteElem, HeadingElem, Numbering, NumberingPattern}; @@ -821,8 +821,8 @@ impl ManualPageCounter { for (_, item) in page.items() { match item { FrameItem::Group(group) => self.visit(engine, &group.frame)?, - FrameItem::Tag(tag) => { - let Some(elem) = tag.elem().to_packed::() else { + FrameItem::Tag(Tag::Start(elem)) => { + let Some(elem) = elem.to_packed::() else { continue; }; if *elem.key() == CounterKey::Page { diff --git a/crates/typst/src/introspection/introspector.rs b/crates/typst/src/introspection/introspector.rs index 453077689..113f9afe0 100644 --- a/crates/typst/src/introspection/introspector.rs +++ b/crates/typst/src/introspection/introspector.rs @@ -1,16 +1,15 @@ -use std::collections::{BTreeSet, HashMap}; +use std::collections::{BTreeSet, HashMap, HashSet}; use std::fmt::{self, Debug, Formatter}; use std::hash::Hash; use std::num::NonZeroUsize; use std::sync::RwLock; -use ecow::{eco_format, EcoVec}; -use indexmap::IndexMap; +use ecow::EcoVec; use smallvec::SmallVec; use crate::diag::{bail, StrResult}; use crate::foundations::{Content, Label, Repr, Selector}; -use crate::introspection::{Location, TagKind}; +use crate::introspection::{Location, Tag}; use crate::layout::{Frame, FrameItem, Page, Point, Position, Transform}; use crate::model::Numbering; use crate::utils::NonZeroExt; @@ -20,16 +19,20 @@ use crate::utils::NonZeroExt; pub struct Introspector { /// The number of pages in the document. pages: usize, - /// All introspectable elements. - elems: IndexMap, - /// Maps labels to their indices in the element list. We use a smallvec such - /// that if the label is unique, we don't need to allocate. - labels: HashMap>, - /// Maps from element keys to the locations of all elements that had this - /// key. Used for introspector-assisted location assignment. - keys: HashMap>, /// The page numberings, indexed by page number minus 1. page_numberings: Vec>, + + /// All introspectable elements. + elems: Vec, + /// Lists all elements with a specific hash key. This is used for + /// introspector-assisted location assignment during measurement. + keys: MultiMap, + + /// Accelerates lookup of elements by location. + locations: HashMap, + /// Accelerates lookup of elements by label. + labels: MultiMap, + /// Caches queries done on the introspector. This is important because /// even if all top-level queries are distinct, they often have shared /// subqueries. Example: Individual counter queries with `before` that @@ -37,81 +40,56 @@ pub struct Introspector { queries: QueryCache, } +/// A pair of content and its position. +type Pair = (Content, Position); + impl Introspector { - /// Applies new frames in-place, reusing the existing allocations. + /// Creates an introspector for a page list. #[typst_macros::time(name = "introspect")] - pub fn rebuild(&mut self, pages: &[Page]) { - self.pages = pages.len(); - self.elems.clear(); - self.labels.clear(); - self.keys.clear(); - self.page_numberings.clear(); - self.queries.clear(); - - for (i, page) in pages.iter().enumerate() { - let page_nr = NonZeroUsize::new(1 + i).unwrap(); - self.extract(&page.frame, page_nr, Transform::identity()); - self.page_numberings.push(page.numbering.clone()); - } + pub fn new(pages: &[Page]) -> Self { + IntrospectorBuilder::new().build(pages) } - /// Extract metadata from a frame. - fn extract(&mut self, frame: &Frame, page: NonZeroUsize, ts: Transform) { - for (pos, item) in frame.items() { - match item { - FrameItem::Group(group) => { - let ts = ts - .pre_concat(Transform::translate(pos.x, pos.y)) - .pre_concat(group.transform); - self.extract(&group.frame, page, ts); - } - FrameItem::Tag(tag) - if tag.kind() == TagKind::Start - && !self.elems.contains_key(&tag.location()) => - { - let pos = pos.transform(ts); - let loc = tag.location(); - let ret = self - .elems - .insert(loc, (tag.elem().clone(), Position { page, point: pos })); - assert!(ret.is_none(), "duplicate locations"); - - // Build the key map. - self.keys.entry(tag.key()).or_default().push(loc); - - // Build the label cache. - if let Some(label) = tag.elem().label() { - self.labels.entry(label).or_default().push(self.elems.len() - 1); - } - } - _ => {} - } - } - } - - /// Iterate over all locatable elements. + /// Iterates over all locatable elements. pub fn all(&self) -> impl Iterator + '_ { - self.elems.values().map(|(c, _)| c) + self.elems.iter().map(|(c, _)| c) } - /// Perform a binary search for `elem` among the `list`. + /// Retrieves the element with the given index. + #[track_caller] + fn get_by_idx(&self, idx: usize) -> &Content { + &self.elems[idx].0 + } + + /// Retrieves the position of the element with the given index. + #[track_caller] + fn get_pos_by_idx(&self, idx: usize) -> Position { + self.elems[idx].1 + } + + /// Retrieves an element by its location. + fn get_by_loc(&self, location: &Location) -> Option<&Content> { + self.locations.get(location).map(|&idx| self.get_by_idx(idx)) + } + + /// Retrieves the position of the element with the given index. + fn get_pos_by_loc(&self, location: &Location) -> Option { + self.locations.get(location).map(|&idx| self.get_pos_by_idx(idx)) + } + + /// Performs a binary search for `elem` among the `list`. fn binary_search(&self, list: &[Content], elem: &Content) -> Result { list.binary_search_by_key(&self.elem_index(elem), |elem| self.elem_index(elem)) } - /// Get an element by its location. - fn get(&self, location: &Location) -> Option<&Content> { - self.elems.get(location).map(|(elem, _)| elem) - } - - /// Get the index of this element among all. + /// Gets the index of this element. fn elem_index(&self, elem: &Content) -> usize { self.loc_index(&elem.location().unwrap()) } - /// Get the index of the element with this location among all. + /// Gets the index of the element with this location among all. fn loc_index(&self, location: &Location) -> usize { - self.elems.get_index_of(location).unwrap_or(usize::MAX) + self.locations.get(location).copied().unwrap_or(usize::MAX) } } @@ -125,20 +103,50 @@ impl Introspector { } let output = match selector { - Selector::Label(label) => self - .labels - .get(label) - .map(|indices| { - indices.iter().map(|&index| self.elems[index].0.clone()).collect() - }) - .unwrap_or_default(), - Selector::Elem(..) | Selector::Can(_) => self + Selector::Elem(..) => self .all() .filter(|elem| selector.matches(elem, None)) .cloned() .collect(), Selector::Location(location) => { - self.get(location).cloned().into_iter().collect() + self.get_by_loc(location).cloned().into_iter().collect() + } + Selector::Label(label) => self + .labels + .get(label) + .iter() + .map(|&idx| self.get_by_idx(idx).clone()) + .collect(), + Selector::Or(selectors) => selectors + .iter() + .flat_map(|sel| self.query(sel)) + .map(|elem| self.elem_index(&elem)) + .collect::>() + .into_iter() + .map(|idx| self.get_by_idx(idx).clone()) + .collect(), + Selector::And(selectors) => { + let mut results: Vec<_> = + selectors.iter().map(|sel| self.query(sel)).collect(); + + // Extract the smallest result list and then keep only those + // elements in the smallest list that are also in all other + // lists. + results + .iter() + .enumerate() + .min_by_key(|(_, vec)| vec.len()) + .map(|(i, _)| i) + .map(|i| results.swap_remove(i)) + .iter() + .flatten() + .filter(|candidate| { + results + .iter() + .all(|other| self.binary_search(other, candidate).is_ok()) + }) + .cloned() + .collect() } Selector::Before { selector, end, inclusive } => { let mut list = self.query(selector); @@ -168,39 +176,8 @@ impl Introspector { } list } - Selector::And(selectors) => { - let mut results: Vec<_> = - selectors.iter().map(|sel| self.query(sel)).collect(); - - // Extract the smallest result list and then keep only those - // elements in the smallest list that are also in all other - // lists. - results - .iter() - .enumerate() - .min_by_key(|(_, vec)| vec.len()) - .map(|(i, _)| i) - .map(|i| results.swap_remove(i)) - .iter() - .flatten() - .filter(|candidate| { - results - .iter() - .all(|other| self.binary_search(other, candidate).is_ok()) - }) - .cloned() - .collect() - } - Selector::Or(selectors) => selectors - .iter() - .flat_map(|sel| self.query(sel)) - .map(|elem| self.elem_index(&elem)) - .collect::>() - .into_iter() - .map(|index| self.elems[index].0.clone()) - .collect(), // Not supported here. - Selector::Regex(_) => EcoVec::new(), + Selector::Can(_) | Selector::Regex(_) => EcoVec::new(), }; self.queries.insert(hash, output.clone()); @@ -210,12 +187,12 @@ impl Introspector { /// Query for the first element that matches the selector. pub fn query_first(&self, selector: &Selector) -> Option { match selector { - Selector::Location(location) => self.get(location).cloned(), + Selector::Location(location) => self.get_by_loc(location).cloned(), Selector::Label(label) => self .labels .get(label) - .and_then(|indices| indices.first()) - .map(|&index| self.elems[index].0.clone()), + .first() + .map(|&idx| self.get_by_idx(idx).clone()), _ => self.query(selector).first().cloned(), } } @@ -224,7 +201,7 @@ impl Introspector { pub fn query_unique(&self, selector: &Selector) -> StrResult { match selector { Selector::Location(location) => self - .get(location) + .get_by_loc(location) .cloned() .ok_or_else(|| "element does not exist in the document".into()), Selector::Label(label) => self.query_label(*label).cloned(), @@ -243,15 +220,11 @@ impl Introspector { /// Query for a unique element with the label. pub fn query_label(&self, label: Label) -> StrResult<&Content> { - let indices = self.labels.get(&label).ok_or_else(|| { - eco_format!("label `{}` does not exist in the document", label.repr()) - })?; - - if indices.len() > 1 { - bail!("label `{}` occurs multiple times in the document", label.repr()); + match *self.labels.get(&label) { + [idx] => Ok(self.get_by_idx(idx)), + [] => bail!("label `{}` does not exist in the document", label.repr()), + _ => bail!("label `{}` occurs multiple times in the document", label.repr()), } - - Ok(&self.elems[indices[0]].0) } /// This is an optimized version of @@ -259,7 +232,7 @@ impl Introspector { pub fn query_count_before(&self, selector: &Selector, end: Location) -> usize { // See `query()` for details. let list = self.query(selector); - if let Some(end) = self.get(&end) { + if let Some(end) = self.get_by_loc(&end) { match self.binary_search(&list, end) { Ok(i) => i + 1, Err(i) => i, @@ -274,14 +247,6 @@ impl Introspector { NonZeroUsize::new(self.pages).unwrap_or(NonZeroUsize::ONE) } - /// Gets the page numbering for the given location, if any. - pub fn page_numbering(&self, location: Location) -> Option<&Numbering> { - let page = self.page(location); - self.page_numberings - .get(page.get() - 1) - .and_then(|slot| slot.as_ref()) - } - /// Find the page number for the given location. pub fn page(&self, location: Location) -> NonZeroUsize { self.position(location).page @@ -289,12 +254,18 @@ impl Introspector { /// Find the position for the given location. pub fn position(&self, location: Location) -> Position { - self.elems - .get(&location) - .map(|&(_, pos)| pos) + self.get_pos_by_loc(&location) .unwrap_or(Position { page: NonZeroUsize::ONE, point: Point::zero() }) } + /// Gets the page numbering for the given location, if any. + pub fn page_numbering(&self, location: Location) -> Option<&Numbering> { + let page = self.page(location); + self.page_numberings + .get(page.get() - 1) + .and_then(|slot| slot.as_ref()) + } + /// Try to find a location for an element with the given `key` hash /// that is closest after the `anchor`. /// @@ -304,7 +275,7 @@ impl Introspector { pub fn locator(&self, key: u128, anchor: Location) -> Option { let anchor = self.loc_index(&anchor); self.keys - .get(&key)? + .get(&key) .iter() .copied() .min_by_key(|loc| self.loc_index(loc).wrapping_sub(anchor)) @@ -317,6 +288,33 @@ impl Debug for Introspector { } } +/// A map from one keys to multiple elements. +#[derive(Clone)] +struct MultiMap(HashMap>); + +impl MultiMap +where + K: Hash + Eq, +{ + fn get(&self, key: &K) -> &[V] { + self.0.get(key).map_or(&[], |vec| vec.as_slice()) + } + + fn insert(&mut self, key: K, value: V) { + self.0.entry(key).or_default().push(value); + } + + fn take(&mut self, key: &K) -> Option> { + self.0.remove(key).map(|vec| vec.into_iter()) + } +} + +impl Default for MultiMap { + fn default() -> Self { + Self(HashMap::new()) + } +} + /// Caches queries. #[derive(Default)] struct QueryCache(RwLock>>); @@ -329,10 +327,6 @@ impl QueryCache { fn insert(&self, hash: u128, output: EcoVec) { self.0.write().unwrap().insert(hash, output); } - - fn clear(&mut self) { - self.0.get_mut().unwrap().clear(); - } } impl Clone for QueryCache { @@ -340,3 +334,120 @@ impl Clone for QueryCache { Self(RwLock::new(self.0.read().unwrap().clone())) } } + +/// Builds the introspector. +#[derive(Default)] +struct IntrospectorBuilder { + page_numberings: Vec>, + seen: HashSet, + insertions: MultiMap>, + keys: MultiMap, + locations: HashMap, + labels: MultiMap, +} + +impl IntrospectorBuilder { + /// Create an empty builder. + fn new() -> Self { + Self::default() + } + + /// Build the introspector. + fn build(mut self, pages: &[Page]) -> Introspector { + self.page_numberings.reserve(pages.len()); + + // Discover all elements. + let mut root = Vec::new(); + for (i, page) in pages.iter().enumerate() { + self.page_numberings.push(page.numbering.clone()); + self.discover( + &mut root, + &page.frame, + NonZeroUsize::new(1 + i).unwrap(), + Transform::identity(), + ); + } + + self.locations.reserve(self.seen.len()); + + // Save all pairs and their descendants in the correct order. + let mut elems = Vec::with_capacity(self.seen.len()); + for pair in root { + self.visit(&mut elems, pair); + } + + Introspector { + pages: pages.len(), + page_numberings: self.page_numberings, + elems, + keys: self.keys, + locations: self.locations, + labels: self.labels, + queries: QueryCache::default(), + } + } + + /// Processes the tags in the frame. + fn discover( + &mut self, + sink: &mut Vec, + frame: &Frame, + page: NonZeroUsize, + ts: Transform, + ) { + for (pos, item) in frame.items() { + match item { + FrameItem::Group(group) => { + let ts = ts + .pre_concat(Transform::translate(pos.x, pos.y)) + .pre_concat(group.transform); + + if let Some(parent) = group.parent { + let mut nested = vec![]; + self.discover(&mut nested, &group.frame, page, ts); + self.insertions.insert(parent, nested); + } else { + self.discover(sink, &group.frame, page, ts); + } + } + FrameItem::Tag(Tag::Start(elem)) => { + let loc = elem.location().unwrap(); + if self.seen.insert(loc) { + let point = pos.transform(ts); + sink.push((elem.clone(), Position { page, point })); + } + } + FrameItem::Tag(Tag::End(loc, key)) => { + self.keys.insert(*key, *loc); + } + _ => {} + } + } + } + + /// Saves a pair and all its descendants into `elems` and populates the + /// acceleration structures. + fn visit(&mut self, elems: &mut Vec, pair: Pair) { + let elem = &pair.0; + let loc = elem.location().unwrap(); + let idx = elems.len(); + + // Populate the location acceleration map. + self.locations.insert(loc, idx); + + // Populate the label acceleration map. + if let Some(label) = elem.label() { + self.labels.insert(label, idx); + } + + // Save the element. + elems.push(pair); + + // Process potential descendants. + if let Some(insertions) = self.insertions.take(&loc) { + for pair in insertions.flatten() { + self.visit(elems, pair); + } + } + } +} diff --git a/crates/typst/src/introspection/location.rs b/crates/typst/src/introspection/location.rs index 70076bcaa..8a7063fc1 100644 --- a/crates/typst/src/introspection/location.rs +++ b/crates/typst/src/introspection/location.rs @@ -105,5 +105,9 @@ impl Repr for Location { } } -/// Makes this element locatable through `engine.locate`. +/// Makes this element as locatable through the introspector. pub trait Locatable {} + +/// Marks this element as not being queryable even though it is locatable for +/// internal reasons. +pub trait Unqueriable {} diff --git a/crates/typst/src/introspection/tag.rs b/crates/typst/src/introspection/tag.rs index 7cdea4032..b2bae28e4 100644 --- a/crates/typst/src/introspection/tag.rs +++ b/crates/typst/src/introspection/tag.rs @@ -7,79 +7,48 @@ use crate::foundations::{ }; use crate::introspection::Location; -/// Holds a locatable element that was realized. +/// Marks the start or end of a locatable element. #[derive(Clone, PartialEq, Hash)] -pub struct Tag { - /// Whether this is a start or end tag. - kind: TagKind, - /// The introspectible element. - elem: Content, - /// The element's key hash. - key: u128, +pub enum Tag { + /// The stored element starts here. + /// + /// Content placed in a tag **must** have a [`Location`] or there will be + /// panics. + Start(Content), + /// The element with the given location and key hash ends here. + /// + /// Note: The key hash is stored here instead of in `Start` simply to make + /// the two enum variants more balanced in size, keeping a `Tag`'s memory + /// size down. There are no semantic reasons for this. + End(Location, u128), } impl Tag { - /// Create a start tag from an element and its key hash. - /// - /// Panics if the element does not have a [`Location`]. - #[track_caller] - pub fn new(elem: Content, key: u128) -> Self { - assert!(elem.location().is_some()); - Self { elem, key, kind: TagKind::Start } - } - - /// Returns the same tag with the given kind. - pub fn with_kind(self, kind: TagKind) -> Self { - Self { kind, ..self } - } - - /// Whether this is a start or end tag. - pub fn kind(&self) -> TagKind { - self.kind - } - - /// The locatable element that the tag holds. - pub fn elem(&self) -> &Content { - &self.elem - } - - /// Access the location of the element. + /// Access the location of the tag. pub fn location(&self) -> Location { - self.elem.location().unwrap() - } - - /// The element's key hash, which forms the base of its location (but is - /// locally disambiguated and combined with outer hashes). - /// - /// We need to retain this for introspector-assisted location assignment - /// during measurement. - pub fn key(&self) -> u128 { - self.key + match self { + Tag::Start(elem) => elem.location().unwrap(), + Tag::End(loc, _) => *loc, + } } } impl Debug for Tag { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "Tag({:?}, {:?})", self.kind, self.elem.elem().name()) + match self { + Tag::Start(elem) => write!(f, "Start({:?})", elem.elem().name()), + Tag::End(..) => f.pad("End"), + } } } -/// Determines whether a tag marks the start or end of an element. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] -pub enum TagKind { - /// The tag indicates that the element starts here. - Start, - /// The tag indicates that the element end here. - End, -} - /// Holds a tag for a locatable element that was realized. /// /// The `TagElem` is handled by all layouters. The held element becomes /// available for introspection in the next compiler iteration. #[elem(Construct, Unlabellable)] pub struct TagElem { - /// The introspectible element. + /// The introspectable element. #[required] #[internal] pub tag: Tag, diff --git a/crates/typst/src/layout/container.rs b/crates/typst/src/layout/container.rs index cc1559c66..d97edd5a9 100644 --- a/crates/typst/src/layout/container.rs +++ b/crates/typst/src/layout/container.rs @@ -190,7 +190,7 @@ impl Packed { // Assign label to the frame. if let Some(label) = self.label() { - frame.group(|group| group.label = Some(label)) + frame.label(label); } // Apply baseline shift. Do this after setting the size and applying the @@ -562,7 +562,7 @@ impl Packed { // Assign label to each frame in the fragment. if let Some(label) = self.label() { - frame.group(|group| group.label = Some(label)); + frame.label(label); } Ok(frame) @@ -723,7 +723,7 @@ impl Packed { // Assign label to each frame in the fragment. if let Some(label) = self.label() { for frame in fragment.iter_mut() { - frame.group(|group| group.label = Some(label)) + frame.label(label); } } diff --git a/crates/typst/src/layout/flow/collect.rs b/crates/typst/src/layout/flow/collect.rs index efb16427f..ffb45fda2 100644 --- a/crates/typst/src/layout/flow/collect.rs +++ b/crates/typst/src/layout/flow/collect.rs @@ -11,7 +11,7 @@ use crate::diag::{bail, SourceResult}; use crate::engine::{Engine, Route, Sink, Traced}; use crate::foundations::{Packed, Resolve, Smart, StyleChain}; use crate::introspection::{ - Introspector, Locator, LocatorLink, SplitLocator, Tag, TagElem, + Introspector, Location, Locator, LocatorLink, SplitLocator, Tag, TagElem, }; use crate::layout::{ layout_frame, Abs, AlignElem, Alignment, Axes, BlockElem, ColbreakElem, @@ -62,7 +62,7 @@ struct Collector<'a, 'x, 'y> { impl<'a> Collector<'a, '_, '_> { /// Perform the collection. fn run(mut self) -> SourceResult>> { - for (idx, &(child, styles)) in self.children.iter().enumerate() { + 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::() { @@ -72,7 +72,7 @@ impl<'a> Collector<'a, '_, '_> { } else if let Some(elem) = child.to_packed::() { self.block(elem, styles); } else if let Some(elem) = child.to_packed::() { - self.place(idx, elem, styles)?; + self.place(elem, styles)?; } else if child.is::() { self.output.push(Child::Flush); } else if let Some(elem) = child.to_packed::() { @@ -220,7 +220,6 @@ impl<'a> Collector<'a, '_, '_> { /// Collects a placed element into a [`PlacedChild`]. fn place( &mut self, - idx: usize, elem: &'a Packed, styles: StyleChain<'a>, ) -> SourceResult<()> { @@ -257,7 +256,6 @@ impl<'a> Collector<'a, '_, '_> { 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 { - idx, align_x, align_y, scope, @@ -553,7 +551,6 @@ impl MultiSpill<'_, '_> { /// A child that encapsulates a prepared placed element. #[derive(Debug)] pub struct PlacedChild<'a> { - pub idx: usize, pub align_x: FixedAlignment, pub align_y: Smart>, pub scope: PlacementScope, @@ -573,16 +570,27 @@ impl PlacedChild<'_> { self.cell.get_or_init(base, |base| { let align = self.alignment.unwrap_or_else(|| Alignment::CENTER); let aligned = AlignElem::set_alignment(align).wrap(); - layout_frame( + + let mut frame = layout_frame( engine, &self.elem.body, self.locator.relayout(), self.styles.chain(&aligned), Region::new(base, Axes::splat(false)), - ) - .map(|frame| frame.post_processed(self.styles)) + )?; + + 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. diff --git a/crates/typst/src/layout/flow/compose.rs b/crates/typst/src/layout/flow/compose.rs index 6f14618ee..3c52af382 100644 --- a/crates/typst/src/layout/flow/compose.rs +++ b/crates/typst/src/layout/flow/compose.rs @@ -1,18 +1,16 @@ use std::num::NonZeroUsize; -use super::{ - distribute, Config, FlowResult, LineNumberConfig, PlacedChild, Skip, Stop, Work, -}; +use super::{distribute, Config, FlowResult, LineNumberConfig, PlacedChild, Stop, Work}; use crate::diag::SourceResult; use crate::engine::Engine; use crate::foundations::{Content, NativeElement, Packed, Resolve, Smart}; use crate::introspection::{ - Counter, CounterDisplayElem, CounterState, CounterUpdate, Locator, SplitLocator, - TagKind, + Counter, CounterDisplayElem, CounterState, CounterUpdate, Location, Locator, + SplitLocator, Tag, }; use crate::layout::{ - layout_fragment, layout_frame, Abs, Axes, Dir, FixedAlignment, Frame, FrameItem, - OuterHAlignment, PlacementScope, Point, Region, Regions, Rel, Size, + layout_fragment, layout_frame, Abs, Axes, Dir, FixedAlignment, Fragment, Frame, + FrameItem, OuterHAlignment, PlacementScope, Point, Region, Regions, Rel, Size, }; use crate::model::{ FootnoteElem, FootnoteEntry, LineNumberingScope, Numbering, ParLineMarker, @@ -246,7 +244,8 @@ impl<'a, 'b> Composer<'a, 'b, '_, '_> { clearance: bool, ) -> FlowResult<()> { // If the float is already processed, skip it. - if self.skipped(Skip::Placed(placed.idx)) { + let loc = placed.location(); + if self.skipped(loc) { return Ok(()); } @@ -317,7 +316,7 @@ impl<'a, 'b> Composer<'a, 'b, '_, '_> { // Put the float there. area.push_float(placed, frame, align_y); - area.skips.push(Skip::Placed(placed.idx)); + area.skips.push(loc); // Trigger relayout. Err(Stop::Relayout(placed.scope)) @@ -391,7 +390,7 @@ impl<'a, 'b> Composer<'a, 'b, '_, '_> { ) -> FlowResult<()> { // Ignore reference footnotes and already processed ones. let loc = elem.location().unwrap(); - if elem.is_ref() || self.skipped(Skip::Footnote(loc)) { + if elem.is_ref() || self.skipped(loc) { return Ok(()); } @@ -420,14 +419,7 @@ impl<'a, 'b> Composer<'a, 'b, '_, '_> { pod.size.y -= flow_need + separator_need + self.config.footnote.gap; // Layout the footnote entry. - let frames = layout_fragment( - self.engine, - &FootnoteEntry::new(elem.clone()).pack(), - Locator::synthesize(elem.location().unwrap()), - self.config.shared, - pod, - )? - .into_frames(); + let frames = layout_footnote(self.engine, self.config, &elem, pod)?.into_frames(); // Find nested footnotes in the entry. let nested = find_in_frames::(&frames); @@ -458,7 +450,7 @@ impl<'a, 'b> Composer<'a, 'b, '_, '_> { // Save the footnote's frame. area.push_footnote(self.config, first); - area.skips.push(Skip::Footnote(loc)); + area.skips.push(loc); regions.size.y -= note_need; // Save the spill. @@ -501,10 +493,10 @@ impl<'a, 'b> Composer<'a, 'b, '_, '_> { /// Checks whether an insertion was already processed and doesn't need to be /// handled again. - fn skipped(&self, skip: Skip) -> bool { - self.work.skips.contains(&skip) - || self.page_insertions.skips.contains(&skip) - || self.column_insertions.skips.contains(&skip) + fn skipped(&self, loc: Location) -> bool { + self.work.skips.contains(&loc) + || self.page_insertions.skips.contains(&loc) + || self.column_insertions.skips.contains(&loc) } /// The amount of width needed by insertions. @@ -528,6 +520,29 @@ fn layout_footnote_separator( ) } +/// Lay out a footnote. +fn layout_footnote( + engine: &mut Engine, + config: &Config, + elem: &Packed, + pod: Regions, +) -> SourceResult { + let loc = elem.location().unwrap(); + layout_fragment( + engine, + &FootnoteEntry::new(elem.clone()).pack(), + Locator::synthesize(loc), + config.shared, + pod, + ) + .map(|mut fragment| { + for frame in &mut fragment { + frame.set_parent(loc); + } + fragment + }) +} + /// An additive list of insertions. #[derive(Default)] struct Insertions<'a, 'b> { @@ -538,7 +553,7 @@ struct Insertions<'a, 'b> { top_size: Abs, bottom_size: Abs, width: Abs, - skips: Vec, + skips: Vec, } impl<'a, 'b> Insertions<'a, 'b> { @@ -836,8 +851,8 @@ fn find_in_frame_impl( let y = y_offset + pos.y; match item { FrameItem::Group(group) => find_in_frame_impl(output, &group.frame, y), - FrameItem::Tag(tag) if tag.kind() == TagKind::Start => { - if let Some(elem) = tag.elem().to_packed::() { + FrameItem::Tag(Tag::Start(elem)) => { + if let Some(elem) = elem.to_packed::() { output.push((y, elem.clone())); } } diff --git a/crates/typst/src/layout/flow/distribute.rs b/crates/typst/src/layout/flow/distribute.rs index a738b3e67..eeb4e76f2 100644 --- a/crates/typst/src/layout/flow/distribute.rs +++ b/crates/typst/src/layout/flow/distribute.rs @@ -20,7 +20,7 @@ pub fn distribute(composer: &mut Composer, regions: Regions) -> FlowResult true, + Ok(()) => distributor.composer.work.done(), Err(Stop::Finish(forced)) => forced, Err(err) => return Err(err), }; diff --git a/crates/typst/src/layout/flow/mod.rs b/crates/typst/src/layout/flow/mod.rs index 5db70ecb2..66ec8e97c 100644 --- a/crates/typst/src/layout/flow/mod.rs +++ b/crates/typst/src/layout/flow/mod.rs @@ -255,18 +255,7 @@ struct Work<'a, 'b> { /// Identifies floats and footnotes that can be skipped if visited because /// they were already handled and incorporated as column or page level /// insertions. - skips: Rc>, -} - -/// Identifies an element that that can be skipped if visited because it was -/// already processed. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -enum Skip { - /// Uniquely identifies a placed elements. We can't use a [`Location`] - /// because `PlaceElem` is not currently locatable. - Placed(usize), - /// Uniquely identifies a footnote. - Footnote(Location), + skips: Rc>, } impl<'a, 'b> Work<'a, 'b> { @@ -304,7 +293,7 @@ impl<'a, 'b> Work<'a, 'b> { /// Add skipped floats and footnotes from the insertion areas to the skip /// set. - fn extend_skips(&mut self, skips: &[Skip]) { + fn extend_skips(&mut self, skips: &[Location]) { if !skips.is_empty() { Rc::make_mut(&mut self.skips).extend(skips.iter().copied()); } diff --git a/crates/typst/src/layout/frame.rs b/crates/typst/src/layout/frame.rs index 2f68e9361..cf4153d6d 100644 --- a/crates/typst/src/layout/frame.rs +++ b/crates/typst/src/layout/frame.rs @@ -7,7 +7,7 @@ use std::sync::Arc; use smallvec::SmallVec; use crate::foundations::{cast, dict, Dict, Label, StyleChain, Value}; -use crate::introspection::Tag; +use crate::introspection::{Location, Tag}; use crate::layout::{ Abs, Axes, Corners, FixedAlignment, HideElem, Length, Point, Rel, Sides, Size, Transform, @@ -407,8 +407,21 @@ impl Frame { } } + /// Add a label to the frame. + pub fn label(&mut self, label: Label) { + self.group(|g| g.label = Some(label)); + } + + /// Set a parent for the frame. As a result, all elements in the frame + /// become logically ordered immediately after the given location. + pub fn set_parent(&mut self, parent: Location) { + if !self.is_empty() { + self.group(|g| g.parent = Some(parent)); + } + } + /// Wrap the frame's contents in a group and modify that group with `f`. - pub fn group(&mut self, f: F) + fn group(&mut self, f: F) where F: FnOnce(&mut GroupItem), { @@ -557,6 +570,9 @@ pub struct GroupItem { pub clip_path: Option, /// The group's label. pub label: Option