diff --git a/src/eval/args.rs b/src/eval/args.rs index 69e6aaee5..4d280ff7f 100644 --- a/src/eval/args.rs +++ b/src/eval/args.rs @@ -39,6 +39,15 @@ impl Args { Self { span, items } } + /// Push a positional argument. + pub fn push(&mut self, span: Span, value: Value) { + self.items.push(Arg { + span: self.span, + name: None, + value: Spanned::new(value, span), + }) + } + /// Consume and cast the first positional argument if there is one. pub fn eat(&mut self) -> TypResult> where diff --git a/src/eval/methods.rs b/src/eval/methods.rs index f6de614f2..6ccd98e6f 100644 --- a/src/eval/methods.rs +++ b/src/eval/methods.rs @@ -2,6 +2,7 @@ use super::{Args, Machine, Regex, StrExt, Value}; use crate::diag::{At, TypResult}; +use crate::model::{Content, Group}; use crate::syntax::Span; use crate::util::EcoString; @@ -66,18 +67,23 @@ pub fn call( _ => missing()?, }, - Value::Dyn(dynamic) => { - if let Some(regex) = dynamic.downcast::() { - match method { - "matches" => { - Value::Bool(regex.matches(&args.expect::("text")?)) - } - _ => missing()?, + Value::Dyn(dynamic) => match method { + "matches" => { + if let Some(regex) = dynamic.downcast::() { + Value::Bool(regex.matches(&args.expect::("text")?)) + } else { + missing()? } - } else { - missing()? } - } + "entry" => { + if let Some(group) = dynamic.downcast::() { + Value::Content(Content::Locate(group.entry(args.expect("recipe")?))) + } else { + missing()? + } + } + _ => missing()?, + }, _ => missing()?, }; diff --git a/src/eval/ops.rs b/src/eval/ops.rs index b3f2f3b47..f88f3ceec 100644 --- a/src/eval/ops.rs +++ b/src/eval/ops.rs @@ -2,7 +2,7 @@ use std::cmp::Ordering; -use super::{Dynamic, RawAlign, RawLength, RawStroke, Smart, StrExt, Value}; +use super::{RawAlign, RawLength, RawStroke, Smart, StrExt, Value}; use crate::diag::StrResult; use crate::geom::{Numeric, Relative, Spec, SpecAxis}; use crate::model; @@ -94,10 +94,10 @@ pub fn add(lhs: Value, rhs: Value) -> StrResult { (Dict(a), Dict(b)) => Dict(a + b), (Color(color), Length(thickness)) | (Length(thickness), Color(color)) => { - Dyn(Dynamic::new(RawStroke { + Value::dynamic(RawStroke { paint: Smart::Custom(color.into()), thickness: Smart::Custom(thickness), - })) + }) } (Dyn(a), Dyn(b)) => { @@ -106,10 +106,10 @@ pub fn add(lhs: Value, rhs: Value) -> StrResult { (a.downcast::(), b.downcast::()) { if a.axis() != b.axis() { - Dyn(Dynamic::new(match a.axis() { + Value::dynamic(match a.axis() { SpecAxis::Horizontal => Spec { x: a, y: b }, SpecAxis::Vertical => Spec { x: b, y: a }, - })) + }) } else { return Err(format!("cannot add two {:?} alignments", a.axis())); } diff --git a/src/eval/value.rs b/src/eval/value.rs index b47d1e910..9b36812ac 100644 --- a/src/eval/value.rs +++ b/src/eval/value.rs @@ -11,7 +11,7 @@ use crate::geom::{ Angle, Color, Dir, Em, Fraction, Length, Paint, Ratio, Relative, RgbaColor, Sides, }; use crate::library::text::RawNode; -use crate::model::{Content, Layout, LayoutNode, Pattern}; +use crate::model::{Content, Group, Layout, LayoutNode, Pattern}; use crate::syntax::Spanned; use crate::util::EcoString; @@ -73,6 +73,14 @@ impl Value { Self::Content(Content::block(node)) } + /// Create a new dynamic value. + pub fn dynamic(any: T) -> Self + where + T: Type + Debug + PartialEq + Hash + Sync + Send + 'static, + { + Self::Dyn(Dynamic::new(any)) + } + /// The name of the stored value's type. pub fn type_name(&self) -> &'static str { match self { @@ -653,6 +661,10 @@ dynamic! { Regex: "regular expression", } +dynamic! { + Group: "group", +} + castable! { usize, Expected: "non-negative integer", diff --git a/src/library/layout/grid.rs b/src/library/layout/grid.rs index 5b6217327..4cad9de68 100644 --- a/src/library/layout/grid.rs +++ b/src/library/layout/grid.rs @@ -204,7 +204,9 @@ impl<'a> GridLayouter<'a> { /// Determines the columns sizes and then layouts the grid row-by-row. pub fn layout(mut self) -> TypResult>> { + 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 @@ -370,10 +372,12 @@ 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.size.y); + self.ctx.pins.unfreeze(); // For each region, we want to know the maximum height any // column requires. diff --git a/src/library/utility/locate.rs b/src/library/layout/locate.rs similarity index 53% rename from src/library/utility/locate.rs rename to src/library/layout/locate.rs index 0352199ff..e94a48bac 100644 --- a/src/library/utility/locate.rs +++ b/src/library/layout/locate.rs @@ -1,8 +1,14 @@ use crate::library::prelude::*; -use crate::model::LocateNode; +use crate::model::{Group, 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))) } + +/// Create a new group of locatable elements. +pub fn group(_: &mut Machine, args: &mut Args) -> TypResult { + let key = args.expect("key")?; + Ok(Value::dynamic(Group::new(key))) +} diff --git a/src/library/layout/mod.rs b/src/library/layout/mod.rs index 588b15aa1..6cf5b550c 100644 --- a/src/library/layout/mod.rs +++ b/src/library/layout/mod.rs @@ -5,6 +5,7 @@ mod columns; mod container; mod flow; mod grid; +mod locate; mod pad; mod page; mod place; @@ -16,6 +17,7 @@ pub use columns::*; pub use container::*; pub use flow::*; pub use grid::*; +pub use locate::*; pub use pad::*; pub use page::*; pub use place::*; diff --git a/src/library/mod.rs b/src/library/mod.rs index 3321a36b5..cf5d6e64f 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -54,6 +54,8 @@ pub fn new() -> Scope { std.def_node::("columns"); std.def_node::("colbreak"); std.def_node::("place"); + std.def_fn("locate", layout::locate); + std.def_fn("group", layout::group); // Graphics. std.def_node::("image"); @@ -92,7 +94,6 @@ 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/prelude.rs b/src/library/prelude.rs index 371d67761..a61157a78 100644 --- a/src/library/prelude.rs +++ b/src/library/prelude.rs @@ -9,8 +9,8 @@ pub use typst_macros::node; pub use crate::diag::{with_alternative, At, Error, StrResult, TypError, TypResult}; pub use crate::eval::{ - Arg, Args, Array, Cast, Dict, Func, Machine, Node, RawAlign, RawLength, RawStroke, - Scope, Smart, Value, + Arg, Args, Array, Cast, Dict, Dynamic, Func, Machine, Node, RawAlign, RawLength, + RawStroke, Scope, Smart, Value, }; pub use crate::frame::*; pub use crate::geom::*; diff --git a/src/library/text/par.rs b/src/library/text/par.rs index 65098b617..709dc756c 100644 --- a/src/library/text/par.rs +++ b/src/library/text/par.rs @@ -194,10 +194,11 @@ impl LinebreakNode { /// Range of a substring of text. type Range = std::ops::Range; -// The characters by which spacing and nodes are replaced in the paragraph's -// full text. -const SPACING_REPLACE: char = ' '; -const NODE_REPLACE: char = '\u{FFFC}'; +// The characters by which spacing, nodes and pins are replaced in the +// paragraph's full text. +const SPACING_REPLACE: char = ' '; // Space +const NODE_REPLACE: char = '\u{FFFC}'; // Object Replacement Character +const PIN_REPLACE: char = '\u{200D}'; // Zero Width Joiner /// A paragraph representation in which children are already layouted and text /// is already preshaped. @@ -287,8 +288,9 @@ impl Segment<'_> { fn len(&self) -> usize { match *self { Self::Text(len) => len, - Self::Spacing(_) | Self::Pin(_) => SPACING_REPLACE.len_utf8(), + Self::Spacing(_) => SPACING_REPLACE.len_utf8(), Self::Node(_) => NODE_REPLACE.len_utf8(), + Self::Pin(_) => PIN_REPLACE.len_utf8(), } } } @@ -323,10 +325,9 @@ impl<'a> Item<'a> { fn len(&self) -> usize { match self { Self::Text(shaped) => shaped.text.len(), - Self::Absolute(_) | Self::Fractional(_) | Self::Pin(_) => { - SPACING_REPLACE.len_utf8() - } + Self::Absolute(_) | Self::Fractional(_) => SPACING_REPLACE.len_utf8(), Self::Frame(_) | Self::Repeat(_, _) => NODE_REPLACE.len_utf8(), + Self::Pin(_) => PIN_REPLACE.len_utf8(), } } @@ -465,8 +466,9 @@ fn collect<'a>( let peeked = iter.peek().and_then(|(child, _)| match child { ParChild::Text(text) => text.chars().next(), ParChild::Quote { .. } => Some('"'), - ParChild::Spacing(_) | ParChild::Pin(_) => Some(SPACING_REPLACE), + ParChild::Spacing(_) => Some(SPACING_REPLACE), ParChild::Node(_) => Some(NODE_REPLACE), + ParChild::Pin(_) => Some(PIN_REPLACE), }); full.push_str(quoter.quote("es, double, peeked)); @@ -484,7 +486,7 @@ fn collect<'a>( Segment::Node(node) } &ParChild::Pin(idx) => { - full.push(SPACING_REPLACE); + full.push(PIN_REPLACE); Segment::Pin(idx) } }; diff --git a/src/library/utility/mod.rs b/src/library/utility/mod.rs index 328156074..10aa7c7a1 100644 --- a/src/library/utility/mod.rs +++ b/src/library/utility/mod.rs @@ -1,12 +1,10 @@ //! 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/model/content.rs b/src/model/content.rs index effe84ae8..dad212c8a 100644 --- a/src/model/content.rs +++ b/src/model/content.rs @@ -44,6 +44,8 @@ pub fn layout(ctx: &mut Context, content: &Content) -> TypResult> } } + // println!("Took {pass} passes"); + Ok(frames) } diff --git a/src/model/locate.rs b/src/model/locate.rs index 9b0d13e73..fd48e5ad4 100644 --- a/src/model/locate.rs +++ b/src/model/locate.rs @@ -1,117 +1,122 @@ +use std::fmt::{self, Debug, Formatter}; use std::sync::Arc; use super::Content; use crate::diag::TypResult; -use crate::eval::{Args, Func, Value}; +use crate::eval::{Args, Dict, Func, Value}; use crate::frame::{Element, Frame}; use crate::geom::{Point, Transform}; 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) -> LocateNode { + LocateNode { recipe, group: Some(self.clone()) } + } +} + +impl Debug for Group { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "group({:?})", self.0) + } +} + /// A node that can realize itself with its own location. #[derive(Debug, Clone, PartialEq, Hash)] -pub struct LocateNode(Spanned); +pub struct LocateNode { + recipe: Spanned, + group: Option, +} impl LocateNode { /// Create a new locate node. pub fn new(recipe: Spanned) -> Self { - Self(recipe) + Self { recipe, group: None } } /// 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 pin = ctx.pins.next(self.group.clone()); - let args = Args::new(self.0.span, [Value::Dict(dict)]); - Ok(Content::Pin(idx) + self.0.v.call_detached(ctx, args)?.display()) + // Determine the index among the peers. + let index = self.group.as_ref().map(|_| { + ctx.pins + .iter() + .filter(|other| { + other.group == self.group && other.loc.flow < pin.loc.flow + }) + .count() + }); + + let dict = pin.encode(index); + let mut args = Args::new(self.recipe.span, [Value::Dict(dict)]); + + // Collect all members if requested. + if self.group.is_some() && self.recipe.v.argc() == Some(2) { + let mut all: Vec<_> = + ctx.pins.iter().filter(|other| other.group == self.group).collect(); + + all.sort_by_key(|pin| pin.loc.flow); + + let array = all + .iter() + .enumerate() + .map(|(index, member)| Value::Dict(member.encode(Some(index)))) + .collect(); + + args.push(self.recipe.span, Value::Array(array)) + } + + Ok(Content::Pin(idx) + self.recipe.v.call_detached(ctx, args)?.display()) } } -/// Manages ordered pins. -#[derive(Debug, Clone, PartialEq, Hash)] +/// Manages pins. +#[derive(Debug, Clone, Hash)] pub struct PinBoard { - /// All currently pinned locations. - pins: Vec, + /// All currently active pins. + pins: Vec, /// The index of the next pin in order. cursor: usize, + /// If larger than zero, the board is frozen. + frozen: usize, } -impl PinBoard { - /// Create an empty pin board. - pub fn new() -> Self { - Self { pins: vec![], cursor: 0 } - } +/// A document pin. +#[derive(Debug, Default, Clone, PartialEq, Hash)] +pub struct Pin { + /// The physical location of the pin in the document. + loc: Location, + /// The group the pin belongs to, if any. + group: Option, +} - /// The number of pins on the board. - pub fn len(&self) -> usize { - self.pins.len() - } +impl Pin { + /// Encode into a user-facing dictionary. + fn encode(&self, index: Option) -> Dict { + let mut dict = dict! { + "page" => Value::Int(self.loc.page as i64), + "x" => Value::Length(self.loc.pos.x.into()), + "y" => Value::Length(self.loc.pos.y.into()), + }; - /// 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); + if let Some(index) = index { + dict.insert("index".into(), Value::Int(index as i64)); } - 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); - } - - _ => {} - } - } + dict } } @@ -122,4 +127,117 @@ pub struct Location { pub page: usize, /// The exact coordinates on the page (from the top left, as usual). pub pos: Point, + /// The flow index. + pub flow: usize, +} + +impl PinBoard { + /// Create an empty pin board. + pub fn new() -> Self { + Self { pins: vec![], cursor: 0, frozen: 0 } + } + + /// The number of pins on the board. + pub fn len(&self) -> usize { + self.pins.len() + } + + /// Iterate over all pins on the board. + pub fn iter(&self) -> std::slice::Iter { + self.pins.iter() + } + + /// 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; + } + + /// Access the next pin. + pub fn next(&mut self, group: Option) -> Pin { + if self.frozen > 0 { + return Pin::default(); + } + + let cursor = self.cursor; + self.jump(self.cursor + 1); + self.pins[cursor].group = group; + self.pins[cursor].clone() + } + + /// The current cursor. + pub fn cursor(&self) -> usize { + self.cursor + } + + /// Set the current cursor. + pub fn jump(&mut self, cursor: usize) { + if self.frozen > 0 { + return; + } + + self.cursor = cursor; + if cursor >= self.pins.len() { + self.pins.resize(cursor, Pin::default()); + } + } + + /// 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]) { + let mut flow = 0; + for (i, frame) in frames.iter().enumerate() { + locate_impl( + &mut self.pins, + &mut flow, + 1 + i, + frame, + Transform::identity(), + ); + } + } + + /// 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() + } +} + +/// Locate all pins in a frame. +fn locate_impl( + pins: &mut [Pin], + flow: &mut usize, + 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); + locate_impl(pins, flow, page, &group.frame, ts); + } + + Element::Pin(idx) => { + let loc = &mut pins[*idx].loc; + loc.page = page; + loc.pos = pos.transform(ts); + loc.flow = *flow; + *flow += 1; + } + + _ => {} + } + } }