From bce553a991f19b2b5bb9efef6b74bd12e15a10c6 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Wed, 9 Jun 2021 14:05:57 +0200 Subject: [PATCH] Tidy up --- src/eval/mod.rs | 3 +- src/eval/ops.rs | 58 +++- src/eval/value.rs | 32 +-- src/geom/gen.rs | 40 ++- src/geom/gridu.rs | 73 ----- src/geom/mod.rs | 14 - src/geom/point.rs | 19 +- src/geom/size.rs | 19 +- src/geom/spec.rs | 27 +- src/layout/frame.rs | 8 - src/layout/grid.rs | 408 ++++++++++++++-------------- src/layout/stack.rs | 165 ++++++----- src/library/grid.rs | 85 +++--- src/parse/mod.rs | 6 +- src/parse/tokens.rs | 8 +- src/pretty.rs | 41 ++- src/syntax/expr.rs | 2 +- src/syntax/token.rs | 6 +- tests/ref/library/grid-table.png | Bin 0 -> 10495 bytes tests/ref/library/grid.png | Bin 17246 -> 4673 bytes tests/typ/library/grid-table.typ | 29 ++ tests/typ/library/grid.typ | 53 +--- tools/support/typst.tmLanguage.json | 4 + 23 files changed, 503 insertions(+), 597 deletions(-) delete mode 100644 src/geom/gridu.rs create mode 100644 tests/ref/library/grid-table.png create mode 100644 tests/typ/library/grid-table.typ diff --git a/src/eval/mod.rs b/src/eval/mod.rs index 80be2e824..4b5feb0d6 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -18,7 +18,6 @@ use std::rc::Rc; use crate::cache::Cache; use crate::color::Color; use crate::diag::{Diag, DiagSet, Pass}; -use crate::geom::TrackSizing; use crate::geom::{Angle, Fractional, Length, Relative}; use crate::loading::{FileHash, Loader}; use crate::parse::parse; @@ -245,6 +244,7 @@ impl Eval for Expr { fn eval(&self, ctx: &mut EvalContext) -> Self::Output { match *self { Self::None(_) => Value::None, + Self::Auto(_) => Value::Auto, Self::Bool(_, v) => Value::Bool(v), Self::Int(_, v) => Value::Int(v), Self::Float(_, v) => Value::Float(v), @@ -252,7 +252,6 @@ impl Eval for Expr { Self::Angle(_, v, unit) => Value::Angle(Angle::with_unit(v, unit)), Self::Percent(_, v) => Value::Relative(Relative::new(v / 100.0)), Self::Fractional(_, v) => Value::Fractional(Fractional::new(v)), - Self::Auto(_) => Value::TrackSizing(TrackSizing::Auto), Self::Color(_, v) => Value::Color(Color::Rgba(v)), Self::Str(_, ref v) => Value::Str(v.clone()), Self::Ident(ref v) => match ctx.scopes.get(&v) { diff --git a/src/eval/ops.rs b/src/eval/ops.rs index 15c09e03a..b6bd5402c 100644 --- a/src/eval/ops.rs +++ b/src/eval/ops.rs @@ -11,8 +11,8 @@ pub fn pos(value: Value) -> Value { Length(v) => Length(v), Angle(v) => Angle(v), Relative(v) => Relative(v), - Fractional(v) => Fractional(v), Linear(v) => Linear(v), + Fractional(v) => Fractional(v), _ => Error, } } @@ -25,8 +25,8 @@ pub fn neg(value: Value) -> Value { Length(v) => Length(-v), Angle(v) => Angle(-v), Relative(v) => Relative(-v), - Fractional(v) => Fractional(-v), Linear(v) => Linear(-v), + Fractional(v) => Fractional(-v), _ => Error, } } @@ -34,29 +34,31 @@ pub fn neg(value: Value) -> Value { /// Compute the sum of two values. pub fn add(lhs: Value, rhs: Value) -> Value { match (lhs, rhs) { - // Math. (Int(a), Int(b)) => Int(a + b), (Int(a), Float(b)) => Float(a as f64 + b), (Float(a), Int(b)) => Float(a + b as f64), (Float(a), Float(b)) => Float(a + b), + (Angle(a), Angle(b)) => Angle(a + b), + (Length(a), Length(b)) => Length(a + b), (Length(a), Relative(b)) => Linear(a + b), (Length(a), Linear(b)) => Linear(a + b), + (Relative(a), Length(b)) => Linear(a + b), (Relative(a), Relative(b)) => Relative(a + b), (Relative(a), Linear(b)) => Linear(a + b), - (Fractional(a), Fractional(b)) => Fractional(a + b), + (Linear(a), Length(b)) => Linear(a + b), (Linear(a), Relative(b)) => Linear(a + b), (Linear(a), Linear(b)) => Linear(a + b), - // Collections. + (Fractional(a), Fractional(b)) => Fractional(a + b), + (Str(a), Str(b)) => Str(a + &b), (Array(a), Array(b)) => Array(concat(a, b)), (Dict(a), Dict(b)) => Dict(concat(a, b)), - // Templates. (Template(a), Template(b)) => Template(concat(a, b)), (Template(a), None) => Template(a), (None, Template(b)) => Template(b), @@ -80,17 +82,23 @@ pub fn sub(lhs: Value, rhs: Value) -> Value { (Int(a), Float(b)) => Float(a as f64 - b), (Float(a), Int(b)) => Float(a - b as f64), (Float(a), Float(b)) => Float(a - b), + (Angle(a), Angle(b)) => Angle(a - b), + (Length(a), Length(b)) => Length(a - b), (Length(a), Relative(b)) => Linear(a - b), (Length(a), Linear(b)) => Linear(a - b), + (Relative(a), Length(b)) => Linear(a - b), (Relative(a), Relative(b)) => Relative(a - b), (Relative(a), Linear(b)) => Linear(a - b), - (Fractional(a), Fractional(b)) => Fractional(a - b), + (Linear(a), Length(b)) => Linear(a - b), (Linear(a), Relative(b)) => Linear(a - b), (Linear(a), Linear(b)) => Linear(a - b), + + (Fractional(a), Fractional(b)) => Fractional(a - b), + _ => Error, } } @@ -102,27 +110,37 @@ pub fn mul(lhs: Value, rhs: Value) -> Value { (Int(a), Float(b)) => Float(a as f64 * b), (Float(a), Int(b)) => Float(a * b as f64), (Float(a), Float(b)) => Float(a * b), + (Length(a), Int(b)) => Length(a * b as f64), (Length(a), Float(b)) => Length(a * b), (Int(a), Length(b)) => Length(a as f64 * b), (Float(a), Length(b)) => Length(a * b), + (Angle(a), Int(b)) => Angle(a * b as f64), (Angle(a), Float(b)) => Angle(a * b), (Int(a), Angle(b)) => Angle(a as f64 * b), (Float(a), Angle(b)) => Angle(a * b), + (Relative(a), Int(b)) => Relative(a * b as f64), (Relative(a), Float(b)) => Relative(a * b), - (Fractional(a), Fractional(b)) => Fractional(a * b.get()), - (Fractional(a), Int(b)) => Fractional(a * b as f64), - (Fractional(a), Float(b)) => Fractional(a * b), - (Int(a), Relative(b)) => Relative(a as f64 * b), - (Int(a), Fractional(b)) => Fractional(a as f64 * b), (Float(a), Relative(b)) => Relative(a * b), - (Float(a), Fractional(b)) => Fractional(a * b), + (Int(a), Relative(b)) => Relative(a as f64 * b), + (Linear(a), Int(b)) => Linear(a * b as f64), (Linear(a), Float(b)) => Linear(a * b), (Int(a), Linear(b)) => Linear(a as f64 * b), (Float(a), Linear(b)) => Linear(a * b), + + (Float(a), Fractional(b)) => Fractional(a * b), + (Fractional(a), Int(b)) => Fractional(a * b as f64), + (Fractional(a), Float(b)) => Fractional(a * b), + (Int(a), Fractional(b)) => Fractional(a as f64 * b), + + (Str(a), Int(b)) => Str(a.repeat(b.max(0) as usize)), + (Int(a), Str(b)) => Str(b.repeat(a.max(0) as usize)), + (Array(a), Int(b)) => Array(repeat(a, b.max(0) as usize)), + (Int(a), Array(b)) => Array(repeat(b, a.max(0) as usize)), + _ => Error, } } @@ -134,20 +152,26 @@ pub fn div(lhs: Value, rhs: Value) -> Value { (Int(a), Float(b)) => Float(a as f64 / b), (Float(a), Int(b)) => Float(a / b as f64), (Float(a), Float(b)) => Float(a / b), + (Length(a), Int(b)) => Length(a / b as f64), (Length(a), Float(b)) => Length(a / b), (Length(a), Length(b)) => Float(a / b), + (Angle(a), Int(b)) => Angle(a / b as f64), (Angle(a), Float(b)) => Angle(a / b), (Angle(a), Angle(b)) => Float(a / b), + (Relative(a), Int(b)) => Relative(a / b as f64), (Relative(a), Float(b)) => Relative(a / b), (Relative(a), Relative(b)) => Float(a / b), - (Fractional(a), Fractional(b)) => Float(a.get() / b.get()), + (Fractional(a), Int(b)) => Fractional(a / b as f64), (Fractional(a), Float(b)) => Fractional(a / b), + (Fractional(a), Fractional(b)) => Float(a / b), + (Linear(a), Int(b)) => Linear(a / b as f64), (Linear(a), Float(b)) => Linear(a / b), + _ => Error, } } @@ -209,3 +233,9 @@ where a.extend(b); a } + +/// Repeat a vector `n` times. +fn repeat(vec: Vec, n: usize) -> Vec { + let len = n * vec.len(); + vec.into_iter().cycle().take(len).collect() +} diff --git a/src/eval/value.rs b/src/eval/value.rs index fa7993ede..51e451901 100644 --- a/src/eval/value.rs +++ b/src/eval/value.rs @@ -8,7 +8,7 @@ use std::rc::Rc; use super::EvalContext; use crate::color::{Color, RgbaColor}; use crate::exec::ExecContext; -use crate::geom::{Angle, Fractional, Length, Linear, Relative, TrackSizing}; +use crate::geom::{Angle, Fractional, Length, Linear, Relative}; use crate::syntax::{Expr, Span, Spanned, Tree}; /// A computational value. @@ -16,6 +16,8 @@ use crate::syntax::{Expr, Span, Spanned, Tree}; pub enum Value { /// The value that indicates the absence of a meaningful value. None, + /// A value that indicates some smart default behaviour. + Auto, /// A boolean: `true, false`. Bool(bool), /// An integer: `120`. @@ -28,12 +30,10 @@ pub enum Value { Angle(Angle), /// A relative value: `50%`. Relative(Relative), - /// A fractional value: `1fr`. - Fractional(Fractional), /// A combination of an absolute length and a relative value: `20% + 5cm`. Linear(Linear), - /// One of the units that can appear in a grid definition. - TrackSizing(TrackSizing), + /// A fractional value: `1fr`. + Fractional(Fractional), /// A color value: `#f79143ff`. Color(Color), /// A string: `"string"`. @@ -73,15 +73,15 @@ impl Value { pub fn type_name(&self) -> &'static str { match self { Self::None => "none", + Self::Auto => "auto", Self::Bool(_) => bool::TYPE_NAME, Self::Int(_) => i64::TYPE_NAME, Self::Float(_) => f64::TYPE_NAME, Self::Length(_) => Length::TYPE_NAME, Self::Angle(_) => Angle::TYPE_NAME, Self::Relative(_) => Relative::TYPE_NAME, - Self::Fractional(_) => Fractional::TYPE_NAME, Self::Linear(_) => Linear::TYPE_NAME, - Self::TrackSizing(_) => TrackSizing::TYPE_NAME, + Self::Fractional(_) => Fractional::TYPE_NAME, Self::Color(_) => Color::TYPE_NAME, Self::Str(_) => String::TYPE_NAME, Self::Array(_) => ArrayValue::TYPE_NAME, @@ -100,14 +100,6 @@ impl Value { (&Self::Float(a), &Self::Int(b)) => a == b as f64, (&Self::Length(a), &Self::Linear(b)) => a == b.abs && b.rel.is_zero(), (&Self::Relative(a), &Self::Linear(b)) => a == b.rel && b.abs.is_zero(), - (&Self::Length(a), &Self::TrackSizing(b)) => TrackSizing::from(a) == b, - (&Self::Relative(a), &Self::TrackSizing(b)) => TrackSizing::from(a) == b, - (&Self::Linear(a), &Self::TrackSizing(b)) => TrackSizing::from(a) == b, - (&Self::Fractional(a), &Self::TrackSizing(b)) => TrackSizing::from(a) == b, - (&Self::TrackSizing(a), &Self::Length(b)) => TrackSizing::from(b) == a, - (&Self::TrackSizing(a), &Self::Relative(b)) => TrackSizing::from(b) == a, - (&Self::TrackSizing(a), &Self::Linear(b)) => TrackSizing::from(b) == a, - (&Self::TrackSizing(a), &Self::Fractional(b)) => TrackSizing::from(b) == a, (&Self::Linear(a), &Self::Length(b)) => a.abs == b && a.rel.is_zero(), (&Self::Linear(a), &Self::Relative(b)) => a.rel == b && a.abs.is_zero(), (Self::Array(a), Self::Array(b)) => { @@ -615,21 +607,13 @@ primitive! { primitive! { Length: "length", Value::Length } primitive! { Angle: "angle", Value::Angle } primitive! { Relative: "relative", Value::Relative } -primitive! { Fractional: "fractional", Value::Fractional } primitive! { Linear: "linear", Value::Linear, Value::Length(v) => v.into(), Value::Relative(v) => v.into(), } -primitive! { - TrackSizing: "GridUnit", - Value::TrackSizing, - Value::Length(v) => v.into(), - Value::Relative(v) => v.into(), - Value::Linear(v) => v.into(), - Value::Fractional(v) => v.into(), -} +primitive! { Fractional: "fractional", Value::Fractional } primitive! { Color: "color", Value::Color } primitive! { String: "string", Value::Str } primitive! { ArrayValue: "array", Value::Array } diff --git a/src/geom/gen.rs b/src/geom/gen.rs index c530ff2b1..075b73775 100644 --- a/src/geom/gen.rs +++ b/src/geom/gen.rs @@ -22,6 +22,14 @@ impl Gen { { Self { cross: value.clone(), main: value } } + + /// Convert to the specific representation. + pub fn to_spec(self, main: SpecAxis) -> Spec { + match main { + SpecAxis::Horizontal => Spec::new(self.main, self.cross), + SpecAxis::Vertical => Spec::new(self.cross, self.main), + } + } } impl Gen { @@ -32,6 +40,16 @@ impl Gen { cross: Length::zero(), } } + + /// Convert to a point. + pub fn to_point(self, main: SpecAxis) -> Point { + self.to_spec(main).to_point() + } + + /// Convert to a size. + pub fn to_size(self, main: SpecAxis) -> Size { + self.to_spec(main).to_size() + } } impl Get for Gen { @@ -52,17 +70,6 @@ impl Get for Gen { } } -impl Switch for Gen { - type Other = Spec; - - fn switch(self, main: SpecAxis) -> Self::Other { - match main { - SpecAxis::Horizontal => Spec::new(self.main, self.cross), - SpecAxis::Vertical => Spec::new(self.cross, self.main), - } - } -} - impl Debug for Gen { fn fmt(&self, f: &mut Formatter) -> fmt::Result { write!(f, "Gen({:?}, {:?})", self.main, self.cross) @@ -88,17 +95,6 @@ impl GenAxis { } } -impl Switch for GenAxis { - type Other = SpecAxis; - - fn switch(self, main: SpecAxis) -> Self::Other { - match self { - Self::Main => main, - Self::Cross => main.other(), - } - } -} - impl Display for GenAxis { fn fmt(&self, f: &mut Formatter) -> fmt::Result { f.pad(match self { diff --git a/src/geom/gridu.rs b/src/geom/gridu.rs deleted file mode 100644 index 70fc17e4f..000000000 --- a/src/geom/gridu.rs +++ /dev/null @@ -1,73 +0,0 @@ -use super::*; - -/// An enum with the length that a grid cell may have. -#[derive(Copy, Clone, PartialEq, Hash)] -pub enum TrackSizing { - /// A length stated in absolute values and fractions of the parent's size. - Linear(Linear), - /// A length that is the fraction of the remaining free space in the parent. - Fractional(Fractional), - /// The cell will fit its contents. - Auto, -} - -impl TrackSizing { - pub fn is_zero(&self) -> bool { - match self { - Self::Linear(l) => l.is_zero(), - Self::Fractional(f) => f.is_zero(), - Self::Auto => false, - } - } - - pub fn preliminary_length(&self, resolve: Length) -> Length { - match self { - Self::Linear(l) => l.resolve(resolve), - _ => resolve, - } - } -} - -impl Display for TrackSizing { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - match self { - Self::Linear(x) => ::fmt(x, f), - Self::Fractional(x) => ::fmt(x, f), - Self::Auto => write!(f, "auto"), - } - } -} - -impl Debug for TrackSizing { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - match self { - Self::Linear(x) => ::fmt(x, f), - Self::Fractional(x) => ::fmt(x, f), - Self::Auto => write!(f, "auto"), - } - } -} - -impl From for TrackSizing { - fn from(abs: Length) -> Self { - Self::Linear(abs.into()) - } -} - -impl From for TrackSizing { - fn from(rel: Relative) -> Self { - Self::Linear(rel.into()) - } -} - -impl From for TrackSizing { - fn from(lin: Linear) -> Self { - Self::Linear(lin) - } -} - -impl From for TrackSizing { - fn from(fr: Fractional) -> Self { - Self::Fractional(fr) - } -} diff --git a/src/geom/mod.rs b/src/geom/mod.rs index fdc3980e7..25344f0e7 100644 --- a/src/geom/mod.rs +++ b/src/geom/mod.rs @@ -7,7 +7,6 @@ mod angle; mod dir; mod fr; mod gen; -mod gridu; mod length; mod linear; mod path; @@ -22,7 +21,6 @@ pub use angle::*; pub use dir::*; pub use fr::*; pub use gen::*; -pub use gridu::*; pub use length::*; pub use linear::*; pub use path::*; @@ -48,15 +46,3 @@ pub trait Get { /// Borrow the component for the specified index mutably. fn get_mut(&mut self, index: Index) -> &mut Self::Component; } - -/// Switch between the specific and generic representations of a type. -/// -/// The generic representation deals with main and cross axes while the specific -/// representation deals with horizontal and vertical axes. -pub trait Switch { - /// The type of the other version. - type Other; - - /// The other version of this type based on the current main axis. - fn switch(self, main: SpecAxis) -> Self::Other; -} diff --git a/src/geom/point.rs b/src/geom/point.rs index babbdfef4..8982dcea9 100644 --- a/src/geom/point.rs +++ b/src/geom/point.rs @@ -26,6 +26,14 @@ impl Point { pub fn splat(value: Length) -> Self { Self { x: value, y: value } } + + /// Convert to the generic representation. + pub fn to_gen(self, main: SpecAxis) -> Gen { + match main { + SpecAxis::Horizontal => Gen::new(self.y, self.x), + SpecAxis::Vertical => Gen::new(self.x, self.y), + } + } } impl Get for Point { @@ -46,17 +54,6 @@ impl Get for Point { } } -impl Switch for Point { - type Other = Gen; - - fn switch(self, main: SpecAxis) -> Self::Other { - match main { - SpecAxis::Horizontal => Gen::new(self.y, self.x), - SpecAxis::Vertical => Gen::new(self.x, self.y), - } - } -} - impl Debug for Point { fn fmt(&self, f: &mut Formatter) -> fmt::Result { write!(f, "Point({:?}, {:?})", self.x, self.y) diff --git a/src/geom/size.rs b/src/geom/size.rs index e20c24afb..b859cb800 100644 --- a/src/geom/size.rs +++ b/src/geom/size.rs @@ -49,6 +49,14 @@ impl Size { pub fn to_point(self) -> Point { Point::new(self.width, self.height) } + + /// Convert to the generic representation. + pub fn to_gen(self, main: SpecAxis) -> Gen { + match main { + SpecAxis::Horizontal => Gen::new(self.height, self.width), + SpecAxis::Vertical => Gen::new(self.width, self.height), + } + } } impl Get for Size { @@ -69,17 +77,6 @@ impl Get for Size { } } -impl Switch for Size { - type Other = Gen; - - fn switch(self, main: SpecAxis) -> Self::Other { - match main { - SpecAxis::Horizontal => Gen::new(self.height, self.width), - SpecAxis::Vertical => Gen::new(self.width, self.height), - } - } -} - impl Debug for Size { fn fmt(&self, f: &mut Formatter) -> fmt::Result { write!(f, "Size({:?}, {:?})", self.width, self.height) diff --git a/src/geom/spec.rs b/src/geom/spec.rs index 713f0a16b..e7a2d056d 100644 --- a/src/geom/spec.rs +++ b/src/geom/spec.rs @@ -25,6 +25,14 @@ impl Spec { vertical: value, } } + + /// Convert to the generic representation. + pub fn to_gen(self, main: SpecAxis) -> Gen { + match main { + SpecAxis::Horizontal => Gen::new(self.vertical, self.horizontal), + SpecAxis::Vertical => Gen::new(self.horizontal, self.vertical), + } + } } impl Spec { @@ -65,17 +73,6 @@ impl Get for Spec { } } -impl Switch for Spec { - type Other = Gen; - - fn switch(self, main: SpecAxis) -> Self::Other { - match main { - SpecAxis::Horizontal => Gen::new(self.vertical, self.horizontal), - SpecAxis::Vertical => Gen::new(self.horizontal, self.vertical), - } - } -} - impl Debug for Spec { fn fmt(&self, f: &mut Formatter) -> fmt::Result { write!(f, "Spec({:?}, {:?})", self.horizontal, self.vertical) @@ -101,14 +98,6 @@ impl SpecAxis { } } -impl Switch for SpecAxis { - type Other = GenAxis; - - fn switch(self, main: SpecAxis) -> Self::Other { - if self == main { GenAxis::Main } else { GenAxis::Cross } - } -} - impl Display for SpecAxis { fn fmt(&self, f: &mut Formatter) -> fmt::Result { f.pad(match self { diff --git a/src/layout/frame.rs b/src/layout/frame.rs index f1dc07e6f..6cecc7a34 100644 --- a/src/layout/frame.rs +++ b/src/layout/frame.rs @@ -35,14 +35,6 @@ impl Frame { self.push(pos + subpos, element); } } - - /// Translate the positions of all elements in the frame by adding the - /// argument to their position. - pub fn translate(&mut self, amount: Point) { - for (pos, _) in &mut self.elements { - *pos += amount; - } - } } /// The building block frames are composed of. diff --git a/src/layout/grid.rs b/src/layout/grid.rs index 9c4c2e0e3..3ba6c16bd 100644 --- a/src/layout/grid.rs +++ b/src/layout/grid.rs @@ -1,216 +1,128 @@ -use std::usize; - use super::*; -use crate::library::GridUnits; -/// A node that stacks its children. +/// A node that arranges its children in a grid. #[derive(Debug, Clone, PartialEq, Hash)] pub struct GridNode { - /// The `main` and `cross` directions of this stack. - /// - /// The children are stacked along the `main` direction. The `cross` - /// direction is required for aligning the children. - pub dir: Dir, - /// The nodes to be stacked. + /// The column (cross) direction of this stack. + pub column_dir: Dir, + /// The nodes to be arranged in a grid. pub children: Vec, - pub tracks: Gen, - pub gutter: Gen, + /// Defines sizing for rows and columns. + pub tracks: Gen, + /// Defines sizing of the gutter between rows and columns. + pub gutter: Gen, } impl Layout for GridNode { fn layout(&self, ctx: &mut LayoutContext, regions: &Regions) -> Vec { - let layout = GridLayouter::new(self, regions).layout(ctx); - layout + GridLayouter::new(self, regions.clone()).layout(ctx) } } -#[derive(Debug)] -enum GridItem<'a> { +impl From for AnyNode { + fn from(grid: GridNode) -> Self { + Self::new(grid) + } +} + +struct GridLayouter<'a> { + cross: SpecAxis, + main: SpecAxis, + cols: Vec, + rows: Vec, + cells: Vec>, + regions: Regions, + rrows: Vec<(usize, Option)>, + rcols: Vec, + finished: Vec, +} + +enum Cell<'a> { Node(&'a AnyNode), Gutter, } -#[derive(Debug)] -struct GridLayouter<'a> { - items: Vec>, - cols: Vec, - rows: Vec, - region: Regions, - dir: Dir, - rrows: Vec<(usize, Option)>, - rcols: Vec, - frames: Vec, -} - impl<'a> GridLayouter<'a> { - fn new( - grid: &'a GridNode, - regions: &Regions, - ) -> Self { - let mut items = vec![]; + fn new(grid: &'a GridNode, regions: Regions) -> Self { let mut col_sizes = vec![]; let mut row_sizes = vec![]; - let cols = grid.tracks.cross.0.len(); - // Create at least as many rows as specified and a row to fit every item. - let rows = if cols > 0 { - let res = grid - .tracks - .main - .0 - .len() - .max(grid.children.len() / cols + (grid.children.len() % cols).clamp(0, 1)); - res - } else { - 0 + let mut cells = vec![]; + + // A grid always needs to have at least one column. + let cols = grid.tracks.cross.0.len().max(1); + + // Create at least as many rows as specified and also at least as many + // as necessary to place each item. + let rows = { + let len = grid.children.len(); + let specified = grid.tracks.main.0.len(); + let necessary = len / cols + (len % cols).clamp(0, 1); + specified.max(necessary) }; - for (i, col_size) in grid.tracks.cross.0.iter().enumerate() { - let last = i == cols - 1; - col_sizes.push(*col_size); - - if !last { - let gutter = grid.gutter.cross.get(i); - col_sizes.push(gutter); + // Collect the track sizing for all columns, including gutter columns. + for i in 0 .. cols { + col_sizes.push(grid.tracks.cross.get(i)); + if i < cols - 1 { + col_sizes.push(grid.gutter.cross.get(i)); } } - for (i, row_size) in (0 .. rows).map(|i| (i, grid.tracks.main.get(i))) { - let last = i == rows - 1; - row_sizes.push(row_size); - - if !last { - let gutter = grid.gutter.main.get(i); - row_sizes.push(gutter); + // Collect the track sizing for all rows, including gutter rows. + for i in 0 .. rows { + row_sizes.push(grid.tracks.main.get(i)); + if i < rows - 1 { + row_sizes.push(grid.gutter.main.get(i)); } } + // Build up the matrix of cells, including gutter cells. for (i, item) in grid.children.iter().enumerate() { - if cols == 0 { - break; - } + cells.push(Cell::Node(item)); let row = i / cols; let col = i % cols; - items.push(GridItem::Node(item)); - - if col != cols - 1 { - // Push gutter - items.push(GridItem::Gutter); - } else if row != rows - 1 { - // Push gutter row. + if col < cols - 1 { + // Push gutter after each child. + cells.push(Cell::Gutter); + } else if row < rows - 1 { + // Except for the last child of each row. + // There we push a gutter row. for _ in 0 .. col_sizes.len() { - items.push(GridItem::Gutter); + cells.push(Cell::Gutter); } } } - // Fill the thing up - while items.len() < col_sizes.len() * row_sizes.len() { - items.push(GridItem::Gutter) + // Fill the thing up. + while cells.len() < col_sizes.len() * row_sizes.len() { + cells.push(Cell::Gutter) } - GridLayouter { + Self { + cross: grid.column_dir.axis(), + main: grid.column_dir.axis().other(), cols: col_sizes, rows: row_sizes, - region: regions.clone(), - dir: grid.dir, - items, + cells, + regions, rrows: vec![], rcols: vec![], - frames: vec![], + finished: vec![], } } - fn get(&self, x: usize, y: usize) -> &GridItem<'_> { - assert!(x < self.cols.len()); - assert!(y < self.rows.len()); - let row_cmp = y * self.cols.len(); - - self.items.get(row_cmp + x).unwrap() - } - - fn main(&self) -> SpecAxis { - self.dir.axis().other() - } - - fn cross(&self) -> SpecAxis { - self.dir.axis() - } - - fn finish_region(&mut self, ctx: &mut LayoutContext, total_frs: f64) { - let mut pos = Gen::splat(Length::zero()); - let pos2point = |mut pos: Gen| { - if !self.dir.is_positive() { - pos.cross = -pos.cross; - } - pos.switch(self.main()).to_point() - }; - let mut frame = Frame::new(Size::zero(), Length::zero()); - let mut total_cross = Length::zero(); - let mut total_main = Length::zero(); - - for (x, &w) in self.rcols.iter().enumerate() { - let total: Length = self.rrows.iter().filter_map(|(_, x)| *x).sum(); - let available = self.region.current.get(self.main()) - total; - total_cross += w; - - for (y, h) in self.rrows.iter() { - let element = self.get(x, *y); - let h = if let Some(len) = h { - *len - } else { - if let TrackSizing::Fractional(f) = self.rows[*y] { - if total_frs > 0.0 { - let res = available * (f.get() / total_frs); - if res.is_finite() { - res - } else { - Length::zero() - } - } else { - Length::zero() - } - } else { - unreachable!() - } - }; - if x == 0 { - total_main += h; - } - - if let GridItem::Node(n) = element { - let item = n.layout(ctx, &Regions::one(Gen::new(w, h).switch(self.main()).to_size(), Spec::splat(false))).remove(0); - frame.push_frame(pos2point(pos), item); - } - - pos.main += h; - } - pos.main = Length::zero(); - pos.cross += self.dir.factor() as f64 * w; - } - - if !self.dir.is_positive() { - frame.translate(Gen::new(total_cross, Length::zero()).switch(self.main()).to_point()); - } - - frame.size = Gen::new(total_cross, total_main).switch(self.main()).to_size(); - frame.baseline = frame.size.height; - - self.frames.push(frame); - - self.rrows.clear(); - self.region.next(); - } - fn layout(mut self, ctx: &mut LayoutContext) -> Vec { // Shrink area by linear sizing. - let mut available = self.region.current.get(self.cross()); + let mut available = self.regions.current.get(self.cross); available -= self .cols .iter() .filter_map(|x| match x { - TrackSizing::Linear(l) => Some(l.resolve(self.region.base.get(self.cross()))), + TrackSizing::Linear(l) => { + Some(l.resolve(self.regions.base.get(self.cross))) + } _ => None, }) .sum(); @@ -232,34 +144,31 @@ impl<'a> GridLayouter<'a> { let mut col_width = vec![]; - // For each of the auto columns, lay out all elements with `preliminary_length` - // rows and build max. + // For each of the auto columns, lay out all elements with + // `preliminary_length` rows and build max. for x in auto_columns { let mut max = Length::zero(); - for (y, row_height) in - self.rows.iter().enumerate().map(|(y, s)| { - (y, s.preliminary_length(self.region.base.get(self.main()))) - }) - { - let item = self.get(x, y); - let size = - Gen::new(self.region.current.get(self.cross()), row_height).switch(self.main()).to_size(); + + for (y, row) in self.rows.iter().enumerate() { + let mut size = self.regions.current; + if let TrackSizing::Linear(l) = row { + *size.get_mut(self.main) = + l.resolve(self.regions.base.get(self.main)); + } + let region = Regions::one(size, Spec::splat(false)); - match item { - GridItem::Node(n) => { - max = max.max( - n.layout(ctx, ®ion).first().unwrap().size.get(self.cross()), - ) - } - GridItem::Gutter => {} + if let Cell::Node(node) = self.get(x, y) { + let frame = node.layout(ctx, ®ion).remove(0); + max = max.max(frame.size.get(self.cross)) } } col_width.push((x, max)); } - // If accumulated auto column size exceeds available size, redistribute space - // proportionally amongst elements that exceed their size allocation. + // If accumulated auto column size exceeds available size, redistribute + // space proportionally amongst elements that exceed their size + // allocation. let mut total: Length = col_width.iter().map(|(_, x)| *x).sum(); if total > available { let alloc = available / col_width.len() as f64; @@ -288,23 +197,25 @@ impl<'a> GridLayouter<'a> { } // Build rcols - for (x, len) in col_width.into_iter().map(|(x, s)| (x, Some(s))).chain(std::iter::once((self.cols.len(), None))) { + for (x, len) in col_width + .into_iter() + .map(|(x, s)| (x, Some(s))) + .chain(std::iter::once((self.cols.len(), None))) + { for i in self.rcols.len() .. x { let len = match self.cols[i] { - TrackSizing::Linear(l) => l.resolve(self.region.base.get(self.cross())), + TrackSizing::Linear(l) => { + l.resolve(self.regions.base.get(self.cross)) + } TrackSizing::Fractional(f) => { if col_frac == 0.0 { Length::zero() } else { let res: Length = (available - total) * (f.get() / col_frac); - if res.is_finite() { - res - } else { - Length::zero() - } + if res.is_finite() { res } else { Length::zero() } } } - TrackSizing::Auto => unreachable!(), + TrackSizing::Auto => unreachable!("x is an auto track"), }; self.rcols.push(len); @@ -317,27 +228,23 @@ impl<'a> GridLayouter<'a> { // Determine non-`fr` row heights let mut total_frs = 0.0; - let mut current = self.region.current.get(self.main()); + let mut current = self.regions.current.get(self.main); - for y in 0..self.rows.len() { - let height = &self.rows[y]; - let resolved = match height { - TrackSizing::Linear(l) => Some(l.resolve(self.region.base.get(self.main()))), + for y in 0 .. self.rows.len() { + let resolved = match self.rows[y] { + TrackSizing::Linear(l) => { + Some(l.resolve(self.regions.base.get(self.main))) + } TrackSizing::Auto => { let mut max = Length::zero(); for (x, len) in self.rcols.iter().enumerate() { - let node = self.get(x, y); - if let GridItem::Node(node) = node { - let frames = node.layout( - ctx, - &Regions::one( - Gen::new(*len, current) - .switch(self.main()) - .to_size(), - Spec::splat(false), - ), + if let Cell::Node(node) = self.get(x, y) { + let regions = Regions::one( + Gen::new(*len, current).to_size(self.main), + Spec::splat(false), ); - max = max.max(frames.first().unwrap().size.get(self.main())); + let frame = node.layout(ctx, ®ions).remove(0); + max = max.max(frame.size.get(self.main)); } } Some(max) @@ -345,13 +252,13 @@ impl<'a> GridLayouter<'a> { TrackSizing::Fractional(f) => { total_frs += f.get(); None - }, + } }; if let Some(resolved) = resolved { - while !current.fits(resolved) && !self.region.in_full_last() { + while !current.fits(resolved) && !self.regions.in_full_last() { self.finish_region(ctx, total_frs); - current = self.region.current.get(self.main()); + current = self.regions.current.get(self.main); total_frs = 0.0; } current -= resolved; @@ -361,12 +268,91 @@ impl<'a> GridLayouter<'a> { } self.finish_region(ctx, total_frs); - self.frames + self.finished + } + + fn finish_region(&mut self, ctx: &mut LayoutContext, total_frs: f64) { + let mut pos = Gen::splat(Length::zero()); + let mut frame = Frame::new(Size::zero(), Length::zero()); + let mut total_cross = Length::zero(); + let mut total_main = Length::zero(); + + for (x, &w) in self.rcols.iter().enumerate() { + let total: Length = self.rrows.iter().filter_map(|(_, x)| *x).sum(); + let available = self.regions.current.get(self.main) - total; + total_cross += w; + + for (y, h) in self.rrows.iter() { + let element = self.get(x, *y); + let h = if let Some(len) = h { + *len + } else if let TrackSizing::Fractional(f) = self.rows[*y] { + if total_frs > 0.0 { + let res = available * (f.get() / total_frs); + if res.is_finite() { res } else { Length::zero() } + } else { + Length::zero() + } + } else { + unreachable!("non-fractional tracks are already resolved"); + }; + + if x == 0 { + total_main += h; + } + + if let Cell::Node(n) = element { + let regions = Regions::one( + Gen::new(w, h).to_size(self.main), + Spec::splat(false), + ); + let item = n.layout(ctx, ®ions).remove(0); + frame.push_frame(pos.to_point(self.main), item); + } + + pos.main += h; + } + pos.main = Length::zero(); + pos.cross += w; + } + + frame.size = Gen::new(total_cross, total_main).to_size(self.main); + frame.baseline = frame.size.height; + + self.rrows.clear(); + self.regions.next(); + self.finished.push(frame); + } + + fn get(&self, x: usize, y: usize) -> &Cell<'a> { + assert!(x < self.cols.len()); + assert!(y < self.rows.len()); + self.cells.get(y * self.cols.len() + x).unwrap() } } -impl From for AnyNode { - fn from(grid: GridNode) -> Self { - Self::new(grid) +/// A list of track sizing definitions. +#[derive(Default, Debug, Clone, PartialEq, Hash)] +pub struct Tracks(pub Vec); + +impl Tracks { + /// Get the sizing for the track at the given `idx`. + fn get(&self, idx: usize) -> TrackSizing { + self.0 + .get(idx) + .or(self.0.last()) + .copied() + .unwrap_or(TrackSizing::Auto) } } + +/// Defines how to size a grid cell along an axis. +#[derive(Debug, Copy, Clone, PartialEq, Hash)] +pub enum TrackSizing { + /// Fit the cell to its contents. + Auto, + /// A length stated in absolute values and fractions of the parent's size. + Linear(Linear), + /// A length that is the fraction of the remaining free space in the parent. + Fractional(Fractional), +} diff --git a/src/layout/stack.rs b/src/layout/stack.rs index 410f53c66..e4c0708d5 100644 --- a/src/layout/stack.rs +++ b/src/layout/stack.rs @@ -29,24 +29,7 @@ pub enum StackChild { impl Layout for StackNode { fn layout(&self, ctx: &mut LayoutContext, regions: &Regions) -> Vec { - let mut layouter = StackLayouter::new(self.dirs, self.aspect, regions.clone()); - for child in &self.children { - match *child { - StackChild::Spacing(amount) => layouter.push_spacing(amount), - StackChild::Any(ref node, aligns) => { - let mut frames = node.layout(ctx, &layouter.regions).into_iter(); - if let Some(frame) = frames.next() { - layouter.push_frame(frame, aligns); - } - - for frame in frames { - layouter.finish_region(); - layouter.push_frame(frame, aligns); - } - } - } - } - layouter.finish() + StackLayouter::new(self, regions.clone()).layout(ctx) } } @@ -56,107 +39,154 @@ impl From for AnyNode { } } -struct StackLayouter { - dirs: Gen, - aspect: Option, +struct StackLayouter<'a> { + /// The directions of the stack. + stack: &'a StackNode, + /// The axis of the main direction. main: SpecAxis, + /// The region to layout into. regions: Regions, - finished: Vec, - frames: Vec<(Length, Frame, Gen)>, + /// Offset, alignment and frame for all children that fit into the current + /// region. The exact positions are not known yet. + frames: Vec<(Length, Gen, Frame)>, + /// The full size of `regions.current` that was available before we started + /// subtracting. full: Size, - size: Gen, + /// The generic size used by the frames for the current region. + used: Gen, + /// The alignment ruler for the current region. ruler: Align, + /// Finished frames for previous regions. + finished: Vec, } -impl StackLayouter { - fn new(dirs: Gen, aspect: Option, mut regions: Regions) -> Self { - if let Some(aspect) = aspect { +impl<'a> StackLayouter<'a> { + fn new(stack: &'a StackNode, mut regions: Regions) -> Self { + if let Some(aspect) = stack.aspect { regions.apply_aspect_ratio(aspect); } Self { - dirs, - aspect, - main: dirs.main.axis(), + stack, + main: stack.dirs.main.axis(), finished: vec![], frames: vec![], full: regions.current, - size: Gen::zero(), + used: Gen::zero(), ruler: Align::Start, regions, } } + fn layout(mut self, ctx: &mut LayoutContext) -> Vec { + for child in &self.stack.children { + match *child { + StackChild::Spacing(amount) => self.push_spacing(amount), + StackChild::Any(ref node, aligns) => { + let mut frames = node.layout(ctx, &self.regions).into_iter(); + if let Some(frame) = frames.next() { + self.push_frame(frame, aligns); + } + + for frame in frames { + self.finish_region(); + self.push_frame(frame, aligns); + } + } + } + } + + self.finish_region(); + self.finished + } + fn push_spacing(&mut self, amount: Length) { + // Cap the spacing to the remaining available space. let remaining = self.regions.current.get_mut(self.main); let capped = amount.min(*remaining); + + // Grow our size and shrink the available space in the region. + self.used.main += capped; *remaining -= capped; - self.size.main += capped; } fn push_frame(&mut self, frame: Frame, aligns: Gen) { + let size = frame.size; + + // Don't allow `Start` after `End` in the same region. if self.ruler > aligns.main { self.finish_region(); } - while !self.regions.current.fits(frame.size) && !self.regions.in_full_last() { + // Adjust the ruler. + self.ruler = aligns.main; + + // Find a fitting region. + while !self.regions.current.fits(size) && !self.regions.in_full_last() { self.finish_region(); } - let offset = self.size.main; - let size = frame.size.switch(self.main); - self.size.main += size.main; - self.size.cross.set_max(size.cross); - self.ruler = aligns.main; - *self.regions.current.get_mut(self.main) -= size.main; - self.frames.push((offset, frame, aligns)); + // Remember the frame with offset and alignment. + self.frames.push((self.used.main, aligns, frame)); + + // Grow our size and shrink available space in the region. + let gen = size.to_gen(self.main); + self.used.main += gen.main; + self.used.cross.set_max(gen.cross); + *self.regions.current.get_mut(self.main) -= gen.main; } fn finish_region(&mut self) { + let used = self.used.to_size(self.main); let fixed = self.regions.fixed; - let used = self.size.switch(self.main).to_size(); - let mut size = Size::new( + // Determine the stack's size dependening on whether the region is + // fixed. + let mut stack_size = Size::new( if fixed.horizontal { self.full.width } else { used.width }, if fixed.vertical { self.full.height } else { used.height }, ); - if let Some(aspect) = self.aspect { - let width = size + // Make sure the stack's size satisfies the aspect ratio. + if let Some(aspect) = self.stack.aspect { + let width = stack_size .width - .max(aspect.into_inner() * size.height) + .max(aspect.into_inner() * stack_size.height) .min(self.full.width) .min(aspect.into_inner() * self.full.height); - size = Size::new(width, width / aspect.into_inner()); + stack_size = Size::new(width, width / aspect.into_inner()); } - let mut output = Frame::new(size, size.height); + let mut output = Frame::new(stack_size, stack_size.height); let mut first = true; - let used = self.size; - let size = size.switch(self.main); - - for (offset, frame, aligns) in std::mem::take(&mut self.frames) { - let child = frame.size.switch(self.main); + // Place all frames. + for (offset, aligns, frame) in std::mem::take(&mut self.frames) { + let stack_size = stack_size.to_gen(self.main); + let child_size = frame.size.to_gen(self.main); // Align along the cross axis. - let cross = aligns - .cross - .resolve(self.dirs.cross, Length::zero() .. size.cross - child.cross); + let cross = aligns.cross.resolve( + self.stack.dirs.cross, + Length::zero() .. stack_size.cross - child_size.cross, + ); // Align along the main axis. let main = aligns.main.resolve( - self.dirs.main, - if self.dirs.main.is_positive() { - offset .. size.main - used.main + offset + self.stack.dirs.main, + if self.stack.dirs.main.is_positive() { + offset .. stack_size.main - self.used.main + offset } else { - let offset_with_self = offset + child.main; - used.main - offset_with_self .. size.main - offset_with_self + let offset_with_self = offset + child_size.main; + self.used.main - offset_with_self + .. stack_size.main - offset_with_self }, ); - let pos = Gen::new(cross, main).switch(self.main).to_point(); + let pos = Gen::new(cross, main).to_point(self.main); + + // The baseline of the stack is that of the first frame. if first { output.baseline = pos.y + frame.baseline; first = false; @@ -165,18 +195,15 @@ impl StackLayouter { output.push_frame(pos, frame); } - self.size = Gen::zero(); - self.ruler = Align::Start; + // Move on to the next region. self.regions.next(); - if let Some(aspect) = self.aspect { + if let Some(aspect) = self.stack.aspect { self.regions.apply_aspect_ratio(aspect); } + self.full = self.regions.current; + self.used = Gen::zero(); + self.ruler = Align::Start; self.finished.push(output); } - - fn finish(mut self) -> Vec { - self.finish_region(); - self.finished - } } diff --git a/src/library/grid.rs b/src/library/grid.rs index 0aa1fc9dc..cfe46bf46 100644 --- a/src/library/grid.rs +++ b/src/library/grid.rs @@ -1,76 +1,79 @@ -use crate::layout::GridNode; +use crate::layout::{GridNode, TrackSizing, Tracks}; use super::*; -/// `stack`: Stack children along an axis. +/// `grid`: Arrange children into a grid. /// /// # Positional parameters /// - Children: variadic, of type `template`. /// /// # Named parameters -/// - Column widths: `columns`, of type `Array`. -/// - Row widths: `rows`, of type `Array`. -/// - Gutter: `gutter-vertical` and `gutter-horizontal` for individual track axis or `gutter` for both, of type `Array` respectively. -/// - Stacking direction: `dir`, of type `direction`. +/// - Column sizing: `columns`, of type `tracks`. +/// - Row sizing: `rows`, of type `tracks`. +/// - Column direction: `column-dir`, of type `direction`. +/// - Gutter: `gutter`, shorthand for equal column and row gutter, of type `tracks`. +/// - Gutter for rows: `gutter-rows`, of type `tracks`. +/// - Gutter for columns: `gutter-columns`, of type `tracks`. /// /// # Return value /// A template that arranges its children along the specified grid cells. /// /// # Relevant types and constants +/// - Type `tracks` +/// - coerces from `array` of `track-sizing` +/// - Type `track-sizing` +/// - `auto` +// - coerces from `length` +// - coerces from `relative` +// - coerces from `linear` +// - coerces from `fractional` /// - Type `direction` /// - `ltr` /// - `rtl` /// - `ttb` /// - `btt` pub fn grid(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { - let cols = args.eat_named::(ctx, "columns").unwrap_or_default(); - let rows = args.eat_named::(ctx, "rows").unwrap_or_default(); - - let gutter = args.eat_named(ctx, "gutter"); - let gutter_vertical = args - .eat_named::(ctx, "gutter-col") - .or_else(|| gutter.clone()) - .unwrap_or_default(); - let gutter_horizontal = args - .eat_named::(ctx, "gutter-row") - .or(gutter) - .unwrap_or_default(); - - let dir = args.eat_named(ctx, "dir"); + let columns = args.eat_named::(ctx, "columns").unwrap_or_default(); + let rows = args.eat_named::(ctx, "rows").unwrap_or_default(); + let column_dir = args.eat_named(ctx, "column-dir"); + let gutter = args.eat_named::(ctx, "gutter").unwrap_or_default(); + let gutter_columns = args.eat_named::(ctx, "gutter-columns"); + let gutter_rows = args.eat_named::(ctx, "gutter-rows"); let children = args.eat_all::(ctx); Value::template("grid", move |ctx| { - let children = - children.iter().map(|child| ctx.exec_template_stack(child).into()).collect(); + let children = children + .iter() + .map(|child| ctx.exec_template_stack(child).into()) + .collect(); + ctx.push(GridNode { - dir: dir.unwrap_or_else(|| ctx.state.lang.dir), + column_dir: column_dir.unwrap_or(ctx.state.lang.dir), children, - gutter: Gen::new(gutter_vertical.clone(), gutter_horizontal.clone()), - tracks: Gen::new(cols.clone(), rows.clone()), + tracks: Gen::new(columns.clone(), rows.clone()), + gutter: Gen::new( + gutter_columns.as_ref().unwrap_or(&gutter).clone(), + gutter_rows.as_ref().unwrap_or(&gutter).clone(), + ), }) }) } -/// A list of [`GridUnit`]s. -#[derive(Default, Debug, Clone, PartialEq, Hash)] -pub struct GridUnits(pub Vec); - -impl GridUnits { - pub fn get(&self, index: usize) -> TrackSizing { - if self.0.is_empty() { - TrackSizing::Auto - } else { - *self.0.get(index).unwrap_or(self.0.last().unwrap()) - } - } -} - value! { - GridUnits: "array of fractional values, lengths, and the `auto` keyword", - Value::TrackSizing(value) => Self(vec![value]), + Tracks: "array of `auto`s, linears, and fractionals", + Value::Int(count) => Self(vec![TrackSizing::Auto; count.max(0) as usize]), Value::Array(values) => Self(values .into_iter() .filter_map(|v| v.cast().ok()) .collect() ), } + +value! { + TrackSizing: "`auto`, linear, or fractional", + Value::Auto => TrackSizing::Auto, + Value::Length(v) => TrackSizing::Linear(v.into()), + Value::Relative(v) => TrackSizing::Linear(v.into()), + Value::Linear(v) => TrackSizing::Linear(v), + Value::Fractional(v) => TrackSizing::Fractional(v), +} diff --git a/src/parse/mod.rs b/src/parse/mod.rs index 1f14f36f5..5ab5b2d8a 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -314,11 +314,6 @@ fn primary(p: &mut Parser, atomic: bool) -> Option { Some(Token::For) => expr_for(p), Some(Token::Import) => expr_import(p), Some(Token::Include) => expr_include(p), - Some(Token::Auto) => { - let start = p.next_start(); - p.assert(Token::Auto); - Some(Expr::Auto(p.span(start))) - } // Nothing. _ => { @@ -334,6 +329,7 @@ fn literal(p: &mut Parser) -> Option { let expr = match p.peek()? { // Basic values. Token::None => Expr::None(span), + Token::Auto => Expr::Auto(span), Token::Bool(b) => Expr::Bool(span, b), Token::Int(i) => Expr::Int(span, i), Token::Float(f) => Expr::Float(span, f), diff --git a/src/parse/tokens.rs b/src/parse/tokens.rs index f82d0ae4b..f3ca25d95 100644 --- a/src/parse/tokens.rs +++ b/src/parse/tokens.rs @@ -350,6 +350,7 @@ impl<'s> Tokens<'s> { "and" => Token::And, "or" => Token::Or, "none" => Token::None, + "auto" => Token::Auto, "true" => Token::Bool(true), "false" => Token::Bool(false), id => keyword(id).unwrap_or(Token::Ident(id)), @@ -474,7 +475,6 @@ fn keyword(id: &str) -> Option> { "for" => Token::For, "in" => Token::In, "while" => Token::While, - "auto" => Token::Auto, "break" => Token::Break, "continue" => Token::Continue, "return" => Token::Return, @@ -759,12 +759,6 @@ mod tests { ("for", For), ("in", In), ("import", Import), - ("while", While), - ("break", Break), - ("continue", Continue), - ("using", Using), - ("auto", Auto), - ("return", Return), ]; for &(s, t) in &list { diff --git a/src/pretty.rs b/src/pretty.rs index 82e81ce64..1281e27b9 100644 --- a/src/pretty.rs +++ b/src/pretty.rs @@ -4,7 +4,7 @@ use std::fmt::{self, Arguments, Write}; use crate::color::{Color, RgbaColor}; use crate::eval::*; -use crate::geom::{Angle, Fractional, Length, Linear, Relative, TrackSizing}; +use crate::geom::{Angle, Fractional, Length, Linear, Relative}; use crate::syntax::*; /// Pretty print an item and return the resulting string. @@ -451,24 +451,24 @@ impl Pretty for Ident { impl Pretty for Value { fn pretty(&self, p: &mut Printer) { match self { - Value::None => p.push_str("none"), - Value::Bool(v) => v.pretty(p), - Value::Int(v) => v.pretty(p), - Value::Float(v) => v.pretty(p), - Value::Length(v) => v.pretty(p), - Value::Angle(v) => v.pretty(p), - Value::Relative(v) => v.pretty(p), - Value::Fractional(v) => v.pretty(p), - Value::Linear(v) => v.pretty(p), - Value::TrackSizing(v) => v.pretty(p), - Value::Color(v) => v.pretty(p), - Value::Str(v) => v.pretty(p), - Value::Array(v) => v.pretty(p), - Value::Dict(v) => v.pretty(p), - Value::Template(v) => v.pretty(p), - Value::Func(v) => v.pretty(p), - Value::Any(v) => v.pretty(p), - Value::Error => p.push_str(""), + Self::None => p.push_str("none"), + Self::Auto => p.push_str("auto"), + Self::Bool(v) => v.pretty(p), + Self::Int(v) => v.pretty(p), + Self::Float(v) => v.pretty(p), + Self::Length(v) => v.pretty(p), + Self::Angle(v) => v.pretty(p), + Self::Relative(v) => v.pretty(p), + Self::Linear(v) => v.pretty(p), + Self::Fractional(v) => v.pretty(p), + Self::Color(v) => v.pretty(p), + Self::Str(v) => v.pretty(p), + Self::Array(v) => v.pretty(p), + Self::Dict(v) => v.pretty(p), + Self::Template(v) => v.pretty(p), + Self::Func(v) => v.pretty(p), + Self::Any(v) => v.pretty(p), + Self::Error => p.push_str(""), } } } @@ -579,9 +579,8 @@ pretty_display! { Length, Angle, Relative, - Fractional, Linear, - TrackSizing, + Fractional, RgbaColor, Color, AnyValue, diff --git a/src/syntax/expr.rs b/src/syntax/expr.rs index 17e4a1966..24850d651 100644 --- a/src/syntax/expr.rs +++ b/src/syntax/expr.rs @@ -9,7 +9,7 @@ use crate::geom::{AngularUnit, LengthUnit}; pub enum Expr { /// The none literal: `none`. None(Span), - /// The `auto` constant. + /// The auto literal: `auto`. Auto(Span), /// A boolean literal: `true`, `false`. Bool(Span, bool), diff --git a/src/syntax/token.rs b/src/syntax/token.rs index 26c01fbbc..2263f806f 100644 --- a/src/syntax/token.rs +++ b/src/syntax/token.rs @@ -76,6 +76,8 @@ pub enum Token<'s> { Or, /// The none literal: `none`. None, + /// The auto literal: `auto`. + Auto, /// The `let` keyword. Let, /// The `if` keyword. @@ -100,8 +102,6 @@ pub enum Token<'s> { Include, /// The `using` keyword. Using, - /// The `auto` keyword. - Auto, /// One or more whitespace characters. /// /// The contained `usize` denotes the number of newlines that were contained @@ -238,6 +238,7 @@ impl<'s> Token<'s> { Self::And => "operator `and`", Self::Or => "operator `or`", Self::None => "`none`", + Self::Auto => "`auto`", Self::Let => "keyword `let`", Self::If => "keyword `if`", Self::Else => "keyword `else`", @@ -250,7 +251,6 @@ impl<'s> Token<'s> { Self::Import => "keyword `import`", Self::Include => "keyword `include`", Self::Using => "keyword `using`", - Self::Auto => "keyword `auto`", Self::Space(_) => "space", Self::Text(_) => "text", Self::UnicodeEscape(_) => "unicode escape sequence", diff --git a/tests/ref/library/grid-table.png b/tests/ref/library/grid-table.png new file mode 100644 index 0000000000000000000000000000000000000000..36837663021f8df4b70a3148d4f7f75a6c1755e6 GIT binary patch literal 10495 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 literal 0 HcmV?d00001 diff --git a/tests/ref/library/grid.png b/tests/ref/library/grid.png index 278ead89a6158ca554a4d83be5467d43a0bc35ce..f7dda520c298117a54db471b334533f5589c65d6 100644 GIT binary patch literal 4673 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 a1S<-2*U_}W(^uCEc$%uZDrHL6A^!qiH_VIx literal 17246 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 w8^s&Z-L