summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMartin Haug <mhaug@live.de>2021-06-08 11:05:09 +0200
committerLaurenz <laurmaedje@gmail.com>2021-06-09 22:44:40 +0200
commit29cfef0a6dfef5820bda339d327638e285aaf4d3 (patch)
tree7a2e16b4c97d4259da1eb63deaa716b620feb4df
parent73fa2eda2c23bd3baeb9e22b99eb0bfb183fc638 (diff)
Add a grid layouter
-rw-r--r--src/eval/mod.rs2
-rw-r--r--src/eval/value.rs21
-rw-r--r--src/geom/gridu.rs73
-rw-r--r--src/geom/mod.rs2
-rw-r--r--src/layout/frame.rs8
-rw-r--r--src/layout/grid.rs372
-rw-r--r--src/layout/mod.rs2
-rw-r--r--src/library/grid.rs76
-rw-r--r--src/library/mod.rs3
-rw-r--r--src/parse/mod.rs5
-rw-r--r--src/parse/tokens.rs7
-rw-r--r--src/pretty.rs6
-rw-r--r--src/syntax/expr.rs3
-rw-r--r--src/syntax/token.rs3
-rw-r--r--src/syntax/visit.rs1
-rw-r--r--tests/ref/library/grid.pngbin0 -> 17246 bytes
-rw-r--r--tests/typ/library/grid.typ83
17 files changed, 665 insertions, 2 deletions
diff --git a/src/eval/mod.rs b/src/eval/mod.rs
index 5bc2f101..80be2e82 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 498403e6..fa7993ed 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 00000000..70fc17e4
--- /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) => <Linear as Display>::fmt(x, f),
+ Self::Fractional(x) => <Fractional as Display>::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) => <Linear as Debug>::fmt(x, f),
+ Self::Fractional(x) => <Fractional as Debug>::fmt(x, f),
+ Self::Auto => write!(f, "auto"),
+ }
+ }
+}
+
+impl From<Length> for TrackSizing {
+ fn from(abs: Length) -> Self {
+ Self::Linear(abs.into())
+ }
+}
+
+impl From<Relative> for TrackSizing {
+ fn from(rel: Relative) -> Self {
+ Self::Linear(rel.into())
+ }
+}
+
+impl From<Linear> for TrackSizing {
+ fn from(lin: Linear) -> Self {
+ Self::Linear(lin)
+ }
+}
+
+impl From<Fractional> for TrackSizing {
+ fn from(fr: Fractional) -> Self {
+ Self::Fractional(fr)
+ }
+}
diff --git a/src/geom/mod.rs b/src/geom/mod.rs
index ce8a7276..fdc3980e 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 6cecc7a3..f1dc07e6 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 00000000..9c4c2e0e
--- /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<AnyNode>,
+ pub tracks: Gen<GridUnits>,
+ pub gutter: Gen<GridUnits>,
+}
+
+impl Layout for GridNode {
+ fn layout(&self, ctx: &mut LayoutContext, regions: &Regions) -> Vec<Frame> {
+ 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<GridItem<'a>>,
+ cols: Vec<TrackSizing>,
+ rows: Vec<TrackSizing>,
+ region: Regions,
+ dir: Dir,
+ rrows: Vec<(usize, Option<Length>)>,
+ rcols: Vec<Length>,
+ frames: Vec<Frame>,
+}
+
+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<Length>| {
+ 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<Frame> {
+ // 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, &region).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<GridNode> for AnyNode {
+ fn from(grid: GridNode) -> Self {
+ Self::new(grid)
+ }
+}
diff --git a/src/layout/mod.rs b/src/layout/mod.rs
index 9d5ccdc0..9d8549e6 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 00000000..0aa1fc9d
--- /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<GridUnit>`.
+/// - Row widths: `rows`, of type `Array<GridUnit>`.
+/// - Gutter: `gutter-vertical` and `gutter-horizontal` for individual track axis or `gutter` for both, of type `Array<GridUnit>` 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::<GridUnits>(ctx, "columns").unwrap_or_default();
+ let rows = args.eat_named::<GridUnits>(ctx, "rows").unwrap_or_default();
+
+ let gutter = args.eat_named(ctx, "gutter");
+ let gutter_vertical = args
+ .eat_named::<GridUnits>(ctx, "gutter-col")
+ .or_else(|| gutter.clone())
+ .unwrap_or_default();
+ let gutter_horizontal = args
+ .eat_named::<GridUnits>(ctx, "gutter-row")
+ .or(gutter)
+ .unwrap_or_default();
+
+ let dir = args.eat_named(ctx, "dir");
+ let children = args.eat_all::<TemplateValue>(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<TrackSizing>);
+
+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 f9e4f68a..8caddc4c 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 3d4cc2ac..1f14f36f 100644
--- a/src/parse/mod.rs
+++ b/src/parse/mod.rs
@@ -314,6 +314,11 @@ fn primary(p: &mut Parser, atomic: bool) -> Option<Expr> {
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 9d3cbc9a..f82d0ae4 100644
--- a/src/parse/tokens.rs
+++ b/src/parse/tokens.rs
@@ -474,6 +474,7 @@ fn keyword(id: &str) -> Option<Token<'static>> {
"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 df1d844c..82e81ce6 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 4dac9c59..17e4a196 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 56ab3dd6..26c01fbb 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 ba7555f2..97e8d4ed 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 00000000..278ead89
--- /dev/null
+++ b/tests/ref/library/grid.png
Binary files differ
diff --git a/tests/typ/library/grid.typ b/tests/typ/library/grid.typ
new file mode 100644
index 00000000..dcc0797f
--- /dev/null
+++ b/tests/typ/library/grid.typ
@@ -0,0 +1,83 @@
+// Test the `grid` function.
+
+---
+#page(width: 100pt, height: 140pt)
+#let rect(width, color) = rect(width: width, height: 2cm, fill: color)
+#grid(
+ columns: (auto, 1fr, 3fr, 0.25cm, 3%, 2mm + 10%),
+ rect(0.5cm, #2a631a),
+ rect(100%, forest),
+ rect(100%, conifer),
+ rect(100%, #ff0000),
+ rect(100%, #00ff00),
+ rect(80%, #00faf0),
+ rect(1cm, #00ff00),
+ rect(0.5cm, #2a631a),
+ rect(100%, forest),
+ rect(100%, conifer),
+ rect(100%, #ff0000),
+ rect(100%, #00ff00),
+)
+
+#grid()
+
+---
+
+#grid(
+ columns: (auto, auto, 40%),
+ gutter: (1fr,),
+ rect(fill: eastern)[dddaa aaa aaa],
+ rect(fill: conifer)[ccc],
+ rect(width: 100%, fill: #dddddd)[aaa],
+)
+
+#grid(
+ columns: (auto, auto, 40%),
+ gutter: (1fr,),
+ rect(fill: eastern)[dddaa aaa aaa],
+ rect(fill: conifer)[ccc],
+ rect(width: 100%, fill: #dddddd)[aaa],
+)
+
+
+---
+
+#page(width: 12cm, height: 2.5cm)
+#grid(
+ columns: (auto, auto, auto, auto, auto),
+ gutter-col: (2fr, 1fr, 1fr),
+ gutter-row: (6pt, 6pt, 6pt, 6pt),
+ [*Quarter*],
+ [Expenditure],
+ [External Revenue],
+ [Financial ROI],
+ [_total_],
+ [*Q1*],
+ [173,472.57 \$],
+ [472,860.91 \$],
+ [51,286.84 \$],
+ [_350,675.18 \$_],
+ [*Q2*],
+ [93,382.12 \$],
+ [439,382.85 \$],
+ [-1,134.30 \$],
+ [_344,866.43 \$_],
+ [*Q3*],
+ [96,421.49 \$],
+ [238,583.54 \$],
+ [3,497.12 \$],
+ [_145,659.17 \$_],
+)
+
+---
+#page(height: 3cm, width: 2cm)
+#grid(
+ dir: ttb,
+ columns: (1fr, 1cm, 1fr, 1fr),
+ rows: (auto, 1fr),
+ rect(height: 100%, fill: #222222)[foo],
+ rect(height: 100%, fill: #547d0a)[bar],
+ rect(height: 100%, fill: eastern)[hab],
+ rect(height: 100%, fill: conifer)[baz],
+ rect(height: 100%, width: 100%, fill: #547d0a)[bar],
+)