From 29cfef0a6dfef5820bda339d327638e285aaf4d3 Mon Sep 17 00:00:00 2001 From: Martin Haug Date: Tue, 8 Jun 2021 11:05:09 +0200 Subject: [PATCH] Add a grid layouter --- src/eval/mod.rs | 2 + src/eval/value.rs | 21 ++- src/geom/gridu.rs | 73 ++++++++ src/geom/mod.rs | 2 + src/layout/frame.rs | 8 + src/layout/grid.rs | 372 +++++++++++++++++++++++++++++++++++++ src/layout/mod.rs | 2 + src/library/grid.rs | 76 ++++++++ src/library/mod.rs | 3 + src/parse/mod.rs | 5 + src/parse/tokens.rs | 7 + src/pretty.rs | 6 +- src/syntax/expr.rs | 3 + src/syntax/token.rs | 3 + src/syntax/visit.rs | 1 + tests/ref/library/grid.png | Bin 0 -> 17246 bytes tests/typ/library/grid.typ | 83 +++++++++ 17 files changed, 665 insertions(+), 2 deletions(-) create mode 100644 src/geom/gridu.rs create mode 100644 src/layout/grid.rs create mode 100644 src/library/grid.rs create mode 100644 tests/ref/library/grid.png create mode 100644 tests/typ/library/grid.typ diff --git a/src/eval/mod.rs b/src/eval/mod.rs index 5bc2f1015..80be2e824 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -18,6 +18,7 @@ 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; @@ -251,6 +252,7 @@ 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/value.rs b/src/eval/value.rs index 498403e69..fa7993ede 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}; +use crate::geom::{Angle, Fractional, Length, Linear, Relative, TrackSizing}; use crate::syntax::{Expr, Span, Spanned, Tree}; /// A computational value. @@ -32,6 +32,8 @@ pub enum Value { 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 color value: `#f79143ff`. Color(Color), /// A string: `"string"`. @@ -79,6 +81,7 @@ impl Value { Self::Relative(_) => Relative::TYPE_NAME, Self::Fractional(_) => Fractional::TYPE_NAME, Self::Linear(_) => Linear::TYPE_NAME, + Self::TrackSizing(_) => TrackSizing::TYPE_NAME, Self::Color(_) => Color::TYPE_NAME, Self::Str(_) => String::TYPE_NAME, Self::Array(_) => ArrayValue::TYPE_NAME, @@ -97,6 +100,14 @@ 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)) => { @@ -611,6 +622,14 @@ primitive! { 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! { Color: "color", Value::Color } primitive! { String: "string", Value::Str } primitive! { ArrayValue: "array", Value::Array } diff --git a/src/geom/gridu.rs b/src/geom/gridu.rs new file mode 100644 index 000000000..70fc17e4f --- /dev/null +++ b/src/geom/gridu.rs @@ -0,0 +1,73 @@ +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 ce8a7276e..fdc3980e7 100644 --- a/src/geom/mod.rs +++ b/src/geom/mod.rs @@ -7,6 +7,7 @@ mod angle; mod dir; mod fr; mod gen; +mod gridu; mod length; mod linear; mod path; @@ -21,6 +22,7 @@ pub use angle::*; pub use dir::*; pub use fr::*; pub use gen::*; +pub use gridu::*; pub use length::*; pub use linear::*; pub use path::*; diff --git a/src/layout/frame.rs b/src/layout/frame.rs index 6cecc7a34..f1dc07e6f 100644 --- a/src/layout/frame.rs +++ b/src/layout/frame.rs @@ -35,6 +35,14 @@ 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 new file mode 100644 index 000000000..9c4c2e0e3 --- /dev/null +++ b/src/layout/grid.rs @@ -0,0 +1,372 @@ +use std::usize; + +use super::*; +use crate::library::GridUnits; + +/// A node that stacks its children. +#[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. + pub children: Vec, + pub tracks: Gen, + 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 + } +} + +#[derive(Debug)] +enum GridItem<'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![]; + 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 + }; + + 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); + } + } + + 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); + } + } + + for (i, item) in grid.children.iter().enumerate() { + if cols == 0 { + break; + } + + 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. + for _ in 0 .. col_sizes.len() { + items.push(GridItem::Gutter); + } + } + } + + // Fill the thing up + while items.len() < col_sizes.len() * row_sizes.len() { + items.push(GridItem::Gutter) + } + + GridLayouter { + cols: col_sizes, + rows: row_sizes, + region: regions.clone(), + dir: grid.dir, + items, + rrows: vec![], + rcols: vec![], + frames: 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()); + available -= self + .cols + .iter() + .filter_map(|x| match x { + TrackSizing::Linear(l) => Some(l.resolve(self.region.base.get(self.cross()))), + _ => None, + }) + .sum(); + + let col_frac: f64 = self + .cols + .iter() + .filter_map(|x| match x { + TrackSizing::Fractional(f) => Some(f.get()), + _ => None, + }) + .sum(); + + let auto_columns = self + .cols + .iter() + .enumerate() + .filter_map(|(i, x)| (x == &TrackSizing::Auto).then(|| i)); + + let mut col_width = vec![]; + + // 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(); + 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 => {} + } + } + + col_width.push((x, max)); + } + + // 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; + + let mut count: usize = 0; + let mut redistributable = Length::zero(); + + for &(_, l) in &col_width { + if l > alloc { + redistributable += l; + count += 1; + } + } + + let x = (available - total + redistributable) / count as f64; + + if !redistributable.is_zero() { + for (_, l) in &mut col_width { + if *l > alloc { + *l = x; + } + } + } + + total = available; + } + + // 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 i in self.rcols.len() .. x { + let len = match self.cols[i] { + TrackSizing::Linear(l) => l.resolve(self.region.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() + } + } + } + TrackSizing::Auto => unreachable!(), + }; + + self.rcols.push(len); + } + + if let Some(len) = len { + self.rcols.push(len); + } + } + + // Determine non-`fr` row heights + let mut total_frs = 0.0; + let mut current = self.region.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()))), + 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), + ), + ); + max = max.max(frames.first().unwrap().size.get(self.main())); + } + } + Some(max) + } + TrackSizing::Fractional(f) => { + total_frs += f.get(); + None + }, + }; + + if let Some(resolved) = resolved { + while !current.fits(resolved) && !self.region.in_full_last() { + self.finish_region(ctx, total_frs); + current = self.region.current.get(self.main()); + total_frs = 0.0; + } + current -= resolved; + } + + self.rrows.push((y, resolved)); + } + + self.finish_region(ctx, total_frs); + self.frames + } +} + +impl From for AnyNode { + fn from(grid: GridNode) -> Self { + Self::new(grid) + } +} diff --git a/src/layout/mod.rs b/src/layout/mod.rs index 9d5ccdc02..9d8549e67 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -3,6 +3,7 @@ mod background; mod fixed; mod frame; +mod grid; mod pad; mod par; mod shaping; @@ -11,6 +12,7 @@ mod stack; pub use background::*; pub use fixed::*; pub use frame::*; +pub use grid::*; pub use pad::*; pub use par::*; pub use shaping::*; diff --git a/src/library/grid.rs b/src/library/grid.rs new file mode 100644 index 000000000..0aa1fc9dc --- /dev/null +++ b/src/library/grid.rs @@ -0,0 +1,76 @@ +use crate::layout::GridNode; + +use super::*; + +/// `stack`: Stack children along an axis. +/// +/// # 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`. +/// +/// # Return value +/// A template that arranges its children along the specified grid cells. +/// +/// # Relevant types and constants +/// - 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 children = args.eat_all::(ctx); + + Value::template("grid", move |ctx| { + 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), + children, + gutter: Gen::new(gutter_vertical.clone(), gutter_horizontal.clone()), + tracks: Gen::new(cols.clone(), rows.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]), + Value::Array(values) => Self(values + .into_iter() + .filter_map(|v| v.cast().ok()) + .collect() + ), +} diff --git a/src/library/mod.rs b/src/library/mod.rs index f9e4f68a9..8caddc4c3 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -6,6 +6,7 @@ mod align; mod basic; mod font; +mod grid; mod image; mod lang; mod math; @@ -20,6 +21,7 @@ pub use self::image::*; pub use align::*; pub use basic::*; pub use font::*; +pub use grid::*; pub use lang::*; pub use math::*; pub use pad::*; @@ -47,6 +49,7 @@ pub fn new() -> Scope { std.def_func("circle", circle); std.def_func("ellipse", ellipse); std.def_func("font", font); + std.def_func("grid", grid); std.def_func("h", h); std.def_func("image", image); std.def_func("lang", lang); diff --git a/src/parse/mod.rs b/src/parse/mod.rs index 3d4cc2ac1..1f14f36f5 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -314,6 +314,11 @@ 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. _ => { diff --git a/src/parse/tokens.rs b/src/parse/tokens.rs index 9d3cbc9a2..f82d0ae4b 100644 --- a/src/parse/tokens.rs +++ b/src/parse/tokens.rs @@ -474,6 +474,7 @@ 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, @@ -758,6 +759,12 @@ 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 df1d844c1..82e81ce64 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}; +use crate::geom::{Angle, Fractional, Length, Linear, Relative, TrackSizing}; use crate::syntax::*; /// Pretty print an item and return the resulting string. @@ -186,6 +186,7 @@ impl Pretty for Expr { fn pretty(&self, p: &mut Printer) { match self { 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), @@ -459,6 +460,7 @@ impl Pretty for Value { 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), @@ -579,6 +581,7 @@ pretty_display! { Relative, Fractional, Linear, + TrackSizing, RgbaColor, Color, AnyValue, @@ -656,6 +659,7 @@ mod tests { fn test_pretty_print_expr() { // Basic expressions. roundtrip("{none}"); + roundtrip("{auto}"); roundtrip("{true}"); roundtrip("{10}"); roundtrip("{3.14}"); diff --git a/src/syntax/expr.rs b/src/syntax/expr.rs index 4dac9c596..17e4a1966 100644 --- a/src/syntax/expr.rs +++ b/src/syntax/expr.rs @@ -9,6 +9,8 @@ use crate::geom::{AngularUnit, LengthUnit}; pub enum Expr { /// The none literal: `none`. None(Span), + /// The `auto` constant. + Auto(Span), /// A boolean literal: `true`, `false`. Bool(Span, bool), /// An integer literal: `120`. @@ -69,6 +71,7 @@ impl Expr { pub fn span(&self) -> Span { match *self { Self::None(span) => span, + Self::Auto(span) => span, Self::Bool(span, _) => span, Self::Int(span, _) => span, Self::Float(span, _) => span, diff --git a/src/syntax/token.rs b/src/syntax/token.rs index 56ab3dd6d..26c01fbbc 100644 --- a/src/syntax/token.rs +++ b/src/syntax/token.rs @@ -100,6 +100,8 @@ 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 @@ -248,6 +250,7 @@ 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/src/syntax/visit.rs b/src/syntax/visit.rs index ba7555f20..97e8d4edf 100644 --- a/src/syntax/visit.rs +++ b/src/syntax/visit.rs @@ -74,6 +74,7 @@ visit! { fn visit_expr(v, node: &Expr) { match node { Expr::None(_) => {} + Expr::Auto(_) => {} Expr::Bool(_, _) => {} Expr::Int(_, _) => {} Expr::Float(_, _) => {} diff --git a/tests/ref/library/grid.png b/tests/ref/library/grid.png new file mode 100644 index 0000000000000000000000000000000000000000..278ead89a6158ca554a4d83be5467d43a0bc35ce GIT binary patch literal 17246 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 w8^s&Z-L