pub struct Locator<'a> { /* private fields */ }
Expand description
Provides locations for elements in the document.
A Location
is a unique ID for an element generated during realization.
§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:
- 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 newLocator
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 usingrelayout
, 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:
-
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.
-
Locations should be as stable as possible across document edits, so that incremental compilation is effective.
-
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 dependent 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 dependent 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 resistance 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 layerx
andh_x
be the full combined hash for layerx
. We computeh_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 dependent 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.
Implementations§
Source§impl<'a> Locator<'a>
impl<'a> Locator<'a>
Sourcepub fn root() -> Self
pub fn root() -> Self
Create a new root-level locator.
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. tilings).
Sourcepub fn synthesize(location: Location) -> Self
pub fn synthesize(location: Location) -> Self
Creates a new synthetic locator.
This can be used to create a new dependent layout based on an element. This is used for layouting footnote entries based on the location of the associated footnote.
Sourcepub fn link(link: &'a LocatorLink<'a>) -> Self
pub fn link(link: &'a LocatorLink<'a>) -> Self
Creates a new locator that points to the given link.
Trait Implementations§
Source§impl<'a> Track for Locator<'a>
impl<'a> Track for Locator<'a>
Source§fn track_mut(&mut self) -> TrackedMut<'_, Self>
fn track_mut(&mut self) -> TrackedMut<'_, Self>
Source§fn track_with<'a>(
&'a self,
constraint: &'a Self::Constraint,
) -> Tracked<'a, Self>
fn track_with<'a>( &'a self, constraint: &'a Self::Constraint, ) -> Tracked<'a, Self>
Source§fn track_mut_with<'a>(
&'a mut self,
constraint: &'a Self::Constraint,
) -> TrackedMut<'a, Self>
fn track_mut_with<'a>( &'a mut self, constraint: &'a Self::Constraint, ) -> TrackedMut<'a, Self>
Source§impl<'a> Validate for Locator<'a>
impl<'a> Validate for Locator<'a>
Source§type Constraint = ImmutableConstraint<__ComemoCall>
type Constraint = ImmutableConstraint<__ComemoCall>
Source§fn validate(&self, constraint: &Self::Constraint) -> bool
fn validate(&self, constraint: &Self::Constraint) -> bool
Source§fn validate_with_id(&self, constraint: &Self::Constraint, id: usize) -> bool
fn validate_with_id(&self, constraint: &Self::Constraint, id: usize) -> bool
Source§fn replay(&mut self, constraint: &Self::Constraint)
fn replay(&mut self, constraint: &Self::Constraint)
Auto Trait Implementations§
impl<'a> Freeze for Locator<'a>
impl<'a> !RefUnwindSafe for Locator<'a>
impl<'a> Send for Locator<'a>
impl<'a> Sync for Locator<'a>
impl<'a> Unpin for Locator<'a>
impl<'a> !UnwindSafe for Locator<'a>
Blanket Implementations§
Source§impl<S, D, Swp, Dwp, T> AdaptInto<D, Swp, Dwp, T> for Swhere
T: Real + Zero + Arithmetics + Clone,
Swp: WhitePoint<T>,
Dwp: WhitePoint<T>,
D: AdaptFrom<S, Swp, Dwp, T>,
impl<S, D, Swp, Dwp, T> AdaptInto<D, Swp, Dwp, T> for Swhere
T: Real + Zero + Arithmetics + Clone,
Swp: WhitePoint<T>,
Dwp: WhitePoint<T>,
D: AdaptFrom<S, Swp, Dwp, T>,
Source§fn adapt_into_using<M>(self, method: M) -> Dwhere
M: TransformMatrix<T>,
fn adapt_into_using<M>(self, method: M) -> Dwhere
M: TransformMatrix<T>,
Source§fn adapt_into(self) -> D
fn adapt_into(self) -> D
Source§impl<T, C> ArraysFrom<C> for Twhere
C: IntoArrays<T>,
impl<T, C> ArraysFrom<C> for Twhere
C: IntoArrays<T>,
Source§fn arrays_from(colors: C) -> T
fn arrays_from(colors: C) -> T
Source§impl<T, C> ArraysInto<C> for Twhere
C: FromArrays<T>,
impl<T, C> ArraysInto<C> for Twhere
C: FromArrays<T>,
Source§fn arrays_into(self) -> C
fn arrays_into(self) -> C
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Source§impl<WpParam, T, U> Cam16IntoUnclamped<WpParam, T> for Uwhere
T: FromCam16Unclamped<WpParam, U>,
impl<WpParam, T, U> Cam16IntoUnclamped<WpParam, T> for Uwhere
T: FromCam16Unclamped<WpParam, U>,
Source§type Scalar = <T as FromCam16Unclamped<WpParam, U>>::Scalar
type Scalar = <T as FromCam16Unclamped<WpParam, U>>::Scalar
parameters
when converting.Source§fn cam16_into_unclamped(
self,
parameters: BakedParameters<WpParam, <U as Cam16IntoUnclamped<WpParam, T>>::Scalar>,
) -> T
fn cam16_into_unclamped( self, parameters: BakedParameters<WpParam, <U as Cam16IntoUnclamped<WpParam, T>>::Scalar>, ) -> T
self
into C
, using the provided parameters.Source§impl<T> CheckedAs for T
impl<T> CheckedAs for T
Source§fn checked_as<Dst>(self) -> Option<Dst>where
T: CheckedCast<Dst>,
fn checked_as<Dst>(self) -> Option<Dst>where
T: CheckedCast<Dst>,
Source§impl<Src, Dst> CheckedCastFrom<Src> for Dstwhere
Src: CheckedCast<Dst>,
impl<Src, Dst> CheckedCastFrom<Src> for Dstwhere
Src: CheckedCast<Dst>,
Source§fn checked_cast_from(src: Src) -> Option<Dst>
fn checked_cast_from(src: Src) -> Option<Dst>
Source§impl<T, C> ComponentsFrom<C> for Twhere
C: IntoComponents<T>,
impl<T, C> ComponentsFrom<C> for Twhere
C: IntoComponents<T>,
Source§fn components_from(colors: C) -> T
fn components_from(colors: C) -> T
Source§impl<T> Downcast for Twhere
T: Any,
impl<T> Downcast for Twhere
T: Any,
Source§fn into_any(self: Box<T>) -> Box<dyn Any>
fn into_any(self: Box<T>) -> Box<dyn Any>
Box<dyn Trait>
(where Trait: Downcast
) to Box<dyn Any>
. Box<dyn Any>
can
then be further downcast
into Box<ConcreteType>
where ConcreteType
implements Trait
.Source§fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
Rc<Trait>
(where Trait: Downcast
) to Rc<Any>
. Rc<Any>
can then be
further downcast
into Rc<ConcreteType>
where ConcreteType
implements Trait
.Source§fn as_any(&self) -> &(dyn Any + 'static)
fn as_any(&self) -> &(dyn Any + 'static)
&Trait
(where Trait: Downcast
) to &Any
. This is needed since Rust cannot
generate &Any
’s vtable from &Trait
’s.Source§fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
&mut Trait
(where Trait: Downcast
) to &Any
. This is needed since Rust cannot
generate &mut Any
’s vtable from &mut Trait
’s.Source§impl<T> DowncastSync for T
impl<T> DowncastSync for T
Source§impl<T> FromAngle<T> for T
impl<T> FromAngle<T> for T
Source§fn from_angle(angle: T) -> T
fn from_angle(angle: T) -> T
angle
.Source§impl<T, U> FromStimulus<U> for Twhere
U: IntoStimulus<T>,
impl<T, U> FromStimulus<U> for Twhere
U: IntoStimulus<T>,
Source§fn from_stimulus(other: U) -> T
fn from_stimulus(other: U) -> T
other
into Self
, while performing the appropriate scaling,
rounding and clamping.Source§impl<T, U> IntoAngle<U> for Twhere
U: FromAngle<T>,
impl<T, U> IntoAngle<U> for Twhere
U: FromAngle<T>,
Source§fn into_angle(self) -> U
fn into_angle(self) -> U
T
.Source§impl<WpParam, T, U> IntoCam16Unclamped<WpParam, T> for Uwhere
T: Cam16FromUnclamped<WpParam, U>,
impl<WpParam, T, U> IntoCam16Unclamped<WpParam, T> for Uwhere
T: Cam16FromUnclamped<WpParam, U>,
Source§type Scalar = <T as Cam16FromUnclamped<WpParam, U>>::Scalar
type Scalar = <T as Cam16FromUnclamped<WpParam, U>>::Scalar
parameters
when converting.Source§fn into_cam16_unclamped(
self,
parameters: BakedParameters<WpParam, <U as IntoCam16Unclamped<WpParam, T>>::Scalar>,
) -> T
fn into_cam16_unclamped( self, parameters: BakedParameters<WpParam, <U as IntoCam16Unclamped<WpParam, T>>::Scalar>, ) -> T
self
into C
, using the provided parameters.Source§impl<T, U> IntoColor<U> for Twhere
U: FromColor<T>,
impl<T, U> IntoColor<U> for Twhere
U: FromColor<T>,
Source§fn into_color(self) -> U
fn into_color(self) -> U
Source§impl<T, U> IntoColorUnclamped<U> for Twhere
U: FromColorUnclamped<T>,
impl<T, U> IntoColorUnclamped<U> for Twhere
U: FromColorUnclamped<T>,
Source§fn into_color_unclamped(self) -> U
fn into_color_unclamped(self) -> U
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self>
fn into_either(self, into_left: bool) -> Either<Self, Self>
self
into a Left
variant of Either<Self, Self>
if into_left
is true
.
Converts self
into a Right
variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
self
into a Left
variant of Either<Self, Self>
if into_left(&self)
returns true
.
Converts self
into a Right
variant of Either<Self, Self>
otherwise. Read moreSource§impl<T> IntoStimulus<T> for T
impl<T> IntoStimulus<T> for T
Source§fn into_stimulus(self) -> T
fn into_stimulus(self) -> T
self
into T
, while performing the appropriate scaling,
rounding and clamping.Source§impl<T> OverflowingAs for T
impl<T> OverflowingAs for T
Source§fn overflowing_as<Dst>(self) -> (Dst, bool)where
T: OverflowingCast<Dst>,
fn overflowing_as<Dst>(self) -> (Dst, bool)where
T: OverflowingCast<Dst>,
Source§impl<Src, Dst> OverflowingCastFrom<Src> for Dstwhere
Src: OverflowingCast<Dst>,
impl<Src, Dst> OverflowingCastFrom<Src> for Dstwhere
Src: OverflowingCast<Dst>,
Source§fn overflowing_cast_from(src: Src) -> (Dst, bool)
fn overflowing_cast_from(src: Src) -> (Dst, bool)
Source§impl<T> Pointable for T
impl<T> Pointable for T
Source§impl<T> SaturatingAs for T
impl<T> SaturatingAs for T
Source§fn saturating_as<Dst>(self) -> Dstwhere
T: SaturatingCast<Dst>,
fn saturating_as<Dst>(self) -> Dstwhere
T: SaturatingCast<Dst>,
Source§impl<Src, Dst> SaturatingCastFrom<Src> for Dstwhere
Src: SaturatingCast<Dst>,
impl<Src, Dst> SaturatingCastFrom<Src> for Dstwhere
Src: SaturatingCast<Dst>,
Source§fn saturating_cast_from(src: Src) -> Dst
fn saturating_cast_from(src: Src) -> Dst
Source§impl<T, C> TryComponentsInto<C> for Twhere
C: TryFromComponents<T>,
impl<T, C> TryComponentsInto<C> for Twhere
C: TryFromComponents<T>,
Source§type Error = <C as TryFromComponents<T>>::Error
type Error = <C as TryFromComponents<T>>::Error
try_into_colors
fails to cast.Source§fn try_components_into(self) -> Result<C, <T as TryComponentsInto<C>>::Error>
fn try_components_into(self) -> Result<C, <T as TryComponentsInto<C>>::Error>
Source§impl<T, U> TryIntoColor<U> for Twhere
U: TryFromColor<T>,
impl<T, U> TryIntoColor<U> for Twhere
U: TryFromColor<T>,
Source§fn try_into_color(self) -> Result<U, OutOfBounds<U>>
fn try_into_color(self) -> Result<U, OutOfBounds<U>>
OutOfBounds
error is returned which contains
the unclamped color. Read more