summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2022-05-26 13:49:44 +0200
committerLaurenz <laurmaedje@gmail.com>2022-05-26 13:49:44 +0200
commita9869c212f7c1bc77a52e301ad014641b014e834 (patch)
tree97b5d6c71940e903482ba2f30cbcedd8f1c17ea3
parent66d8f4569a9f13270c5f477e0730f127a22333e2 (diff)
Locatable groups
-rw-r--r--src/eval/args.rs9
-rw-r--r--src/eval/methods.rs26
-rw-r--r--src/eval/ops.rs10
-rw-r--r--src/eval/value.rs14
-rw-r--r--src/library/layout/grid.rs4
-rw-r--r--src/library/layout/locate.rs (renamed from src/library/utility/locate.rs)8
-rw-r--r--src/library/layout/mod.rs2
-rw-r--r--src/library/mod.rs3
-rw-r--r--src/library/prelude.rs4
-rw-r--r--src/library/text/par.rs22
-rw-r--r--src/library/utility/mod.rs2
-rw-r--r--src/model/content.rs2
-rw-r--r--src/model/locate.rs224
13 files changed, 245 insertions, 85 deletions
diff --git a/src/eval/args.rs b/src/eval/args.rs
index 69e6aaee..4d280ff7 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<T>(&mut self) -> TypResult<Option<T>>
where
diff --git a/src/eval/methods.rs b/src/eval/methods.rs
index f6de614f..6ccd98e6 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::<Regex>() {
- match method {
- "matches" => {
- Value::Bool(regex.matches(&args.expect::<EcoString>("text")?))
- }
- _ => missing()?,
+ Value::Dyn(dynamic) => match method {
+ "matches" => {
+ if let Some(regex) = dynamic.downcast::<Regex>() {
+ Value::Bool(regex.matches(&args.expect::<EcoString>("text")?))
+ } else {
+ missing()?
}
- } else {
- missing()?
}
- }
+ "entry" => {
+ if let Some(group) = dynamic.downcast::<Group>() {
+ 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 b3f2f3b4..f88f3cee 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<Value> {
(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<Value> {
(a.downcast::<RawAlign>(), b.downcast::<RawAlign>())
{
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 b47d1e91..9b36812a 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<T>(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 5b621732..4cad9de6 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<Vec<Arc<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
@@ -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
index 0352199f..e94a48ba 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<Value> {
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<Value> {
+ 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 588b15aa..6cf5b550 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 3321a36b..cf5d6e64 100644
--- a/src/library/mod.rs
+++ b/src/library/mod.rs
@@ -54,6 +54,8 @@ 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");
@@ -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 371d6776..a61157a7 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 65098b61..709dc756 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<usize>;
-// 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(&quotes, 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 32815607..10aa7c7a 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 effe84ae..dad212c8 100644
--- a/src/model/content.rs
+++ b/src/model/content.rs
@@ -44,6 +44,8 @@ pub fn layout(ctx: &mut Context, content: &Content) -> TypResult<Vec<Arc<Frame>>
}
}
+ // println!("Took {pass} passes");
+
Ok(frames)
}
diff --git a/src/model/locate.rs b/src/model/locate.rs
index 9b0d13e7..fd48e5ad 100644
--- a/src/model/locate.rs
+++ b/src/model/locate.rs
@@ -1,51 +1,140 @@
+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<Func>) -> 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<Func>);
+pub struct LocateNode {
+ recipe: Spanned<Func>,
+ group: Option<Group>,
+}
impl LocateNode {
/// Create a new locate node.
pub fn new(recipe: Spanned<Func>) -> Self {
- Self(recipe)
+ Self { recipe, group: None }
}
/// Realize the node.
pub fn realize(&self, ctx: &mut Context) -> TypResult<Content> {
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());
+
+ // 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))
+ }
- let args = Args::new(self.0.span, [Value::Dict(dict)]);
- Ok(Content::Pin(idx) + self.0.v.call_detached(ctx, args)?.display())
+ 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<Location>,
+ /// All currently active pins.
+ pins: Vec<Pin>,
/// The index of the next pin in order.
cursor: usize,
+ /// If larger than zero, the board is frozen.
+ frozen: usize,
+}
+
+/// 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<Group>,
+}
+
+impl Pin {
+ /// Encode into a user-facing dictionary.
+ fn encode(&self, index: Option<usize>) -> 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()),
+ };
+
+ if let Some(index) = index {
+ dict.insert("index".into(), Value::Int(index as i64));
+ }
+
+ dict
+ }
+}
+
+/// 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,
+ /// The flow index.
+ pub flow: usize,
}
impl PinBoard {
/// Create an empty pin board.
pub fn new() -> Self {
- Self { pins: vec![], cursor: 0 }
+ Self { pins: vec![], cursor: 0, frozen: 0 }
}
/// The number of pins on the board.
@@ -53,16 +142,31 @@ impl PinBoard {
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()
+ /// Iterate over all pins on the board.
+ pub fn iter(&self) -> std::slice::Iter<Pin> {
+ 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 location.
- pub fn next(&mut self) -> Location {
+ /// Access the next pin.
+ pub fn next(&mut self, group: Option<Group>) -> Pin {
+ if self.frozen > 0 {
+ return Pin::default();
+ }
+
let cursor = self.cursor;
self.jump(self.cursor + 1);
- self.pins[cursor]
+ self.pins[cursor].group = group;
+ self.pins[cursor].clone()
}
/// The current cursor.
@@ -72,11 +176,14 @@ impl PinBoard {
/// 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 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.
@@ -87,39 +194,50 @@ impl PinBoard {
/// Locate all pins in the frames.
pub fn locate(&mut self, frames: &[Arc<Frame>]) {
+ let mut flow = 0;
for (i, frame) in frames.iter().enumerate() {
- self.locate_impl(1 + i, frame, Transform::identity());
+ locate_impl(
+ &mut self.pins,
+ &mut flow,
+ 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);
- }
-
- _ => {}
- }
- }
+ /// 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()
}
}
-/// 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,
+/// 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;
+ }
+
+ _ => {}
+ }
+ }
}