Remove pins and memoization

This commit is contained in:
Laurenz 2022-09-19 11:14:58 +02:00
parent e5f958b921
commit 4ec3bcee48
19 changed files with 18 additions and 894 deletions

@ -3,7 +3,7 @@ use std::num::NonZeroUsize;
use super::{Regex, Value};
use crate::diag::{with_alternative, StrResult};
use crate::geom::{Corners, Dir, Paint, Sides};
use crate::model::{Content, Group, Layout, LayoutNode, Pattern};
use crate::model::{Content, Layout, LayoutNode, Pattern};
use crate::syntax::Spanned;
use crate::util::EcoString;
@ -128,10 +128,6 @@ dynamic! {
Regex: "regular expression",
}
dynamic! {
Group: "group",
}
castable! {
usize,
Expected: "non-negative integer",

@ -2,7 +2,6 @@
use super::{Args, Machine, Value};
use crate::diag::{At, TypResult};
use crate::model::{Content, Group};
use crate::syntax::Span;
use crate::util::EcoString;
@ -109,22 +108,6 @@ pub fn call(
_ => return missing(),
},
Value::Dyn(dynamic) => {
if let Some(group) = dynamic.downcast::<Group>() {
match method {
"entry" => Value::Content(Content::Locate(
group.entry(args.expect("recipe")?, args.named("value")?),
)),
"all" => {
Value::Content(Content::Locate(group.all(args.expect("recipe")?)))
}
_ => return missing(),
}
} else {
return missing();
}
}
_ => return missing(),
};

@ -62,15 +62,6 @@ pub fn evaluate(
panic!("Tried to cyclicly evaluate {}", path);
}
// Check whether the module was already evaluated.
if let Some(module) = ctx.modules.get(&id) {
if module.valid(&ctx.sources) {
return Ok(module.clone());
} else {
ctx.modules.remove(&id);
}
}
route.push(id);
// Parse the file.
@ -91,16 +82,11 @@ pub fn evaluate(
}
// Assemble the module.
let module = Module {
Ok(Module {
scope: vm.scopes.top,
content: result?,
deps: vm.deps,
};
// Save the evaluated module.
ctx.modules.insert(id, module.clone());
Ok(module)
})
}
/// An evaluated module, ready for importing or layouting.

