From 66d8f4569a9f13270c5f477e0730f127a22333e2 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Thu, 26 May 2022 11:59:53 +0200 Subject: [PATCH] Locate me! --- src/eval/mod.rs | 2 +- src/export/pdf.rs | 1 + src/export/render.rs | 1 + src/frame.rs | 5 +- src/lib.rs | 13 ++-- src/library/mod.rs | 1 + src/library/text/par.rs | 43 +++++++--- src/library/utility/locate.rs | 8 ++ src/library/utility/mod.rs | 2 + src/memo.rs | 20 ++--- src/model/content.rs | 50 +++++++++++- src/model/layout.rs | 14 +++- src/model/locate.rs | 125 ++++++++++++++++++++++++++++++ src/model/mod.rs | 2 + src/model/recipe.rs | 20 ++--- tests/ref/layout/locate-break.png | Bin 0 -> 122 bytes tests/ref/layout/locate.png | Bin 0 -> 2795 bytes tests/typ/layout/locate-break.typ | 5 ++ tests/typ/layout/locate.typ | 22 ++++++ 19 files changed, 289 insertions(+), 45 deletions(-) create mode 100644 src/library/utility/locate.rs create mode 100644 src/model/locate.rs create mode 100644 tests/ref/layout/locate-break.png create mode 100644 tests/ref/layout/locate.png create mode 100644 tests/typ/layout/locate-break.typ create mode 100644 tests/typ/layout/locate.typ diff --git a/src/eval/mod.rs b/src/eval/mod.rs index e54dfce42..d76257fa9 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -797,7 +797,7 @@ impl Eval for ShowExpr { body, }); - Ok(Recipe { pattern, func, span }) + Ok(Recipe { pattern, func: Spanned::new(func, span) }) } } diff --git a/src/export/pdf.rs b/src/export/pdf.rs index aeb5c47eb..5e8896f75 100644 --- a/src/export/pdf.rs +++ b/src/export/pdf.rs @@ -490,6 +490,7 @@ impl<'a> PageExporter<'a> { Element::Shape(ref shape) => self.write_shape(x, y, shape), Element::Image(id, size) => self.write_image(x, y, id, size), Element::Link(ref dest, size) => self.write_link(pos, dest, size), + Element::Pin(_) => {} } } } diff --git a/src/export/render.rs b/src/export/render.rs index aa60e67e6..163707ebc 100644 --- a/src/export/render.rs +++ b/src/export/render.rs @@ -62,6 +62,7 @@ fn render_frame( render_image(canvas, ts, mask, ctx.images.get(id), size); } Element::Link(_, _) => {} + Element::Pin(_) => {} } } } diff --git a/src/frame.rs b/src/frame.rs index 6475f92a2..289de6dad 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -218,6 +218,8 @@ pub enum Element { Image(ImageId, Size), /// A link to an external resource and its trigger region. Link(Destination, Size), + /// A pin identified by index. + Pin(usize), } impl Debug for Element { @@ -227,7 +229,8 @@ impl Debug for Element { Self::Text(text) => write!(f, "{text:?}"), Self::Shape(shape) => write!(f, "{shape:?}"), Self::Image(image, _) => write!(f, "{image:?}"), - Self::Link(target, _) => write!(f, "Link({target:?})"), + Self::Link(dest, _) => write!(f, "Link({dest:?})"), + Self::Pin(idx) => write!(f, "Pin({idx})"), } } } diff --git a/src/lib.rs b/src/lib.rs index 17225b328..34e87c5e7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -61,7 +61,7 @@ use crate::font::FontStore; use crate::frame::Frame; use crate::image::ImageStore; use crate::loading::Loader; -use crate::model::StyleMap; +use crate::model::{PinBoard, StyleMap}; use crate::source::{SourceId, SourceStore}; /// Typeset a source file into a collection of layouted frames. @@ -76,27 +76,30 @@ pub fn typeset(ctx: &mut Context, id: SourceId) -> TypResult>> { /// The core context which holds the configuration and stores. pub struct Context { - /// The context's configuration. - pub config: Config, /// Stores loaded source files. pub sources: SourceStore, /// Stores parsed font faces. pub fonts: FontStore, /// Stores decoded images. pub images: ImageStore, + /// The context's configuration. + config: Config, /// Stores evaluated modules. - pub modules: HashMap, + modules: HashMap, + /// Stores document pins. + pins: PinBoard, } impl Context { /// Create a new context. pub fn new(loader: Arc, config: Config) -> Self { Self { - config, sources: SourceStore::new(Arc::clone(&loader)), fonts: FontStore::new(Arc::clone(&loader)), images: ImageStore::new(loader), + config, modules: HashMap::new(), + pins: PinBoard::new(), } } } diff --git a/src/library/mod.rs b/src/library/mod.rs index ac0cbb92b..3321a36b5 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -92,6 +92,7 @@ pub fn new() -> Scope { std.def_fn("roman", utility::roman); std.def_fn("symbol", utility::symbol); std.def_fn("lorem", utility::lorem); + std.def_fn("locate", utility::locate); // Predefined colors. std.def_const("black", Color::BLACK); diff --git a/src/library/text/par.rs b/src/library/text/par.rs index 1269ffed5..65098b617 100644 --- a/src/library/text/par.rs +++ b/src/library/text/par.rs @@ -26,6 +26,8 @@ pub enum ParChild { Spacing(Spacing), /// An arbitrary inline-level node. Node(LayoutNode), + /// A pin identified by index. + Pin(usize), } #[node] @@ -100,6 +102,7 @@ impl Debug for ParChild { Self::Quote { double } => write!(f, "Quote({double})"), Self::Spacing(kind) => write!(f, "{:?}", kind), Self::Node(node) => node.fmt(f), + Self::Pin(idx) => write!(f, "Pin({idx})"), } } } @@ -275,6 +278,8 @@ enum Segment<'a> { Spacing(Spacing), /// An arbitrary inline-level layout node. Node(&'a LayoutNode), + /// A pin identified by index. + Pin(usize), } impl Segment<'_> { @@ -282,7 +287,7 @@ impl Segment<'_> { fn len(&self) -> usize { match *self { Self::Text(len) => len, - Self::Spacing(_) => SPACING_REPLACE.len_utf8(), + Self::Spacing(_) | Self::Pin(_) => SPACING_REPLACE.len_utf8(), Self::Node(_) => NODE_REPLACE.len_utf8(), } } @@ -301,6 +306,8 @@ enum Item<'a> { Frame(Arc), /// A repeating node. Repeat(&'a RepeatNode, StyleChain<'a>), + /// A pin identified by index. + Pin(usize), } impl<'a> Item<'a> { @@ -316,7 +323,9 @@ impl<'a> Item<'a> { fn len(&self) -> usize { match self { Self::Text(shaped) => shaped.text.len(), - Self::Absolute(_) | Self::Fractional(_) => SPACING_REPLACE.len_utf8(), + Self::Absolute(_) | Self::Fractional(_) | Self::Pin(_) => { + SPACING_REPLACE.len_utf8() + } Self::Frame(_) | Self::Repeat(_, _) => NODE_REPLACE.len_utf8(), } } @@ -324,10 +333,10 @@ impl<'a> Item<'a> { /// The natural width of the item. fn width(&self) -> Length { match self { - Item::Text(shaped) => shaped.width, - Item::Absolute(v) => *v, - Item::Fractional(_) | Self::Repeat(_, _) => Length::zero(), - Item::Frame(frame) => frame.size.x, + Self::Text(shaped) => shaped.width, + Self::Absolute(v) => *v, + Self::Frame(frame) => frame.size.x, + Self::Fractional(_) | Self::Repeat(_, _) | Self::Pin(_) => Length::zero(), } } } @@ -447,7 +456,7 @@ fn collect<'a>( } Segment::Text(full.len() - prev) } - ParChild::Quote { double } => { + &ParChild::Quote { double } => { let prev = full.len(); if styles.get(TextNode::SMART_QUOTES) { let lang = styles.get(TextNode::LANG); @@ -456,24 +465,28 @@ fn collect<'a>( let peeked = iter.peek().and_then(|(child, _)| match child { ParChild::Text(text) => text.chars().next(), ParChild::Quote { .. } => Some('"'), - ParChild::Spacing(_) => Some(SPACING_REPLACE), + ParChild::Spacing(_) | ParChild::Pin(_) => Some(SPACING_REPLACE), ParChild::Node(_) => Some(NODE_REPLACE), }); - full.push_str(quoter.quote("es, *double, peeked)); + full.push_str(quoter.quote("es, double, peeked)); } else { - full.push(if *double { '"' } else { '\'' }); + full.push(if double { '"' } else { '\'' }); } Segment::Text(full.len() - prev) } - ParChild::Spacing(spacing) => { + &ParChild::Spacing(spacing) => { full.push(SPACING_REPLACE); - Segment::Spacing(*spacing) + Segment::Spacing(spacing) } ParChild::Node(node) => { full.push(NODE_REPLACE); Segment::Node(node) } + &ParChild::Pin(idx) => { + full.push(SPACING_REPLACE); + Segment::Pin(idx) + } }; if let Some(last) = full.chars().last() { @@ -540,6 +553,7 @@ fn prepare<'a>( items.push(Item::Frame(frame)); } } + Segment::Pin(idx) => items.push(Item::Pin(idx)), } cursor = end; @@ -1171,6 +1185,11 @@ fn commit( } offset = before + width; } + Item::Pin(idx) => { + let mut frame = Frame::new(Size::zero()); + frame.push(Point::zero(), Element::Pin(*idx)); + push(&mut offset, MaybeShared::Owned(frame)); + } } } diff --git a/src/library/utility/locate.rs b/src/library/utility/locate.rs new file mode 100644 index 000000000..0352199ff --- /dev/null +++ b/src/library/utility/locate.rs @@ -0,0 +1,8 @@ +use crate::library::prelude::*; +use crate::model::LocateNode; + +/// Format content with access to its location on the page. +pub fn locate(_: &mut Machine, args: &mut Args) -> TypResult { + let node = LocateNode::new(args.expect("recipe")?); + Ok(Value::Content(Content::Locate(node))) +} diff --git a/src/library/utility/mod.rs b/src/library/utility/mod.rs index 10aa7c7a1..328156074 100644 --- a/src/library/utility/mod.rs +++ b/src/library/utility/mod.rs @@ -1,10 +1,12 @@ //! Computational utility functions. mod color; +mod locate; mod math; mod string; pub use color::*; +pub use locate::*; pub use math::*; pub use string::*; diff --git a/src/memo.rs b/src/memo.rs index 6545ebcc8..2eee071cc 100644 --- a/src/memo.rs +++ b/src/memo.rs @@ -4,7 +4,7 @@ use std::any::Any; use std::cell::RefCell; use std::collections::HashMap; use std::fmt::{self, Display, Formatter}; -use std::hash::Hash; +use std::hash::{Hash, Hasher}; thread_local! { /// The thread-local cache. @@ -60,7 +60,7 @@ where O: 'static, G: Fn(&O) -> R, { - let hash = fxhash::hash64(&input); + let hash = fxhash::hash64(&(f, &input)); let result = with(|cache| { let entry = cache.get_mut(&hash)?; entry.age = 0; @@ -111,13 +111,13 @@ impl Display for Eviction { } // These impls are temporary and incorrect. -macro_rules! skip { - ($ty:ty) => { - impl Hash for $ty { - fn hash(&self, _: &mut H) {} - } - }; + +impl Hash for crate::font::FontStore { + fn hash(&self, _: &mut H) {} } -skip!(crate::font::FontStore); -skip!(crate::Context); +impl Hash for crate::Context { + fn hash(&self, state: &mut H) { + self.pins.hash(state); + } +} diff --git a/src/model/content.rs b/src/model/content.rs index c09979d52..effe84ae8 100644 --- a/src/model/content.rs +++ b/src/model/content.rs @@ -7,8 +7,8 @@ use std::ops::{Add, AddAssign}; use typed_arena::Arena; use super::{ - Barrier, CollapsingBuilder, Interruption, Key, Layout, LayoutNode, Property, Show, - ShowNode, StyleEntry, StyleMap, StyleVecBuilder, Target, + Barrier, CollapsingBuilder, Interruption, Key, Layout, LayoutNode, LocateNode, + Property, Show, ShowNode, StyleEntry, StyleMap, StyleVecBuilder, Target, }; use crate::diag::StrResult; use crate::library::layout::{FlowChild, FlowNode, PageNode, PlaceNode, Spacing}; @@ -20,7 +20,35 @@ use crate::library::text::{ use crate::util::EcoString; /// Layout content into a collection of pages. +/// +/// Relayouts until all pinned locations are converged. pub fn layout(ctx: &mut Context, content: &Content) -> TypResult>> { + 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); + + let count = ctx.pins.len(); + let resolved = ctx.pins.resolved(&prev); + + // Quit if we're done or if we've had five passes. + if resolved == count || pass >= 5 { + break; + } + } + + Ok(frames) +} + +/// Layout content into a collection of pages once. +fn layout_once(ctx: &mut Context, content: &Content) -> TypResult>> { let copy = ctx.config.styles.clone(); let styles = StyleChain::with_root(©); let scratch = Scratch::default(); @@ -88,6 +116,10 @@ pub enum Content { /// A node that can be realized with styles, optionally with attached /// properties. Show(ShowNode, Option), + /// 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. Styled(Arc<(Self, StyleMap)>), /// A sequence of multiple nodes. @@ -272,6 +304,8 @@ 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(); map.fmt(f)?; @@ -388,6 +422,7 @@ 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), @@ -436,6 +471,12 @@ 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), @@ -641,6 +682,9 @@ impl<'a> ParBuilder<'a> { Content::Inline(node) => { self.0.supportive(ParChild::Node(node.clone()), styles); } + &Content::Pin(idx) => { + self.0.ignorant(ParChild::Pin(idx), styles); + } _ => return false, } @@ -660,7 +704,7 @@ impl<'a> ParBuilder<'a> { && children .items() .find_map(|child| match child { - ParChild::Spacing(_) => None, + ParChild::Spacing(_) | ParChild::Pin(_) => None, ParChild::Text(_) | ParChild::Quote { .. } => Some(true), ParChild::Node(_) => Some(false), }) diff --git a/src/model/layout.rs b/src/model/layout.rs index 6dfbcb90e..49720be4d 100644 --- a/src/model/layout.rs +++ b/src/model/layout.rs @@ -221,13 +221,19 @@ impl Layout for LayoutNode { regions: &Regions, styles: StyleChain, ) -> TypResult>> { - crate::memo::memoized( - (self, ctx, regions, styles), + let (result, cursor) = crate::memo::memoized( + (self, &mut *ctx, regions, styles), |(node, ctx, regions, styles)| { let entry = StyleEntry::Barrier(Barrier::new(node.id())); - node.0.layout(ctx, regions, entry.chain(&styles)) + let result = node.0.layout(ctx, regions, entry.chain(&styles)); + (result, ctx.pins.cursor()) }, - ) + ); + + // Replay the side effect in case of caching. This should currently be + // more or less the only relevant side effect on the context. + ctx.pins.jump(cursor); + result } fn pack(self) -> LayoutNode { diff --git a/src/model/locate.rs b/src/model/locate.rs new file mode 100644 index 000000000..9b0d13e73 --- /dev/null +++ b/src/model/locate.rs @@ -0,0 +1,125 @@ +use std::sync::Arc; + +use super::Content; +use crate::diag::TypResult; +use crate::eval::{Args, Func, Value}; +use crate::frame::{Element, Frame}; +use crate::geom::{Point, Transform}; +use crate::syntax::Spanned; +use crate::Context; + +/// A node that can realize itself with its own location. +#[derive(Debug, Clone, PartialEq, Hash)] +pub struct LocateNode(Spanned); + +impl LocateNode { + /// Create a new locate node. + pub fn new(recipe: Spanned) -> Self { + Self(recipe) + } + + /// Realize the node. + pub fn realize(&self, ctx: &mut Context) -> TypResult { + let idx = ctx.pins.cursor(); + let location = ctx.pins.next(); + let dict = dict! { + "page" => Value::Int(location.page as i64), + "x" => Value::Length(location.pos.x.into()), + "y" => Value::Length(location.pos.y.into()), + }; + + let args = Args::new(self.0.span, [Value::Dict(dict)]); + Ok(Content::Pin(idx) + self.0.v.call_detached(ctx, args)?.display()) + } +} + +/// Manages ordered pins. +#[derive(Debug, Clone, PartialEq, Hash)] +pub struct PinBoard { + /// All currently pinned locations. + pins: Vec, + /// The index of the next pin in order. + cursor: usize, +} + +impl PinBoard { + /// Create an empty pin board. + pub fn new() -> Self { + Self { pins: vec![], cursor: 0 } + } + + /// The number of pins on the board. + pub fn len(&self) -> usize { + self.pins.len() + } + + /// How many pins are resolved in comparison to an earlier snapshot. + pub fn resolved(&self, prev: &Self) -> usize { + self.pins.iter().zip(&prev.pins).filter(|(a, b)| a == b).count() + } + + /// Access the next pin location. + pub fn next(&mut self) -> Location { + let cursor = self.cursor; + self.jump(self.cursor + 1); + self.pins[cursor] + } + + /// The current cursor. + pub fn cursor(&self) -> usize { + self.cursor + } + + /// Set the current cursor. + pub fn jump(&mut self, cursor: usize) { + if cursor >= self.pins.len() { + let loc = self.pins.last().copied().unwrap_or_default(); + self.pins.resize(cursor + 1, loc); + } + self.cursor = cursor; + } + + /// Reset the cursor and remove all unused pins. + pub fn reset(&mut self) { + self.pins.truncate(self.cursor); + self.cursor = 0; + } + + /// Locate all pins in the frames. + pub fn locate(&mut self, frames: &[Arc]) { + for (i, frame) in frames.iter().enumerate() { + self.locate_impl(1 + i, frame, Transform::identity()); + } + } + + /// Locate all pins in a frame. + fn locate_impl(&mut self, page: usize, 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); + self.locate_impl(page, &group.frame, ts); + } + + Element::Pin(idx) => { + let pin = &mut self.pins[*idx]; + pin.page = page; + pin.pos = pos.transform(ts); + } + + _ => {} + } + } + } +} + +/// A physical location in a document. +#[derive(Debug, Default, Copy, Clone, PartialEq, Hash)] +pub struct Location { + /// The page, starting at 1. + pub page: usize, + /// The exact coordinates on the page (from the top left, as usual). + pub pos: Point, +} diff --git a/src/model/mod.rs b/src/model/mod.rs index 5c8b82c0b..379b633f6 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -5,6 +5,7 @@ mod styles; mod collapse; mod content; mod layout; +mod locate; mod property; mod recipe; mod show; @@ -12,6 +13,7 @@ mod show; pub use collapse::*; pub use content::*; pub use layout::*; +pub use locate::*; pub use property::*; pub use recipe::*; pub use show::*; diff --git a/src/model/recipe.rs b/src/model/recipe.rs index e4417adfe..6261e7045 100644 --- a/src/model/recipe.rs +++ b/src/model/recipe.rs @@ -4,7 +4,7 @@ use super::{Content, Interruption, NodeId, Show, ShowNode, StyleChain, StyleEntr use crate::diag::TypResult; use crate::eval::{Args, Func, Regex, Value}; use crate::library::structure::{EnumNode, ListNode}; -use crate::syntax::Span; +use crate::syntax::Spanned; use crate::Context; /// A show rule recipe. @@ -13,9 +13,7 @@ pub struct Recipe { /// The patterns to customize. pub pattern: Pattern, /// The function that defines the recipe. - pub func: Func, - /// The span to report all erros with. - pub span: Span, + pub func: Spanned, } impl Recipe { @@ -81,13 +79,13 @@ impl Recipe { where F: FnOnce() -> Value, { - let args = if self.func.argc() == Some(0) { - Args::new(self.span, []) + let args = if self.func.v.argc() == Some(0) { + Args::new(self.func.span, []) } else { - Args::new(self.span, [arg()]) + Args::new(self.func.span, [arg()]) }; - Ok(self.func.call_detached(ctx, args)?.display()) + Ok(self.func.v.call_detached(ctx, args)?.display()) } /// What kind of structure the property interrupts. @@ -104,7 +102,11 @@ impl Recipe { impl Debug for Recipe { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "Recipe matching {:?} from {:?}", self.pattern, self.span) + write!( + f, + "Recipe matching {:?} from {:?}", + self.pattern, self.func.span + ) } } diff --git a/tests/ref/layout/locate-break.png b/tests/ref/layout/locate-break.png new file mode 100644 index 0000000000000000000000000000000000000000..25abb4a094b634121f41868d8f66cfe6946a8b52 GIT binary patch literal 122 zcmeAS@N?(olHy`uVBq!ia0y~yU}OQZe=!0HhWt4!RY1zo)5S5Q;?~>C8+jQFI1U)t zJubD&wogcMI5cy!mDA^^ueI8fk1BqXebZgb{yXXZ!F@N%H|Qg{|K8f2VPEH;yicbU OWT>aBpUXO@geCw$buLH% literal 0 HcmV?d00001 diff --git a/tests/ref/layout/locate.png b/tests/ref/layout/locate.png new file mode 100644 index 0000000000000000000000000000000000000000..015c5a6dde1fb21ee0351dd131dc87bc32862f1b GIT binary patch literal 2795 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 R`q@hW(7kgPR-tJh@ehF~E!_YB literal 0 HcmV?d00001 diff --git a/tests/typ/layout/locate-break.typ b/tests/typ/layout/locate-break.typ new file mode 100644 index 000000000..28631cfa7 --- /dev/null +++ b/tests/typ/layout/locate-break.typ @@ -0,0 +1,5 @@ +// Test locate with crazy pagebreaks. + +--- +#set page(height: 10pt) +{3 * locate(me => me.page * pagebreak())} diff --git a/tests/typ/layout/locate.typ b/tests/typ/layout/locate.typ new file mode 100644 index 000000000..ec2262c50 --- /dev/null +++ b/tests/typ/layout/locate.typ @@ -0,0 +1,22 @@ +// 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)