From f91cad7d7829556e24d219e55db7da56a966523f Mon Sep 17 00:00:00 2001 From: Laurenz Date: Sun, 9 Jun 2024 15:23:56 +0200 Subject: [PATCH] Pure location assignment (#4352) --- crates/typst-ide/src/analyze.rs | 4 +- crates/typst-timing/src/lib.rs | 2 + crates/typst-utils/src/hash.rs | 19 +- crates/typst/src/diag.rs | 3 +- crates/typst/src/engine.rs | 8 +- crates/typst/src/eval/call.rs | 5 +- crates/typst/src/eval/mod.rs | 6 +- crates/typst/src/foundations/content.rs | 4 +- crates/typst/src/foundations/func.rs | 1 - crates/typst/src/introspection/counter.rs | 10 +- .../typst/src/introspection/introspector.rs | 65 +-- crates/typst/src/introspection/location.rs | 32 +- crates/typst/src/introspection/locator.rs | 421 ++++++++++++++---- crates/typst/src/introspection/mod.rs | 37 +- crates/typst/src/introspection/state.rs | 6 +- crates/typst/src/layout/columns.rs | 9 +- crates/typst/src/layout/container.rs | 27 +- crates/typst/src/layout/flow.rs | 68 ++- crates/typst/src/layout/frame.rs | 10 +- crates/typst/src/layout/grid/cells.rs | 72 ++- crates/typst/src/layout/grid/layout.rs | 116 +++-- crates/typst/src/layout/grid/lines.rs | 11 +- crates/typst/src/layout/grid/mod.rs | 13 +- crates/typst/src/layout/grid/repeated.rs | 42 +- crates/typst/src/layout/grid/rowspans.rs | 50 ++- crates/typst/src/layout/inline/mod.rs | 44 +- crates/typst/src/layout/layout.rs | 34 +- crates/typst/src/layout/measure.rs | 22 +- crates/typst/src/layout/mod.rs | 47 +- crates/typst/src/layout/pad.rs | 8 +- crates/typst/src/layout/page.rs | 13 +- crates/typst/src/layout/place.rs | 4 +- crates/typst/src/layout/repeat.rs | 9 +- crates/typst/src/layout/stack.rs | 30 +- crates/typst/src/layout/transform.rs | 29 +- crates/typst/src/lib.rs | 4 +- crates/typst/src/math/ctx.rs | 27 +- crates/typst/src/math/equation.rs | 22 +- crates/typst/src/math/mod.rs | 6 +- crates/typst/src/model/cite.rs | 15 +- crates/typst/src/model/document.rs | 12 +- crates/typst/src/model/enum.rs | 18 +- crates/typst/src/model/heading.rs | 17 +- crates/typst/src/model/list.rs | 19 +- crates/typst/src/model/par.rs | 3 + crates/typst/src/model/table.rs | 13 +- crates/typst/src/realize/mod.rs | 21 +- crates/typst/src/realize/process.rs | 21 +- crates/typst/src/visualize/image/mod.rs | 5 +- crates/typst/src/visualize/line.rs | 6 +- crates/typst/src/visualize/path.rs | 6 +- crates/typst/src/visualize/pattern.rs | 4 +- crates/typst/src/visualize/polygon.rs | 6 +- crates/typst/src/visualize/shape.rs | 156 ++++--- tests/ref/issue-2480-counter-reset-2.png | Bin 0 -> 576 bytes tests/ref/issue-2480-counter-reset.png | Bin 0 -> 355 bytes tests/ref/measure-citation-deeply-nested.png | Bin 0 -> 706 bytes tests/ref/measure-citation-in-flow.png | Bin 0 -> 743 bytes tests/ref/measure-counter-multiple-times.png | Bin 0 -> 471 bytes tests/ref/measure-counter-width.png | Bin 0 -> 2754 bytes tests/ref/table-contextual-measurement.png | Bin 0 -> 453 bytes tests/ref/table-header-citation.png | Bin 0 -> 624 bytes tests/ref/table-header-counter.png | Bin 0 -> 359 bytes tests/ref/table-header-footer-madness.png | Bin 0 -> 613 bytes tests/suite/introspection/counter.typ | 28 ++ tests/suite/layout/measure.typ | 72 +++ tests/suite/layout/table.typ | 49 ++ 67 files changed, 1287 insertions(+), 524 deletions(-) create mode 100644 tests/ref/issue-2480-counter-reset-2.png create mode 100644 tests/ref/issue-2480-counter-reset.png create mode 100644 tests/ref/measure-citation-deeply-nested.png create mode 100644 tests/ref/measure-citation-in-flow.png create mode 100644 tests/ref/measure-counter-multiple-times.png create mode 100644 tests/ref/measure-counter-width.png create mode 100644 tests/ref/table-contextual-measurement.png create mode 100644 tests/ref/table-header-citation.png create mode 100644 tests/ref/table-header-counter.png create mode 100644 tests/ref/table-header-footer-madness.png diff --git a/crates/typst-ide/src/analyze.rs b/crates/typst-ide/src/analyze.rs index 748970b83..d27ce1766 100644 --- a/crates/typst-ide/src/analyze.rs +++ b/crates/typst-ide/src/analyze.rs @@ -3,7 +3,7 @@ use ecow::{eco_vec, EcoString, EcoVec}; use typst::engine::{Engine, Route}; use typst::eval::{Tracer, Vm}; use typst::foundations::{Context, Label, Scopes, Styles, Value}; -use typst::introspection::{Introspector, Locator}; +use typst::introspection::Introspector; use typst::model::{BibliographyElem, Document}; use typst::syntax::{ast, LinkedNode, Span, SyntaxKind}; use typst::World; @@ -58,14 +58,12 @@ pub fn analyze_import(world: &dyn World, source: &LinkedNode) -> Option { return Some(source); } - let mut locator = Locator::default(); let introspector = Introspector::default(); let mut tracer = Tracer::new(); let engine = Engine { world: world.track(), route: Route::default(), introspector: introspector.track(), - locator: &mut locator, tracer: tracer.track_mut(), }; diff --git a/crates/typst-timing/src/lib.rs b/crates/typst-timing/src/lib.rs index a972cf729..4e711d5fb 100644 --- a/crates/typst-timing/src/lib.rs +++ b/crates/typst-timing/src/lib.rs @@ -59,6 +59,8 @@ enum EventKind { /// Enable the timer. #[inline] pub fn enable() { + // We only need atomicity and no synchronization of other + // operations, so `Relaxed` is fine. ENABLED.store(true, Relaxed); } diff --git a/crates/typst-utils/src/hash.rs b/crates/typst-utils/src/hash.rs index 82bb76aff..dabae24c2 100644 --- a/crates/typst-utils/src/hash.rs +++ b/crates/typst-utils/src/hash.rs @@ -2,8 +2,9 @@ use std::any::Any; use std::fmt::{self, Debug}; use std::hash::{Hash, Hasher}; use std::ops::{Deref, DerefMut}; +use std::sync::atomic::Ordering; -use portable_atomic::{AtomicU128, Ordering}; +use portable_atomic::AtomicU128; use siphasher::sip128::{Hasher128, SipHasher13}; /// A wrapper type with lazily-computed hash. @@ -70,7 +71,9 @@ impl LazyHash { /// Get the hash, returns zero if not computed yet. #[inline] fn load_hash(&self) -> u128 { - self.hash.load(Ordering::SeqCst) + // We only need atomicity and no synchronization of other operations, so + // `Relaxed` is fine. + self.hash.load(Ordering::Relaxed) } } @@ -78,20 +81,18 @@ impl LazyHash { /// Get the hash or compute it if not set yet. #[inline] fn load_or_compute_hash(&self) -> u128 { - let hash = self.load_hash(); + let mut hash = self.load_hash(); if hash == 0 { - let hashed = hash_item(&self.value); - self.hash.store(hashed, Ordering::SeqCst); - hashed - } else { - hash + hash = hash_item(&self.value); + self.hash.store(hash, Ordering::Relaxed); } + hash } /// Reset the hash to zero. #[inline] fn reset_hash(&mut self) { - // Because we have a mutable reference, we can skip the atomic + // Because we have a mutable reference, we can skip the atomic. *self.hash.get_mut() = 0; } } diff --git a/crates/typst/src/diag.rs b/crates/typst/src/diag.rs index 15e6a49fa..db8a93fd8 100644 --- a/crates/typst/src/diag.rs +++ b/crates/typst/src/diag.rs @@ -366,13 +366,12 @@ where } } -impl At for Result { +impl At for HintedStrResult { fn at(self, span: Span) -> SourceResult { self.map_err(|err| { let mut components = err.0.into_iter(); let message = components.next().unwrap(); let diag = SourceDiagnostic::error(span, message).with_hints(components); - eco_vec![diag] }) } diff --git a/crates/typst/src/engine.rs b/crates/typst/src/engine.rs index 03e4957ce..a57a8f1ca 100644 --- a/crates/typst/src/engine.rs +++ b/crates/typst/src/engine.rs @@ -6,7 +6,7 @@ use comemo::{Track, Tracked, TrackedMut, Validate}; use crate::diag::SourceResult; use crate::eval::Tracer; -use crate::introspection::{Introspector, Locator}; +use crate::introspection::Introspector; use crate::syntax::FileId; use crate::World; @@ -19,8 +19,6 @@ pub struct Engine<'a> { /// The route the engine took during compilation. This is used to detect /// cyclic imports and excessive nesting. pub route: Route<'a>, - /// Provides stable identities to elements. - pub locator: &'a mut Locator<'a>, /// The tracer for inspection of the values an expression produces. pub tracer: TrackedMut<'a, Tracer>, } @@ -148,6 +146,8 @@ impl<'a> Route<'a> { /// Whether the route's depth is less than or equal to the given depth. pub fn within(&self, depth: usize) -> bool { + // We only need atomicity and no synchronization of other operations, so + // `Relaxed` is fine. use Ordering::Relaxed; let upper = self.upper.load(Relaxed); @@ -183,8 +183,6 @@ impl Clone for Route<'_> { outer: self.outer, id: self.id, len: self.len, - // The ordering doesn't really matter since it's the upper bound - // is only an optimization. upper: AtomicUsize::new(self.upper.load(Ordering::Relaxed)), } } diff --git a/crates/typst/src/eval/call.rs b/crates/typst/src/eval/call.rs index 61e5f318b..d4b81f029 100644 --- a/crates/typst/src/eval/call.rs +++ b/crates/typst/src/eval/call.rs @@ -8,7 +8,7 @@ use crate::foundations::{ call_method_mut, is_mutating_method, Arg, Args, Bytes, Capturer, Closure, Content, Context, Func, IntoValue, NativeElement, Scope, Scopes, Value, }; -use crate::introspection::{Introspector, Locator}; +use crate::introspection::Introspector; use crate::math::{Accent, AccentElem, LrElem}; use crate::symbols::Symbol; use crate::syntax::ast::{self, AstNode}; @@ -276,7 +276,6 @@ pub(crate) fn call_closure( world: Tracked, introspector: Tracked, route: Tracked, - locator: Tracked, tracer: TrackedMut, context: Tracked, mut args: Args, @@ -292,12 +291,10 @@ pub(crate) fn call_closure( scopes.top = closure.captured.clone(); // Prepare the engine. - let mut locator = Locator::chained(locator); let engine = Engine { world, introspector, route: Route::extend(route), - locator: &mut locator, tracer, }; diff --git a/crates/typst/src/eval/mod.rs b/crates/typst/src/eval/mod.rs index 3622c0127..2e5eeafd0 100644 --- a/crates/typst/src/eval/mod.rs +++ b/crates/typst/src/eval/mod.rs @@ -28,7 +28,7 @@ use comemo::{Track, Tracked, TrackedMut}; use crate::diag::{bail, SourceResult}; use crate::engine::{Engine, Route}; use crate::foundations::{Cast, Context, Module, NativeElement, Scope, Scopes, Value}; -use crate::introspection::{Introspector, Locator}; +use crate::introspection::Introspector; use crate::math::EquationElem; use crate::syntax::{ast, parse, parse_code, parse_math, Source, Span}; use crate::World; @@ -49,13 +49,11 @@ pub fn eval( } // Prepare the engine. - let mut locator = Locator::new(); let introspector = Introspector::default(); let engine = Engine { world, route: Route::extend(route).with_id(id), introspector: introspector.track(), - locator: &mut locator, tracer, }; @@ -118,13 +116,11 @@ pub fn eval_string( // Prepare the engine. let mut tracer = Tracer::new(); - let mut locator = Locator::new(); let introspector = Introspector::default(); let engine = Engine { world, introspector: introspector.track(), route: Route::default(), - locator: &mut locator, tracer: tracer.track_mut(), }; diff --git a/crates/typst/src/foundations/content.rs b/crates/typst/src/foundations/content.rs index 4cae8db7e..8a3252fbc 100644 --- a/crates/typst/src/foundations/content.rs +++ b/crates/typst/src/foundations/content.rs @@ -18,7 +18,7 @@ use crate::foundations::{ NativeElement, Recipe, RecipeIndex, Repr, Selector, Str, Style, StyleChain, Styles, Value, }; -use crate::introspection::{Location, TagElem}; +use crate::introspection::Location; use crate::layout::{AlignElem, Alignment, Axes, Length, MoveElem, PadElem, Rel, Sides}; use crate::model::{Destination, EmphElem, LinkElem, StrongElem}; use crate::realize::{Behave, Behaviour}; @@ -494,7 +494,7 @@ impl Content { pub fn backlinked(self, loc: Location) -> Self { let mut backlink = Content::empty().spanned(self.span()); backlink.set_location(loc); - TagElem::packed(backlink) + self + self } /// Set alignments for this content. diff --git a/crates/typst/src/foundations/func.rs b/crates/typst/src/foundations/func.rs index 21ded1f2a..f5b4b3ca0 100644 --- a/crates/typst/src/foundations/func.rs +++ b/crates/typst/src/foundations/func.rs @@ -298,7 +298,6 @@ impl Func { engine.world, engine.introspector, engine.route.track(), - engine.locator.track(), TrackedMut::reborrow_mut(&mut engine.tracer), context, args, diff --git a/crates/typst/src/introspection/counter.rs b/crates/typst/src/introspection/counter.rs index 2a7e9ea7d..df16026fe 100644 --- a/crates/typst/src/introspection/counter.rs +++ b/crates/typst/src/introspection/counter.rs @@ -13,7 +13,7 @@ use crate::foundations::{ Element, Func, IntoValue, Label, LocatableSelector, NativeElement, Packed, Repr, Selector, Show, Smart, Str, StyleChain, Value, }; -use crate::introspection::{Introspector, Locatable, Location, Locator}; +use crate::introspection::{Introspector, Locatable, Location}; use crate::layout::{Frame, FrameItem, PageElem}; use crate::math::EquationElem; use crate::model::{FigureElem, HeadingElem, Numbering, NumberingPattern}; @@ -282,7 +282,6 @@ impl Counter { engine.world, engine.introspector, engine.route.track(), - engine.locator.track(), TrackedMut::reborrow_mut(&mut engine.tracer), ) } @@ -294,15 +293,12 @@ impl Counter { world: Tracked, introspector: Tracked, route: Tracked, - locator: Tracked, tracer: TrackedMut, ) -> SourceResult> { - let mut locator = Locator::chained(locator); let mut engine = Engine { world, introspector, route: Route::extend(route).unnested(), - locator: &mut locator, tracer, }; @@ -815,8 +811,8 @@ impl ManualPageCounter { for (_, item) in page.items() { match item { FrameItem::Group(group) => self.visit(engine, &group.frame)?, - FrameItem::Tag(elem) => { - let Some(elem) = elem.to_packed::() else { + FrameItem::Tag(tag) => { + let Some(elem) = tag.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 ebd787fe4..fe59cb007 100644 --- a/crates/typst/src/introspection/introspector.rs +++ b/crates/typst/src/introspection/introspector.rs @@ -16,7 +16,7 @@ use crate::model::Numbering; use crate::utils::NonZeroExt; /// Can be queried for elements and their positions. -#[derive(Clone)] +#[derive(Default, Clone)] pub struct Introspector { /// The number of pages in the document. pages: usize, @@ -25,6 +25,9 @@ pub struct Introspector { /// 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>, /// Caches queries done on the introspector. This is important because @@ -41,6 +44,7 @@ impl Introspector { self.pages = pages.len(); self.elems.clear(); self.labels.clear(); + self.keys.clear(); self.page_numberings.clear(); self.queries.clear(); @@ -61,18 +65,21 @@ impl Introspector { .pre_concat(group.transform); self.extract(&group.frame, page, ts); } - FrameItem::Tag(elem) - if !self.elems.contains_key(&elem.location().unwrap()) => + FrameItem::Tag(tag) + if !self.elems.contains_key(&tag.elem.location().unwrap()) => { let pos = pos.transform(ts); - let ret = self.elems.insert( - elem.location().unwrap(), - (elem.clone(), Position { page, point: pos }), - ); + let loc = tag.elem.location().unwrap(); + 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) = elem.label() { + if let Some(label) = tag.elem.label() { self.labels.entry(label).or_default().push(self.elems.len() - 1); } } @@ -86,21 +93,24 @@ impl Introspector { self.elems.values().map(|(c, _)| c) } + /// Perform 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. - fn index(&self, elem: &Content) -> usize { - self.elems - .get_index_of(&elem.location().unwrap()) - .unwrap_or(usize::MAX) + fn elem_index(&self, elem: &Content) -> usize { + self.loc_index(&elem.location().unwrap()) } - /// Perform a binary search for `elem` among the `list`. - fn binary_search(&self, list: &[Content], elem: &Content) -> Result { - list.binary_search_by_key(&self.index(elem), |elem| self.index(elem)) + /// Get 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) } } @@ -183,7 +193,7 @@ impl Introspector { Selector::Or(selectors) => selectors .iter() .flat_map(|sel| self.query(sel)) - .map(|elem| self.index(&elem)) + .map(|elem| self.elem_index(&elem)) .collect::>() .into_iter() .map(|index| self.elems[index].0.clone()) @@ -283,17 +293,20 @@ impl Introspector { .map(|&(_, pos)| pos) .unwrap_or(Position { page: NonZeroUsize::ONE, point: Point::zero() }) } -} -impl Default for Introspector { - fn default() -> Self { - Self { - pages: 0, - elems: IndexMap::new(), - labels: HashMap::new(), - page_numberings: vec![], - queries: QueryCache::default(), - } + /// Try to find a location for an element with the given `key` hash + /// that is closest after the `anchor`. + /// + /// This is used for introspector-assisted location assignment during + /// measurement. See the "Dealing with Measurement" section of the + /// [`Locator`](crate::introspection::Locator) docs for more details. + pub fn locator(&self, key: u128, anchor: Location) -> Option { + let anchor = self.loc_index(&anchor); + self.keys + .get(&key)? + .iter() + .copied() + .min_by_key(|loc| self.loc_index(loc).wrapping_sub(anchor)) } } diff --git a/crates/typst/src/introspection/location.rs b/crates/typst/src/introspection/location.rs index 7b279b39e..5cd6aae05 100644 --- a/crates/typst/src/introspection/location.rs +++ b/crates/typst/src/introspection/location.rs @@ -1,3 +1,4 @@ +use std::fmt::{self, Debug, Formatter}; use std::num::NonZeroUsize; use ecow::EcoString; @@ -21,26 +22,27 @@ use crate::model::Numbering; /// elements, but you will find only those that have an explicit label attached /// to them. This limitation will be resolved in the future. #[ty(scope)] -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub struct Location { - /// The hash of the element. - pub hash: u128, - /// An unique number among elements with the same hash. This is the reason - /// we need a `Locator` everywhere. - pub disambiguator: usize, -} +#[derive(Copy, Clone, Eq, PartialEq, Hash)] +pub struct Location(u128); impl Location { + /// Create a new location from a unique hash. + pub fn new(hash: u128) -> Self { + Self(hash) + } + + /// Extract the raw hash. + pub fn hash(self) -> u128 { + self.0 + } + /// Produces a well-known variant of this location. /// /// This is a synthetic location created from another one and is used, for /// example, in bibliography management to create individual linkable /// locations for reference entries from the bibliography's location. pub fn variant(self, n: usize) -> Self { - Self { - hash: crate::utils::hash128(&(self.hash, n)), - ..self - } + Self(crate::utils::hash128(&(self.0, n))) } } @@ -91,6 +93,12 @@ impl Location { } } +impl Debug for Location { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "Location({})", self.0) + } +} + impl Repr for Location { fn repr(&self) -> EcoString { "..".into() diff --git a/crates/typst/src/introspection/locator.rs b/crates/typst/src/introspection/locator.rs index 9c02f6db1..4df94a2c4 100644 --- a/crates/typst/src/introspection/locator.rs +++ b/crates/typst/src/introspection/locator.rs @@ -1,117 +1,350 @@ -use std::cell::RefCell; use std::collections::HashMap; +use std::fmt::{self, Debug, Formatter}; +use std::hash::Hash; +use std::sync::OnceLock; -use comemo::{Track, Tracked, Validate}; +use comemo::{Tracked, Validate}; -use crate::introspection::Location; -use crate::layout::{Frame, FrameItem}; +use crate::introspection::{Introspector, Location}; /// Provides locations for elements in the document. /// -/// A [`Location`] consists of an element's hash plus a disambiguator. Just the -/// hash is not enough because we can have multiple equal elements with the same -/// hash (not a hash collision, just equal elements!). Between these, we -/// disambiguate with an increasing number. In principle, the disambiguator -/// could just be counted up. However, counting is an impure operation and as -/// such we can't count across a memoization boundary. [^1] +/// A [`Location`] is a unique ID for an element generated during realization. /// -/// Instead, we only mutate within a single "layout run" and combine the results -/// with disambiguators from an outer tracked locator. Thus, the locators form a -/// "tracked chain". When a layout run ends, its mutations are discarded and, on -/// the other side of the memoization boundary, we -/// [reconstruct](Self::visit_frame) them from the resulting [frames](Frame). +/// # How to use this +/// The same content may yield different results when laid out in different +/// parts of the document. To reflect this, every layout operation receives a +/// locator and every layout operation requires a locator. In code: /// -/// [^1]: Well, we could with [`TrackedMut`](comemo::TrackedMut), but the -/// overhead is quite high, especially since we need to save & undo the counting -/// when only measuring. -#[derive(Default, Clone)] +/// - all layouters receive an owned `Locator` +/// - all layout functions take an owned `Locator` +/// +/// When a layouter only requires a single sublayout call, it can simply pass on +/// its locator. When a layouter needs to call multiple sublayouters, we need to +/// make an explicit decision: +/// +/// - Split: When we're layouting multiple distinct children (or other pieces of +/// content), we need to split up the locator with [`Locator::split`]. This +/// allows us to produce multiple new `Locator`s for the sublayouts. When we +/// split the locator, each sublocator will be a distinct entity and using it +/// to e.g. layout the same piece of figure content will yield distinctly +/// numbered figures. +/// +/// - Relayout: When we're layouting the same content multiple times (e.g. when +/// measuring something), we can call [`Locator::relayout`] to use the same +/// locator multiple times. This indicates to the compiler that it's actually +/// the same content. Using it to e.g. layout the same piece of figure content +/// will yield the same figure number both times. Typically, when we layout +/// something multiple times using `relayout`, only one of the outputs +/// actually ends up in the document, while the other outputs are only used +/// for measurement and then discarded. +/// +/// The `Locator` intentionally does not implement `Copy` and `Clone` so that it +/// can only be used once. This ensures that whenever we are layouting multiple +/// things, we make an explicit decision whether we want to split or relayout. +/// +/// # How it works +/// There are two primary considerations for the assignment of locations: +/// +/// 1. Locations should match up over multiple layout iterations, so that +/// elements can be identified as being the same: That's the whole point of +/// them. +/// +/// 2. Locations should be as stable as possible across document edits, so that +/// incremental compilation is effective. +/// +/// 3. We want to assign them with as little long-lived state as possible to +/// enable parallelization of the layout process. +/// +/// Let's look at a few different assignment strategies to get a feeling for +/// these requirements: +/// +/// - A very simple way to generate unique IDs would be to just increase a +/// counter for each element. In this setup, (1) is somewhat satisfied: In +/// principle, the counter will line up across iterations, but things start to +/// break down once we generate content dependant on introspection since the +/// IDs generated for that new content will shift the IDs for all following +/// elements in the document. (2) is not satisfied since an edit in the middle +/// of the document shifts all later IDs. (3) is obviously not satisfied. +/// Conclusion: Not great. +/// +/// - To make things more robust, we can incorporate some stable knowledge about +/// the element into the ID. For this, we can use the element's span since it +/// is already mostly unique: Elements resulting from different source code +/// locations are guaranteed to have different spans. However, we can also +/// have multiple distinct elements generated from the same source location: +/// e.g. `#for _ in range(5) { figure(..) }`. To handle this case, we can then +/// disambiguate elements with the same span with an increasing counter. In +/// this setup, (1) is mostly satisfied: Unless we do stuff like generating +/// colliding counter updates dependant on introspection, things will line up. +/// (2) is also reasonably well satisfied, as typical edits will only affect +/// the single element at the currently edited span. Only if we edit inside of +/// a function, loop, or similar construct, we will affect multiple elements. +/// (3) is still a problem though, since we count up. +/// +/// - What's left is to get rid of the mutable state. Note that layout is a +/// recursive process and has a tree-shaped execution graph. Thus, we can try +/// to determine an element's ID based on the path of execution taken in this +/// graph. Something like "3rd element in layer 1, 7th element in layer 2, +/// ..". This is basically the first approach, but on a per-layer basis. Thus, +/// we can again apply our trick from the second approach, and use the span + +/// disambiguation strategy on a per-layer basis: "1st element with span X in +/// layer 1, 3rd element with span Y in layer 2". The chance for a collision +/// is now pretty low and our state is wholly local to each level. So, if we +/// want to parallelize layout within a layer, we can generate the IDs for +/// that layer upfront and then start forking out. The final remaining +/// question is how we can compactly encode this information: For this, as +/// always, we use hashing! We incorporate the ID information from each layer +/// into a single hash and thanks to the collision resistence of 128-bit +/// SipHash, we get almost guaranteed unique locations. We don't even store +/// the full layer information at all, but rather hash _hierarchically:_ Let +/// `k_x` be our local per-layer ID for layer `x` and `h_x` be the full +/// combined hash for layer `x`. We compute `h_n = hash(h_(n-1), k_n)`. +/// +/// So that's what's going on conceptually in this type. For efficient +/// memoization, we do all of this in a tracked fashion, such that we only +/// observe the hash for all the layers above us, if we actually need to +/// generate a [`Location`]. Thus, if we have a piece of content that does not +/// contain any locatable elements, we can cache its layout even if it occurs in +/// different places. +/// +/// # Dealing with measurement +/// As explained above, any kind of measurement the compiler performs requires a +/// locator that matches the one used during real layout. This ensures that the +/// locations assigned during measurement match up exactly with the locations of +/// real document elements. Without this guarantee, many introspection-driven +/// features (like counters, state, and citations) don't work correctly (since +/// they perform queries dependant on concrete locations). +/// +/// This is all fine and good, but things get really tricky when the _user_ +/// measures such introspecting content since the user isn't kindly managing +/// locators for us. Our standard `Locator` workflow assigns locations that +/// depend a lot on the exact placement in the hierarchy of elements. For this +/// reason, something that is measured, but then placed into something like a +/// grid will get a location influenced by the grid. Without a locator, we can't +/// make the connection between the measured content and the real content, so we +/// can't ensure that the locations match up. +/// +/// One possible way to deal with this is to force the user to uniquely identify +/// content before being measured after all. This would mean that the user needs +/// to come up with an identifier that is unique within the surrounding context +/// block and attach it to the content in some way. However, after careful +/// consideration, I have concluded that this is simply too big of an ask from +/// users: Understanding why this is even necessary is pretty complicated and +/// how to best come up with a unique ID is even more so. +/// +/// For this reason, I chose an alternative best-effort approach: The locator +/// has a custom "measurement mode" (entered through [`LocatorLink::measure`]), +/// in which it does its best to assign locations that match up. Specifically, +/// it uses the key hashes of the individual locatable elements in the measured +/// content (which may not be unique if content is reused) and combines them +/// with the context's location to find the most likely matching real element. +/// This approach works correctly almost all of the time (especially for +/// "normal" hand-written content where the key hashes rarely collide, as +/// opposed to code-heavy things where they do). +/// +/// Support for enhancing this with user-provided uniqueness can still be added +/// in the future. It will most likely anyway be added simply because it's +/// automatically included when we add a way to "freeze" content for things like +/// slidehows. But it will be opt-in because it's just too much complication. pub struct Locator<'a> { - /// Maps from a hash to the maximum number we've seen for this hash. This - /// number becomes the `disambiguator`. - hashes: RefCell>, - /// An outer `Locator`, from which we can get disambiguator for hashes - /// outside of the current "layout run". - /// - /// We need to override the constraint's lifetime here so that `Tracked` is - /// covariant over the constraint. If it becomes invariant, we're in for a - /// world of lifetime pain. - outer: Option as Validate>::Constraint>>, + /// A local hash that incorporates all layers since the last memoization + /// boundary. + local: u128, + /// A pointer to an outer cached locator, which contributes the information + /// for all the layers beyond the memoization boundary on-demand. + outer: Option<&'a LocatorLink<'a>>, } impl<'a> Locator<'a> { - /// Create a new locator. - pub fn new() -> Self { - Self::default() - } - - /// Create a new chained locator. - pub fn chained(outer: Tracked<'a, Self>) -> Self { - Self { outer: Some(outer), ..Default::default() } - } - - /// Start tracking this locator. + /// Create a new root-level locator. /// - /// In comparison to [`Track::track`], this method skips this chain link - /// if it does not contribute anything. - pub fn track(&self) -> Tracked<'_, Self> { - match self.outer { - Some(outer) if self.hashes.borrow().is_empty() => outer, - _ => Track::track(self), + /// Should typically only be created at the document level, though there + /// are a few places where we use it as well that just don't support + /// introspection (e.g. drawable patterns). + pub fn root() -> Self { + Self { local: 0, outer: None } + } + + /// Creates a new synthetic locator. + /// + /// This can be used to create a new dependant layout based on an element. + /// This is used for layouting footnote entries based on the location + /// of the associated footnote. + pub fn synthesize(location: Location) -> Self { + Self { local: location.hash(), outer: None } + } + + /// Creates a new locator that points to the given link. + pub fn link(link: &'a LocatorLink<'a>) -> Self { + Self { local: 0, outer: Some(link) } + } +} + +impl<'a> Locator<'a> { + /// Returns a type that can be used to generate `Locator`s for multiple + /// child elements. See the type-level docs for more details. + pub fn split(self) -> SplitLocator<'a> { + SplitLocator { + local: self.local, + outer: self.outer, + disambiguators: HashMap::new(), } } - /// Produce a stable identifier for this call site. - pub fn locate(&mut self, hash: u128) -> Location { - // Get the current disambiguator for this hash. - let disambiguator = self.disambiguator_impl(hash); - - // Bump the next disambiguator up by one. - self.hashes.get_mut().insert(hash, disambiguator + 1); - - // Create the location in its default variant. - Location { hash, disambiguator } - } - - /// Advance past a frame. - pub fn visit_frame(&mut self, frame: &Frame) { - for (_, item) in frame.items() { - match item { - FrameItem::Group(group) => self.visit_frame(&group.frame), - FrameItem::Tag(elem) => { - let hashes = self.hashes.get_mut(); - let loc = elem.location().unwrap(); - let entry = hashes.entry(loc.hash).or_default(); - - // Next disambiguator needs to be at least one larger than - // the maximum we've seen so far. - *entry = (*entry).max(loc.disambiguator + 1); - } - _ => {} - } - } - } - - /// Advance past a number of frames. - pub fn visit_frames<'b>(&mut self, frames: impl IntoIterator) { - for frame in frames { - self.visit_frame(frame); - } - } - - /// The current disambiguator for the given hash. - fn disambiguator_impl(&self, hash: u128) -> usize { - *self - .hashes - .borrow_mut() - .entry(hash) - .or_insert_with(|| self.outer.map_or(0, |outer| outer.disambiguator(hash))) + /// Creates a copy of this locator for measurement or relayout of the same + /// content. See the type-level docs for more details. + /// + /// This is effectively just `Clone`, but the `Locator` doesn't implement + /// `Clone` to make this operation explicit. + pub fn relayout(&self) -> Self { + Self { local: self.local, outer: self.outer } } } #[comemo::track] impl<'a> Locator<'a> { - /// The current disambiguator for the hash. - fn disambiguator(&self, hash: u128) -> usize { - self.disambiguator_impl(hash) + /// Resolves the locator based on its local and the outer information. + fn resolve(&self) -> Resolved { + match self.outer { + None => Resolved::Hash(self.local), + Some(outer) => match outer.resolve() { + Resolved::Hash(outer) => { + Resolved::Hash(crate::utils::hash128(&(self.local, outer))) + } + Resolved::Measure(anchor) => Resolved::Measure(anchor), + }, + } + } +} + +impl Debug for Locator<'_> { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "Locator({:?})", self.resolve()) + } +} + +/// The fully resolved value of a locator. +#[derive(Debug, Copy, Clone, Hash)] +enum Resolved { + /// The full hash, incorporating the local and all outer information. + Hash(u128), + /// Indicates that the locator is in measurement mode, with the given anchor + /// location. + Measure(Location), +} + +/// A type that generates unique sublocators. +pub struct SplitLocator<'a> { + /// A local hash that incorporates all layers since the last memoization + /// boundary. + local: u128, + /// A pointer to an outer cached locator, which contributes the information + /// for all the layers beyond the memoization boundary on-demand. + outer: Option<&'a LocatorLink<'a>>, + /// Simply counts up the number of times we've seen each local hash. + disambiguators: HashMap, +} + +impl<'a> SplitLocator<'a> { + /// Produces a sublocator for a subtree keyed by `key`. The keys do *not* + /// need to be unique among the `next()` calls on this split locator. (They + /// can even all be `&()`.) + /// + /// However, stable & mostly unique keys lead to more stable locations + /// throughout edits, improving incremental compilation performance. + /// + /// A common choice for a key is the span of the content that will be + /// layouted with this locator. + pub fn next(&mut self, key: &K) -> Locator<'a> { + self.next_inner(crate::utils::hash128(key)) + } + + /// Produces a sublocator for a subtree. + pub fn next_inner(&mut self, key: u128) -> Locator<'a> { + // Produce a locator disambiguator, for elements with the same key + // within this `SplitLocator`. + let disambiguator = { + let slot = self.disambiguators.entry(key).or_default(); + std::mem::replace(slot, *slot + 1) + }; + + // Combine the key, disambiguator and local hash into a sub-local hash. + // The outer information is not yet merged into this, it is added + // on-demand in `Locator::resolve`. + let local = crate::utils::hash128(&(key, disambiguator, self.local)); + + Locator { outer: self.outer, local } + } + + /// Produces a unique location for an element. + pub fn next_location( + &mut self, + introspector: Tracked, + key: u128, + ) -> Location { + match self.next_inner(key).resolve() { + Resolved::Hash(hash) => Location::new(hash), + Resolved::Measure(anchor) => { + // If we aren't able to find a matching element in the document, + // default to the anchor, so that it's at least remotely in + // the right area (so that counters can be resolved). + introspector.locator(key, anchor).unwrap_or(anchor) + } + } + } +} + +/// A locator can be linked to this type to only access information across the +/// memoization boundary on-demand, improving the cache hit chance. +pub struct LocatorLink<'a> { + /// The link itself. + kind: LinkKind<'a>, + /// The cached resolved link. + resolved: OnceLock, +} + +/// The different kinds of locator links. +enum LinkKind<'a> { + /// An outer `Locator`, which we can resolved if necessary. + /// + /// We need to override the constraint's lifetime here so that `Tracked` is + /// covariant over the constraint. If it becomes invariant, we're in for a + /// world of lifetime pain. + Outer(Tracked<'a, Locator<'a>, as Validate>::Constraint>), + /// A link which indicates that we are in measurement mode. + Measure(Location), +} + +impl<'a> LocatorLink<'a> { + /// Create a locator link. + pub fn new(outer: Tracked<'a, Locator<'a>>) -> Self { + LocatorLink { + kind: LinkKind::Outer(outer), + resolved: OnceLock::new(), + } + } + + /// Creates a link that puts any linked downstream locator into measurement + /// mode. + /// + /// Read the "Dealing with measurement" section of the [`Locator`] docs for + /// more details. + pub fn measure(anchor: Location) -> Self { + LocatorLink { + kind: LinkKind::Measure(anchor), + resolved: OnceLock::new(), + } + } + + /// Resolve the link. + /// + /// The result is cached in this link, so that we don't traverse the link + /// chain over and over again. + fn resolve(&self) -> Resolved { + *self.resolved.get_or_init(|| match self.kind { + LinkKind::Outer(outer) => outer.resolve(), + LinkKind::Measure(anchor) => Resolved::Measure(anchor), + }) } } diff --git a/crates/typst/src/introspection/mod.rs b/crates/typst/src/introspection/mod.rs index 1a7c02399..c9dba244a 100644 --- a/crates/typst/src/introspection/mod.rs +++ b/crates/typst/src/introspection/mod.rs @@ -23,6 +23,8 @@ pub use self::metadata::*; pub use self::query_::*; pub use self::state::*; +use std::fmt::{self, Debug, Formatter}; + use crate::diag::{bail, SourceResult}; use crate::engine::Engine; use crate::foundations::{ @@ -56,7 +58,7 @@ pub fn define(global: &mut Scope) { global.define_func::(); } -/// Holds a locatable element that was realized. +/// 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. @@ -65,14 +67,13 @@ pub struct TagElem { /// The introspectible element. #[required] #[internal] - pub elem: Content, + pub tag: Tag, } impl TagElem { /// Create a packed tag element. - pub fn packed(elem: Content) -> Content { - let span = elem.span(); - let mut content = Self::new(elem).pack().spanned(span); + pub fn packed(tag: Tag) -> Content { + let mut content = Self::new(tag).pack(); // We can skip preparation for the `TagElem`. content.mark_prepared(); content @@ -92,3 +93,29 @@ impl Behave for Packed { Behaviour::Invisible } } + +/// Holds a locatable element that was realized. +#[derive(Clone, PartialEq, Hash)] +pub struct Tag { + /// The introspectible element. + pub elem: Content, + /// 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(crate) key: u128, +} + +impl Tag { + /// Create a tag from an element and its key hash. + pub fn new(elem: Content, key: u128) -> Self { + Self { elem, key } + } +} + +impl Debug for Tag { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "Tag({:?})", self.elem) + } +} diff --git a/crates/typst/src/introspection/state.rs b/crates/typst/src/introspection/state.rs index 5cbd2fc3f..bf97c8746 100644 --- a/crates/typst/src/introspection/state.rs +++ b/crates/typst/src/introspection/state.rs @@ -9,7 +9,7 @@ use crate::foundations::{ LocatableSelector, NativeElement, Packed, Repr, Selector, Show, Str, StyleChain, Value, }; -use crate::introspection::{Introspector, Locatable, Location, Locator}; +use crate::introspection::{Introspector, Locatable, Location}; use crate::syntax::Span; use crate::World; @@ -215,7 +215,6 @@ impl State { engine.world, engine.introspector, engine.route.track(), - engine.locator.track(), TrackedMut::reborrow_mut(&mut engine.tracer), ) } @@ -227,15 +226,12 @@ impl State { world: Tracked, introspector: Tracked, route: Tracked, - locator: Tracked, tracer: TrackedMut, ) -> SourceResult> { - let mut locator = Locator::chained(locator); let mut engine = Engine { world, introspector, route: Route::extend(route).unnested(), - locator: &mut locator, tracer, }; let mut state = self.init.clone(); diff --git a/crates/typst/src/layout/columns.rs b/crates/typst/src/layout/columns.rs index c249a227a..503cb857d 100644 --- a/crates/typst/src/layout/columns.rs +++ b/crates/typst/src/layout/columns.rs @@ -3,6 +3,7 @@ use std::num::NonZeroUsize; use crate::diag::SourceResult; use crate::engine::Engine; use crate::foundations::{elem, Content, NativeElement, Packed, Show, StyleChain}; +use crate::introspection::Locator; use crate::layout::{ Abs, Axes, BlockElem, Dir, Fragment, Frame, Length, Point, Ratio, Regions, Rel, Size, }; @@ -62,7 +63,8 @@ impl Show for Packed { fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult { Ok(BlockElem::multi_layouter(self.clone(), layout_columns) .with_rootable(true) - .pack()) + .pack() + .spanned(self.span())) } } @@ -71,6 +73,7 @@ impl Show for Packed { fn layout_columns( elem: &Packed, engine: &mut Engine, + locator: Locator, styles: StyleChain, regions: Regions, ) -> SourceResult { @@ -79,7 +82,7 @@ fn layout_columns( // Separating the infinite space into infinite columns does not make // much sense. if !regions.size.x.is_finite() { - return body.layout(engine, styles, regions); + return body.layout(engine, locator, styles, regions); } // Determine the width of the gutter and each column. @@ -104,7 +107,7 @@ fn layout_columns( }; // Layout the children. - let mut frames = body.layout(engine, styles, pod)?.into_iter(); + let mut frames = body.layout(engine, locator, styles, pod)?.into_iter(); let mut finished = vec![]; let dir = TextElem::dir_in(styles); diff --git a/crates/typst/src/layout/container.rs b/crates/typst/src/layout/container.rs index 26db64b6b..908a193ec 100644 --- a/crates/typst/src/layout/container.rs +++ b/crates/typst/src/layout/container.rs @@ -7,6 +7,7 @@ use crate::foundations::{ cast, elem, Args, AutoValue, Construct, Content, NativeElement, Packed, Resolve, Smart, StyleChain, Value, }; +use crate::introspection::Locator; use crate::layout::{ Abs, Axes, Corners, Em, Fr, Fragment, Frame, FrameKind, Length, Region, Regions, Rel, Sides, Size, Spacing, VElem, @@ -120,6 +121,7 @@ impl Packed { pub fn layout( &self, engine: &mut Engine, + locator: Locator, styles: StyleChain, region: Size, ) -> SourceResult { @@ -140,7 +142,7 @@ impl Packed { // If we have a child, layout it into the body. Boxes are boundaries // for gradient relativeness, so we set the `FrameKind` to `Hard`. Some(body) => body - .layout(engine, styles, pod.into_regions())? + .layout(engine, locator, styles, pod.into_regions())? .into_frame() .with_kind(FrameKind::Hard), }; @@ -251,6 +253,7 @@ impl InlineElem { callback: fn( content: &Packed, engine: &mut Engine, + locator: Locator, styles: StyleChain, region: Size, ) -> SourceResult>, @@ -264,10 +267,11 @@ impl Packed { pub fn layout( &self, engine: &mut Engine, + locator: Locator, styles: StyleChain, region: Size, ) -> SourceResult> { - self.body().call(engine, styles, region) + self.body().call(engine, locator, styles, region) } } @@ -460,6 +464,7 @@ impl BlockElem { f: fn( content: &Packed, engine: &mut Engine, + locator: Locator, styles: StyleChain, region: Region, ) -> SourceResult, @@ -477,6 +482,7 @@ impl BlockElem { f: fn( content: &Packed, engine: &mut Engine, + locator: Locator, styles: StyleChain, regions: Regions, ) -> SourceResult, @@ -493,6 +499,7 @@ impl Packed { pub fn layout( &self, engine: &mut Engine, + locator: Locator, styles: StyleChain, regions: Regions, ) -> SourceResult { @@ -530,7 +537,8 @@ impl Packed { // If we have content as our body, just layout it. Some(BlockChild::Content(body)) => { - let mut fragment = body.measure(engine, styles, pod)?; + let mut fragment = + body.layout(engine, locator.relayout(), styles, pod)?; // If the body is automatically sized and produced more than one // fragment, ensure that the width was consistent across all @@ -551,11 +559,7 @@ impl Packed { expand: Axes::new(true, pod.expand.y), ..pod }; - fragment = body.layout(engine, styles, pod)?; - } else { - // Apply the side effect to turn the `measure` into a - // `layout`. - engine.locator.visit_frames(&fragment); + fragment = body.layout(engine, locator, styles, pod)?; } fragment @@ -565,7 +569,7 @@ impl Packed { // base region, give it that. Some(BlockChild::SingleLayouter(callback)) => { let pod = Region::new(pod.base(), pod.expand); - callback.call(engine, styles, pod).map(Fragment::frame)? + callback.call(engine, locator, styles, pod).map(Fragment::frame)? } // If we have a child that wants to layout with full region access, @@ -577,7 +581,7 @@ impl Packed { Some(BlockChild::MultiLayouter(callback)) => { let expand = (pod.expand | regions.expand) & pod.size.map(Abs::is_finite); let pod = Regions { expand, ..pod }; - callback.call(engine, styles, pod)? + callback.call(engine, locator, styles, pod)? } }; @@ -927,6 +931,7 @@ mod callbacks { callback! { InlineCallback = ( engine: &mut Engine, + locator: Locator, styles: StyleChain, region: Size, ) -> SourceResult> @@ -935,6 +940,7 @@ mod callbacks { callback! { BlockSingleCallback = ( engine: &mut Engine, + locator: Locator, styles: StyleChain, region: Region, ) -> SourceResult @@ -943,6 +949,7 @@ mod callbacks { callback! { BlockMultiCallback = ( engine: &mut Engine, + locator: Locator, styles: StyleChain, regions: Regions, ) -> SourceResult diff --git a/crates/typst/src/layout/flow.rs b/crates/typst/src/layout/flow.rs index 8ae681837..cdf6034d5 100644 --- a/crates/typst/src/layout/flow.rs +++ b/crates/typst/src/layout/flow.rs @@ -11,7 +11,7 @@ use crate::engine::Engine; use crate::foundations::{ elem, Args, Construct, Content, NativeElement, Packed, Resolve, Smart, StyleChain, }; -use crate::introspection::TagElem; +use crate::introspection::{Locator, SplitLocator, Tag, TagElem}; use crate::layout::{ Abs, AlignElem, Axes, BlockElem, ColbreakElem, FixedAlignment, FlushElem, Fr, Fragment, Frame, FrameItem, PlaceElem, Point, Regions, Rel, Size, Spacing, VElem, @@ -43,6 +43,7 @@ impl Packed { pub fn layout( &self, engine: &mut Engine, + locator: Locator, styles: StyleChain, regions: Regions, ) -> SourceResult { @@ -66,7 +67,7 @@ impl Packed { alone = child.is::(); } - let mut layouter = FlowLayouter::new(regions, styles, alone); + let mut layouter = FlowLayouter::new(locator, styles, regions, alone); for (child, styles) in self.children().chain(&styles) { if let Some(elem) = child.to_packed::() { layouter.layout_tag(elem); @@ -105,10 +106,12 @@ impl Debug for FlowElem { struct FlowLayouter<'a> { /// Whether this is the root flow. root: bool, - /// The regions to layout children into. - regions: Regions<'a>, + /// Provides unique locations to the flow's children. + locator: SplitLocator<'a>, /// The shared styles. styles: StyleChain<'a>, + /// The regions to layout children into. + regions: Regions<'a>, /// Whether the flow should expand to fill the region. expand: Axes, /// The initial size of `regions.size` that was available before we started @@ -121,7 +124,7 @@ struct FlowLayouter<'a> { /// Spacing and layouted blocks for the current region. items: Vec, /// A queue of tags that will be attached to the next frame. - pending_tags: Vec, + pending_tags: Vec, /// A queue of floating elements. pending_floats: Vec, /// Whether we have any footnotes in the current region. @@ -192,7 +195,12 @@ impl FlowItem { impl<'a> FlowLayouter<'a> { /// Create a new flow layouter. - fn new(mut regions: Regions<'a>, styles: StyleChain<'a>, alone: bool) -> Self { + fn new( + locator: Locator<'a>, + styles: StyleChain<'a>, + mut regions: Regions<'a>, + alone: bool, + ) -> Self { let expand = regions.expand; let root = std::mem::replace(&mut regions.root, false); @@ -204,8 +212,9 @@ impl<'a> FlowLayouter<'a> { Self { root, - regions, + locator: locator.split(), styles, + regions, expand, initial: regions.size, last_was_par: false, @@ -223,8 +232,8 @@ impl<'a> FlowLayouter<'a> { } /// Place explicit metadata into the flow. - fn layout_tag(&mut self, tag: &Packed) { - self.pending_tags.push(tag.elem.clone()); + fn layout_tag(&mut self, elem: &Packed) { + self.pending_tags.push(elem.tag.clone()); } /// Layout vertical spacing. @@ -259,6 +268,7 @@ impl<'a> FlowLayouter<'a> { let lines = par .layout( engine, + self.locator.next(&par.span()), styles, consecutive, self.regions.base(), @@ -330,7 +340,12 @@ impl<'a> FlowLayouter<'a> { // Layout the block itself. let sticky = block.sticky(styles); - let fragment = block.layout(engine, styles, self.regions)?; + let fragment = block.layout( + engine, + self.locator.next(&block.span()), + styles, + self.regions, + )?; // How to align the block. let align = AlignElem::alignment_in(styles).resolve(styles); @@ -378,7 +393,14 @@ impl<'a> FlowLayouter<'a> { align.x().unwrap_or_default().resolve(styles) }); let y_align = alignment.map(|align| align.y().map(|y| y.resolve(styles))); - let mut frame = placed.layout(engine, styles, self.regions.base())?.into_frame(); + let mut frame = placed + .layout( + engine, + self.locator.next(&placed.span()), + styles, + self.regions.base(), + )? + .into_frame(); frame.post_process(styles); let item = FlowItem::Placed { frame, x_align, y_align, delta, float, clearance }; self.layout_item(engine, item) @@ -390,7 +412,7 @@ impl<'a> FlowLayouter<'a> { frame.prepend_multiple( self.pending_tags .drain(..) - .map(|elem| (Point::zero(), FrameItem::Tag(elem))), + .map(|tag| (Point::zero(), FrameItem::Tag(tag))), ); } } @@ -631,7 +653,7 @@ impl<'a> FlowLayouter<'a> { if force && !self.pending_tags.is_empty() { let pos = Point::with_y(offset); output.push_multiple( - self.pending_tags.drain(..).map(|elem| (pos, FrameItem::Tag(elem))), + self.pending_tags.drain(..).map(|tag| (pos, FrameItem::Tag(tag))), ); } @@ -718,7 +740,6 @@ impl FlowLayouter<'_> { let prev_items_len = self.items.len(); let prev_size = self.regions.size; let prev_has_footnotes = self.has_footnotes; - let prev_locator = engine.locator.clone(); // Process footnotes one at a time. let mut k = 0; @@ -735,7 +756,12 @@ impl FlowLayouter<'_> { self.regions.size.y -= self.footnote_config.gap; let frames = FootnoteEntry::new(notes[k].clone()) .pack() - .layout(engine, self.styles, self.regions.with_root(false))? + .layout( + engine, + Locator::synthesize(notes[k].location().unwrap()), + self.styles, + self.regions.with_root(false), + )? .into_frames(); // If the entries didn't fit, abort (to keep footnote and entry @@ -749,7 +775,6 @@ impl FlowLayouter<'_> { self.items.truncate(prev_items_len); self.regions.size = prev_size; self.has_footnotes = prev_has_footnotes; - *engine.locator = prev_locator; return Ok(false); } @@ -784,7 +809,10 @@ impl FlowLayouter<'_> { let pod = Regions::one(self.regions.base(), expand); let separator = &self.footnote_config.separator; - let mut frame = separator.layout(engine, self.styles, pod)?.into_frame(); + // FIXME: Shouldn't use `root()` here. + let mut frame = separator + .layout(engine, Locator::root(), self.styles, pod)? + .into_frame(); frame.size_mut().y += self.footnote_config.clearance; frame.translate(Point::with_y(self.footnote_config.clearance)); @@ -801,10 +829,10 @@ fn find_footnotes(notes: &mut Vec>, frame: &Frame) { for (_, item) in frame.items() { match item { FrameItem::Group(group) => find_footnotes(notes, &group.frame), - FrameItem::Tag(elem) - if !notes.iter().any(|note| note.location() == elem.location()) => + FrameItem::Tag(tag) + if !notes.iter().any(|note| note.location() == tag.elem.location()) => { - let Some(footnote) = elem.to_packed::() else { + let Some(footnote) = tag.elem.to_packed::() else { continue; }; notes.push(footnote.clone()); diff --git a/crates/typst/src/layout/frame.rs b/crates/typst/src/layout/frame.rs index 42fc1d72c..09a4362de 100644 --- a/crates/typst/src/layout/frame.rs +++ b/crates/typst/src/layout/frame.rs @@ -6,7 +6,8 @@ use std::sync::Arc; use smallvec::SmallVec; -use crate::foundations::{cast, dict, Content, Dict, StyleChain, Value}; +use crate::foundations::{cast, dict, Dict, StyleChain, Value}; +use crate::introspection::Tag; use crate::layout::{ Abs, Axes, Corners, FixedAlignment, HideElem, Length, Point, Rel, Sides, Size, Transform, @@ -521,8 +522,9 @@ pub enum FrameItem { Image(Image, Size, Span), /// An internal or external link to a destination. Link(Destination, Size), - /// An introspectable element that produced something within this frame. - Tag(Content), + /// An introspectable element that produced something within this frame + /// alongside its key. + Tag(Tag), } impl Debug for FrameItem { @@ -533,7 +535,7 @@ impl Debug for FrameItem { Self::Shape(shape, _) => write!(f, "{shape:?}"), Self::Image(image, _, _) => write!(f, "{image:?}"), Self::Link(dest, _) => write!(f, "Link({dest:?})"), - Self::Tag(elem) => write!(f, "Tag({elem:?})"), + Self::Tag(tag) => write!(f, "{tag:?}"), } } } diff --git a/crates/typst/src/layout/grid/cells.rs b/crates/typst/src/layout/grid/cells.rs index efa7bb6ea..201bf1086 100644 --- a/crates/typst/src/layout/grid/cells.rs +++ b/crates/typst/src/layout/grid/cells.rs @@ -12,7 +12,10 @@ use crate::foundations::{ Array, CastInfo, Content, Context, Fold, FromValue, Func, IntoValue, Reflect, Resolve, Smart, StyleChain, Value, }; -use crate::layout::{Abs, Alignment, Axes, Length, LinePosition, Rel, Sides, Sizing}; +use crate::introspection::Locator; +use crate::layout::{ + Abs, Alignment, Axes, Fragment, Length, LinePosition, Regions, Rel, Sides, Sizing, +}; use crate::syntax::Span; use crate::utils::NonZeroExt; use crate::visualize::{Paint, Stroke}; @@ -155,10 +158,11 @@ where } /// Represents a cell in CellGrid, to be laid out by GridLayouter. -#[derive(Clone)] -pub struct Cell { +pub struct Cell<'a> { /// The cell's body. pub body: Content, + /// The cell's locator. + pub locator: Locator<'a>, /// The cell's fill. pub fill: Option, /// The amount of columns spanned by the cell. @@ -184,11 +188,12 @@ pub struct Cell { pub breakable: bool, } -impl From for Cell { - /// Create a simple cell given its body. - fn from(body: Content) -> Self { +impl<'a> Cell<'a> { + /// Create a simple cell given its body and its locator. + pub fn new(body: Content, locator: Locator<'a>) -> Self { Self { body, + locator, fill: None, colspan: NonZeroUsize::ONE, rowspan: NonZeroUsize::ONE, @@ -197,13 +202,32 @@ impl From for Cell { breakable: true, } } + + /// Layout the cell into the given regions. + /// + /// The `disambiguator` indicates which instance of this cell this should be + /// layouted as. For normal cells, it is always `0`, but for headers and + /// footers, it indicates the index of the header/footer among all. See the + /// [`Locator`] docs for more details on the concepts behind this. + pub fn layout( + &self, + engine: &mut Engine, + disambiguator: usize, + styles: StyleChain, + regions: Regions, + ) -> SourceResult { + let mut locator = self.locator.relayout(); + if disambiguator > 0 { + locator = locator.split().next_inner(disambiguator as u128); + } + self.body.layout(engine, locator, styles, regions) + } } /// A grid entry. -#[derive(Clone)] -pub(super) enum Entry { +pub(super) enum Entry<'a> { /// An entry which holds a cell. - Cell(Cell), + Cell(Cell<'a>), /// An entry which is merged with another cell. Merged { /// The index of the cell this entry is merged with. @@ -211,9 +235,9 @@ pub(super) enum Entry { }, } -impl Entry { +impl<'a> Entry<'a> { /// Obtains the cell inside this entry, if this is not a merged cell. - fn as_cell(&self) -> Option<&Cell> { + fn as_cell(&self) -> Option<&Cell<'a>> { match self { Self::Cell(cell) => Some(cell), Self::Merged { .. } => None, @@ -269,7 +293,7 @@ pub trait ResolvableCell { /// the `breakable` field. /// Returns a final Cell. #[allow(clippy::too_many_arguments)] - fn resolve_cell( + fn resolve_cell<'a>( self, x: usize, y: usize, @@ -278,8 +302,9 @@ pub trait ResolvableCell { inset: Sides>>, stroke: Sides>>>>, breakable: bool, + locator: Locator<'a>, styles: StyleChain, - ) -> Cell; + ) -> Cell<'a>; /// Returns this cell's column override. fn x(&self, styles: StyleChain) -> Smart; @@ -298,9 +323,9 @@ pub trait ResolvableCell { } /// A grid of cells, including the columns, rows, and cell data. -pub struct CellGrid { +pub struct CellGrid<'a> { /// The grid cells. - pub(super) entries: Vec, + pub(super) entries: Vec>, /// The column tracks including gutter tracks. pub(super) cols: Vec, /// The row tracks including gutter tracks. @@ -321,12 +346,12 @@ pub struct CellGrid { pub(super) has_gutter: bool, } -impl CellGrid { +impl<'a> CellGrid<'a> { /// Generates the cell grid, given the tracks and cells. pub fn new( tracks: Axes<&[Sizing]>, gutter: Axes<&[Sizing]>, - cells: impl IntoIterator, + cells: impl IntoIterator>, ) -> Self { let entries = cells.into_iter().map(Entry::Cell).collect(); Self::new_internal(tracks, gutter, vec![], vec![], None, None, entries) @@ -342,6 +367,7 @@ impl CellGrid { pub fn resolve( tracks: Axes<&[Sizing]>, gutter: Axes<&[Sizing]>, + locator: Locator<'a>, children: C, fill: &Celled>, align: &Celled>, @@ -357,6 +383,8 @@ impl CellGrid { C: IntoIterator>, C::IntoIter: ExactSizeIterator, { + let mut locator = locator.split(); + // Number of content columns: Always at least one. let c = tracks.x.len().max(1); @@ -660,6 +688,7 @@ impl CellGrid { inset.resolve(engine, styles, x, y)?, stroke.resolve(engine, styles, x, y)?, resolve_breakable(y, rowspan), + locator.next(&cell_span), styles, ); @@ -687,7 +716,7 @@ impl CellGrid { // (they can be overridden later); however, if no cells // occupy them as we finish building the grid, then such // positions will be replaced by empty cells. - resolved_cells.resize(new_len, None); + resolved_cells.resize_with(new_len, || None); } // The vector is large enough to contain the cell, so we can @@ -921,6 +950,7 @@ impl CellGrid { inset.resolve(engine, styles, x, y)?, stroke.resolve(engine, styles, x, y)?, resolve_breakable(y, 1), + locator.next(&()), styles, ); Ok(Entry::Cell(new_cell)) @@ -1101,7 +1131,7 @@ impl CellGrid { hlines: Vec>, header: Option>, footer: Option>, - entries: Vec, + entries: Vec>, ) -> Self { let mut cols = vec![]; let mut rows = vec![]; @@ -1163,7 +1193,7 @@ impl CellGrid { /// /// Returns `None` if it's a gutter cell. #[track_caller] - pub(super) fn entry(&self, x: usize, y: usize) -> Option<&Entry> { + pub(super) fn entry(&self, x: usize, y: usize) -> Option<&Entry<'a>> { assert!(x < self.cols.len()); assert!(y < self.rows.len()); @@ -1185,7 +1215,7 @@ impl CellGrid { /// /// Returns `None` if it's a gutter cell or merged position. #[track_caller] - pub(super) fn cell(&self, x: usize, y: usize) -> Option<&Cell> { + pub(super) fn cell(&self, x: usize, y: usize) -> Option<&Cell<'a>> { self.entry(x, y).and_then(Entry::as_cell) } diff --git a/crates/typst/src/layout/grid/layout.rs b/crates/typst/src/layout/grid/layout.rs index ec9d1e15a..9c5d7c21b 100644 --- a/crates/typst/src/layout/grid/layout.rs +++ b/crates/typst/src/layout/grid/layout.rs @@ -21,7 +21,7 @@ use crate::visualize::Geometry; /// Performs grid layout. pub struct GridLayouter<'a> { /// The grid of cells. - pub(super) grid: &'a CellGrid, + pub(super) grid: &'a CellGrid<'a>, /// The regions to layout children into. pub(super) regions: Regions<'a>, /// The inherited styles. @@ -78,8 +78,8 @@ pub(super) enum Row { /// where this row is laid out, and it can only be false when a row uses /// `layout_multi_row`, which in turn is only used by breakable auto rows. Frame(Frame, usize, bool), - /// Fractional row with y index. - Fr(Fr, usize), + /// Fractional row with y index and disambiguator. + Fr(Fr, usize, usize), } impl Row { @@ -87,7 +87,7 @@ impl Row { fn index(&self) -> usize { match self { Self::Frame(_, y, _) => *y, - Self::Fr(_, y) => *y, + Self::Fr(_, y, _) => *y, } } } @@ -97,7 +97,7 @@ impl<'a> GridLayouter<'a> { /// /// This prepares grid layout by unifying content and gutter tracks. pub fn new( - grid: &'a CellGrid, + grid: &'a CellGrid<'a>, regions: Regions<'a>, styles: StyleChain<'a>, span: Span, @@ -133,7 +133,7 @@ impl<'a> GridLayouter<'a> { if let Some(Repeatable::Repeated(footer)) = &self.grid.footer { // Ensure rows in the first region will be aware of the possible // presence of the footer. - self.prepare_footer(footer, engine)?; + self.prepare_footer(footer, engine, 0)?; if matches!(self.grid.header, None | Some(Repeatable::NotRepeated(_))) { // No repeatable header, so we won't subtract it later. self.regions.size.y -= self.footer_height; @@ -144,7 +144,7 @@ impl<'a> GridLayouter<'a> { if let Some(Repeatable::Repeated(header)) = &self.grid.header { if y < header.end { if y == 0 { - self.layout_header(header, engine)?; + self.layout_header(header, engine, 0)?; self.regions.size.y -= self.footer_height; } // Skip header rows during normal layout. @@ -155,16 +155,16 @@ impl<'a> GridLayouter<'a> { if let Some(Repeatable::Repeated(footer)) = &self.grid.footer { if y >= footer.start { if y == footer.start { - self.layout_footer(footer, engine)?; + self.layout_footer(footer, engine, self.finished.len())?; } continue; } } - self.layout_row(y, engine)?; + self.layout_row(y, engine, 0)?; } - self.finish_region(engine)?; + self.finish_region(engine, true)?; // Layout any missing rowspans. // There are only two possibilities for rowspans not yet laid out @@ -189,27 +189,30 @@ impl<'a> GridLayouter<'a> { &mut self, y: usize, engine: &mut Engine, + disambiguator: usize, ) -> SourceResult<()> { // Skip to next region if current one is full, but only for content // rows, not for gutter rows, and only if we aren't laying out an // unbreakable group of rows. let is_content_row = !self.grid.is_gutter_track(y); if self.unbreakable_rows_left == 0 && self.regions.is_full() && is_content_row { - self.finish_region(engine)?; + self.finish_region(engine, false)?; } if is_content_row { // Gutter rows have no rowspans or possibly unbreakable cells. - self.check_for_rowspans(y); + self.check_for_rowspans(disambiguator, y); self.check_for_unbreakable_rows(y, engine)?; } // Don't layout gutter rows at the top of a region. if is_content_row || !self.lrows.is_empty() { match self.grid.rows[y] { - Sizing::Auto => self.layout_auto_row(engine, y)?, - Sizing::Rel(v) => self.layout_relative_row(engine, v, y)?, - Sizing::Fr(v) => self.lrows.push(Row::Fr(v, y)), + Sizing::Auto => self.layout_auto_row(engine, disambiguator, y)?, + Sizing::Rel(v) => { + self.layout_relative_row(engine, disambiguator, v, y)? + } + Sizing::Fr(v) => self.lrows.push(Row::Fr(v, y, disambiguator)), } } @@ -841,7 +844,7 @@ impl<'a> GridLayouter<'a> { let size = Size::new(available, height); let pod = Regions::one(size, Axes::splat(false)); - let frame = cell.body.measure(engine, self.styles, pod)?.into_frame(); + let frame = cell.layout(engine, 0, self.styles, pod)?.into_frame(); resolved.set_max(frame.width() - already_covered_width); } @@ -901,11 +904,17 @@ impl<'a> GridLayouter<'a> { /// Layout a row with automatic height. Such a row may break across multiple /// regions. - fn layout_auto_row(&mut self, engine: &mut Engine, y: usize) -> SourceResult<()> { + fn layout_auto_row( + &mut self, + engine: &mut Engine, + disambiguator: usize, + y: usize, + ) -> SourceResult<()> { // Determine the size for each region of the row. If the first region // ends up empty for some column, skip the region and remeasure. let mut resolved = match self.measure_auto_row( engine, + disambiguator, y, true, self.unbreakable_rows_left, @@ -913,9 +922,16 @@ impl<'a> GridLayouter<'a> { )? { Some(resolved) => resolved, None => { - self.finish_region(engine)?; - self.measure_auto_row(engine, y, false, self.unbreakable_rows_left, None)? - .unwrap() + self.finish_region(engine, false)?; + self.measure_auto_row( + engine, + disambiguator, + y, + false, + self.unbreakable_rows_left, + None, + )? + .unwrap() } }; @@ -926,7 +942,7 @@ impl<'a> GridLayouter<'a> { // Layout into a single region. if let &[first] = resolved.as_slice() { - let frame = self.layout_single_row(engine, first, y)?; + let frame = self.layout_single_row(engine, disambiguator, first, y)?; self.push_row(frame, y, true); if self @@ -966,12 +982,12 @@ impl<'a> GridLayouter<'a> { } // Layout into multiple regions. - let fragment = self.layout_multi_row(engine, &resolved, y)?; + let fragment = self.layout_multi_row(engine, disambiguator, &resolved, y)?; let len = fragment.len(); for (i, frame) in fragment.into_iter().enumerate() { self.push_row(frame, y, i + 1 == len); if i + 1 < len { - self.finish_region(engine)?; + self.finish_region(engine, false)?; } } @@ -989,6 +1005,7 @@ impl<'a> GridLayouter<'a> { pub(super) fn measure_auto_row( &self, engine: &mut Engine, + disambiguator: usize, y: usize, can_skip: bool, unbreakable_rows_left: usize, @@ -1069,7 +1086,8 @@ impl<'a> GridLayouter<'a> { pod }; - let frames = cell.body.measure(engine, self.styles, pod)?.into_frames(); + let frames = + cell.layout(engine, disambiguator, self.styles, pod)?.into_frames(); // Skip the first region if one cell in it is empty. Then, // remeasure. @@ -1145,6 +1163,7 @@ impl<'a> GridLayouter<'a> { &pending_rowspans, unbreakable_rows_left, row_group_data, + disambiguator, engine, )?; } @@ -1159,11 +1178,12 @@ impl<'a> GridLayouter<'a> { fn layout_relative_row( &mut self, engine: &mut Engine, + disambiguator: usize, v: Rel, y: usize, ) -> SourceResult<()> { let resolved = v.resolve(self.styles).relative_to(self.regions.base().y); - let frame = self.layout_single_row(engine, resolved, y)?; + let frame = self.layout_single_row(engine, disambiguator, resolved, y)?; if self .grid @@ -1185,7 +1205,7 @@ impl<'a> GridLayouter<'a> { && !self.regions.size.y.fits(height) && !in_last_with_offset(self.regions, self.header_height + self.footer_height) { - self.finish_region(engine)?; + self.finish_region(engine, false)?; // Don't skip multiple regions for gutter and don't push a row. if self.grid.is_gutter_track(y) { @@ -1202,6 +1222,7 @@ impl<'a> GridLayouter<'a> { fn layout_single_row( &mut self, engine: &mut Engine, + disambiguator: usize, height: Abs, y: usize, ) -> SourceResult { @@ -1232,7 +1253,9 @@ impl<'a> GridLayouter<'a> { // rows. pod.full = self.regions.full; } - let frame = cell.body.layout(engine, self.styles, pod)?.into_frame(); + let frame = cell + .layout(engine, disambiguator, self.styles, pod)? + .into_frame(); let mut pos = pos; if self.is_rtl { // In the grid, cell colspans expand to the right, @@ -1261,6 +1284,7 @@ impl<'a> GridLayouter<'a> { fn layout_multi_row( &mut self, engine: &mut Engine, + disambiguator: usize, heights: &[Abs], y: usize, ) -> SourceResult { @@ -1286,7 +1310,8 @@ impl<'a> GridLayouter<'a> { pod.size.x = width; // Push the layouted frames into the individual output frames. - let fragment = cell.body.layout(engine, self.styles, pod)?; + let fragment = + cell.layout(engine, disambiguator, self.styles, pod)?; for (output, frame) in outputs.iter_mut().zip(fragment) { let mut pos = pos; if self.is_rtl { @@ -1314,7 +1339,11 @@ impl<'a> GridLayouter<'a> { } /// Finish rows for one region. - pub(super) fn finish_region(&mut self, engine: &mut Engine) -> SourceResult<()> { + pub(super) fn finish_region( + &mut self, + engine: &mut Engine, + last: bool, + ) -> SourceResult<()> { if self .lrows .last() @@ -1369,7 +1398,7 @@ impl<'a> GridLayouter<'a> { && self.lrows.iter().all(|row| row.index() < footer.start) { laid_out_footer_start = Some(footer.start); - self.layout_footer(footer, engine)?; + self.layout_footer(footer, engine, self.finished.len())?; } } @@ -1379,7 +1408,7 @@ impl<'a> GridLayouter<'a> { for row in &self.lrows { match row { Row::Frame(frame, _, _) => used += frame.height(), - Row::Fr(v, _) => fr += *v, + Row::Fr(v, _, _) => fr += *v, } } @@ -1400,10 +1429,10 @@ impl<'a> GridLayouter<'a> { for row in std::mem::take(&mut self.lrows) { let (frame, y, is_last) = match row { Row::Frame(frame, y, is_last) => (frame, y, is_last), - Row::Fr(v, y) => { + Row::Fr(v, y, disambiguator) => { let remaining = self.regions.full - used; let height = v.share(fr, remaining); - (self.layout_single_row(engine, height, y)?, y, true) + (self.layout_single_row(engine, disambiguator, height, y)?, y, true) } }; @@ -1499,17 +1528,20 @@ impl<'a> GridLayouter<'a> { self.finish_region_internal(output, rrows); - if let Some(Repeatable::Repeated(footer)) = &self.grid.footer { - self.prepare_footer(footer, engine)?; - } + if !last { + let disambiguator = self.finished.len(); + if let Some(Repeatable::Repeated(footer)) = &self.grid.footer { + self.prepare_footer(footer, engine, disambiguator)?; + } - if let Some(Repeatable::Repeated(header)) = &self.grid.header { - // Add a header to the new region. - self.layout_header(header, engine)?; - } + if let Some(Repeatable::Repeated(header)) = &self.grid.header { + // Add a header to the new region. + self.layout_header(header, engine, disambiguator)?; + } - // Ensure rows don't try to overrun the footer. - self.regions.size.y -= self.footer_height; + // Ensure rows don't try to overrun the footer. + self.regions.size.y -= self.footer_height; + } Ok(()) } diff --git a/crates/typst/src/layout/grid/lines.rs b/crates/typst/src/layout/grid/lines.rs index 469f53053..660811c7b 100644 --- a/crates/typst/src/layout/grid/lines.rs +++ b/crates/typst/src/layout/grid/lines.rs @@ -602,12 +602,14 @@ mod test { use super::super::cells::Entry; use super::*; use crate::foundations::Content; + use crate::introspection::Locator; use crate::layout::{Axes, Cell, Sides, Sizing}; use crate::utils::NonZeroExt; - fn sample_cell() -> Cell { + fn sample_cell() -> Cell<'static> { Cell { body: Content::default(), + locator: Locator::root(), fill: None, colspan: NonZeroUsize::ONE, rowspan: NonZeroUsize::ONE, @@ -617,9 +619,10 @@ mod test { } } - fn cell_with_colspan_rowspan(colspan: usize, rowspan: usize) -> Cell { + fn cell_with_colspan_rowspan(colspan: usize, rowspan: usize) -> Cell<'static> { Cell { body: Content::default(), + locator: Locator::root(), fill: None, colspan: NonZeroUsize::try_from(colspan).unwrap(), rowspan: NonZeroUsize::try_from(rowspan).unwrap(), @@ -629,7 +632,7 @@ mod test { } } - fn sample_grid_for_vlines(gutters: bool) -> CellGrid { + fn sample_grid_for_vlines(gutters: bool) -> CellGrid<'static> { const COLS: usize = 4; const ROWS: usize = 6; let entries = vec![ @@ -1152,7 +1155,7 @@ mod test { } } - fn sample_grid_for_hlines(gutters: bool) -> CellGrid { + fn sample_grid_for_hlines(gutters: bool) -> CellGrid<'static> { const COLS: usize = 4; const ROWS: usize = 9; let entries = vec![ diff --git a/crates/typst/src/layout/grid/mod.rs b/crates/typst/src/layout/grid/mod.rs index 57e28730c..326e356a6 100644 --- a/crates/typst/src/layout/grid/mod.rs +++ b/crates/typst/src/layout/grid/mod.rs @@ -22,6 +22,7 @@ use crate::foundations::{ cast, elem, scope, Array, Content, Fold, NativeElement, Packed, Show, Smart, StyleChain, Value, }; +use crate::introspection::Locator; use crate::layout::{ Abs, Alignment, Axes, BlockElem, Dir, Fragment, Length, OuterHAlignment, OuterVAlignment, Regions, Rel, Sides, Sizing, @@ -338,7 +339,9 @@ impl GridElem { impl Show for Packed { fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult { - Ok(BlockElem::multi_layouter(self.clone(), layout_grid).pack()) + Ok(BlockElem::multi_layouter(self.clone(), layout_grid) + .pack() + .spanned(self.span())) } } @@ -347,6 +350,7 @@ impl Show for Packed { fn layout_grid( elem: &Packed, engine: &mut Engine, + locator: Locator, styles: StyleChain, regions: Regions, ) -> SourceResult { @@ -380,6 +384,7 @@ fn layout_grid( let grid = CellGrid::resolve( tracks, gutter, + locator, children, fill, align, @@ -854,7 +859,7 @@ impl Default for Packed { } impl ResolvableCell for Packed { - fn resolve_cell( + fn resolve_cell<'a>( mut self, x: usize, y: usize, @@ -863,8 +868,9 @@ impl ResolvableCell for Packed { inset: Sides>>, stroke: Sides>>>>, breakable: bool, + locator: Locator<'a>, styles: StyleChain, - ) -> Cell { + ) -> Cell<'a> { let cell = &mut *self; let colspan = cell.colspan(styles); let rowspan = cell.rowspan(styles); @@ -916,6 +922,7 @@ impl ResolvableCell for Packed { cell.push_breakable(Smart::Custom(breakable)); Cell { body: self.pack(), + locator, fill, colspan, rowspan, diff --git a/crates/typst/src/layout/grid/repeated.rs b/crates/typst/src/layout/grid/repeated.rs index f0e9a4c7b..acb00f046 100644 --- a/crates/typst/src/layout/grid/repeated.rs +++ b/crates/typst/src/layout/grid/repeated.rs @@ -50,8 +50,10 @@ impl<'a> GridLayouter<'a> { &mut self, header: &Header, engine: &mut Engine, + disambiguator: usize, ) -> SourceResult<()> { - let header_rows = self.simulate_header(header, &self.regions, engine)?; + let header_rows = + self.simulate_header(header, &self.regions, engine, disambiguator)?; let mut skipped_region = false; while self.unbreakable_rows_left == 0 && !self.regions.size.y.fits(header_rows.height + self.footer_height) @@ -71,8 +73,9 @@ impl<'a> GridLayouter<'a> { if skipped_region { // Simulate the footer again; the region's 'full' might have // changed. - self.footer_height = - self.simulate_footer(footer, &self.regions, engine)?.height; + self.footer_height = self + .simulate_footer(footer, &self.regions, engine, disambiguator)? + .height; } } @@ -81,7 +84,7 @@ impl<'a> GridLayouter<'a> { // within 'layout_row'. self.unbreakable_rows_left += header.end; for y in 0..header.end { - self.layout_row(y, engine)?; + self.layout_row(y, engine, disambiguator)?; } Ok(()) } @@ -92,16 +95,20 @@ impl<'a> GridLayouter<'a> { header: &Header, regions: &Regions<'_>, engine: &mut Engine, + disambiguator: usize, ) -> SourceResult { // Note that we assume the invariant that any rowspan in a header is // fully contained within that header. Therefore, there won't be any // unbreakable rowspans exceeding the header's rows, and we can safely // assume that the amount of unbreakable rows following the first row // in the header will be precisely the rows in the header. - let header_row_group = - self.simulate_unbreakable_row_group(0, Some(header.end), regions, engine)?; - - Ok(header_row_group) + self.simulate_unbreakable_row_group( + 0, + Some(header.end), + regions, + engine, + disambiguator, + ) } /// Updates `self.footer_height` by simulating the footer, and skips to fitting region. @@ -109,8 +116,11 @@ impl<'a> GridLayouter<'a> { &mut self, footer: &Footer, engine: &mut Engine, + disambiguator: usize, ) -> SourceResult<()> { - let footer_height = self.simulate_footer(footer, &self.regions, engine)?.height; + let footer_height = self + .simulate_footer(footer, &self.regions, engine, disambiguator)? + .height; let mut skipped_region = false; while self.unbreakable_rows_left == 0 && !self.regions.size.y.fits(footer_height) @@ -125,7 +135,8 @@ impl<'a> GridLayouter<'a> { self.footer_height = if skipped_region { // Simulate the footer again; the region's 'full' might have // changed. - self.simulate_footer(footer, &self.regions, engine)?.height + self.simulate_footer(footer, &self.regions, engine, disambiguator)? + .height } else { footer_height }; @@ -139,6 +150,7 @@ impl<'a> GridLayouter<'a> { &mut self, footer: &Footer, engine: &mut Engine, + disambiguator: usize, ) -> SourceResult<()> { // Ensure footer rows have their own height available. // Won't change much as we're creating an unbreakable row group @@ -148,7 +160,7 @@ impl<'a> GridLayouter<'a> { let footer_len = self.grid.rows.len() - footer.start; self.unbreakable_rows_left += footer_len; for y in footer.start..self.grid.rows.len() { - self.layout_row(y, engine)?; + self.layout_row(y, engine, disambiguator)?; } Ok(()) @@ -160,19 +172,19 @@ impl<'a> GridLayouter<'a> { footer: &Footer, regions: &Regions<'_>, engine: &mut Engine, + disambiguator: usize, ) -> SourceResult { // Note that we assume the invariant that any rowspan in a footer is // fully contained within that footer. Therefore, there won't be any // unbreakable rowspans exceeding the footer's rows, and we can safely // assume that the amount of unbreakable rows following the first row // in the footer will be precisely the rows in the footer. - let footer_row_group = self.simulate_unbreakable_row_group( + self.simulate_unbreakable_row_group( footer.start, Some(self.grid.rows.len() - footer.start), regions, engine, - )?; - - Ok(footer_row_group) + disambiguator, + ) } } diff --git a/crates/typst/src/layout/grid/rowspans.rs b/crates/typst/src/layout/grid/rowspans.rs index 282616adb..85ec49c99 100644 --- a/crates/typst/src/layout/grid/rowspans.rs +++ b/crates/typst/src/layout/grid/rowspans.rs @@ -12,6 +12,8 @@ pub(super) struct Rowspan { pub(super) x: usize, /// First row of this rowspan. pub(super) y: usize, + /// The disambiguator for laying out the cells. + pub(super) disambiguator: usize, /// Amount of rows spanned by the cell at (x, y). pub(super) rowspan: usize, /// Whether all rows of the rowspan are part of an unbreakable row group. @@ -100,6 +102,7 @@ impl<'a> GridLayouter<'a> { let Rowspan { x, y, + disambiguator, rowspan, is_effectively_unbreakable, dx, @@ -136,7 +139,7 @@ impl<'a> GridLayouter<'a> { } // Push the layouted frames directly into the finished frames. - let fragment = cell.body.layout(engine, self.styles, pod)?; + let fragment = cell.layout(engine, disambiguator, self.styles, pod)?; let (current_region, current_rrows) = current_region_data.unzip(); for ((i, finished), frame) in self .finished @@ -179,7 +182,7 @@ impl<'a> GridLayouter<'a> { /// Checks if a row contains the beginning of one or more rowspan cells. /// If so, adds them to the rowspans vector. - pub(super) fn check_for_rowspans(&mut self, y: usize) { + pub(super) fn check_for_rowspans(&mut self, disambiguator: usize, y: usize) { // We will compute the horizontal offset of each rowspan in advance. // For that reason, we must reverse the column order when using RTL. let offsets = points(self.rcols.iter().copied().rev_if(self.is_rtl)); @@ -193,6 +196,7 @@ impl<'a> GridLayouter<'a> { self.rowspans.push(Rowspan { x, y, + disambiguator, rowspan, // The field below will be updated in // 'check_for_unbreakable_rows'. @@ -241,6 +245,7 @@ impl<'a> GridLayouter<'a> { amount_unbreakable_rows, &self.regions, engine, + 0, )?; // Skip to fitting region. @@ -250,7 +255,7 @@ impl<'a> GridLayouter<'a> { self.header_height + self.footer_height, ) { - self.finish_region(engine)?; + self.finish_region(engine, false)?; } // Update unbreakable rows left. @@ -291,6 +296,7 @@ impl<'a> GridLayouter<'a> { amount_unbreakable_rows: Option, regions: &Regions<'_>, engine: &mut Engine, + disambiguator: usize, ) -> SourceResult { let mut row_group = UnbreakableRowGroup::default(); let mut unbreakable_rows_left = amount_unbreakable_rows.unwrap_or(0); @@ -319,6 +325,7 @@ impl<'a> GridLayouter<'a> { Sizing::Auto => self .measure_auto_row( engine, + disambiguator, y, false, unbreakable_rows_left, @@ -657,6 +664,7 @@ impl<'a> GridLayouter<'a> { /// auto row will have to expand, given the current sizes of the auto row /// in each region and the pending rowspans' data (parent Y, rowspan amount /// and vector of requested sizes). + #[allow(clippy::too_many_arguments)] pub(super) fn simulate_and_measure_rowspans_in_auto_row( &self, y: usize, @@ -664,6 +672,7 @@ impl<'a> GridLayouter<'a> { pending_rowspans: &[(usize, usize, Vec)], unbreakable_rows_left: usize, row_group_data: Option<&UnbreakableRowGroup>, + mut disambiguator: usize, engine: &mut Engine, ) -> SourceResult<()> { // To begin our simulation, we have to unify the sizes demanded by @@ -726,6 +735,7 @@ impl<'a> GridLayouter<'a> { // expand) because we popped the last resolved size from the // resolved vector, above. simulated_regions.next(); + disambiguator += 1; // Subtract the initial header and footer height, since that's the // height we used when subtracting from the region backlog's @@ -749,6 +759,7 @@ impl<'a> GridLayouter<'a> { engine, last_resolved_size, unbreakable_rows_left, + disambiguator, )?; if !simulations_stabilized { @@ -839,6 +850,7 @@ impl<'a> GridLayouter<'a> { engine: &mut Engine, last_resolved_size: Option, unbreakable_rows_left: usize, + mut disambiguator: usize, ) -> SourceResult { // The max amount this row can expand will be the total size requested // by rowspans which was not yet resolved. It is worth noting that, @@ -861,6 +873,7 @@ impl<'a> GridLayouter<'a> { // of the requested rowspan height, we give up. for _attempt in 0..5 { let rowspan_simulator = RowspanSimulator::new( + disambiguator, simulated_regions, self.header_height, self.footer_height, @@ -947,6 +960,7 @@ impl<'a> GridLayouter<'a> { extra_amount_to_grow -= simulated_regions.size.y.max(Abs::zero()); simulated_regions.next(); simulated_regions.size.y -= self.header_height + self.footer_height; + disambiguator += 1; } simulated_regions.size.y -= extra_amount_to_grow; } @@ -958,6 +972,8 @@ impl<'a> GridLayouter<'a> { /// Auxiliary structure holding state during rowspan simulation. struct RowspanSimulator<'a> { + /// The number of finished regions. + finished: usize, /// The state of regions during the simulation. regions: Regions<'a>, /// The height of the header in the currently simulated region. @@ -974,8 +990,14 @@ struct RowspanSimulator<'a> { impl<'a> RowspanSimulator<'a> { /// Creates new rowspan simulation state with the given regions and initial /// header and footer heights. Other fields should always start as zero. - fn new(regions: Regions<'a>, header_height: Abs, footer_height: Abs) -> Self { + fn new( + finished: usize, + regions: Regions<'a>, + header_height: Abs, + footer_height: Abs, + ) -> Self { Self { + finished, regions, header_height, footer_height, @@ -1024,6 +1046,7 @@ impl<'a> RowspanSimulator<'a> { None, &self.regions, engine, + 0, )?; while !self.regions.size.y.fits(row_group.height) && !in_last_with_offset( @@ -1099,16 +1122,21 @@ impl<'a> RowspanSimulator<'a> { // backlog to consider the initial header and footer heights; however, // our simulation checks what happens AFTER the auto row, so we can // just use the original backlog from `self.regions`. + let disambiguator = self.finished; let header_height = if let Some(Repeatable::Repeated(header)) = &layouter.grid.header { - layouter.simulate_header(header, &self.regions, engine)?.height + layouter + .simulate_header(header, &self.regions, engine, disambiguator)? + .height } else { Abs::zero() }; let footer_height = if let Some(Repeatable::Repeated(footer)) = &layouter.grid.footer { - layouter.simulate_footer(footer, &self.regions, engine)?.height + layouter + .simulate_footer(footer, &self.regions, engine, disambiguator)? + .height } else { Abs::zero() }; @@ -1120,6 +1148,7 @@ impl<'a> RowspanSimulator<'a> { && !self.regions.in_last() { self.regions.next(); + self.finished += 1; skipped_region = true; } @@ -1127,7 +1156,9 @@ impl<'a> RowspanSimulator<'a> { self.header_height = if skipped_region { // Simulate headers again, at the new region, as // the full region height may change. - layouter.simulate_header(header, &self.regions, engine)?.height + layouter + .simulate_header(header, &self.regions, engine, disambiguator)? + .height } else { header_height }; @@ -1137,7 +1168,9 @@ impl<'a> RowspanSimulator<'a> { self.footer_height = if skipped_region { // Simulate footers again, at the new region, as // the full region height may change. - layouter.simulate_footer(footer, &self.regions, engine)?.height + layouter + .simulate_footer(footer, &self.regions, engine, disambiguator)? + .height } else { footer_height }; @@ -1162,6 +1195,7 @@ impl<'a> RowspanSimulator<'a> { self.total_spanned_height -= self.latest_spanned_gutter_height; self.latest_spanned_gutter_height = Abs::zero(); self.regions.next(); + self.finished += 1; self.simulate_header_footer_layout(layouter, engine) } diff --git a/crates/typst/src/layout/inline/mod.rs b/crates/typst/src/layout/inline/mod.rs index 4fffa7eb1..e6e3d88ce 100644 --- a/crates/typst/src/layout/inline/mod.rs +++ b/crates/typst/src/layout/inline/mod.rs @@ -1,7 +1,7 @@ mod linebreak; mod shaping; -use comemo::{Tracked, TrackedMut}; +use comemo::{Track, Tracked, TrackedMut}; use unicode_bidi::{BidiInfo, Level as BidiLevel}; use unicode_script::{Script, UnicodeScript}; @@ -14,7 +14,7 @@ use crate::diag::{bail, SourceResult}; use crate::engine::{Engine, Route}; use crate::eval::Tracer; use crate::foundations::{Packed, Resolve, Smart, StyleChain}; -use crate::introspection::{Introspector, Locator, TagElem}; +use crate::introspection::{Introspector, Locator, LocatorLink, Tag, TagElem}; use crate::layout::{ Abs, AlignElem, BoxElem, Dir, Em, FixedAlignment, Fr, Fragment, Frame, FrameItem, HElem, InlineElem, InlineItem, Point, Size, Sizing, Spacing, @@ -33,6 +33,7 @@ use crate::World; pub(crate) fn layout_inline( children: &StyleVec, engine: &mut Engine, + locator: Locator, styles: StyleChain, consecutive: bool, region: Size, @@ -45,25 +46,25 @@ pub(crate) fn layout_inline( world: Tracked, introspector: Tracked, route: Tracked, - locator: Tracked, tracer: TrackedMut, + locator: Tracked, styles: StyleChain, consecutive: bool, region: Size, expand: bool, ) -> SourceResult { - let mut locator = Locator::chained(locator); + let link = LocatorLink::new(locator); + let locator = Locator::link(&link); let mut engine = Engine { world, introspector, route: Route::extend(route), - locator: &mut locator, tracer, }; // Collect all text into one string for BiDi analysis. let (text, segments, spans) = - collect(children, &mut engine, &styles, region, consecutive)?; + collect(children, &mut engine, locator, &styles, region, consecutive)?; // Perform BiDi analysis and then prepare paragraph layout by building a // representation on which we can do line breaking without layouting @@ -78,21 +79,18 @@ pub(crate) fn layout_inline( finalize(&mut engine, &p, &lines, region, expand, shrink) } - let fragment = cached( + cached( children, engine.world, engine.introspector, engine.route.track(), - engine.locator.track(), TrackedMut::reborrow_mut(&mut engine.tracer), + locator.track(), styles, consecutive, region, expand, - )?; - - engine.locator.visit_frames(&fragment); - Ok(fragment) + ) } /// Range of a substring of text. @@ -223,11 +221,11 @@ enum Item<'a> { /// Absolute spacing between other items, and whether it is weak. Absolute(Abs, bool), /// Fractional spacing between other items. - Fractional(Fr, Option<(&'a Packed, StyleChain<'a>)>), + Fractional(Fr, Option<(&'a Packed, Locator<'a>, StyleChain<'a>)>), /// Layouted inline-level content. Frame(Frame, StyleChain<'a>), /// A tag. - Tag(&'a Packed), + Tag(&'a Tag), /// An item that is invisible and needs to be skipped, e.g. a Unicode /// isolate. Skip(&'static str), @@ -431,12 +429,14 @@ impl<'a> Line<'a> { fn collect<'a>( children: &'a StyleVec, engine: &mut Engine<'_>, + locator: Locator<'a>, styles: &'a StyleChain<'a>, region: Size, consecutive: bool, ) -> SourceResult<(String, Vec>, SpanMapper)> { let mut collector = Collector::new(2 + children.len()); let mut iter = children.chain(styles).peekable(); + let mut locator = locator.split(); let first_line_indent = ParElem::first_line_indent_in(*styles); if !first_line_indent.is_zero() @@ -535,7 +535,7 @@ fn collect<'a>( } else if let Some(elem) = child.to_packed::() { collector.push_item(Item::Skip(LTR_ISOLATE)); - for item in elem.layout(engine, styles, region)? { + for item in elem.layout(engine, locator.next(&elem.span()), styles, region)? { match item { InlineItem::Space(space, weak) => { collector.push_item(Item::Absolute(space, weak)); @@ -548,14 +548,15 @@ fn collect<'a>( collector.push_item(Item::Skip(POP_ISOLATE)); } else if let Some(elem) = child.to_packed::() { + let loc = locator.next(&elem.span()); if let Sizing::Fr(v) = elem.width(styles) { - collector.push_item(Item::Fractional(v, Some((elem, styles)))); + collector.push_item(Item::Fractional(v, Some((elem, loc, styles)))); } else { - let frame = elem.layout(engine, styles, region)?; + let frame = elem.layout(engine, loc, styles, region)?; collector.push_item(Item::Frame(frame, styles)); } } else if let Some(elem) = child.to_packed::() { - collector.push_item(Item::Tag(elem)); + collector.push_item(Item::Tag(&elem.tag)); } else { bail!(child.span(), "unexpected paragraph child"); }; @@ -1408,9 +1409,10 @@ fn commit( } Item::Fractional(v, elem) => { let amount = v.share(fr, remaining); - if let Some((elem, styles)) = elem { + if let Some((elem, loc, styles)) = elem { let region = Size::new(amount, full); - let mut frame = elem.layout(engine, *styles, region)?; + let mut frame = + elem.layout(engine, loc.relayout(), *styles, region)?; frame.post_process(*styles); frame.translate(Point::with_y(TextElem::baseline_in(*styles))); push(&mut offset, frame); @@ -1432,7 +1434,7 @@ fn commit( } Item::Tag(tag) => { let mut frame = Frame::soft(Size::zero()); - frame.push(Point::zero(), FrameItem::Tag(tag.elem.clone())); + frame.push(Point::zero(), FrameItem::Tag((*tag).clone())); frames.push((offset, frame)); } Item::Skip(_) => {} diff --git a/crates/typst/src/layout/layout.rs b/crates/typst/src/layout/layout.rs index b7293640a..efe5d1247 100644 --- a/crates/typst/src/layout/layout.rs +++ b/crates/typst/src/layout/layout.rs @@ -76,18 +76,26 @@ struct LayoutElem { impl Show for Packed { fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult { - Ok(BlockElem::multi_layouter(self.clone(), |elem, engine, styles, regions| { - // Gets the current region's base size, which will be the size of the - // outer container, or of the page if there is no such container. - let Size { x, y } = regions.base(); - let loc = elem.location().unwrap(); - let context = Context::new(Some(loc), Some(styles)); - let result = elem - .func() - .call(engine, context.track(), [dict! { "width" => x, "height" => y }])? - .display(); - result.layout(engine, styles, regions) - }) - .pack()) + Ok(BlockElem::multi_layouter( + self.clone(), + |elem, engine, locator, styles, regions| { + // Gets the current region's base size, which will be the size of the + // outer container, or of the page if there is no such container. + let Size { x, y } = regions.base(); + let loc = elem.location().unwrap(); + let context = Context::new(Some(loc), Some(styles)); + let result = elem + .func() + .call( + engine, + context.track(), + [dict! { "width" => x, "height" => y }], + )? + .display(); + result.layout(engine, locator, styles, regions) + }, + ) + .pack() + .spanned(self.span())) } } diff --git a/crates/typst/src/layout/measure.rs b/crates/typst/src/layout/measure.rs index cae9d9e02..af093f243 100644 --- a/crates/typst/src/layout/measure.rs +++ b/crates/typst/src/layout/measure.rs @@ -5,6 +5,7 @@ use crate::engine::Engine; use crate::foundations::{ dict, func, Content, Context, Dict, Resolve, Smart, StyleChain, Styles, }; +use crate::introspection::{Locator, LocatorLink}; use crate::layout::{Abs, Axes, Length, Regions, Size}; use crate::syntax::Span; @@ -85,13 +86,24 @@ pub fn measure( None => context.styles().at(span)?, }; - let available = Axes::new( - width.resolve(styles).unwrap_or(Abs::inf()), - height.resolve(styles).unwrap_or(Abs::inf()), + // Create a pod region with the available space. + let pod = Regions::one( + Axes::new( + width.resolve(styles).unwrap_or(Abs::inf()), + height.resolve(styles).unwrap_or(Abs::inf()), + ), + Axes::splat(false), ); - let pod = Regions::one(available, Axes::splat(false)); - let frame = content.measure(engine, styles, pod)?.into_frame(); + // We put the locator into a special "measurement mode" to ensure that + // introspection-driven features within the content continue to work. Read + // the "Dealing with measurement" section of the [`Locator`] docs for more + // details. + let here = context.location().at(span)?; + let link = LocatorLink::measure(here); + let locator = Locator::link(&link); + + let frame = content.layout(engine, locator, styles, pod)?.into_frame(); let Size { x, y } = frame.size(); Ok(dict! { "width" => x, "height" => y }) } diff --git a/crates/typst/src/layout/mod.rs b/crates/typst/src/layout/mod.rs index ac1452ca6..843b43f54 100644 --- a/crates/typst/src/layout/mod.rs +++ b/crates/typst/src/layout/mod.rs @@ -69,13 +69,13 @@ pub use self::transform::*; pub(crate) use self::inline::*; -use comemo::{Tracked, TrackedMut}; +use comemo::{Track, Tracked, TrackedMut}; use crate::diag::{bail, SourceResult}; use crate::engine::{Engine, Route}; use crate::eval::Tracer; use crate::foundations::{category, Category, Content, Scope, StyleChain}; -use crate::introspection::{Introspector, Locator}; +use crate::introspection::{Introspector, Locator, LocatorLink}; use crate::model::Document; use crate::realize::{realize_doc, realize_flow, Arenas}; use crate::World; @@ -138,21 +138,20 @@ impl Content { world: Tracked, introspector: Tracked, route: Tracked, - locator: Tracked, tracer: TrackedMut, styles: StyleChain, ) -> SourceResult { - let mut locator = Locator::chained(locator); + let mut locator = Locator::root().split(); let mut engine = Engine { world, introspector, route: Route::extend(route).unnested(), - locator: &mut locator, tracer, }; let arenas = Arenas::default(); - let (document, styles) = realize_doc(&mut engine, &arenas, content, styles)?; - document.layout(&mut engine, styles) + let (document, styles) = + realize_doc(&mut engine, locator.next(&()), &arenas, content, styles)?; + document.layout(&mut engine, locator.next(&()), styles) } cached( @@ -160,7 +159,6 @@ impl Content { engine.world, engine.introspector, engine.route.track(), - engine.locator.track(), TrackedMut::reborrow_mut(&mut engine.tracer), styles, ) @@ -170,22 +168,7 @@ impl Content { pub fn layout( &self, engine: &mut Engine, - styles: StyleChain, - regions: Regions, - ) -> SourceResult { - let fragment = self.measure(engine, styles, regions)?; - engine.locator.visit_frames(&fragment); - Ok(fragment) - } - - /// Layout without side effects. - /// - /// For the results to be valid, the element must either be layouted again - /// or the measurement must be confirmed through a call to - /// `engine.locator.visit_frames(&fragment)`. - pub fn measure( - &self, - engine: &mut Engine, + locator: Locator, styles: StyleChain, regions: Regions, ) -> SourceResult { @@ -196,17 +179,17 @@ impl Content { world: Tracked, introspector: Tracked, route: Tracked, - locator: Tracked, tracer: TrackedMut, + locator: Tracked, styles: StyleChain, regions: Regions, ) -> SourceResult { - let mut locator = Locator::chained(locator); + let link = LocatorLink::new(locator); + let locator = Locator::link(&link); let mut engine = Engine { world, introspector, route: Route::extend(route), - locator: &mut locator, tracer, }; @@ -219,14 +202,16 @@ impl Content { // If we are in a `PageElem`, this might already be a realized flow. if let Some(flow) = content.to_packed::() { - return flow.layout(&mut engine, styles, regions); + return flow.layout(&mut engine, locator, styles, regions); } // Layout the content by first turning it into a `FlowElem` and then // layouting that. + let mut locator = locator.split(); let arenas = Arenas::default(); - let (flow, styles) = realize_flow(&mut engine, &arenas, content, styles)?; - flow.layout(&mut engine, styles, regions) + let (flow, styles) = + realize_flow(&mut engine, locator.next(&()), &arenas, content, styles)?; + flow.layout(&mut engine, locator.next(&()), styles, regions) } cached( @@ -234,8 +219,8 @@ impl Content { engine.world, engine.introspector, engine.route.track(), - engine.locator.track(), TrackedMut::reborrow_mut(&mut engine.tracer), + locator.track(), styles, regions, ) diff --git a/crates/typst/src/layout/pad.rs b/crates/typst/src/layout/pad.rs index 6e1e6258f..814ccdc5f 100644 --- a/crates/typst/src/layout/pad.rs +++ b/crates/typst/src/layout/pad.rs @@ -3,6 +3,7 @@ use crate::engine::Engine; use crate::foundations::{ elem, Content, NativeElement, Packed, Resolve, Show, StyleChain, }; +use crate::introspection::Locator; use crate::layout::{ Abs, BlockElem, Fragment, Frame, Length, Point, Regions, Rel, Sides, Size, }; @@ -64,7 +65,9 @@ pub struct PadElem { impl Show for Packed { fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult { - Ok(BlockElem::multi_layouter(self.clone(), layout_pad).pack()) + Ok(BlockElem::multi_layouter(self.clone(), layout_pad) + .pack() + .spanned(self.span())) } } @@ -73,6 +76,7 @@ impl Show for Packed { fn layout_pad( elem: &Packed, engine: &mut Engine, + locator: Locator, styles: StyleChain, regions: Regions, ) -> SourceResult { @@ -87,7 +91,7 @@ fn layout_pad( let pod = regions.map(&mut backlog, |size| shrink(size, &padding)); // Layout child into padded regions. - let mut fragment = elem.body().layout(engine, styles, pod)?; + let mut fragment = elem.body().layout(engine, locator, styles, pod)?; for frame in &mut fragment { grow(frame, &padding); diff --git a/crates/typst/src/layout/page.rs b/crates/typst/src/layout/page.rs index 42c267ed5..1c1e05150 100644 --- a/crates/typst/src/layout/page.rs +++ b/crates/typst/src/layout/page.rs @@ -12,7 +12,9 @@ use crate::foundations::{ cast, elem, AutoValue, Cast, Content, Context, Dict, Fold, Func, NativeElement, Packed, Resolve, Smart, StyleChain, Value, }; -use crate::introspection::{Counter, CounterDisplayElem, CounterKey, ManualPageCounter}; +use crate::introspection::{ + Counter, CounterDisplayElem, CounterKey, Locator, ManualPageCounter, +}; use crate::layout::{ Abs, AlignElem, Alignment, Axes, ColumnsElem, Dir, Frame, HAlignment, Length, OuterVAlignment, Point, Ratio, Regions, Rel, Sides, Size, SpecificAlignment, @@ -349,10 +351,13 @@ impl Packed { pub fn layout( &self, engine: &mut Engine, + locator: Locator, styles: StyleChain, page_counter: &mut ManualPageCounter, extend_to: Option, ) -> SourceResult> { + let mut locator = locator.split(); + // When one of the lengths is infinite the page fits its content along // that axis. let width = self.width(styles).unwrap_or(Abs::inf()); @@ -400,7 +405,9 @@ impl Packed { regions.root = true; // Layout the child. - let mut frames = child.layout(engine, styles, regions)?.into_frames(); + let mut frames = child + .layout(engine, locator.next(&self.span()), styles, regions)? + .into_frames(); // Align the child to the pagebreak's parity. // Check for page count after adding the pending frames @@ -504,7 +511,7 @@ impl Packed { let sub = content .clone() .styled(AlignElem::set_alignment(align)) - .layout(engine, styles, pod)? + .layout(engine, locator.next(&content.span()), styles, pod)? .into_frame(); if ptr::eq(marginal, header) || ptr::eq(marginal, background) { diff --git a/crates/typst/src/layout/place.rs b/crates/typst/src/layout/place.rs index d87913275..78922c1bb 100644 --- a/crates/typst/src/layout/place.rs +++ b/crates/typst/src/layout/place.rs @@ -1,6 +1,7 @@ use crate::diag::{bail, At, Hint, SourceResult}; use crate::engine::Engine; use crate::foundations::{elem, scope, Content, Packed, Smart, StyleChain, Unlabellable}; +use crate::introspection::Locator; use crate::layout::{ Alignment, Axes, Em, Fragment, Length, Regions, Rel, Size, VAlignment, }; @@ -108,6 +109,7 @@ impl Packed { pub fn layout( &self, engine: &mut Engine, + locator: Locator, styles: StyleChain, base: Size, ) -> SourceResult { @@ -134,7 +136,7 @@ impl Packed { .aligned(alignment.unwrap_or_else(|| Alignment::CENTER)); let pod = Regions::one(base, Axes::splat(false)); - let frame = child.layout(engine, styles, pod)?.into_frame(); + let frame = child.layout(engine, locator, styles, pod)?.into_frame(); Ok(Fragment::frame(frame)) } } diff --git a/crates/typst/src/layout/repeat.rs b/crates/typst/src/layout/repeat.rs index 089054669..4a0911e48 100644 --- a/crates/typst/src/layout/repeat.rs +++ b/crates/typst/src/layout/repeat.rs @@ -3,6 +3,7 @@ use crate::engine::Engine; use crate::foundations::{ elem, Content, NativeElement, Packed, Resolve, Show, StyleChain, }; +use crate::introspection::Locator; use crate::layout::{ Abs, AlignElem, Axes, BlockElem, Fragment, Frame, Point, Regions, Size, }; @@ -38,7 +39,9 @@ pub struct RepeatElem { impl Show for Packed { fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult { - Ok(BlockElem::multi_layouter(self.clone(), layout_repeat).pack()) + Ok(BlockElem::multi_layouter(self.clone(), layout_repeat) + .pack() + .spanned(self.span())) } } @@ -47,11 +50,13 @@ impl Show for Packed { fn layout_repeat( elem: &Packed, engine: &mut Engine, + locator: Locator, styles: StyleChain, regions: Regions, ) -> SourceResult { let pod = Regions::one(regions.size, Axes::new(false, false)); - let piece = elem.body().layout(engine, styles, pod)?.into_frame(); + let piece = elem.body().layout(engine, locator, styles, pod)?.into_frame(); + let align = AlignElem::alignment_in(styles).resolve(styles); let fill = regions.size.x; diff --git a/crates/typst/src/layout/stack.rs b/crates/typst/src/layout/stack.rs index 8271ff00a..3d8002ab2 100644 --- a/crates/typst/src/layout/stack.rs +++ b/crates/typst/src/layout/stack.rs @@ -6,6 +6,7 @@ use crate::engine::Engine; use crate::foundations::{ cast, elem, Content, NativeElement, Packed, Resolve, Show, StyleChain, StyledElem, }; +use crate::introspection::{Locator, SplitLocator}; use crate::layout::{ Abs, AlignElem, Axes, Axis, BlockElem, Dir, FixedAlignment, Fr, Fragment, Frame, HElem, Point, Regions, Size, Spacing, VElem, @@ -56,7 +57,9 @@ pub struct StackElem { impl Show for Packed { fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult { - Ok(BlockElem::multi_layouter(self.clone(), layout_stack).pack()) + Ok(BlockElem::multi_layouter(self.clone(), layout_stack) + .pack() + .spanned(self.span())) } } @@ -93,10 +96,13 @@ cast! { fn layout_stack( elem: &Packed, engine: &mut Engine, + locator: Locator, styles: StyleChain, regions: Regions, ) -> SourceResult { - let mut layouter = StackLayouter::new(elem.span(), elem.dir(styles), regions, styles); + let mut layouter = + StackLayouter::new(elem.span(), elem.dir(styles), locator, styles, regions); + let axis = layouter.dir.axis(); // Spacing to insert before the next block. @@ -145,10 +151,12 @@ struct StackLayouter<'a> { dir: Dir, /// The axis of the stacking direction. axis: Axis, - /// The regions to layout children into. - regions: Regions<'a>, + /// Provides unique locations to the stack's children. + locator: SplitLocator<'a>, /// The inherited styles. styles: StyleChain<'a>, + /// The regions to layout children into. + regions: Regions<'a>, /// Whether the stack itself should expand to fill the region. expand: Axes, /// The initial size of the current region before we started subtracting. @@ -179,8 +187,9 @@ impl<'a> StackLayouter<'a> { fn new( span: Span, dir: Dir, - mut regions: Regions<'a>, + locator: Locator<'a>, styles: StyleChain<'a>, + mut regions: Regions<'a>, ) -> Self { let axis = dir.axis(); let expand = regions.expand; @@ -192,8 +201,9 @@ impl<'a> StackLayouter<'a> { span, dir, axis, - regions, + locator: locator.split(), styles, + regions, expand, initial: regions.size, used: GenericSize::zero(), @@ -247,7 +257,13 @@ impl<'a> StackLayouter<'a> { } .resolve(styles); - let fragment = block.layout(engine, styles, self.regions)?; + let fragment = block.layout( + engine, + self.locator.next(&block.span()), + styles, + self.regions, + )?; + let len = fragment.len(); for (i, frame) in fragment.into_iter().enumerate() { // Grow our size, shrink the region and save the frame for later. diff --git a/crates/typst/src/layout/transform.rs b/crates/typst/src/layout/transform.rs index 0e9b0ca63..7172466f3 100644 --- a/crates/typst/src/layout/transform.rs +++ b/crates/typst/src/layout/transform.rs @@ -3,6 +3,7 @@ use crate::engine::Engine; use crate::foundations::{ elem, Content, NativeElement, Packed, Resolve, Show, StyleChain, }; +use crate::introspection::Locator; use crate::layout::{ Abs, Alignment, Angle, Axes, BlockElem, FixedAlignment, Frame, HAlignment, Length, Point, Ratio, Region, Regions, Rel, Size, VAlignment, @@ -41,7 +42,9 @@ pub struct MoveElem { impl Show for Packed { fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult { - Ok(BlockElem::single_layouter(self.clone(), layout_move).pack()) + Ok(BlockElem::single_layouter(self.clone(), layout_move) + .pack() + .spanned(self.span())) } } @@ -50,12 +53,13 @@ impl Show for Packed { fn layout_move( elem: &Packed, engine: &mut Engine, + locator: Locator, styles: StyleChain, region: Region, ) -> SourceResult { let mut frame = elem .body() - .layout(engine, styles, region.into_regions())? + .layout(engine, locator, styles, region.into_regions())? .into_frame(); let delta = Axes::new(elem.dx(styles), elem.dy(styles)).resolve(styles); let delta = delta.zip_map(region.size, Rel::relative_to); @@ -126,7 +130,9 @@ pub struct RotateElem { impl Show for Packed { fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult { - Ok(BlockElem::single_layouter(self.clone(), layout_rotate).pack()) + Ok(BlockElem::single_layouter(self.clone(), layout_rotate) + .pack() + .spanned(self.span())) } } @@ -135,6 +141,7 @@ impl Show for Packed { fn layout_rotate( elem: &Packed, engine: &mut Engine, + locator: Locator, styles: StyleChain, region: Region, ) -> SourceResult { @@ -151,6 +158,7 @@ fn layout_rotate( measure_and_layout( engine, + locator, region, size, styles, @@ -219,7 +227,9 @@ pub struct ScaleElem { impl Show for Packed { fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult { - Ok(BlockElem::single_layouter(self.clone(), layout_scale).pack()) + Ok(BlockElem::single_layouter(self.clone(), layout_scale) + .pack() + .spanned(self.span())) } } @@ -228,6 +238,7 @@ impl Show for Packed { fn layout_scale( elem: &Packed, engine: &mut Engine, + locator: Locator, styles: StyleChain, region: Region, ) -> SourceResult { @@ -240,6 +251,7 @@ fn layout_scale( measure_and_layout( engine, + locator, region, size, styles, @@ -379,6 +391,7 @@ impl Default for Transform { #[allow(clippy::too_many_arguments)] fn measure_and_layout( engine: &mut Engine, + locator: Locator, region: Region, size: Size, styles: StyleChain, @@ -390,11 +403,11 @@ fn measure_and_layout( if reflow { // Measure the size of the body. let pod = Regions::one(size, Axes::splat(false)); - let frame = body.measure(engine, styles, pod)?.into_frame(); + let frame = body.layout(engine, locator.relayout(), styles, pod)?.into_frame(); // Actually perform the layout. let pod = Regions::one(frame.size(), Axes::splat(true)); - let mut frame = body.layout(engine, styles, pod)?.into_frame(); + let mut frame = body.layout(engine, locator, styles, pod)?.into_frame(); let Axes { x, y } = align.zip_map(frame.size(), FixedAlignment::position); // Compute the transform. @@ -410,7 +423,9 @@ fn measure_and_layout( Ok(frame) } else { // Layout the body. - let mut frame = body.layout(engine, styles, region.into_regions())?.into_frame(); + let mut frame = body + .layout(engine, locator, styles, region.into_regions())? + .into_frame(); let Axes { x, y } = align.zip_map(frame.size(), FixedAlignment::position); // Compute the transform. diff --git a/crates/typst/src/lib.rs b/crates/typst/src/lib.rs index 49f2c32bb..6d445edd6 100644 --- a/crates/typst/src/lib.rs +++ b/crates/typst/src/lib.rs @@ -69,7 +69,7 @@ use crate::eval::Tracer; use crate::foundations::{ Array, Bytes, Content, Datetime, Dict, Module, Scope, StyleChain, Styles, Value, }; -use crate::introspection::{Introspector, Locator}; +use crate::introspection::Introspector; use crate::layout::{Alignment, Dir}; use crate::model::Document; use crate::syntax::package::PackageSpec; @@ -129,12 +129,10 @@ fn typeset( tracer.delayed(); let constraint = ::Constraint::new(); - let mut locator = Locator::new(); let mut engine = Engine { world, route: Route::default(), tracer: tracer.track_mut(), - locator: &mut locator, introspector: document.introspector.track_with(&constraint), }; diff --git a/crates/typst/src/math/ctx.rs b/crates/typst/src/math/ctx.rs index b5ed66827..ac3fbca85 100644 --- a/crates/typst/src/math/ctx.rs +++ b/crates/typst/src/math/ctx.rs @@ -12,6 +12,7 @@ use unicode_segmentation::UnicodeSegmentation; use crate::diag::SourceResult; use crate::engine::Engine; use crate::foundations::{Content, Packed, StyleChain}; +use crate::introspection::{Locator, SplitLocator}; use crate::layout::{Abs, Axes, BoxElem, Em, Frame, Regions, Size}; use crate::math::{ scaled_font_size, styled_char, EquationElem, FrameFragment, GlyphFragment, @@ -49,6 +50,7 @@ macro_rules! percent { pub struct MathContext<'a, 'b, 'v> { // External. pub engine: &'v mut Engine<'b>, + pub locator: SplitLocator<'v>, pub regions: Regions<'static>, // Font-related. pub font: &'a Font, @@ -65,6 +67,7 @@ pub struct MathContext<'a, 'b, 'v> { impl<'a, 'b, 'v> MathContext<'a, 'b, 'v> { pub fn new( engine: &'v mut Engine<'b>, + locator: Locator<'v>, styles: StyleChain<'a>, base: Size, font: &'a Font, @@ -103,6 +106,7 @@ impl<'a, 'b, 'v> MathContext<'a, 'b, 'v> { Self { engine, + locator: locator.split(), regions: Regions::one(base, Axes::splat(false)), font, ttf: font.ttf(), @@ -174,7 +178,12 @@ impl<'a, 'b, 'v> MathContext<'a, 'b, 'v> { ) -> SourceResult { let local = TextElem::set_size(TextSize(scaled_font_size(self, styles).into())).wrap(); - boxed.layout(self.engine, styles.chain(&local), self.regions.base()) + boxed.layout( + self.engine, + self.locator.next(&boxed.span()), + styles.chain(&local), + self.regions.base(), + ) } /// Layout the given [`Content`] into a [`Frame`]. @@ -186,7 +195,12 @@ impl<'a, 'b, 'v> MathContext<'a, 'b, 'v> { let local = TextElem::set_size(TextSize(scaled_font_size(self, styles).into())).wrap(); Ok(content - .layout(self.engine, styles.chain(&local), self.regions)? + .layout( + self.engine, + self.locator.next(&content.span()), + styles.chain(&local), + self.regions, + )? .into_frame()) } @@ -290,7 +304,14 @@ impl<'a, 'b, 'v> MathContext<'a, 'b, 'v> { let par = ParElem::new(StyleVec::wrap(eco_vec![text])); let frame = Packed::new(par) .spanned(span) - .layout(self.engine, styles, false, Size::splat(Abs::inf()), false)? + .layout( + self.engine, + self.locator.next(&span), + styles, + false, + Size::splat(Abs::inf()), + false, + )? .into_frame(); Ok(FrameFragment::new(self, styles, frame) diff --git a/crates/typst/src/math/equation.rs b/crates/typst/src/math/equation.rs index d6a43d756..054b28230 100644 --- a/crates/typst/src/math/equation.rs +++ b/crates/typst/src/math/equation.rs @@ -8,7 +8,7 @@ use crate::foundations::{ elem, Content, NativeElement, Packed, Resolve, Show, ShowSet, Smart, StyleChain, Styles, Synthesize, }; -use crate::introspection::{Count, Counter, CounterUpdate, Locatable}; +use crate::introspection::{Count, Counter, CounterUpdate, Locatable, Locator}; use crate::layout::{ Abs, AlignElem, Alignment, Axes, BlockElem, Em, FixedAlignment, Fragment, Frame, InlineElem, InlineItem, OuterHAlignment, Point, Regions, Size, SpecificAlignment, @@ -166,9 +166,13 @@ impl Synthesize for Packed { impl Show for Packed { fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { if self.block(styles) { - Ok(BlockElem::multi_layouter(self.clone(), layout_equation_block).pack()) + Ok(BlockElem::multi_layouter(self.clone(), layout_equation_block) + .pack() + .spanned(self.span())) } else { - Ok(InlineElem::layouter(self.clone(), layout_equation_inline).pack()) + Ok(InlineElem::layouter(self.clone(), layout_equation_inline) + .pack() + .spanned(self.span())) } } } @@ -265,7 +269,8 @@ impl LayoutMath for Packed { #[typst_macros::time(span = elem.span())] fn layout_equation_inline( elem: &Packed, - engine: &mut Engine<'_>, + engine: &mut Engine, + locator: Locator, styles: StyleChain, region: Size, ) -> SourceResult> { @@ -273,7 +278,7 @@ fn layout_equation_inline( let font = find_math_font(engine, styles, elem.span())?; - let mut ctx = MathContext::new(engine, styles, region, &font); + let mut ctx = MathContext::new(engine, locator, styles, region, &font); let run = ctx.layout_into_run(elem, styles)?; let mut items = if run.row_count() == 1 { @@ -311,6 +316,7 @@ fn layout_equation_inline( fn layout_equation_block( elem: &Packed, engine: &mut Engine, + locator: Locator, styles: StyleChain, regions: Regions, ) -> SourceResult { @@ -319,7 +325,9 @@ fn layout_equation_block( let span = elem.span(); let font = find_math_font(engine, styles, span)?; - let mut ctx = MathContext::new(engine, styles, regions.base(), &font); + let mut locator = locator.split(); + let mut ctx = + MathContext::new(engine, locator.next(&()), styles, regions.base(), &font); let full_equation_builder = ctx .layout_into_run(elem, styles)? .multiline_frame_builder(&ctx, styles); @@ -395,7 +403,7 @@ fn layout_equation_block( let number = Counter::of(EquationElem::elem()) .display_at_loc(engine, elem.location().unwrap(), styles, numbering)? .spanned(span) - .layout(engine, styles, pod)? + .layout(engine, locator.next(&()), styles, pod)? .into_frame(); static NUMBER_GUTTER: Em = Em::new(0.5); diff --git a/crates/typst/src/math/mod.rs b/crates/typst/src/math/mod.rs index 3b493b81a..6ef3df9a2 100644 --- a/crates/typst/src/math/mod.rs +++ b/crates/typst/src/math/mod.rs @@ -232,7 +232,7 @@ impl LayoutMath for Content { return elem.layout_math(ctx, styles); } - if let Some(realized) = process(ctx.engine, self, styles)? { + if let Some(realized) = process(ctx.engine, &mut ctx.locator, self, styles)? { return realized.layout_math(ctx, styles); } @@ -296,9 +296,9 @@ impl LayoutMath for Content { return Ok(()); } - if let Some(tag) = self.to_packed::() { + if let Some(elem) = self.to_packed::() { let mut frame = Frame::soft(Size::zero()); - frame.push(Point::zero(), FrameItem::Tag(tag.elem.clone())); + frame.push(Point::zero(), FrameItem::Tag(elem.tag.clone())); ctx.push(FrameFragment::new(ctx, styles, frame)); return Ok(()); } diff --git a/crates/typst/src/model/cite.rs b/crates/typst/src/model/cite.rs index 2d5de1521..e156e0ec9 100644 --- a/crates/typst/src/model/cite.rs +++ b/crates/typst/src/model/cite.rs @@ -1,4 +1,4 @@ -use crate::diag::{bail, At, SourceResult}; +use crate::diag::{error, At, HintedString, SourceResult}; use crate::engine::Engine; use crate::foundations::{ cast, elem, Cast, Content, Label, Packed, Show, Smart, StyleChain, Synthesize, @@ -159,6 +159,17 @@ impl Show for Packed { .citations .get(&location) .cloned() - .unwrap_or_else(|| bail!(span, "failed to format citation (this is a bug)")) + .ok_or_else(failed_to_format_citation) + .at(span)? } } + +/// The error message when a citation wasn't found in the pre-formatted list. +#[cold] +fn failed_to_format_citation() -> HintedString { + error!( + "cannot format citation in isolation"; + hint: "check whether this citation is measured \ + without being inserted into the document" + ) +} diff --git a/crates/typst/src/model/document.rs b/crates/typst/src/model/document.rs index 7bc517ee3..341c077c8 100644 --- a/crates/typst/src/model/document.rs +++ b/crates/typst/src/model/document.rs @@ -6,7 +6,7 @@ use crate::foundations::{ cast, elem, Args, Array, Construct, Content, Datetime, Packed, Smart, StyleChain, Value, }; -use crate::introspection::{Introspector, ManualPageCounter}; +use crate::introspection::{Introspector, Locator, ManualPageCounter}; use crate::layout::{Page, PageElem}; use crate::realize::StyleVec; @@ -76,6 +76,7 @@ impl Packed { pub fn layout( &self, engine: &mut Engine, + locator: Locator, styles: StyleChain, ) -> SourceResult { let mut pages = Vec::with_capacity(self.children().len()); @@ -83,13 +84,20 @@ impl Packed { let children = self.children(); let mut iter = children.chain(&styles).peekable(); + let mut locator = locator.split(); while let Some((child, styles)) = iter.next() { if let Some(page) = child.to_packed::() { let extend_to = iter .peek() .and_then(|(next, _)| *next.to_packed::()?.clear_to()?); - let run = page.layout(engine, styles, &mut page_counter, extend_to)?; + let run = page.layout( + engine, + locator.next(&page.span()), + styles, + &mut page_counter, + extend_to, + )?; pages.extend(run); } else { bail!(child.span(), "unexpected document child"); diff --git a/crates/typst/src/model/enum.rs b/crates/typst/src/model/enum.rs index d03c774cc..9d4cdf42e 100644 --- a/crates/typst/src/model/enum.rs +++ b/crates/typst/src/model/enum.rs @@ -9,6 +9,7 @@ use crate::foundations::{ cast, elem, scope, Array, Content, Context, NativeElement, Packed, Show, Smart, StyleChain, Styles, }; +use crate::introspection::Locator; use crate::layout::{ Alignment, Axes, BlockElem, Cell, CellGrid, Em, Fragment, GridLayouter, HAlignment, Length, Regions, Sizing, Spacing, VAlignment, VElem, @@ -215,7 +216,9 @@ impl EnumElem { impl Show for Packed { fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { - let mut realized = BlockElem::multi_layouter(self.clone(), layout_enum).pack(); + let mut realized = BlockElem::multi_layouter(self.clone(), layout_enum) + .pack() + .spanned(self.span()); if self.tight(styles) { let leading = ParElem::leading_in(styles); @@ -232,6 +235,7 @@ impl Show for Packed { fn layout_enum( elem: &Packed, engine: &mut Engine, + locator: Locator, styles: StyleChain, regions: Regions, ) -> SourceResult { @@ -246,6 +250,7 @@ fn layout_enum( }; let mut cells = vec![]; + let mut locator = locator.split(); let mut number = elem.start(styles); let mut parents = EnumElem::parents_in(styles); @@ -280,11 +285,12 @@ fn layout_enum( let resolved = resolved.aligned(number_align).styled(TextElem::set_overhang(false)); - cells.push(Cell::from(Content::empty())); - cells.push(Cell::from(resolved)); - cells.push(Cell::from(Content::empty())); - cells.push(Cell::from( - item.body().clone().styled(EnumElem::set_parents(smallvec![number])), + cells.push(Cell::new(Content::empty(), locator.next(&()))); + cells.push(Cell::new(resolved, locator.next(&()))); + cells.push(Cell::new(Content::empty(), locator.next(&()))); + cells.push(Cell::new( + item.body.clone().styled(EnumElem::set_parents(smallvec![number])), + locator.next(&item.body.span()), )); number = number.saturating_add(1); } diff --git a/crates/typst/src/model/heading.rs b/crates/typst/src/model/heading.rs index 478b4315b..e160eeea6 100644 --- a/crates/typst/src/model/heading.rs +++ b/crates/typst/src/model/heading.rs @@ -6,7 +6,9 @@ use crate::foundations::{ elem, Content, NativeElement, Packed, Resolve, Show, ShowSet, Smart, StyleChain, Styles, Synthesize, }; -use crate::introspection::{Count, Counter, CounterUpdate, Locatable}; +use crate::introspection::{ + Count, Counter, CounterUpdate, Locatable, Locator, LocatorLink, +}; use crate::layout::{ Abs, Axes, BlockChild, BlockElem, Em, HElem, Length, Regions, VElem, }; @@ -221,20 +223,27 @@ impl Show for Packed { let mut realized = self.body().clone(); let hanging_indent = self.hanging_indent(styles); - let mut indent = match hanging_indent { Smart::Custom(length) => length.resolve(styles), Smart::Auto => Abs::zero(), }; if let Some(numbering) = (**self).numbering(styles).as_ref() { + let location = self.location().unwrap(); let numbering = Counter::of(HeadingElem::elem()) - .display_at_loc(engine, self.location().unwrap(), styles, numbering)? + .display_at_loc(engine, location, styles, numbering)? .spanned(span); if hanging_indent.is_auto() { let pod = Regions::one(Axes::splat(Abs::inf()), Axes::splat(false)); - let size = numbering.measure(engine, styles, pod)?.into_frame().size(); + + // We don't have a locator for the numbering here, so we just + // use the measurement infrastructure for now. + let link = LocatorLink::measure(location); + let size = numbering + .layout(engine, Locator::link(&link), styles, pod)? + .into_frame() + .size(); indent = size.x + SPACING_TO_NUMBERING.resolve(styles); } diff --git a/crates/typst/src/model/list.rs b/crates/typst/src/model/list.rs index ffecc4003..d8e580522 100644 --- a/crates/typst/src/model/list.rs +++ b/crates/typst/src/model/list.rs @@ -6,6 +6,7 @@ use crate::foundations::{ cast, elem, scope, Array, Content, Context, Depth, Func, NativeElement, Packed, Show, Smart, StyleChain, Styles, Value, }; +use crate::introspection::Locator; use crate::layout::{ Axes, BlockElem, Cell, CellGrid, Em, Fragment, GridLayouter, HAlignment, Length, Regions, Sizing, Spacing, VAlignment, VElem, @@ -139,7 +140,9 @@ impl ListElem { impl Show for Packed { fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { - let mut realized = BlockElem::multi_layouter(self.clone(), layout_list).pack(); + let mut realized = BlockElem::multi_layouter(self.clone(), layout_list) + .pack() + .spanned(self.span()); if self.tight(styles) { let leading = ParElem::leading_in(styles); @@ -156,6 +159,7 @@ impl Show for Packed { fn layout_list( elem: &Packed, engine: &mut Engine, + locator: Locator, styles: StyleChain, regions: Regions, ) -> SourceResult { @@ -176,11 +180,16 @@ fn layout_list( .aligned(HAlignment::Start + VAlignment::Top); let mut cells = vec![]; + let mut locator = locator.split(); + for item in elem.children() { - cells.push(Cell::from(Content::empty())); - cells.push(Cell::from(marker.clone())); - cells.push(Cell::from(Content::empty())); - cells.push(Cell::from(item.body().clone().styled(ListElem::set_depth(Depth(1))))); + cells.push(Cell::new(Content::empty(), locator.next(&()))); + cells.push(Cell::new(marker.clone(), locator.next(&marker.span()))); + cells.push(Cell::new(Content::empty(), locator.next(&()))); + cells.push(Cell::new( + item.body.clone().styled(ListElem::set_depth(Depth(1))), + locator.next(&item.body.span()), + )); } let grid = CellGrid::new( diff --git a/crates/typst/src/model/par.rs b/crates/typst/src/model/par.rs index 7615ddbae..8d5178b1c 100644 --- a/crates/typst/src/model/par.rs +++ b/crates/typst/src/model/par.rs @@ -6,6 +6,7 @@ use crate::foundations::{ elem, Args, Cast, Construct, Content, NativeElement, Packed, Set, Smart, StyleChain, Unlabellable, }; +use crate::introspection::Locator; use crate::layout::{Em, Fragment, Length, Size}; use crate::realize::StyleVec; @@ -138,6 +139,7 @@ impl Packed { pub fn layout( &self, engine: &mut Engine, + locator: Locator, styles: StyleChain, consecutive: bool, region: Size, @@ -146,6 +148,7 @@ impl Packed { crate::layout::layout_inline( &self.children, engine, + locator, styles, consecutive, region, diff --git a/crates/typst/src/model/table.rs b/crates/typst/src/model/table.rs index 0c56b7e42..da7b60827 100644 --- a/crates/typst/src/model/table.rs +++ b/crates/typst/src/model/table.rs @@ -8,6 +8,7 @@ use crate::engine::Engine; use crate::foundations::{ cast, elem, scope, Content, Fold, NativeElement, Packed, Show, Smart, StyleChain, }; +use crate::introspection::Locator; use crate::layout::{ show_grid_cell, Abs, Alignment, Axes, BlockElem, Cell, CellGrid, Celled, Dir, Fragment, GridCell, GridFooter, GridHLine, GridHeader, GridLayouter, GridVLine, @@ -262,7 +263,9 @@ impl TableElem { impl Show for Packed { fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult { - Ok(BlockElem::multi_layouter(self.clone(), layout_table).pack()) + Ok(BlockElem::multi_layouter(self.clone(), layout_table) + .pack() + .spanned(self.span())) } } @@ -271,6 +274,7 @@ impl Show for Packed { fn layout_table( elem: &Packed, engine: &mut Engine, + locator: Locator, styles: StyleChain, regions: Regions, ) -> SourceResult { @@ -304,6 +308,7 @@ fn layout_table( let grid = CellGrid::resolve( tracks, gutter, + locator, children, fill, align, @@ -800,7 +805,7 @@ impl Default for Packed { } impl ResolvableCell for Packed { - fn resolve_cell( + fn resolve_cell<'a>( mut self, x: usize, y: usize, @@ -809,8 +814,9 @@ impl ResolvableCell for Packed { inset: Sides>>, stroke: Sides>>>>, breakable: bool, + locator: Locator<'a>, styles: StyleChain, - ) -> Cell { + ) -> Cell<'a> { let cell = &mut *self; let colspan = cell.colspan(styles); let rowspan = cell.rowspan(styles); @@ -862,6 +868,7 @@ impl ResolvableCell for Packed { cell.push_breakable(Smart::Custom(breakable)); Cell { body: self.pack(), + locator, fill, colspan, rowspan, diff --git a/crates/typst/src/realize/mod.rs b/crates/typst/src/realize/mod.rs index a21f8faf0..40e9a9b06 100644 --- a/crates/typst/src/realize/mod.rs +++ b/crates/typst/src/realize/mod.rs @@ -21,7 +21,7 @@ use crate::engine::{Engine, Route}; use crate::foundations::{ Content, NativeElement, Packed, SequenceElem, StyleChain, StyledElem, Styles, }; -use crate::introspection::TagElem; +use crate::introspection::{Locator, SplitLocator, TagElem}; use crate::layout::{ AlignElem, BlockElem, BoxElem, ColbreakElem, FlowElem, FlushElem, HElem, InlineElem, PageElem, PagebreakElem, Parity, PlaceElem, VElem, @@ -39,11 +39,12 @@ use crate::text::{LinebreakElem, SmartQuoteElem, SpaceElem, TextElem}; #[typst_macros::time(name = "realize doc")] pub fn realize_doc<'a>( engine: &mut Engine, + locator: Locator, arenas: &'a Arenas<'a>, content: &'a Content, styles: StyleChain<'a>, ) -> SourceResult<(Packed, StyleChain<'a>)> { - let mut builder = Builder::new(engine, arenas, true); + let mut builder = Builder::new(engine, locator, arenas, true); builder.accept(content, styles)?; builder.interrupt_page(Some(styles), true)?; Ok(builder.doc.unwrap().finish()) @@ -53,11 +54,12 @@ pub fn realize_doc<'a>( #[typst_macros::time(name = "realize flow")] pub fn realize_flow<'a>( engine: &mut Engine, + locator: Locator, arenas: &'a Arenas<'a>, content: &'a Content, styles: StyleChain<'a>, ) -> SourceResult<(Packed, StyleChain<'a>)> { - let mut builder = Builder::new(engine, arenas, false); + let mut builder = Builder::new(engine, locator, arenas, false); builder.accept(content, styles)?; builder.interrupt_par()?; Ok(builder.flow.finish()) @@ -67,6 +69,8 @@ pub fn realize_flow<'a>( struct Builder<'a, 'v, 't> { /// The engine. engine: &'v mut Engine<'t>, + /// Assigns unique locations to elements. + locator: SplitLocator<'v>, /// Scratch arenas for building. arenas: &'a Arenas<'a>, /// The current document building state. @@ -82,9 +86,15 @@ struct Builder<'a, 'v, 't> { } impl<'a, 'v, 't> Builder<'a, 'v, 't> { - fn new(engine: &'v mut Engine<'t>, arenas: &'a Arenas<'a>, top: bool) -> Self { + fn new( + engine: &'v mut Engine<'t>, + locator: Locator<'v>, + arenas: &'a Arenas<'a>, + top: bool, + ) -> Self { Self { engine, + locator: locator.split(), arenas, doc: top.then(DocBuilder::default), flow: FlowBuilder::default(), @@ -107,7 +117,8 @@ impl<'a, 'v, 't> Builder<'a, 'v, 't> { .store(EquationElem::new(content.clone()).pack().spanned(content.span())); } - if let Some(realized) = process(self.engine, content, styles)? { + if let Some(realized) = process(self.engine, &mut self.locator, content, styles)? + { self.engine.route.increase(); if !self.engine.route.within(Route::MAX_SHOW_RULE_DEPTH) { bail!( diff --git a/crates/typst/src/realize/process.rs b/crates/typst/src/realize/process.rs index 45d8dc8ce..a0ba8fdb3 100644 --- a/crates/typst/src/realize/process.rs +++ b/crates/typst/src/realize/process.rs @@ -8,9 +8,9 @@ use crate::foundations::{ Content, Context, Packed, Recipe, RecipeIndex, Regex, Selector, Show, ShowSet, Style, StyleChain, Styles, Synthesize, Transformation, }; -use crate::introspection::{Locatable, TagElem}; +use crate::introspection::{Locatable, SplitLocator, Tag, TagElem}; use crate::text::TextElem; -use crate::utils::{hash128, SmallBitSet}; +use crate::utils::SmallBitSet; /// What to do with an element when encountering it during realization. struct Verdict<'a> { @@ -34,6 +34,7 @@ enum ShowStep<'a> { /// Processes the given `target` element when encountering it during realization. pub fn process( engine: &mut Engine, + locator: &mut SplitLocator, target: &Content, styles: StyleChain, ) -> SourceResult> { @@ -49,7 +50,7 @@ pub fn process( // prepare it. let mut tag = None; if !prepared { - tag = prepare(engine, &mut target, &mut map, styles)?; + tag = prepare(engine, locator, &mut target, &mut map, styles)?; } // Apply a step, if there is one. @@ -181,6 +182,7 @@ fn verdict<'a>( /// This is only executed the first time an element is visited. fn prepare( engine: &mut Engine, + locator: &mut SplitLocator, target: &mut Content, map: &mut Styles, styles: StyleChain, @@ -191,11 +193,14 @@ fn prepare( // // The element could already have a location even if it is not prepared // when it stems from a query. - let mut located = target.location().is_some(); - if !located && (target.can::() || target.label().is_some()) { - let location = engine.locator.locate(hash128(&target)); + let mut key = None; + if target.location().is_some() { + key = Some(crate::utils::hash128(&target)); + } else if target.can::() || target.label().is_some() { + let hash = crate::utils::hash128(&target); + let location = locator.next_location(engine.introspector, hash); target.set_location(location); - located = true; + key = Some(hash); } // Apply built-in show-set rules. User-defined show-set rules are already @@ -220,7 +225,7 @@ fn prepare( // materialization, so that it includes the synthesized fields. Do it before // marking as prepared so that show-set rules will apply to this element // when queried. - let tag = located.then(|| TagElem::packed(target.clone())); + let tag = key.map(|key| TagElem::packed(Tag::new(target.clone(), key))); // Ensure that this preparation only runs once by marking the element as // prepared. diff --git a/crates/typst/src/visualize/image/mod.rs b/crates/typst/src/visualize/image/mod.rs index 91922d1b3..e5916f1d4 100644 --- a/crates/typst/src/visualize/image/mod.rs +++ b/crates/typst/src/visualize/image/mod.rs @@ -19,6 +19,7 @@ use crate::foundations::{ cast, elem, func, scope, Bytes, Cast, Content, NativeElement, Packed, Show, Smart, StyleChain, }; +use crate::introspection::Locator; use crate::layout::{ Abs, Axes, BlockElem, FixedAlignment, Frame, FrameItem, Length, Point, Region, Rel, Size, @@ -159,7 +160,8 @@ impl Show for Packed { Ok(BlockElem::single_layouter(self.clone(), layout_image) .with_width(self.width(styles)) .with_height(self.height(styles)) - .pack()) + .pack() + .spanned(self.span())) } } @@ -174,6 +176,7 @@ impl Figurable for Packed {} fn layout_image( elem: &Packed, engine: &mut Engine, + _: Locator, styles: StyleChain, region: Region, ) -> SourceResult { diff --git a/crates/typst/src/visualize/line.rs b/crates/typst/src/visualize/line.rs index 0d5cb4b71..f25fc58d2 100644 --- a/crates/typst/src/visualize/line.rs +++ b/crates/typst/src/visualize/line.rs @@ -1,6 +1,7 @@ use crate::diag::{bail, SourceResult}; use crate::engine::Engine; use crate::foundations::{elem, Content, NativeElement, Packed, Show, StyleChain}; +use crate::introspection::Locator; use crate::layout::{ Abs, Angle, Axes, BlockElem, Frame, FrameItem, Length, Region, Rel, Size, }; @@ -60,7 +61,9 @@ pub struct LineElem { impl Show for Packed { fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult { - Ok(BlockElem::single_layouter(self.clone(), layout_line).pack()) + Ok(BlockElem::single_layouter(self.clone(), layout_line) + .pack() + .spanned(self.span())) } } @@ -69,6 +72,7 @@ impl Show for Packed { fn layout_line( elem: &Packed, _: &mut Engine, + _: Locator, styles: StyleChain, region: Region, ) -> SourceResult { diff --git a/crates/typst/src/visualize/path.rs b/crates/typst/src/visualize/path.rs index 0005618e4..df9114267 100644 --- a/crates/typst/src/visualize/path.rs +++ b/crates/typst/src/visualize/path.rs @@ -6,6 +6,7 @@ use crate::foundations::{ array, cast, elem, Array, Content, NativeElement, Packed, Reflect, Resolve, Show, Smart, StyleChain, }; +use crate::introspection::Locator; use crate::layout::{ Abs, Axes, BlockElem, Frame, FrameItem, Length, Point, Region, Rel, Size, }; @@ -72,7 +73,9 @@ pub struct PathElem { impl Show for Packed { fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult { - Ok(BlockElem::single_layouter(self.clone(), layout_path).pack()) + Ok(BlockElem::single_layouter(self.clone(), layout_path) + .pack() + .spanned(self.span())) } } @@ -81,6 +84,7 @@ impl Show for Packed { fn layout_path( elem: &Packed, _: &mut Engine, + _: Locator, styles: StyleChain, region: Region, ) -> SourceResult { diff --git a/crates/typst/src/visualize/pattern.rs b/crates/typst/src/visualize/pattern.rs index e467d7896..804c87df2 100644 --- a/crates/typst/src/visualize/pattern.rs +++ b/crates/typst/src/visualize/pattern.rs @@ -6,6 +6,7 @@ use ecow::{eco_format, EcoString}; use crate::diag::{bail, SourceResult}; use crate::engine::Engine; use crate::foundations::{func, repr, scope, ty, Content, Smart, StyleChain}; +use crate::introspection::Locator; use crate::layout::{Abs, Axes, Frame, Length, Regions, Size}; use crate::syntax::{Span, Spanned}; use crate::utils::{LazyHash, Numeric}; @@ -189,9 +190,10 @@ impl Pattern { // Layout the pattern. let world = engine.world; let library = world.library(); + let locator = Locator::root(); let styles = StyleChain::new(&library.styles); let pod = Regions::one(region, Axes::splat(false)); - let mut frame = body.layout(engine, styles, pod)?.into_frame(); + let mut frame = body.layout(engine, locator, styles, pod)?.into_frame(); // Set the size of the frame if the size is enforced. if let Smart::Custom(size) = size { diff --git a/crates/typst/src/visualize/polygon.rs b/crates/typst/src/visualize/polygon.rs index 305f3cb15..120f41fcb 100644 --- a/crates/typst/src/visualize/polygon.rs +++ b/crates/typst/src/visualize/polygon.rs @@ -5,6 +5,7 @@ use crate::engine::Engine; use crate::foundations::{ elem, func, scope, Content, NativeElement, Packed, Resolve, Show, Smart, StyleChain, }; +use crate::introspection::Locator; use crate::layout::{Axes, BlockElem, Em, Frame, FrameItem, Length, Point, Region, Rel}; use crate::syntax::Span; use crate::utils::Numeric; @@ -125,7 +126,9 @@ impl PolygonElem { impl Show for Packed { fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult { - Ok(BlockElem::single_layouter(self.clone(), layout_polygon).pack()) + Ok(BlockElem::single_layouter(self.clone(), layout_polygon) + .pack() + .spanned(self.span())) } } @@ -134,6 +137,7 @@ impl Show for Packed { fn layout_polygon( elem: &Packed, _: &mut Engine, + _: Locator, styles: StyleChain, region: Region, ) -> SourceResult { diff --git a/crates/typst/src/visualize/shape.rs b/crates/typst/src/visualize/shape.rs index 7404763e5..8564e1ddf 100644 --- a/crates/typst/src/visualize/shape.rs +++ b/crates/typst/src/visualize/shape.rs @@ -3,6 +3,7 @@ use std::f64::consts::SQRT_2; use crate::diag::SourceResult; use crate::engine::Engine; use crate::foundations::{elem, Content, NativeElement, Packed, Show, Smart, StyleChain}; +use crate::introspection::Locator; use crate::layout::{ Abs, Axes, BlockElem, Corner, Corners, Frame, FrameItem, Length, Point, Ratio, Region, Regions, Rel, Sides, Size, @@ -134,24 +135,29 @@ pub struct RectElem { impl Show for Packed { fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { - Ok(BlockElem::single_layouter(self.clone(), |elem, engine, styles, region| { - layout_shape( - engine, - styles, - region, - ShapeKind::Rect, - elem.body(styles), - elem.fill(styles), - elem.stroke(styles), - elem.inset(styles), - elem.outset(styles), - elem.radius(styles), - elem.span(), - ) - }) + Ok(BlockElem::single_layouter( + self.clone(), + |elem, engine, locator, styles, region| { + layout_shape( + engine, + locator, + styles, + region, + ShapeKind::Rect, + elem.body(styles), + elem.fill(styles), + elem.stroke(styles), + elem.inset(styles), + elem.outset(styles), + elem.radius(styles), + elem.span(), + ) + }, + ) .with_width(self.width(styles)) .with_height(self.height(styles)) - .pack()) + .pack() + .spanned(self.span())) } } @@ -239,24 +245,29 @@ pub struct SquareElem { impl Show for Packed { fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { - Ok(BlockElem::single_layouter(self.clone(), |elem, engine, styles, regions| { - layout_shape( - engine, - styles, - regions, - ShapeKind::Square, - elem.body(styles), - elem.fill(styles), - elem.stroke(styles), - elem.inset(styles), - elem.outset(styles), - elem.radius(styles), - elem.span(), - ) - }) + Ok(BlockElem::single_layouter( + self.clone(), + |elem, engine, locator, styles, regions| { + layout_shape( + engine, + locator, + styles, + regions, + ShapeKind::Square, + elem.body(styles), + elem.fill(styles), + elem.stroke(styles), + elem.inset(styles), + elem.outset(styles), + elem.radius(styles), + elem.span(), + ) + }, + ) .with_width(self.width(styles)) .with_height(self.height(styles)) - .pack()) + .pack() + .spanned(self.span())) } } @@ -316,24 +327,29 @@ pub struct EllipseElem { impl Show for Packed { fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { - Ok(BlockElem::single_layouter(self.clone(), |elem, engine, styles, regions| { - layout_shape( - engine, - styles, - regions, - ShapeKind::Ellipse, - elem.body(styles), - elem.fill(styles), - elem.stroke(styles).map(|s| Sides::splat(Some(s))), - elem.inset(styles), - elem.outset(styles), - Corners::splat(None), - elem.span(), - ) - }) + Ok(BlockElem::single_layouter( + self.clone(), + |elem, engine, locator, styles, regions| { + layout_shape( + engine, + locator, + styles, + regions, + ShapeKind::Ellipse, + elem.body(styles), + elem.fill(styles), + elem.stroke(styles).map(|s| Sides::splat(Some(s))), + elem.inset(styles), + elem.outset(styles), + Corners::splat(None), + elem.span(), + ) + }, + ) .with_width(self.width(styles)) .with_height(self.height(styles)) - .pack()) + .pack() + .spanned(self.span())) } } @@ -418,24 +434,29 @@ pub struct CircleElem { impl Show for Packed { fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { - Ok(BlockElem::single_layouter(self.clone(), |elem, engine, styles, regions| { - layout_shape( - engine, - styles, - regions, - ShapeKind::Circle, - elem.body(styles), - elem.fill(styles), - elem.stroke(styles).map(|s| Sides::splat(Some(s))), - elem.inset(styles), - elem.outset(styles), - Corners::splat(None), - elem.span(), - ) - }) + Ok(BlockElem::single_layouter( + self.clone(), + |elem, engine, locator, styles, regions| { + layout_shape( + engine, + locator, + styles, + regions, + ShapeKind::Circle, + elem.body(styles), + elem.fill(styles), + elem.stroke(styles).map(|s| Sides::splat(Some(s))), + elem.inset(styles), + elem.outset(styles), + Corners::splat(None), + elem.span(), + ) + }, + ) .with_width(self.width(styles)) .with_height(self.height(styles)) - .pack()) + .pack() + .spanned(self.span())) } } @@ -444,6 +465,7 @@ impl Show for Packed { #[allow(clippy::too_many_arguments)] fn layout_shape( engine: &mut Engine, + locator: Locator, styles: StyleChain, region: Region, kind: ShapeKind, @@ -471,14 +493,16 @@ fn layout_shape( } // Layout the child. - frame = child.layout(engine, styles, pod.into_regions())?.into_frame(); + frame = child + .layout(engine, locator.relayout(), styles, pod.into_regions())? + .into_frame(); // If the child is a square or circle, relayout with full expansion into // square region to make sure the result is really quadratic. if kind.is_quadratic() { let length = frame.size().max_by_side().min(pod.size.min_by_side()); let quad_pod = Regions::one(Size::splat(length), Axes::splat(true)); - frame = child.layout(engine, styles, quad_pod)?.into_frame(); + frame = child.layout(engine, locator, styles, quad_pod)?.into_frame(); } // Apply the inset. diff --git a/tests/ref/issue-2480-counter-reset-2.png b/tests/ref/issue-2480-counter-reset-2.png new file mode 100644 index 0000000000000000000000000000000000000000..26b8502c2ed11b7eb4d7d67c002ab4875bf61e73 GIT binary patch literal 576 zcmV-G0>AxrG7H#*wy@~uO^MAd84_pV|IyPx_3RbX!75qQo1@BLooD_#M z=L?;^;eUCJJYl0)${S*#L!;6D-&=;S#OT$cgQmlJN zbrq1>g(F>1@i@g{{dEH*-M!*4Gy$_$wjC^gHQ^l}C|O#+i^EGFmnlYLKU6?A9L~=a zAszu>J`9CmMg&e@&(!PA_ai-kCFXKfI`b{xB;YS`>Ps`_h!Ob8?WUucO&wOib7weN zx#Y7|RX}PCj*bHMtT?=G(qL|HN*s1hXyn}|#Nk(Or!=g8<>ANL$(Gnnt)WjBhQT`k z`r##{;o`$bzxXshtpc*)@Jc4ZpOw$@0ckTk@t?UkCk?OO18gObhL?Mf*||Sxh)Kgx z*;R%or={Tv)t%Kc!ER|deD3NvH|*VM6_6T+6SF{2zFcG0D*A;tNW;n2w2s5NDh}Iw zo*trUy5A}d4+JnK8z~=HsGI literal 0 HcmV?d00001 diff --git a/tests/ref/issue-2480-counter-reset.png b/tests/ref/issue-2480-counter-reset.png new file mode 100644 index 0000000000000000000000000000000000000000..5dd52b52b7a86cbeea36dfb6f55418471bbc007c GIT binary patch literal 355 zcmV-p0i6DcP)%90hQ1pv$P1XlTu;rs@I`8l$PrK`I(tYK=mOJ|Jx?3S1%xYN$lv9R$A}RQwne zUIjt?emUNNqdW)k{BPjL|9Rl>hvNciT!IA)7CaO9mjgRDE3A#(f=$88Bsg>&R)83R z$DyE~0!QAk+F`&zYa0}J=xcCbzg_Dw;E%nFU55eNnQUGGB3Q8Cd4O4?KNc)l@O;Cc z&vgoX|5Nf%;KAtfl>+1c=!63UYi$Nx?DXAWz}f4EiULHiV8Ma~Plv5TVEhy?4m;5e zp!|4BDntIx77ivnb!;h#-uaGg9L{A5=%&)m1qzTVfV{=2aRc!?pZSe9f$x#EPij-W z2@ZT~!92VUyno@vI2_yx!+YSl(jg#NuwcPcX9Ap`Vy_uem-_$!002ovPDHLkV1jFd Bnb80M literal 0 HcmV?d00001 diff --git a/tests/ref/measure-citation-deeply-nested.png b/tests/ref/measure-citation-deeply-nested.png new file mode 100644 index 0000000000000000000000000000000000000000..4027fd768314f3f67a3e6056994c691828b0630a GIT binary patch literal 706 zcmV;z0zLhSP)FM_0 z>h|90=H}-0-RSh#<@46$-QVZ))Z+5e;quYo@XOus$=mM4*zUvE?!wp6(b4U}*3Hk? z?7h>?&CTq*)6C4w>$%bDw$JLd&grtu>9Nb`ugd7I$-%+F=d8%Tz{I`3$G^Y7yuQY| zyu{_B!ne4*CdefUBIIrrCU`nwpx}c&68Nq}Fkvk&~N|l9ttMp44fa)M%TIj*io3n$u>Q z(`A{`WSNMHlF?q3hK7dFT$6)@h|pS+f`f<7R*!&yfX-Bo%~6bce1v&bA|Ib!J=bm^MLJT(vv3;aqis(}8mEdj; zbeeXsDr;J%2VJbA5WW#&?Q2MgWwXM-lM7xbIIaM)MS;`wq12bXUjGe?j{#G|4SN{E z-V6}hLcJ&6qF-!PIGA=C!*e4*sup#h$uTR#{=0xIN%ETvZQa153p@{?=Dg>1bS)Ue zpW`Nm>l2BD?8^@i(N6G90X6NQUZ!TiIlBo!asP#)j6R>0DbqV{ETzUVYP|*B$_Cad osl=%2Y94^WlV?P)_{rmg-`uh6$`TFFMa` z=;r6`=jZ9?=jY|+<=x-s-{0Tf-rn8a-P_yS*xKUS+S=LK+1S|F*4Eb4)!Ee4*3;D3 z)YR0|)6>$@($CM&%gf8k%F4*d$j8UW#>U3O!^6M7zq`A;xVpl)xVW~qwzajjw6wId zv$LCc;rlzH(rKF^!prD|ipP!zdo}HbYoSdASo12%J zp_rJMmzS58mX?*3m6Vi}larH?k&}>+kdKd%j*gCujggFujEahiiHV7ahlhrSh=YZV zgM)*Df`Wg6iGhKEfPjF1e}8>_eS3R*b#--bad~iXaBgmJZEbC4W@cq&Wn*JwU|?Wa zT4GXDS5Hw{QBhG)P*6-xR!d7uN=iyeNl8RTPDMpUK|w)3KR-M?JUcr(H#avlG&C|Y zGA=GID=RA~C@3W*B_bjsVdKL?0003ZNklQNr3=CA};mP7;U{LY0 zPs>`TPC%`u+C#{Pg(y@%H=h_4@Ag`t9@i=-OO4_TTFD-01Y! z<@46$^VH(<(ctgL+U~^I?Z4IRzSQcq&grww=c~u#p1hs#Na$wPz3 zJ%7bFd&M?-#WZ=uGk9QNU|(Nfz8`BqKtS8aZb1M50N6=HK~#9!?bXRn!!Q(u;XjnQ z11N>aEwQYnSur=A|Alv=CS z$KO5=S(b0Ha7QFbH$MQ5GI|%cJIP=&I3<$B@Y4-ucyE&axPKCL_`ikeF@hXo4qv~5 zxnv5z#PU8Hp7DJS=I84fUf!;VquuFja|oF3F~Hs~kv+Ecb5TkyHwTZIP^lN7VgdjF N002ovPDHLkV1k1N0GI#( literal 0 HcmV?d00001 diff --git a/tests/ref/measure-counter-width.png b/tests/ref/measure-counter-width.png new file mode 100644 index 0000000000000000000000000000000000000000..3a92f816420ff777ddcc771e1cb8f08eae6830db GIT binary patch literal 2754 zcmV;z3O)6SP)a1=04(aZx z2}!GXAKTs1{&~;t){aWAxcb=K-i-~31syfq+lTaYcJ*@rTefUb^ooiK-nKt?g@uK6 zb#;mU;ll@4SJ&?DZuGXcHWwF{kdP3$l%AgMzPYSb9Pz4Z}gcMbPefj zXo=ly($4|3w|{S5=gpf(sawEq#E20c9UX$5wY7CzT-?>GSE=sXw~u9U?%cW5)YSU= z`kgy>c6N4d+O&yF?(Xi)L2z*J{rmR^1n5bVCQ&vtG(33l;M%ooypu+Ud<&lfTYshc z?Rul;j1p~63z>07nWXe{PX*|t{kDmFt)rITY)UU_&96+>w-223uasj>jVVRaIX^#t z>(;H$pFekWblkCH2QOo5YkTF&m4JYN4I4Jd>lqjr7#SH28#at;b8|BtOifLLf`X_z zJ3DK<#^S|`K@f%8v$L~Nrca;V($cbU;X?GcZ{J?HaN*ITMUEcUv(I@)K({U2IjJ5xbja7&cgmD0cpK?CfIu@s-k$d> zD=Skj^>lT|Z8NQl$wpTs<%nZ-Ur<|o{O=vpp^y#Q>q}mXl*ASP7_rSo}MX~6b zpj7tN$*dwh9bH(c{3_nXw&Ya+T9fqd}H|2UnFt2vbGR#^l29?Y&7!QBhGSxCLp!wDtA%8GlJh2@4z# ze)sNOPft$}2M+Y)N<~1H&Ny*&ywH;;PkdfB!MP)B_hy9Dj`J7%9)G+n&5>K zQ{O0Ll_{iBds!U^JAN=EKXL%D6N;Xfmp5k2nEtSLX=!N+sT3U@{pQUZ(P@0~;sxQ! z7qDnvUE8Isn=c|3U(GZ1LI>`&vo1M?oxdjA^Hz6gvmE7FK!xO0ToNrTB3WL1Wnb3s}04N=!p- zL?#@bHm>8&ojU}65Nwb;LRu0xqfDDNjq>EllLUbZ$8_=HMciFrVBn%fi>MN~>mZeC zYHDPdIXOAYmoFE#de^R9Uk2!=rY87FC>UN#hehL{6LwqGMP_O|1YewT)DgcC`G*@& zQPS92RE_6B=?|%di-4#EyLIcq<#rK_ne`}Ru|0g)0uBxdzJ z6xb!o@>kJuTONo-Lo>lECGN8kQZVK(cThA0UKm20B?Y*UbJ8dVzGlrDki}z6m@okv zZ~OM`LS&(9C*frp~He*Jp*BMS=)e}Df$1scL-l;%^XX$OzlW(IN^Z)#&u;({O2g$%g2`c=>>huMoO}%ou`i z0oupM2U-rA0zE!HULtHoprQy~h5*noXN-!J6ya_}GA_Fj5cUaluJJpP2;e;^xk^3) zxiYXwxhC(#e#s(hE3Of)1BqU7OXCT|qGYiMZ3OPbY#63Q&j8I;*hHb<0GbeyaFJ}p z$&)A3$Rr8nq{z{uM~4Q`|PL?UNB0!gUMAGY!pGBO^f>4?~qXpyVqFyB~@o`}gl>V!rBA2NuMs z1BHR%B_&f$BiT-2J|XWRITbqp(_al`GlbzoCmoX9Faa8(E@stTB}(S2D+**5aHBxva|&X&z%!5t3Q3Kr@>Z-fOdF2@Z!WJbsdysfH_@47T&VqD);SuW z`D~~LECVjp0ms_2ye0{>*9MiK4S?f?J)07*qo IM6N<$g4OIY?EnA( literal 0 HcmV?d00001 diff --git a/tests/ref/table-contextual-measurement.png b/tests/ref/table-contextual-measurement.png new file mode 100644 index 0000000000000000000000000000000000000000..1a97cbbd848c6deb174741f5d0507c64e3007a5b GIT binary patch literal 453 zcmV;$0XqJPP)YU6bA6lUD6?MAUgR74q3YS4rUU;uIW&Rdj|!lTKfiFL|oJ%nVi(6uD0MHf=)sT zT?EmnR{|vt$vv$(HdoT`8w%y)^he1bav&9DC>6kf0hb8wxRQv=X*(qBI)aG9X*Wn% z3Ppt10{%5D17t#S7yH{9Dk%X_6>H5wl7T!1s+detUU<+ODY-tj^r zO-|dwG5$yJP0gO9)}KC{noXtFpAnW`az=+hx6TpB0FCZLP zb}E)%0Ib4{;JF&xBDTfy!}P|-YH66>_*g9s(>HoX3m7n9z<>b*228-z#>Z)CnBMs4 v7TY4W#i`rk=rJVn#A!pkNCp@%;J?f_&_*yhX@p5i00000NkvXXu0mjfYOT&P literal 0 HcmV?d00001 diff --git a/tests/ref/table-header-citation.png b/tests/ref/table-header-citation.png new file mode 100644 index 0000000000000000000000000000000000000000..0495d5af12928622d3843d0b6d1129cf57dd1026 GIT binary patch literal 624 zcmV-$0+0QPP)+9?0=k4X?<>ch#-QVZh+~nKa+u7OK*4Eb4)!Eh6 z)zj42($dn;(b>+>*viVv$jHdW#l^tDz`MJQBh1zR!dD)MMXtKL_|P9KtDe}J3U1^J3BNqG%hYKEiEl5C@3N#A{-nX z6ciK+3JL}W1^@s6TtA@y0003ONkl#6o%nr8!Sh~F81EBgS}uu)U_Ad z|NkF^a61X>NXwC zhX|jFjNS(bFWz-XZ!zURoE+|yzT_a6E0*D#$2E)J6>;tA>%rn@nQtWk0K``{5dL`p zjGDY3GiowwGHNnvGHP#Nwu%p*hv@w0000< KMNUMnLSTYtMlZqu literal 0 HcmV?d00001 diff --git a/tests/ref/table-header-counter.png b/tests/ref/table-header-counter.png new file mode 100644 index 0000000000000000000000000000000000000000..04a8e92d186ed1b43690112b3716faae72700fd0 GIT binary patch literal 359 zcmeAS@N?(olHy`uVBq!ia0vp^6+m3c0VEi%2k%EaktaqI0(TW_aCiDQM= zXGz~)l#!7mkd;xG*%!Cq;Mx_&91kb(dM$Qty>RVOz>cqMwGB=k$KqNX=9B~#2(t1Q zS=%>vpC~?f`2Q~^dp`4+f_d7Bi&PpK8=0H(FR_}pm)AY`5_Zqm=rOzAle50hq$~n{ z#4`7LX|K}1$MTI;zx%us~I{mWdZ@Nn|nuGw$r$p6SmKJWW! z(sA(w5O{DPF?5Hafq}tdwg1NtOn!L3eHY8Won6!0xOUv$xHY2qanVtg54RV4K5$zX z{Yvvezs9K`g#$ga?#u|B41v#*uB2RHVFQDCM#*jNf7$cyI35Cppr@;!%Q~loCICPp Bp-cb( literal 0 HcmV?d00001 diff --git a/tests/ref/table-header-footer-madness.png b/tests/ref/table-header-footer-madness.png new file mode 100644 index 0000000000000000000000000000000000000000..4e4f771ea44ccabf63271fb46cbf1c2514683ddd GIT binary patch literal 613 zcmeAS@N?(olHy`uVBq!ia0vp^6$}iFlR1D4hGV}we={&J>3F(0hE&{od)qcUI8f&J z$8}b_HP=~LSZJ`-Ebv~)anv=4_27;l0kZ2{*hPIxHVdq2;#-u!?s&zumNlqr;UlFj z0Wl(LoRwBYtWtRU^48||-}#r%zvr!K6Xr9Y{$AF@QJSIoU@eRPm03UiZ1vdJ@tS=K z?J9bZru;3hXw#r_9dboIV_siRN_RL9mdK II;Vst0G{a^0ssI2 literal 0 HcmV?d00001 diff --git a/tests/suite/introspection/counter.typ b/tests/suite/introspection/counter.typ index 8a5315f95..c0b179211 100644 --- a/tests/suite/introspection/counter.typ +++ b/tests/suite/introspection/counter.typ @@ -76,3 +76,31 @@ At Beta, it was #context { // Hint: 2-28 try wrapping this in a `context` expression // Hint: 2-28 the `context` expression should wrap everything that depends on this function #counter("key").at(