@ -246,16 +246,8 @@ fn render_outline_glyph(
// Rasterize the glyph with `pixglyph`.
// Try to retrieve a prepared glyph or prepare it from scratch if it
// doesn't exist, yet.
let bitmap = crate::memo::memoized_ref(
(&ctx.fonts, text.face_id, id),
|(fonts, face_id, id)| {
(
pixglyph::Glyph::load(fonts.get(face_id).ttf(), id),
((), (), ()),
)
},
|glyph| glyph.as_ref().map(|g| g.rasterize(ts.tx, ts.ty, ppem)),
)?;
let glyph = pixglyph::Glyph::load(ctx.fonts.get(text.face_id).ttf(), id)?;
let bitmap = glyph.rasterize(ts.tx, ts.ty, ppem);
let cw = canvas.width() as i32;
let ch = canvas.height() as i32;

@ -234,10 +234,6 @@ fn shared_prefix_words(left: &str, right: &str) -> usize {
.count()
}
impl_track_empty!(FontStore);
impl_track_hash!(FaceId);
impl_track_hash!(GlyphId);
/// A font face.
pub struct Face {
/// The raw face data, possibly shared with other faces from the same

@ -34,8 +34,6 @@
#[macro_use]
pub mod util;
#[macro_use]
pub mod memo;
#[macro_use]
pub mod geom;
#[macro_use]
pub mod diag;
@ -52,19 +50,16 @@ pub mod parse;
pub mod source;
pub mod syntax;
use std::collections::HashMap;
use std::hash::Hasher;
use std::path::PathBuf;
use std::sync::Arc;
use crate::diag::TypResult;
use crate::eval::{Module, Scope};
use crate::eval::Scope;
use crate::font::FontStore;
use crate::frame::Frame;
use crate::image::ImageStore;
use crate::loading::Loader;
use crate::memo::Track;
use crate::model::{PinBoard, PinConstraint, StyleMap};
use crate::model::StyleMap;
use crate::source::{SourceId, SourceStore};
/// Typeset a source file into a collection of layouted frames.
@ -89,10 +84,6 @@ pub struct Context {
pub images: ImageStore,
/// The context's configuration.
config: Config,
/// Stores evaluated modules.
modules: HashMap<SourceId, Module>,
/// Stores document pins.
pins: PinBoard,
}
impl Context {
@ -104,24 +95,10 @@ impl Context {
fonts: FontStore::new(Arc::clone(&loader)),
images: ImageStore::new(loader),
config,
modules: HashMap::new(),
pins: PinBoard::new(),
}
}
}
impl Track for &mut Context {
type Constraint = PinConstraint;
fn key<H: Hasher>(&self, hasher: &mut H) {
self.pins.key(hasher);
}
fn matches(&self, constraint: &Self::Constraint) -> bool {
self.pins.matches(constraint)
}
}
/// Compilation configuration.
pub struct Config {
/// The compilation root.

@ -204,9 +204,7 @@ impl<'a> GridLayouter<'a> {
/// Determines the columns sizes and then layouts the grid row-by-row.
pub fn layout(mut self) -> TypResult<Vec<Frame>> {
self.ctx.pins.freeze();
self.measure_columns()?;
self.ctx.pins.unfreeze();
for y in 0 .. self.rows.len() {
// Skip to next region if current one is full, but only for content
@ -372,12 +370,10 @@ impl<'a> GridLayouter<'a> {
pod.base.x = self.regions.base.x;
}
self.ctx.pins.freeze();
let mut sizes = node
.layout(self.ctx, &pod, self.styles)?
.into_iter()
.map(|frame| frame.height());
self.ctx.pins.unfreeze();
// For each region, we want to know the maximum height any
// column requires.

@ -1,14 +0,0 @@
use crate::library::prelude::*;
use crate::model::{Group, LocateNode};
/// Format content with access to its location on the page.
pub fn locate(_: &mut Machine, args: &mut Args) -> TypResult<Value> {
let node = LocateNode::single(args.expect("recipe")?);
Ok(Value::Content(Content::Locate(node)))
}
/// Create a new group of locatable elements.
pub fn group(_: &mut Machine, args: &mut Args) -> TypResult<Value> {
let key = args.expect("key")?;
Ok(Value::dynamic(Group::new(key)))
}

@ -5,7 +5,6 @@ mod columns;
mod container;
mod flow;
mod grid;
mod locate;
mod pad;
mod page;
mod place;
@ -17,7 +16,6 @@ pub use columns::*;
pub use container::*;
pub use flow::*;
pub use grid::*;
pub use locate::*;
pub use pad::*;
pub use page::*;
pub use place::*;

@ -57,8 +57,6 @@ pub fn new() -> Scope {
std.def_node::<layout::ColumnsNode>("columns");
std.def_node::<layout::ColbreakNode>("colbreak");
std.def_node::<layout::PlaceNode>("place");
std.def_fn("locate", layout::locate);
std.def_fn("group", layout::group);
// Graphics.
std.def_node::<graphics::ImageNode>("image");

@ -1,203 +0,0 @@
//! Function memoization.
use std::any::Any;
use std::cell::RefCell;
use std::collections::HashMap;
use std::fmt::{self, Display, Formatter};
use std::hash::Hasher;
thread_local! {
/// The thread-local cache.
static CACHE: RefCell<Cache> = RefCell::default();
}
/// A map from hashes to cache entries.
type Cache = HashMap<u64, CacheEntry>;
/// Access the cache mutably.
fn with<F, R>(f: F) -> R
where
F: FnOnce(&mut Cache) -> R,
{
CACHE.with(|cell| f(&mut cell.borrow_mut()))
}
/// An entry in the cache.
struct CacheEntry {
/// The memoized function's result plus constraints on the input in the form
/// `(O, I::Contrast)`.
data: Box<dyn Any>,
/// How many evictions have passed since the entry has been last used.
age: usize,
}
/// Execute a memoized function call.
///
/// This [tracks](Track) all inputs to the function and then either returns a
/// cached version from the thread-local cache or executes the function and
/// saves a copy of the results in the cache.
///
/// Note that `f` must be a pure function.
pub fn memoized<I, O>(input: I, f: fn(input: I) -> (O, I::Constraint)) -> O
where
I: Track,
O: Clone + 'static,
{
memoized_ref(input, f, Clone::clone)
}
/// Execute a function and then call another function with a reference to the
/// result.
///
/// This [tracks](Track) all inputs to the function and then either
/// - calls `g` with a cached version from the thread-local cache,
/// - or executes `f`, calls `g` with the fresh version and saves the result in
/// the cache.
///
/// Note that `f` must be a pure function, while `g` does not need to be pure.
pub fn memoized_ref<I, O, G, R>(
input: I,
f: fn(input: I) -> (O, I::Constraint),
g: G,
) -> R
where
I: Track,
O: 'static,
G: Fn(&O) -> R,
{
let mut state = fxhash::FxHasher64::default();
input.key(&mut state);
let key = state.finish();
let result = with(|cache| {
let entry = cache.get_mut(&key)?;
entry.age = 0;
entry
.data
.downcast_ref::<(O, I::Constraint)>()
.filter(|(_, constraint)| input.matches(constraint))
.map(|(output, _)| g(output))
});
result.unwrap_or_else(|| {
let output = f(input);
let result = g(&output.0);
let entry = CacheEntry {
data: Box::new(output) as Box<(O, I::Constraint)> as Box<dyn Any>,
age: 0,
};
with(|cache| cache.insert(key, entry));
result
})
}
/// Garbage-collect the thread-local cache.
///
/// This deletes elements which haven't been used in a while and returns details
/// about the eviction.
pub fn evict() -> Eviction {
with(|cache| {
const MAX_AGE: usize = 5;
let before = cache.len();
cache.retain(|_, entry| {
entry.age += 1;
entry.age <= MAX_AGE
});
Eviction { before, after: cache.len() }
})
}
/// Details about a cache eviction.
pub struct Eviction {
/// The number of items in the cache before the eviction.
pub before: usize,
/// The number of items in the cache after the eviction.
pub after: usize,
}
impl Display for Eviction {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
writeln!(f, "Before: {}", self.before)?;
writeln!(f, "Evicted: {}", self.before - self.after)?;
writeln!(f, "After: {}", self.after)
}
}
/// Tracks input dependencies of a memoized function.
pub trait Track {
/// The type of constraint generated by this input.
type Constraint: 'static;
/// Feed the key portion of the input into a hasher.
fn key<H: Hasher>(&self, hasher: &mut H);
/// Whether this instance matches the given constraint.
fn matches(&self, constraint: &Self::Constraint) -> bool;
}
impl<T: Track> Track for &T {
type Constraint = T::Constraint;
fn key<H: Hasher>(&self, hasher: &mut H) {
Track::key(*self, hasher)
}
fn matches(&self, constraint: &Self::Constraint) -> bool {
Track::matches(*self, constraint)
}
}
macro_rules! impl_track_empty {
($ty:ty) => {
impl $crate::memo::Track for $ty {
type Constraint = ();
fn key<H: std::hash::Hasher>(&self, _: &mut H) {}
fn matches(&self, _: &Self::Constraint) -> bool {
true
}
}
};
}
macro_rules! impl_track_hash {
($ty:ty) => {
impl $crate::memo::Track for $ty {
type Constraint = ();
fn key<H: std::hash::Hasher>(&self, hasher: &mut H) {
std::hash::Hash::hash(self, hasher)
}
fn matches(&self, _: &Self::Constraint) -> bool {
true
}
}
};
}
macro_rules! impl_track_tuple {
($($idx:tt: $field:ident),*) => {
#[allow(unused_variables)]
impl<$($field: Track),*> Track for ($($field,)*) {
type Constraint = ($($field::Constraint,)*);
fn key<H: Hasher>(&self, hasher: &mut H) {
$(self.$idx.key(hasher);)*
}
fn matches(&self, constraint: &Self::Constraint) -> bool {
true $(&& self.$idx.matches(&constraint.$idx))*
}
}
};
}
impl_track_tuple! {}
impl_track_tuple! { 0: A }
impl_track_tuple! { 0: A, 1: B }
impl_track_tuple! { 0: A, 1: B, 2: C }
impl_track_tuple! { 0: A, 1: B, 2: C, 3: D }

@ -7,8 +7,8 @@ use std::ops::{Add, AddAssign};
use typed_arena::Arena;
use super::{
Barrier, CollapsingBuilder, Interruption, Key, Layout, LayoutNode, LocateNode,
Property, Show, ShowNode, StyleEntry, StyleMap, StyleVecBuilder, Target,
Barrier, CollapsingBuilder, Interruption, Key, Layout, LayoutNode, Property, Show,
ShowNode, StyleEntry, StyleMap, StyleVecBuilder, Target,
};
use crate::diag::StrResult;
use crate::library::layout::{FlowChild, FlowNode, PageNode, PlaceNode, Spacing};
@ -23,30 +23,6 @@ use crate::util::EcoString;
///
/// Relayouts until all pinned locations are converged.
pub fn layout(ctx: &mut Context, content: &Content) -> TypResult<Vec<Frame>> {
let mut pass = 0;
let mut frames;
loop {
let prev = ctx.pins.clone();
let result = layout_once(ctx, content);
ctx.pins.reset();
frames = result?;
pass += 1;
ctx.pins.locate(&frames);
// Quit if we're done or if we've had five passes.
let unresolved = ctx.pins.unresolved(&prev);
if unresolved == 0 || pass >= 5 {
break;
}
}
Ok(frames)
}
/// Layout content into a collection of pages once.
fn layout_once(ctx: &mut Context, content: &Content) -> TypResult<Vec<Frame>> {
let copy = ctx.config.styles.clone();
let styles = StyleChain::with_root(&copy);
let scratch = Scratch::default();
@ -114,8 +90,6 @@ pub enum Content {
/// A node that can be realized with styles, optionally with attached
/// properties.
Show(ShowNode, Option<Dict>),
/// A node that can be realized with its location on the page.
Locate(LocateNode),
/// A pin identified by index.
Pin(usize),
/// Content with attached styles.
@ -307,7 +281,6 @@ impl Debug for Content {
Self::Pagebreak { weak } => write!(f, "Pagebreak({weak})"),
Self::Page(page) => page.fmt(f),
Self::Show(node, _) => node.fmt(f),
Self::Locate(node) => node.fmt(f),
Self::Pin(idx) => write!(f, "Pin({idx})"),
Self::Styled(styled) => {
let (sub, map) = styled.as_ref();
@ -425,7 +398,6 @@ impl<'a, 'ctx> Builder<'a, 'ctx> {
}
Content::Show(node, _) => return self.show(node, styles),
Content::Locate(node) => return self.locate(node, styles),
Content::Styled(styled) => return self.styled(styled, styles),
Content::Sequence(seq) => return self.sequence(seq, styles),
@ -474,12 +446,6 @@ impl<'a, 'ctx> Builder<'a, 'ctx> {
Ok(())
}
fn locate(&mut self, node: &LocateNode, styles: StyleChain<'a>) -> TypResult<()> {
let realized = node.realize(self.ctx)?;
let stored = self.scratch.templates.alloc(realized);
self.accept(stored, styles)
}
fn styled(
&mut self,
(content, map): &'a (Content, StyleMap),

@ -5,7 +5,7 @@ use std::fmt::{self, Debug, Formatter, Write};
use std::hash::Hash;
use std::sync::Arc;
use super::{Barrier, NodeId, PinConstraint, Resolve, StyleChain, StyleEntry};
use super::{Barrier, NodeId, Resolve, StyleChain, StyleEntry};
use crate::diag::TypResult;
use crate::eval::{RawAlign, RawLength};
use crate::frame::{Element, Frame};
@ -131,8 +131,6 @@ impl Regions {
}
}
impl_track_hash!(Regions);
/// A type-erased layouting node with a precomputed hash.
#[derive(Clone, Hash)]
pub struct LayoutNode(Arc<Prehashed<dyn Bounds>>);
@ -222,43 +220,17 @@ impl Layout for LayoutNode {
regions: &Regions,
styles: StyleChain,
) -> TypResult<Vec<Frame>> {
let prev = ctx.pins.dirty.replace(false);
let barrier = StyleEntry::Barrier(Barrier::new(self.id()));
let styles = barrier.chain(&styles);
let (result, at, fresh, dirty) = crate::memo::memoized(
(self, &mut *ctx, regions, styles),
|(node, ctx, regions, styles)| {
let hash = fxhash::hash64(&ctx.pins);
let at = ctx.pins.cursor();
let barrier = StyleEntry::Barrier(Barrier::new(node.id()));
let styles = barrier.chain(&styles);
let mut result = node.0.layout(ctx, regions, styles);
if let Some(role) = styles.role() {
result = result.map(|mut frames| {
for frame in frames.iter_mut() {
frame.apply_role(role);
}
frames
});
}
let fresh = ctx.pins.from(at);
let dirty = ctx.pins.dirty.get();
let constraint = PinConstraint(dirty.then(|| hash));
((result, at, fresh, dirty), ((), constraint, (), ()))
},
);
ctx.pins.dirty.replace(prev || dirty);
// Replay the side effect in case of caching. This should currently be
// more or less the only relevant side effect on the context.
if dirty {
ctx.pins.replay(at, fresh);
let mut frames = self.0.layout(ctx, regions, styles)?;
if let Some(role) = styles.role() {
for frame in &mut frames {
frame.apply_role(role);
}
}
result
Ok(frames)
}
fn pack(self) -> LayoutNode {
@ -266,8 +238,6 @@ impl Layout for LayoutNode {
}
}
impl_track_hash!(LayoutNode);
impl Default for LayoutNode {
fn default() -> Self {
EmptyNode.pack()

@ -1,397 +0,0 @@
use std::cell::Cell;
use std::fmt::{self, Debug, Formatter};
use std::hash::{Hash, Hasher};
use std::num::NonZeroUsize;
use std::sync::Arc;
use super::Content;
use crate::diag::TypResult;
use crate::eval::{Args, Array, Dict, Func, Value};
use crate::frame::{Element, Frame, Location};
use crate::geom::{Point, Transform};
use crate::memo::Track;
use crate::syntax::Spanned;
use crate::util::EcoString;
use crate::Context;
/// A group of locatable elements.
#[derive(Clone, Eq, PartialEq, Hash)]
pub struct Group(EcoString);
impl Group {
/// Create a group of elements that is identified by a string key.
pub fn new(key: EcoString) -> Self {
Self(key)
}
/// Add an entry to the group.
pub fn entry(&self, recipe: Spanned<Func>, value: Option<Value>) -> LocateNode {
LocateNode::entry(self.clone(), recipe, value)
}
/// Do something with all entries of a group.
pub fn all(&self, recipe: Spanned<Func>) -> LocateNode {
LocateNode::all(self.clone(), recipe)
}
}
impl Debug for Group {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "group({:?})", self.0)
}
}
/// A node that can be realized with pinned document locations.
#[derive(Debug, Clone, PartialEq, Hash)]
pub struct LocateNode(Arc<Repr>);
impl LocateNode {
/// Create a new locatable single node.
pub fn single(recipe: Spanned<Func>) -> Self {
Self(Arc::new(Repr::Single(SingleNode(recipe))))
}
/// Create a new locatable group entry node.
pub fn entry(group: Group, recipe: Spanned<Func>, value: Option<Value>) -> Self {
Self(Arc::new(Repr::Entry(EntryNode { group, recipe, value })))
}
/// Create a new node with access to all of a group's members.
pub fn all(group: Group, recipe: Spanned<Func>) -> Self {
Self(Arc::new(Repr::All(AllNode { group, recipe })))
}
/// Realize the node.
pub fn realize(&self, ctx: &mut Context) -> TypResult<Content> {
match self.0.as_ref() {
Repr::Single(single) => single.realize(ctx),
Repr::Entry(entry) => entry.realize(ctx),
Repr::All(all) => all.realize(ctx),
}
}
}
/// The different kinds of locate nodes.
#[derive(Debug, Clone, PartialEq, Hash)]
enum Repr {
/// A single `locate(me => ...)`.
Single(SingleNode),
/// A locatable group entry.
Entry(EntryNode),
/// A recipe for all entries of a group.
All(AllNode),
}
/// An ungrouped locatable node.
#[derive(Debug, Clone, PartialEq, Hash)]
struct SingleNode(Spanned<Func>);
impl SingleNode {
fn realize(&self, ctx: &mut Context) -> TypResult<Content> {
let idx = ctx.pins.cursor;
let pin = ctx.pins.get_or_create(None, None);
let dict = pin.encode(None);
let args = Args::new(self.0.span, [Value::Dict(dict)]);
Ok(Content::Pin(idx) + self.0.v.call_detached(ctx, args)?.display())
}
}
/// A locatable grouped node which can interact with its peers' details.
#[derive(Debug, Clone, PartialEq, Hash)]
struct EntryNode {
/// Which group the node belongs to.
group: Group,
/// The recipe to execute.
recipe: Spanned<Func>,
/// An arbitrary attached value.
value: Option<Value>,
}
impl EntryNode {
fn realize(&self, ctx: &mut Context) -> TypResult<Content> {
let idx = ctx.pins.cursor;
let pin = ctx.pins.get_or_create(Some(self.group.clone()), self.value.clone());
// Determine the index among the peers.
let index = ctx
.pins
.iter()
.enumerate()
.filter(|&(k, other)| {
other.is_in(&self.group)
&& if k < idx {
other.flow <= pin.flow
} else {
other.flow < pin.flow
}
})
.count();
// Prepare first argument.
let dict = pin.encode(Some(index));
let mut args = Args::new(self.recipe.span, [Value::Dict(dict)]);
// Collect all group members if second argument is requested.
if self.recipe.v.argc() == Some(2) {
let all = ctx.pins.encode_group(&self.group);
args.push(self.recipe.span, Value::Array(all))
}
Ok(Content::Pin(idx) + self.recipe.v.call_detached(ctx, args)?.display())
}
}
/// A node with access to a group's members.
#[derive(Debug, Clone, PartialEq, Hash)]
struct AllNode {
/// Which group the node has access to.
group: Group,
/// The recipe to execute.
recipe: Spanned<Func>,
}
impl AllNode {
fn realize(&self, ctx: &mut Context) -> TypResult<Content> {
let all = ctx.pins.encode_group(&self.group);
let args = Args::new(self.recipe.span, [Value::Array(all)]);
Ok(self.recipe.v.call_detached(ctx, args)?.display())
}
}
/// Manages document pins.
#[derive(Debug, Clone)]
pub struct PinBoard {
/// All currently active pins.
list: Vec<Pin>,
/// The index of the next pin, in order.
cursor: usize,
/// If larger than zero, the board is frozen and the cursor will not be
/// advanced. This is used to disable pinning during measure-only layouting.
frozen: usize,
/// Whether the board was accessed.
pub(super) dirty: Cell<bool>,
}
impl PinBoard {
/// Create an empty pin board.
pub fn new() -> Self {
Self {
list: vec![],
cursor: 0,
frozen: 0,
dirty: Cell::new(false),
}
}
}
/// Internal methods for implementation of locatable nodes.
impl PinBoard {
/// Access or create the next pin.
fn get_or_create(&mut self, group: Option<Group>, value: Option<Value>) -> Pin {
self.dirty.set(true);
if self.frozen() {
return Pin::default();
}
let cursor = self.cursor;
self.cursor += 1;
if self.cursor >= self.list.len() {
self.list.resize(self.cursor, Pin::default());
}
let pin = &mut self.list[cursor];
pin.group = group;
pin.value = value;
pin.clone()
}
/// Encode a group into a user-facing array.
fn encode_group(&self, group: &Group) -> Array {
self.dirty.set(true);
let mut all: Vec<_> = self.iter().filter(|pin| pin.is_in(group)).collect();
all.sort_by_key(|pin| pin.flow);
all.iter()
.enumerate()
.map(|(index, member)| Value::Dict(member.encode(Some(index))))
.collect()
}
/// Iterate over all pins on the board.
fn iter(&self) -> std::slice::Iter<Pin> {
self.dirty.set(true);
self.list.iter()
}
}
/// Caching related methods.
impl PinBoard {
/// The current cursor.
pub fn cursor(&self) -> usize {
self.cursor
}
/// All pins from `prev` to the current cursor.
pub fn from(&self, prev: usize) -> Vec<Pin> {
self.list[prev .. self.cursor].to_vec()
}
/// Add the given pins at the given location and set the cursor behind them.
pub fn replay(&mut self, at: usize, pins: Vec<Pin>) {
if !self.frozen() {
self.cursor = at + pins.len();
let end = self.cursor.min(self.list.len());
self.list.splice(at .. end, pins);
}
}
}
/// Control methods that are called during layout.
impl PinBoard {
/// Freeze the board to prevent modifications.
pub fn freeze(&mut self) {
self.frozen += 1;
}
/// Freeze the board to prevent modifications.
pub fn unfreeze(&mut self) {
self.frozen -= 1;
}
/// Whether the board is currently frozen.
pub fn frozen(&self) -> bool {
self.frozen > 0
}
}
/// Methods that are called in between layout passes.
impl PinBoard {
/// Reset the cursor and remove all unused pins.
pub fn reset(&mut self) {
self.list.truncate(self.cursor);
self.cursor = 0;
self.dirty.set(false);
}
/// Locate all pins in the frames.
pub fn locate(&mut self, frames: &[Frame]) {
let mut flow = 0;
for (i, frame) in frames.iter().enumerate() {
locate_in_frame(
&mut self.list,
&mut flow,
NonZeroUsize::new(1 + i).unwrap(),
frame,
Transform::identity(),
);
}
}
/// How many pins are unresolved in comparison to an earlier snapshot.
pub fn unresolved(&self, prev: &Self) -> usize {
self.list.len() - self.list.iter().zip(&prev.list).filter(|(a, b)| a == b).count()
}
}
/// Locate all pins in a frame.
fn locate_in_frame(
pins: &mut [Pin],
flow: &mut usize,
page: NonZeroUsize,
frame: &Frame,
ts: Transform,
) {
for &(pos, ref element) in frame.elements() {
match element {
Element::Group(group) => {
let ts = ts
.pre_concat(Transform::translate(pos.x, pos.y))
.pre_concat(group.transform);
locate_in_frame(pins, flow, page, &group.frame, ts);
}
Element::Pin(idx) => {
let pin = &mut pins[*idx];
pin.loc.page = page;
pin.loc.pos = pos.transform(ts);
pin.flow = *flow;
*flow += 1;
}
_ => {}
}
}
}
impl Hash for PinBoard {
fn hash<H: Hasher>(&self, state: &mut H) {
self.list.hash(state);
self.cursor.hash(state);
self.frozen.hash(state);
}
}
/// Describes pin usage.
#[derive(Debug, Copy, Clone)]
pub struct PinConstraint(pub Option<u64>);
impl Track for PinBoard {
type Constraint = PinConstraint;
fn key<H: Hasher>(&self, _: &mut H) {}
fn matches(&self, constraint: &Self::Constraint) -> bool {
match constraint.0 {
Some(hash) => fxhash::hash64(self) == hash,
None => true,
}
}
}
/// A document pin.
#[derive(Debug, Clone, PartialEq, Hash)]
pub struct Pin {
/// The physical location of the pin in the document.
loc: Location,
/// The flow index.
flow: usize,
/// The group the pin belongs to, if any.
group: Option<Group>,
/// An arbitrary attached value.
value: Option<Value>,
}
impl Pin {
/// Whether the pin is part of the given group.
fn is_in(&self, group: &Group) -> bool {
self.group.as_ref() == Some(group)
}
/// Encode into a user-facing dictionary.
fn encode(&self, index: Option<usize>) -> Dict {
let mut dict = self.loc.encode();
if let Some(value) = &self.value {
dict.insert("value".into(), value.clone());
}
if let Some(index) = index {
dict.insert("index".into(), Value::Int(index as i64));
}
dict
}
}
impl Default for Pin {
fn default() -> Self {
Self {
loc: Location {
page: NonZeroUsize::new(1).unwrap(),
pos: Point::zero(),
},
flow: 0,
group: None,
value: None,
}
}
}

@ -5,7 +5,6 @@ mod styles;
mod collapse;
mod content;
mod layout;
mod locate;
mod property;
mod recipe;
mod show;
@ -13,7 +12,6 @@ mod show;
pub use collapse::*;
pub use content::*;
pub use layout::*;
pub use locate::*;
pub use property::*;
pub use recipe::*;
pub use show::*;

@ -413,8 +413,6 @@ impl PartialEq for StyleChain<'_> {
}
}
impl_track_hash!(StyleChain<'_>);
/// An iterator over the values in a style chain.
struct Values<'a, K> {
entries: Entries<'a>,

@ -1,5 +0,0 @@
// Test locate with crazy pagebreaks.
---
#set page(height: 10pt)
{3 * locate(me => me.page * pagebreak())}

@ -1,89 +0,0 @@
// Test locatable groups.
---
// Test counting.
#let letters = group("\u{1F494}")
#let counter = letters.entry(
(me, all) => [{1 + me.index} / {all.len()}]
)
#counter \
#box(counter) \
#counter \
---
// Test minimal citation engine with references before the document.
#let cited = group("citations")
#let num(cited, key) = {
let index = 0
for item in cited {
if item.value == key {
index = item.index
break
}
}
[\[{index + 1}\]]
}
#let cite(key) = cited.entry(value: key, (_, all) => num(all, key))
{cited.all(all => grid(
columns: (auto, 1fr),
gutter: 5pt,
..{
let seen = ()
for item in all {
if item.value in seen { continue }
(num(all, item.value), item.value)
seen.push(item.value)
}
}
))}
As shown in #cite("abc") and #cite("def") and #cite("abc") ...
---
// Test lovely sidebar.
#let lovely = group("lovely")
#let words = ("Juliet", "soft", "fair", "maid")
#let regex = regex(words.map(p => "(" + p + ")").join("|"))
#show word: regex as underline(word) + lovely.entry(_ => {})
#set page(
paper: "a8",
margins: (left: 25pt, rest: 15pt),
foreground: lovely.all(entries => {
let seen = ()
for y in entries.map(it => it.y) {
if y in seen { continue }
let line = entries.filter(it => it.y == y)
for i, it in line {
let x = 10pt - 4pt * (line.len() - i - 1)
place(dx: x, dy: it.y - 8pt, [💗])
}
seen.push(y)
}
}),
)
But, soft! what light through yonder window breaks? It is the east, and Juliet
is the sun. Arise, fair sun, and kill the envious moon, Who is already sick and
pale with grief, That thou her maid art far more fair than she: Be not her maid,
since she is envious.
---
// Test that `all` contains `me`.
// Ref: false
#show it: heading as group("headings").entry(
(me, all) => {
let last
for prev in all {
last = prev
if prev.index == me.index {
break
}
}
assert(last == me)
}
)
= A
== B

@ -1,22 +0,0 @@
// Test locate me.
---
#set page(height: 60pt)
#let pin = locate(me => box({
let c(length) = str(int(length / 1pt ) )
square(size: 1.5pt, fill: blue)
h(0.15em)
text(0.5em)[{me.page}, #c(me.x), #c(me.y)]
}))
#place(rotate(origin: top + left, 25deg, move(dx: 40pt, pin)))
#pin
#h(10pt)
#box(pin) \
#pin
#place(bottom + right, pin)
#pagebreak()
#align(center + horizon, pin + [\ ] + pin)