diff --git a/crates/typst/src/foundations/auto.rs b/crates/typst/src/foundations/auto.rs index dd36e814f..5cb03f7f1 100644 --- a/crates/typst/src/foundations/auto.rs +++ b/crates/typst/src/foundations/auto.rs @@ -128,8 +128,20 @@ impl Smart { } } - /// Retusn `Auto` if `self` is `Auto`, otherwise calls the provided function onthe contained - /// value and returns the result. + /// Keeps `self` if it contains a custom value, otherwise returns the + /// output of the given function. + pub fn or_else(self, f: F) -> Self + where + F: FnOnce() -> Self, + { + match self { + Self::Custom(x) => Self::Custom(x), + Self::Auto => f(), + } + } + + /// Returns `Auto` if `self` is `Auto`, otherwise calls the provided + /// function on the contained value and returns the result. pub fn and_then(self, f: F) -> Smart where F: FnOnce(T) -> Smart, diff --git a/crates/typst/src/layout/grid.rs b/crates/typst/src/layout/grid/layout.rs similarity index 67% rename from crates/typst/src/layout/grid.rs rename to crates/typst/src/layout/grid/layout.rs index 56f8190ba..24d641a66 100644 --- a/crates/typst/src/layout/grid.rs +++ b/crates/typst/src/layout/grid/layout.rs @@ -1,282 +1,17 @@ -use std::num::NonZeroUsize; - -use smallvec::{smallvec, SmallVec}; - use crate::diag::{bail, At, SourceResult, StrResult}; use crate::engine::Engine; use crate::foundations::{ - cast, elem, Array, CastInfo, Content, FromValue, Func, IntoValue, NativeElement, - Reflect, Resolve, Smart, StyleChain, Value, + Array, CastInfo, Content, FromValue, Func, IntoValue, Reflect, Resolve, Smart, + StyleChain, Value, }; use crate::layout::{ - Abs, Align, AlignElem, Axes, Dir, Fr, Fragment, Frame, FrameItem, Layout, Length, - Point, Regions, Rel, Sides, Size, Sizing, + Abs, Align, Axes, Dir, Fr, Fragment, Frame, FrameItem, Layout, Length, Point, + Regions, Rel, Sides, Size, Sizing, }; use crate::syntax::Span; use crate::text::TextElem; use crate::util::Numeric; -use crate::visualize::{FixedStroke, Geometry, Paint, Stroke}; - -/// Arranges content in a grid. -/// -/// The grid element allows you to arrange content in a grid. You can define the -/// number of rows and columns, as well as the size of the gutters between them. -/// There are multiple sizing modes for columns and rows that can be used to -/// create complex layouts. -/// -/// The sizing of the grid is determined by the track sizes specified in the -/// arguments. Because each of the sizing parameters accepts the same values, we -/// will explain them just once, here. Each sizing argument accepts an array of -/// individual track sizes. A track size is either: -/// -/// - `{auto}`: The track will be sized to fit its contents. It will be at most -/// as large as the remaining space. If there is more than one `{auto}` track -/// which, and together they claim more than the available space, the `{auto}` -/// tracks will fairly distribute the available space among themselves. -/// -/// - A fixed or relative length (e.g. `{10pt}` or `{20% - 1cm}`): The track -/// will be exactly of this size. -/// -/// - A fractional length (e.g. `{1fr}`): Once all other tracks have been sized, -/// the remaining space will be divided among the fractional tracks according -/// to their fractions. For example, if there are two fractional tracks, each -/// with a fraction of `{1fr}`, they will each take up half of the remaining -/// space. -/// -/// To specify a single track, the array can be omitted in favor of a single -/// value. To specify multiple `{auto}` tracks, enter the number of tracks -/// instead of an array. For example, `columns:` `{3}` is equivalent to -/// `columns:` `{(auto, auto, auto)}`. -/// -/// # Examples -/// The example below demonstrates the different track sizing options. -/// -/// ```example -/// // We use `rect` to emphasize the -/// // area of cells. -/// #set rect( -/// inset: 8pt, -/// fill: rgb("e4e5ea"), -/// width: 100%, -/// ) -/// -/// #grid( -/// columns: (60pt, 1fr, 2fr), -/// rows: (auto, 60pt), -/// gutter: 3pt, -/// rect[Fixed width, auto height], -/// rect[1/3 of the remains], -/// rect[2/3 of the remains], -/// rect(height: 100%)[Fixed height], -/// image("tiger.jpg", height: 100%), -/// image("tiger.jpg", height: 100%), -/// ) -/// ``` -/// -/// You can also [spread]($arguments/#spreading) an array of strings or content -/// into a grid to populate its cells. -/// -/// ```example -/// #grid( -/// columns: 5, -/// gutter: 5pt, -/// ..range(25).map(str) -/// ) -/// ``` -#[elem(Layout)] -pub struct GridElem { - /// The column sizes. - /// - /// Either specify a track size array or provide an integer to create a grid - /// with that many `{auto}`-sized columns. Note that opposed to rows and - /// gutters, providing a single track size will only ever create a single - /// column. - #[borrowed] - pub columns: TrackSizings, - - /// The row sizes. - /// - /// If there are more cells than fit the defined rows, the last row is - /// repeated until there are no more cells. - #[borrowed] - pub rows: TrackSizings, - - /// The gaps between rows & columns. - /// - /// If there are more gutters than defined sizes, the last gutter is repeated. - #[external] - pub gutter: TrackSizings, - - /// The gaps between columns. Takes precedence over `gutter`. - #[parse( - let gutter = args.named("gutter")?; - args.named("column-gutter")?.or_else(|| gutter.clone()) - )] - #[borrowed] - pub column_gutter: TrackSizings, - - /// The gaps between rows. Takes precedence over `gutter`. - #[parse(args.named("row-gutter")?.or_else(|| gutter.clone()))] - #[borrowed] - pub row_gutter: TrackSizings, - - /// How to fill the cells. - /// - /// This can be a color or a function that returns a color. The function is - /// passed the cells' column and row index, starting at zero. This can be - /// used to implement striped grids. - /// - /// ```example - /// #grid( - /// fill: (col, row) => if calc.even(col + row) { luma(240) } else { white }, - /// align: center + horizon, - /// columns: 4, - /// [X], [O], [X], [O], - /// [O], [X], [O], [X], - /// [X], [O], [X], [O], - /// [O], [X], [O], [X] - /// ) - /// ``` - #[borrowed] - pub fill: Celled>, - - /// How to align the cells' content. - /// - /// This can either be a single alignment, an array of alignments - /// (corresponding to each column) or a function that returns an alignment. - /// The function is passed the cells' column and row index, starting at zero. - /// If set to `{auto}`, the outer alignment is used. - /// - /// ```example - /// #grid( - /// columns: 3, - /// align: (x, y) => (left, center, right).at(x), - /// [Hello], [Hello], [Hello], - /// [A], [B], [C], - /// ) - /// ``` - #[borrowed] - pub align: Celled>, - - /// How to [stroke]($stroke) the cells. - /// - /// Grids have no strokes by default, which can be changed by setting this - /// option to the desired stroke. - /// - /// _Note:_ Richer stroke customization for individual cells is not yet - /// implemented, but will be in the future. In the meantime, you can use the - /// third-party [tablex library](https://github.com/PgBiel/typst-tablex/). - #[resolve] - #[fold] - pub stroke: Option, - - /// How much to pad the cells' content. - /// - /// ```example - /// #grid( - /// inset: 10pt, - /// fill: (_, row) => (red, blue).at(row), - /// [Hello], - /// [World], - /// ) - /// - /// #grid( - /// columns: 2, - /// inset: ( - /// x: 20pt, - /// y: 10pt, - /// ), - /// fill: (col, _) => (red, blue).at(col), - /// [Hello], - /// [World], - /// ) - /// ``` - #[fold] - #[default(Sides::splat(Abs::pt(0.0).into()))] - pub inset: Sides>>, - - /// The contents of the grid cells. - /// - /// The cells are populated in row-major order. - #[variadic] - pub children: Vec, -} - -impl Layout for GridElem { - #[typst_macros::time(name = "grid", span = self.span())] - fn layout( - &self, - engine: &mut Engine, - styles: StyleChain, - regions: Regions, - ) -> SourceResult { - let inset = self.inset(styles); - let align = self.align(styles); - let columns = self.columns(styles); - let rows = self.rows(styles); - let column_gutter = self.column_gutter(styles); - let row_gutter = self.row_gutter(styles); - let fill = self.fill(styles); - let stroke = self.stroke(styles).map(Stroke::unwrap_or_default); - - let tracks = Axes::new(columns.0.as_slice(), rows.0.as_slice()); - let gutter = Axes::new(column_gutter.0.as_slice(), row_gutter.0.as_slice()); - let cells = - apply_align_inset_to_cells(engine, &tracks, &self.children, align, inset)?; - - // Prepare grid layout by unifying content and gutter tracks. - let layouter = GridLayouter::new( - tracks, - gutter, - &cells, - fill, - &stroke, - regions, - styles, - self.span(), - ); - - // Measure the columns and layout the grid row-by-row. - Ok(layouter.layout(engine)?.fragment) - } -} - -pub fn apply_align_inset_to_cells( - engine: &mut Engine, - tracks: &Axes<&[Sizing]>, - cells: &[Content], - align: &Celled>, - inset: Sides>, -) -> SourceResult> { - let cols = tracks.x.len().max(1); - cells - .iter() - .enumerate() - .map(|(i, child)| { - let mut child = child.clone().padded(inset); - - let x = i % cols; - let y = i / cols; - if let Smart::Custom(alignment) = align.resolve(engine, x, y)? { - child = child.styled(AlignElem::set_alignment(alignment)); - } - - Ok(child) - }) - .collect() -} - -/// Track sizing definitions. -#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] -pub struct TrackSizings(pub SmallVec<[Sizing; 4]>); - -cast! { - TrackSizings, - self => self.0.into_value(), - sizing: Sizing => Self(smallvec![sizing]), - count: NonZeroUsize => Self(smallvec![Sizing::Auto; count.get()]), - values: Array => Self(values.into_iter().map(Value::cast).collect::>()?), -} +use crate::visualize::{FixedStroke, Geometry, Paint}; /// A value that can be configured per cell. #[derive(Debug, Clone, PartialEq, Hash)] @@ -347,87 +82,70 @@ impl FromValue for Celled { } } -/// Performs grid layout. -pub struct GridLayouter<'a> { +/// Represents a cell in CellGrid, to be laid out by GridLayouter. +pub struct Cell { + /// The cell's body. + pub body: Content, + /// The cell's fill. + pub fill: Option, +} + +impl From for Cell { + /// Create a simple cell given its body. + fn from(body: Content) -> Self { + Self { body, fill: None } + } +} + +impl Layout for Cell { + fn layout( + &self, + engine: &mut Engine, + styles: StyleChain, + regions: Regions, + ) -> SourceResult { + self.body.layout(engine, styles, regions) + } +} + +/// Used for cell-like elements which are aware of their final properties in +/// the table, and may have property overrides. +pub trait ResolvableCell { + /// Resolves the cell's fields, given its coordinates and default grid-wide + /// fill, align and inset properties. + /// Returns a final Cell. + fn resolve_cell( + self, + x: usize, + y: usize, + fill: &Option, + align: Smart, + inset: Sides>, + styles: StyleChain, + ) -> Cell; +} + +/// A grid of cells, including the columns, rows, and cell data. +pub struct CellGrid { /// The grid cells. - cells: &'a [Content], - /// Whether this is an RTL grid. - is_rtl: bool, - /// Whether this grid has gutters. - has_gutter: bool, + cells: Vec, /// The column tracks including gutter tracks. cols: Vec, /// The row tracks including gutter tracks. rows: Vec, - // How to fill the cells. - #[allow(dead_code)] - fill: &'a Celled>, - // How to stroke the cells. - #[allow(dead_code)] - stroke: &'a Option, - /// The regions to layout children into. - regions: Regions<'a>, - /// The inherited styles. - styles: StyleChain<'a>, - /// Resolved column sizes. - rcols: Vec, - /// The sum of `rcols`. - width: Abs, - /// Resolve row sizes, by region. - rrows: Vec>, - /// Rows in the current region. - lrows: Vec, - /// The initial size of the current region before we started subtracting. - initial: Size, - /// Frames for finished regions. - finished: Vec, - /// The span of the grid element. - span: Span, + /// Whether this grid has gutters. + has_gutter: bool, + /// Whether this is an RTL grid. + is_rtl: bool, } -/// The resulting sizes of columns and rows in a grid. -#[derive(Debug)] -pub struct GridLayout { - /// The fragment. - pub fragment: Fragment, - /// The column widths. - pub cols: Vec, - /// The heights of the resulting rows segments, by region. - pub rows: Vec>, -} - -/// Details about a resulting row piece. -#[derive(Debug)] -pub struct RowPiece { - /// The height of the segment. - pub height: Abs, - /// The index of the row. - pub y: usize, -} - -/// Produced by initial row layout, auto and relative rows are already finished, -/// fractional rows not yet. -enum Row { - /// Finished row frame of auto or relative row with y index. - Frame(Frame, usize), - /// Fractional row with y index. - Fr(Fr, usize), -} - -impl<'a> GridLayouter<'a> { - /// Create a new grid layouter. - /// - /// This prepares grid layout by unifying content and gutter tracks. - #[allow(clippy::too_many_arguments)] +impl CellGrid { + /// Generates the cell grid, given the tracks and resolved cells. pub fn new( tracks: Axes<&[Sizing]>, gutter: Axes<&[Sizing]>, - cells: &'a [Content], - fill: &'a Celled>, - stroke: &'a Option, - regions: Regions<'a>, - styles: StyleChain<'a>, - span: Span, + cells: Vec, + styles: StyleChain, ) -> Self { let mut cols = vec![]; let mut rows = vec![]; @@ -479,22 +197,154 @@ impl<'a> GridLayouter<'a> { cols.reverse(); } + Self { cols, rows, cells, has_gutter, is_rtl } + } + + /// Resolves all cells in the grid before creating it. + /// Allows them to keep track of their final properties and adjust their + /// fields accordingly. + /// Cells must implement Clone as they will be owned. Additionally, they + /// must implement Default in order to fill the last row of the grid with + /// empty cells, if it is not completely filled. + #[allow(clippy::too_many_arguments)] + pub fn resolve( + tracks: Axes<&[Sizing]>, + gutter: Axes<&[Sizing]>, + cells: &[T], + fill: &Celled>, + align: &Celled>, + inset: Sides>, + engine: &mut Engine, + styles: StyleChain, + ) -> SourceResult { + // Number of content columns: Always at least one. + let c = tracks.x.len().max(1); + + // If not all columns in the last row have cells, we will add empty + // cells and complete the row so that those positions are susceptible + // to show rules and receive grid styling. + // We apply '% c' twice so that 'cells_remaining' is zero when + // the last row is already filled (then 'cell_count % c' would be zero). + let cell_count = cells.len(); + let cells_remaining = (c - cell_count % c) % c; + let cells = cells + .iter() + .cloned() + .chain(std::iter::repeat_with(T::default).take(cells_remaining)) + .enumerate() + .map(|(i, cell)| { + let x = i % c; + let y = i / c; + + Ok(cell.resolve_cell( + x, + y, + &fill.resolve(engine, x, y)?, + align.resolve(engine, x, y)?, + inset, + styles, + )) + }) + .collect::>>()?; + + Ok(Self::new(tracks, gutter, cells, styles)) + } + + /// Get the content of the cell in column `x` and row `y`. + /// + /// Returns `None` if it's a gutter cell. + #[track_caller] + fn cell(&self, mut x: usize, y: usize) -> Option<&Cell> { + assert!(x < self.cols.len()); + assert!(y < self.rows.len()); + + // Columns are reorder, but the cell slice is not. + if self.is_rtl { + x = self.cols.len() - 1 - x; + } + + if self.has_gutter { + // Even columns and rows are children, odd ones are gutter. + if x % 2 == 0 && y % 2 == 0 { + let c = 1 + self.cols.len() / 2; + self.cells.get((y / 2) * c + x / 2) + } else { + None + } + } else { + let c = self.cols.len(); + self.cells.get(y * c + x) + } + } +} + +/// Performs grid layout. +pub struct GridLayouter<'a> { + /// The grid of cells. + grid: &'a CellGrid, + // How to stroke the cells. + stroke: &'a Option, + /// The regions to layout children into. + regions: Regions<'a>, + /// The inherited styles. + styles: StyleChain<'a>, + /// Resolved column sizes. + rcols: Vec, + /// The sum of `rcols`. + width: Abs, + /// Resolve row sizes, by region. + rrows: Vec>, + /// Rows in the current region. + lrows: Vec, + /// The initial size of the current region before we started subtracting. + initial: Size, + /// Frames for finished regions. + finished: Vec, + /// The span of the grid element. + span: Span, +} + +/// Details about a resulting row piece. +#[derive(Debug)] +pub struct RowPiece { + /// The height of the segment. + pub height: Abs, + /// The index of the row. + pub y: usize, +} + +/// Produced by initial row layout, auto and relative rows are already finished, +/// fractional rows not yet. +enum Row { + /// Finished row frame of auto or relative row with y index. + Frame(Frame, usize), + /// Fractional row with y index. + Fr(Fr, usize), +} + +impl<'a> GridLayouter<'a> { + /// Create a new grid layouter. + /// + /// This prepares grid layout by unifying content and gutter tracks. + #[allow(clippy::too_many_arguments)] + pub fn new( + grid: &'a CellGrid, + stroke: &'a Option, + regions: Regions<'a>, + styles: StyleChain<'a>, + span: Span, + ) -> Self { // We use these regions for auto row measurement. Since at that moment, // columns are already sized, we can enable horizontal expansion. let mut regions = regions; regions.expand = Axes::new(true, false); Self { - cells, - is_rtl, - has_gutter, - rows, - fill, + grid, stroke, regions, styles, - rcols: vec![Abs::zero(); cols.len()], - cols, + rcols: vec![Abs::zero(); grid.cols.len()], width: Abs::zero(), rrows: vec![], lrows: vec![], @@ -505,17 +355,17 @@ impl<'a> GridLayouter<'a> { } /// Determines the columns sizes and then layouts the grid row-by-row. - pub fn layout(mut self, engine: &mut Engine) -> SourceResult { + pub fn layout(mut self, engine: &mut Engine) -> SourceResult { self.measure_columns(engine)?; - for y in 0..self.rows.len() { + for y in 0..self.grid.rows.len() { // Skip to next region if current one is full, but only for content // rows, not for gutter rows. - if self.regions.is_full() && (!self.has_gutter || y % 2 == 0) { + if self.regions.is_full() && (!self.grid.has_gutter || y % 2 == 0) { self.finish_region(engine)?; } - match self.rows[y] { + match self.grid.rows[y] { Sizing::Auto => self.layout_auto_row(engine, y)?, Sizing::Rel(v) => self.layout_relative_row(engine, v, y)?, Sizing::Fr(v) => self.lrows.push(Row::Fr(v, y)), @@ -524,19 +374,13 @@ impl<'a> GridLayouter<'a> { self.finish_region(engine)?; - if self.stroke.is_some() || !matches!(self.fill, Celled::Value(None)) { - self.render_fills_strokes(engine)?; - } + self.render_fills_strokes()?; - Ok(GridLayout { - fragment: Fragment::frames(self.finished), - cols: self.rcols, - rows: self.rrows, - }) + Ok(Fragment::frames(self.finished)) } /// Add lines and backgrounds. - fn render_fills_strokes(&mut self, engine: &mut Engine) -> SourceResult<()> { + fn render_fills_strokes(&mut self) -> SourceResult<()> { for (frame, rows) in self.finished.iter_mut().zip(&self.rrows) { if self.rcols.is_empty() || rows.is_empty() { continue; @@ -573,7 +417,9 @@ impl<'a> GridLayouter<'a> { for (x, &col) in self.rcols.iter().enumerate() { let mut dy = Abs::zero(); for row in rows { - if let Some(fill) = self.fill.resolve(engine, x, row.y)? { + let fill = + self.grid.cell(x, row.y).and_then(|cell| cell.fill.clone()); + if let Some(fill) = fill { let pos = Point::new(dx, dy); let size = Size::new(col, row.height); let rect = Geometry::Rect(size).filled(fill); @@ -598,7 +444,7 @@ impl<'a> GridLayouter<'a> { // Resolve the size of all relative columns and compute the sum of all // fractional tracks. - for (&col, rcol) in self.cols.iter().zip(&mut self.rcols) { + for (&col, rcol) in self.grid.cols.iter().zip(&mut self.rcols) { match col { Sizing::Auto => {} Sizing::Rel(v) => { @@ -644,17 +490,17 @@ impl<'a> GridLayouter<'a> { // Determine size of auto columns by laying out all cells in those // columns, measuring them and finding the largest one. - for (x, &col) in self.cols.iter().enumerate() { + for (x, &col) in self.grid.cols.iter().enumerate() { if col != Sizing::Auto { continue; } let mut resolved = Abs::zero(); - for y in 0..self.rows.len() { - if let Some(cell) = self.cell(x, y) { + for y in 0..self.grid.rows.len() { + if let Some(cell) = self.grid.cell(x, y) { // For relative rows, we can already resolve the correct // base and for auto and fr we could only guess anyway. - let height = match self.rows[y] { + let height = match self.grid.rows[y] { Sizing::Rel(v) => { v.resolve(self.styles).relative_to(self.regions.base().y) } @@ -682,7 +528,7 @@ impl<'a> GridLayouter<'a> { return; } - for (&col, rcol) in self.cols.iter().zip(&mut self.rcols) { + for (&col, rcol) in self.grid.cols.iter().zip(&mut self.rcols) { if let Sizing::Fr(v) = col { *rcol = v.share(fr, remaining); } @@ -703,7 +549,7 @@ impl<'a> GridLayouter<'a> { last = fair; fair = redistribute / (overlarge as f64); - for (&col, &rcol) in self.cols.iter().zip(&self.rcols) { + for (&col, &rcol) in self.grid.cols.iter().zip(&self.rcols) { // Remove an auto column if it is not overlarge (rcol <= fair), // but also hasn't already been removed (rcol > last). if col == Sizing::Auto && rcol <= fair && rcol > last { @@ -715,7 +561,7 @@ impl<'a> GridLayouter<'a> { } // Redistribute space fairly among overlarge columns. - for (&col, rcol) in self.cols.iter().zip(&mut self.rcols) { + for (&col, rcol) in self.grid.cols.iter().zip(&mut self.rcols) { if col == Sizing::Auto && *rcol > fair { *rcol = fair; } @@ -783,7 +629,7 @@ impl<'a> GridLayouter<'a> { let mut resolved: Vec = vec![]; for (x, &rcol) in self.rcols.iter().enumerate() { - if let Some(cell) = self.cell(x, y) { + if let Some(cell) = self.grid.cell(x, y) { let mut pod = self.regions; pod.size.x = rcol; @@ -831,7 +677,7 @@ impl<'a> GridLayouter<'a> { self.finish_region(engine)?; // Don't skip multiple regions for gutter and don't push a row. - if self.has_gutter && y % 2 == 1 { + if self.grid.has_gutter && y % 2 == 1 { return Ok(()); } } @@ -856,10 +702,10 @@ impl<'a> GridLayouter<'a> { let mut pos = Point::zero(); for (x, &rcol) in self.rcols.iter().enumerate() { - if let Some(cell) = self.cell(x, y) { + if let Some(cell) = self.grid.cell(x, y) { let size = Size::new(rcol, height); let mut pod = Regions::one(size, Axes::splat(true)); - if self.rows[y] == Sizing::Auto { + if self.grid.rows[y] == Sizing::Auto { pod.full = self.regions.full; } let frame = cell.layout(engine, self.styles, pod)?.into_frame(); @@ -894,7 +740,7 @@ impl<'a> GridLayouter<'a> { // Layout the row. let mut pos = Point::zero(); for (x, &rcol) in self.rcols.iter().enumerate() { - if let Some(cell) = self.cell(x, y) { + if let Some(cell) = self.grid.cell(x, y) { pod.size.x = rcol; // Push the layouted frames into the individual output frames. @@ -964,33 +810,6 @@ impl<'a> GridLayouter<'a> { Ok(()) } - - /// Get the content of the cell in column `x` and row `y`. - /// - /// Returns `None` if it's a gutter cell. - #[track_caller] - fn cell(&self, mut x: usize, y: usize) -> Option<&'a Content> { - assert!(x < self.cols.len()); - assert!(y < self.rows.len()); - - // Columns are reorder, but the cell slice is not. - if self.is_rtl { - x = self.cols.len() - 1 - x; - } - - if self.has_gutter { - // Even columns and rows are children, odd ones are gutter. - if x % 2 == 0 && y % 2 == 0 { - let c = 1 + self.cols.len() / 2; - self.cells.get((y / 2) * c + x / 2) - } else { - None - } - } else { - let c = self.cols.len(); - self.cells.get(y * c + x) - } - } } /// Turn an iterator of extents into an iterator of offsets before, in between, diff --git a/crates/typst/src/layout/grid/mod.rs b/crates/typst/src/layout/grid/mod.rs new file mode 100644 index 000000000..4d66fd4a7 --- /dev/null +++ b/crates/typst/src/layout/grid/mod.rs @@ -0,0 +1,383 @@ +mod layout; + +pub use self::layout::{Cell, CellGrid, Celled, GridLayouter, ResolvableCell}; + +use std::num::NonZeroUsize; + +use smallvec::{smallvec, SmallVec}; + +use crate::diag::{SourceResult, StrResult}; +use crate::engine::Engine; +use crate::foundations::{ + cast, elem, scope, Array, Content, Fold, NativeElement, Show, Smart, StyleChain, + Value, +}; +use crate::layout::{ + Abs, Align, AlignElem, Axes, Fragment, Layout, Length, Regions, Rel, Sides, Sizing, +}; +use crate::visualize::{Paint, Stroke}; + +/// Arranges content in a grid. +/// +/// The grid element allows you to arrange content in a grid. You can define the +/// number of rows and columns, as well as the size of the gutters between them. +/// There are multiple sizing modes for columns and rows that can be used to +/// create complex layouts. +/// +/// The sizing of the grid is determined by the track sizes specified in the +/// arguments. Because each of the sizing parameters accepts the same values, we +/// will explain them just once, here. Each sizing argument accepts an array of +/// individual track sizes. A track size is either: +/// +/// - `{auto}`: The track will be sized to fit its contents. It will be at most +/// as large as the remaining space. If there is more than one `{auto}` track +/// which, and together they claim more than the available space, the `{auto}` +/// tracks will fairly distribute the available space among themselves. +/// +/// - A fixed or relative length (e.g. `{10pt}` or `{20% - 1cm}`): The track +/// will be exactly of this size. +/// +/// - A fractional length (e.g. `{1fr}`): Once all other tracks have been sized, +/// the remaining space will be divided among the fractional tracks according +/// to their fractions. For example, if there are two fractional tracks, each +/// with a fraction of `{1fr}`, they will each take up half of the remaining +/// space. +/// +/// To specify a single track, the array can be omitted in favor of a single +/// value. To specify multiple `{auto}` tracks, enter the number of tracks +/// instead of an array. For example, `columns:` `{3}` is equivalent to +/// `columns:` `{(auto, auto, auto)}`. +/// +/// # Styling the grid +/// The grid's appearance can be customized through different parameters, such +/// as `fill` to give all cells a background; `align` to change how cells are +/// aligned; `inset` to optionally add internal padding to each cell; and +/// `stroke` to optionally enable grid lines with a certain stroke. +/// +/// If you need to override one of the above options for a single cell, you can +/// use the [`grid.cell`]($grid.cell) element. Alternatively, if you need the +/// appearance options to depend on a cell's position (column and row), you may +/// specify a function to `fill` or `align` of the form +/// `(column, row) => value`. You may also use a show rule on +/// [`grid.cell`]($grid.cell) - see that element's examples for more information. +/// +/// # Examples +/// The example below demonstrates the different track sizing options. +/// +/// ```example +/// // We use `rect` to emphasize the +/// // area of cells. +/// #set rect( +/// inset: 8pt, +/// fill: rgb("e4e5ea"), +/// width: 100%, +/// ) +/// +/// #grid( +/// columns: (60pt, 1fr, 2fr), +/// rows: (auto, 60pt), +/// gutter: 3pt, +/// rect[Fixed width, auto height], +/// rect[1/3 of the remains], +/// rect[2/3 of the remains], +/// rect(height: 100%)[Fixed height], +/// image("tiger.jpg", height: 100%), +/// image("tiger.jpg", height: 100%), +/// ) +/// ``` +/// +/// You can also [spread]($arguments/#spreading) an array of strings or content +/// into a grid to populate its cells. +/// +/// ```example +/// #grid( +/// columns: 5, +/// gutter: 5pt, +/// ..range(25).map(str) +/// ) +/// ``` +#[elem(scope, Layout)] +pub struct GridElem { + /// The column sizes. + /// + /// Either specify a track size array or provide an integer to create a grid + /// with that many `{auto}`-sized columns. Note that opposed to rows and + /// gutters, providing a single track size will only ever create a single + /// column. + #[borrowed] + pub columns: TrackSizings, + + /// The row sizes. + /// + /// If there are more cells than fit the defined rows, the last row is + /// repeated until there are no more cells. + #[borrowed] + pub rows: TrackSizings, + + /// The gaps between rows & columns. + /// + /// If there are more gutters than defined sizes, the last gutter is repeated. + #[external] + pub gutter: TrackSizings, + + /// The gaps between columns. Takes precedence over `gutter`. + #[parse( + let gutter = args.named("gutter")?; + args.named("column-gutter")?.or_else(|| gutter.clone()) + )] + #[borrowed] + pub column_gutter: TrackSizings, + + /// The gaps between rows. Takes precedence over `gutter`. + #[parse(args.named("row-gutter")?.or_else(|| gutter.clone()))] + #[borrowed] + pub row_gutter: TrackSizings, + + /// How to fill the cells. + /// + /// This can be a color or a function that returns a color. The function is + /// passed the cells' column and row index, starting at zero. This can be + /// used to implement striped grids. + /// + /// ```example + /// #grid( + /// fill: (col, row) => if calc.even(col + row) { luma(240) } else { white }, + /// align: center + horizon, + /// columns: 4, + /// [X], [O], [X], [O], + /// [O], [X], [O], [X], + /// [X], [O], [X], [O], + /// [O], [X], [O], [X] + /// ) + /// ``` + #[borrowed] + pub fill: Celled>, + + /// How to align the cells' content. + /// + /// This can either be a single alignment, an array of alignments + /// (corresponding to each column) or a function that returns an alignment. + /// The function is passed the cells' column and row index, starting at zero. + /// If set to `{auto}`, the outer alignment is used. + /// + /// ```example + /// #grid( + /// columns: 3, + /// align: (x, y) => (left, center, right).at(x), + /// [Hello], [Hello], [Hello], + /// [A], [B], [C], + /// ) + /// ``` + #[borrowed] + pub align: Celled>, + + /// How to [stroke]($stroke) the cells. + /// + /// Grids have no strokes by default, which can be changed by setting this + /// option to the desired stroke. + /// + /// _Note:_ Richer stroke customization for individual cells is not yet + /// implemented, but will be in the future. In the meantime, you can use the + /// third-party [tablex library](https://github.com/PgBiel/typst-tablex/). + #[resolve] + #[fold] + pub stroke: Option, + + /// How much to pad the cells' content. + /// + /// ```example + /// #grid( + /// inset: 10pt, + /// fill: (_, row) => (red, blue).at(row), + /// [Hello], + /// [World], + /// ) + /// + /// #grid( + /// columns: 2, + /// inset: ( + /// x: 20pt, + /// y: 10pt, + /// ), + /// fill: (col, _) => (red, blue).at(col), + /// [Hello], + /// [World], + /// ) + /// ``` + #[fold] + #[default(Sides::splat(Abs::pt(0.0).into()))] + pub inset: Sides>>, + + /// The contents of the grid cells. + /// + /// The cells are populated in row-major order. + #[variadic] + pub children: Vec, +} + +#[scope] +impl GridElem { + #[elem] + type GridCell; +} + +impl Layout for GridElem { + #[typst_macros::time(name = "grid", span = self.span())] + fn layout( + &self, + engine: &mut Engine, + styles: StyleChain, + regions: Regions, + ) -> SourceResult { + let inset = self.inset(styles); + let align = self.align(styles); + let columns = self.columns(styles); + let rows = self.rows(styles); + let column_gutter = self.column_gutter(styles); + let row_gutter = self.row_gutter(styles); + let fill = self.fill(styles); + let stroke = self.stroke(styles).map(Stroke::unwrap_or_default); + + let tracks = Axes::new(columns.0.as_slice(), rows.0.as_slice()); + let gutter = Axes::new(column_gutter.0.as_slice(), row_gutter.0.as_slice()); + let grid = CellGrid::resolve( + tracks, + gutter, + self.children(), + fill, + align, + inset, + engine, + styles, + )?; + + let layouter = GridLayouter::new(&grid, &stroke, regions, styles, self.span()); + + // Measure the columns and layout the grid row-by-row. + layouter.layout(engine) + } +} + +/// Track sizing definitions. +#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] +pub struct TrackSizings(pub SmallVec<[Sizing; 4]>); + +cast! { + TrackSizings, + self => self.0.into_value(), + sizing: Sizing => Self(smallvec![sizing]), + count: NonZeroUsize => Self(smallvec![Sizing::Auto; count.get()]), + values: Array => Self(values.into_iter().map(Value::cast).collect::>()?), +} + +/// A cell in the grid. Use this to either override grid properties for a +/// particular cell, or in show rules to apply certain styles to multiple cells +/// at once. +/// +/// For example, you can override the fill, alignment or inset for a single +/// cell: +/// +/// ```example +/// #grid( +/// columns: 2, +/// fill: red, +/// align: left, +/// inset: 5pt, +/// [ABC], [ABC], +/// grid.cell(fill: blue)[C], [D], +/// grid.cell(align: center)[E], [F], +/// [G], grid.cell(inset: 0pt)[H] +/// ) +/// ``` +#[elem(name = "cell", title = "Grid Cell", Show)] +pub struct GridCell { + /// The cell's body. + #[required] + body: Content, + + /// The cell's fill override. + fill: Smart>, + + /// The cell's alignment override. + align: Smart, + + /// The cell's inset override. + inset: Smart>>>, +} + +cast! { + GridCell, + v: Content => v.into(), +} + +impl Default for GridCell { + fn default() -> Self { + Self::new(Content::default()) + } +} + +impl ResolvableCell for GridCell { + fn resolve_cell( + mut self, + _: usize, + _: usize, + fill: &Option, + align: Smart, + inset: Sides>, + styles: StyleChain, + ) -> Cell { + let fill = self.fill(styles).unwrap_or_else(|| fill.clone()); + self.push_fill(Smart::Custom(fill.clone())); + self.push_align(match align { + Smart::Custom(align) => { + Smart::Custom(self.align(styles).map_or(align, |inner| inner.fold(align))) + } + // Don't fold if the grid is using outer alignment. Use the + // cell's alignment instead (which, in the end, will fold with + // the outer alignment when it is effectively displayed). + Smart::Auto => self.align(styles), + }); + self.push_inset(Smart::Custom( + self.inset(styles).map_or(inset, |inner| inner.fold(inset)).map(Some), + )); + + Cell { body: self.pack(), fill } + } +} + +impl Show for GridCell { + fn show(&self, _engine: &mut Engine, styles: StyleChain) -> SourceResult { + show_grid_cell(self.body().clone(), self.inset(styles), self.align(styles)) + } +} + +impl From for GridCell { + fn from(value: Content) -> Self { + value + .to::() + .cloned() + .unwrap_or_else(|| Self::new(value.clone())) + } +} + +/// Function with common code to display a grid cell or table cell. +pub fn show_grid_cell( + mut body: Content, + inset: Smart>>>, + align: Smart, +) -> SourceResult { + let inset = inset.unwrap_or_default().map(Option::unwrap_or_default); + + if inset != Sides::default() { + // Only pad if some inset is not 0pt. + // Avoids a bug where using .padded() in any way inside Show causes + // alignment in align(...) to break. + body = body.padded(inset); + } + + if let Smart::Custom(alignment) = align { + body = body.styled(AlignElem::set_alignment(alignment)); + } + + Ok(body) +} diff --git a/crates/typst/src/model/bibliography.rs b/crates/typst/src/model/bibliography.rs index fa35936b3..62e6d986f 100644 --- a/crates/typst/src/model/bibliography.rs +++ b/crates/typst/src/model/bibliography.rs @@ -29,7 +29,7 @@ use crate::foundations::{ }; use crate::introspection::{Introspector, Locatable, Location}; use crate::layout::{ - BlockElem, Em, GridElem, HElem, PadElem, Sizing, TrackSizings, VElem, + BlockElem, Em, GridCell, GridElem, HElem, PadElem, Sizing, TrackSizings, VElem, }; use crate::model::{ CitationForm, CiteGroup, Destination, FootnoteElem, HeadingElem, LinkElem, ParElem, @@ -239,8 +239,8 @@ impl Show for BibliographyElem { if references.iter().any(|(prefix, _)| prefix.is_some()) { let mut cells = vec![]; for (prefix, reference) in references { - cells.push(prefix.clone().unwrap_or_default()); - cells.push(reference.clone()); + cells.push(GridCell::new(prefix.clone().unwrap_or_default())); + cells.push(GridCell::new(reference.clone())); } seq.push(VElem::new(row_gutter).with_weakness(3).pack()); @@ -947,7 +947,7 @@ impl ElemRenderer<'_> { if let Some(prefix) = suf_prefix { const COLUMN_GUTTER: Em = Em::new(0.65); - content = GridElem::new(vec![prefix, content]) + content = GridElem::new(vec![GridCell::new(prefix), GridCell::new(content)]) .spanned(self.span) .with_columns(TrackSizings(smallvec![Sizing::Auto; 2])) .with_column_gutter(TrackSizings(smallvec![COLUMN_GUTTER.into()])) diff --git a/crates/typst/src/model/enum.rs b/crates/typst/src/model/enum.rs index bb44f438a..7f3ffae1a 100644 --- a/crates/typst/src/model/enum.rs +++ b/crates/typst/src/model/enum.rs @@ -6,8 +6,8 @@ use crate::foundations::{ cast, elem, scope, Array, Content, Fold, NativeElement, Smart, StyleChain, }; use crate::layout::{ - Align, Axes, BlockElem, Celled, Em, Fragment, GridLayouter, HAlign, Layout, Length, - Regions, Sizing, Spacing, VAlign, + Align, Axes, BlockElem, Cell, CellGrid, Em, Fragment, GridLayouter, HAlign, Layout, + Length, Regions, Sizing, Spacing, VAlign, }; use crate::model::{Numbering, NumberingPattern, ParElem}; use crate::text::TextElem; @@ -259,16 +259,17 @@ impl Layout for EnumElem { let resolved = resolved.aligned(number_align).styled(TextElem::set_overhang(false)); - cells.push(Content::empty()); - cells.push(resolved); - cells.push(Content::empty()); - cells.push(item.body().clone().styled(Self::set_parents(Parent(number)))); + cells.push(Cell::from(Content::empty())); + cells.push(Cell::from(resolved)); + cells.push(Cell::from(Content::empty())); + cells.push(Cell::from( + item.body().clone().styled(Self::set_parents(Parent(number))), + )); number = number.saturating_add(1); } - let fill = Celled::Value(None); let stroke = None; - let layouter = GridLayouter::new( + let grid = CellGrid::new( Axes::with_x(&[ Sizing::Rel(indent.into()), Sizing::Auto, @@ -276,15 +277,12 @@ impl Layout for EnumElem { Sizing::Auto, ]), Axes::with_y(&[gutter.into()]), - &cells, - &fill, - &stroke, - regions, + cells, styles, - self.span(), ); + let layouter = GridLayouter::new(&grid, &stroke, regions, styles, self.span()); - Ok(layouter.layout(engine)?.fragment) + layouter.layout(engine) } } diff --git a/crates/typst/src/model/list.rs b/crates/typst/src/model/list.rs index 48913fc77..520657bb2 100644 --- a/crates/typst/src/model/list.rs +++ b/crates/typst/src/model/list.rs @@ -5,8 +5,8 @@ use crate::foundations::{ Value, }; use crate::layout::{ - Axes, BlockElem, Celled, Em, Fragment, GridLayouter, HAlign, Layout, Length, Regions, - Sizing, Spacing, VAlign, + Axes, BlockElem, Cell, CellGrid, Em, Fragment, GridLayouter, HAlign, Layout, Length, + Regions, Sizing, Spacing, VAlign, }; use crate::model::ParElem; use crate::text::TextElem; @@ -160,15 +160,14 @@ impl Layout for ListElem { let mut cells = vec![]; for item in self.children() { - cells.push(Content::empty()); - cells.push(marker.clone()); - cells.push(Content::empty()); - cells.push(item.body().clone().styled(Self::set_depth(Depth))); + cells.push(Cell::from(Content::empty())); + cells.push(Cell::from(marker.clone())); + cells.push(Cell::from(Content::empty())); + cells.push(Cell::from(item.body().clone().styled(Self::set_depth(Depth)))); } - let fill = Celled::Value(None); let stroke = None; - let layouter = GridLayouter::new( + let grid = CellGrid::new( Axes::with_x(&[ Sizing::Rel(indent.into()), Sizing::Auto, @@ -176,15 +175,12 @@ impl Layout for ListElem { Sizing::Auto, ]), Axes::with_y(&[gutter.into()]), - &cells, - &fill, - &stroke, - regions, + cells, styles, - self.span(), ); + let layouter = GridLayouter::new(&grid, &stroke, regions, styles, self.span()); - Ok(layouter.layout(engine)?.fragment) + layouter.layout(engine) } } diff --git a/crates/typst/src/model/table.rs b/crates/typst/src/model/table.rs index 339204455..169289aa0 100644 --- a/crates/typst/src/model/table.rs +++ b/crates/typst/src/model/table.rs @@ -1,9 +1,11 @@ use crate::diag::SourceResult; use crate::engine::Engine; -use crate::foundations::{elem, Content, NativeElement, Smart, StyleChain}; +use crate::foundations::{ + cast, elem, scope, Content, Fold, NativeElement, Show, Smart, StyleChain, +}; use crate::layout::{ - apply_align_inset_to_cells, Abs, Align, Axes, Celled, Fragment, GridLayouter, Layout, - Length, Regions, Rel, Sides, TrackSizings, + show_grid_cell, Abs, Align, Axes, Cell, CellGrid, Celled, Fragment, GridLayouter, + Layout, Length, Regions, Rel, ResolvableCell, Sides, TrackSizings, }; use crate::model::Figurable; use crate::text::{Lang, LocalName, Region}; @@ -13,9 +15,15 @@ use crate::visualize::{Paint, Stroke}; /// /// Tables are used to arrange content in cells. Cells can contain arbitrary /// content, including multiple paragraphs and are specified in row-major order. -/// Because tables are just grids with configurable cell properties, refer to -/// the [grid documentation]($grid) for more information on how to size the -/// table tracks. +/// Because tables are just grids with different defaults for some cell +/// properties (notably `stroke` and `inset`), refer to the +/// [grid documentation]($grid) for more information on how to size the table +/// tracks and specify the cell appearance properties. +/// +/// Note that, to override a particular cell's properties or apply show rules +/// on table cells, you can use the [`table.cell`]($table.cell) element (but +/// not `grid.cell`, which is exclusive to grids). See its documentation for +/// more information. /// /// To give a table a caption and make it [referenceable]($ref), put it into a /// [figure]($figure). @@ -39,7 +47,7 @@ use crate::visualize::{Paint, Stroke}; /// [$a$: edge length] /// ) /// ``` -#[elem(Layout, LocalName, Figurable)] +#[elem(scope, Layout, LocalName, Figurable)] pub struct TableElem { /// The column sizes. See the [grid documentation]($grid) for more /// information on track sizing. @@ -149,7 +157,13 @@ pub struct TableElem { /// The contents of the table cells. #[variadic] - pub children: Vec, + pub children: Vec, +} + +#[scope] +impl TableElem { + #[elem] + type TableCell; } impl Layout for TableElem { @@ -171,22 +185,20 @@ impl Layout for TableElem { let tracks = Axes::new(columns.0.as_slice(), rows.0.as_slice()); let gutter = Axes::new(column_gutter.0.as_slice(), row_gutter.0.as_slice()); - let cells = - apply_align_inset_to_cells(engine, &tracks, self.children(), align, inset)?; - - // Prepare grid layout by unifying content and gutter tracks. - let layouter = GridLayouter::new( + let grid = CellGrid::resolve( tracks, gutter, - &cells, + self.children(), fill, - &stroke, - regions, + align, + inset, + engine, styles, - self.span(), - ); + )?; - Ok(layouter.layout(engine)?.fragment) + let layouter = GridLayouter::new(&grid, &stroke, regions, styles, self.span()); + + layouter.layout(engine) } } @@ -227,3 +239,92 @@ impl LocalName for TableElem { } impl Figurable for TableElem {} + +/// A cell in the table. Use this to either override table properties for a +/// particular cell, or in show rules to apply certain styles to multiple cells +/// at once. +/// +/// For example, you can override the fill, alignment or inset for a single +/// cell: +/// +/// ```example +/// #table( +/// columns: 2, +/// fill: green, +/// align: right, +/// [*Name*], [*Data*], +/// table.cell(fill: blue)[J.], [Organizer], +/// table.cell(align: center)[K.], [Leader], +/// [M.], table.cell(inset: 0pt)[Player] +/// ) +/// ``` +#[elem(name = "cell", title = "Table Cell", Show)] +pub struct TableCell { + /// The cell's body. + #[required] + body: Content, + + /// The cell's fill override. + fill: Smart>, + + /// The cell's alignment override. + align: Smart, + + /// The cell's inset override. + inset: Smart>>>, +} + +cast! { + TableCell, + v: Content => v.into(), +} + +impl Default for TableCell { + fn default() -> Self { + Self::new(Content::default()) + } +} + +impl ResolvableCell for TableCell { + fn resolve_cell( + mut self, + _: usize, + _: usize, + fill: &Option, + align: Smart, + inset: Sides>, + styles: StyleChain, + ) -> Cell { + let fill = self.fill(styles).unwrap_or_else(|| fill.clone()); + self.push_fill(Smart::Custom(fill.clone())); + self.push_align(match align { + Smart::Custom(align) => { + Smart::Custom(self.align(styles).map_or(align, |inner| inner.fold(align))) + } + // Don't fold if the table is using outer alignment. Use the + // cell's alignment instead (which, in the end, will fold with + // the outer alignment when it is effectively displayed). + Smart::Auto => self.align(styles), + }); + self.push_inset(Smart::Custom( + self.inset(styles).map_or(inset, |inner| inner.fold(inset)).map(Some), + )); + + Cell { body: self.pack(), fill } + } +} + +impl Show for TableCell { + fn show(&self, _engine: &mut Engine, styles: StyleChain) -> SourceResult { + show_grid_cell(self.body().clone(), self.inset(styles), self.align(styles)) + } +} + +impl From for TableCell { + fn from(value: Content) -> Self { + value + .to::() + .cloned() + .unwrap_or_else(|| Self::new(value.clone())) + } +} diff --git a/tests/ref/layout/grid-cell.png b/tests/ref/layout/grid-cell.png new file mode 100644 index 000000000..fb6831236 Binary files /dev/null and b/tests/ref/layout/grid-cell.png differ diff --git a/tests/ref/layout/grid-styling.png b/tests/ref/layout/grid-styling.png index ae5c05193..c626d2e7a 100644 Binary files a/tests/ref/layout/grid-styling.png and b/tests/ref/layout/grid-styling.png differ diff --git a/tests/ref/layout/table-cell.png b/tests/ref/layout/table-cell.png new file mode 100644 index 000000000..fa3d04ccb Binary files /dev/null and b/tests/ref/layout/table-cell.png differ diff --git a/tests/ref/layout/table.png b/tests/ref/layout/table.png index fe4554d9d..b6b31eb16 100644 Binary files a/tests/ref/layout/table.png and b/tests/ref/layout/table.png differ diff --git a/tests/typ/layout/grid-cell.typ b/tests/typ/layout/grid-cell.typ new file mode 100644 index 000000000..ced16a97a --- /dev/null +++ b/tests/typ/layout/grid-cell.typ @@ -0,0 +1,107 @@ +// Test basic styling using the grid.cell element. + +--- +// Cell override +#grid( + align: left, + fill: red, + stroke: blue, + inset: 5pt, + columns: 2, + [AAAAA], [BBBBB], + [A], [B], + grid.cell(align: right)[C], [D], + align(right)[E], [F], + align(horizon)[G], [A\ A\ A], + grid.cell(align: horizon)[G2], [A\ A\ A], + grid.cell(inset: 0pt)[I], [F], + [H], grid.cell(fill: blue)[J] +) + +--- +// Cell show rule +#show grid.cell: it => [Zz] + +#grid( + align: left, + fill: red, + stroke: blue, + inset: 5pt, + columns: 2, + [AAAAA], [BBBBB], + [A], [B], + grid.cell(align: right)[C], [D], + align(right)[E], [F], + align(horizon)[G], [A\ A\ A] +) + +--- +#show grid.cell: it => (it.align, it.fill) +#grid( + align: left, + row-gutter: 5pt, + [A], + grid.cell(align: right)[B], + grid.cell(fill: aqua)[B], +) + +--- +// Cell set rules +#set grid.cell(align: center) +#show grid.cell: it => (it.align, it.fill, it.inset) +#set grid.cell(inset: 20pt) +#grid( + align: left, + row-gutter: 5pt, + [A], + grid.cell(align: right)[B], + grid.cell(fill: aqua)[B], +) + +--- +// Test folding per-cell properties (align and inset) +#grid( + columns: (1fr, 1fr), + rows: (2.5em, auto), + align: right, + inset: 5pt, + fill: (x, y) => (green, aqua).at(calc.rem(x + y, 2)), + [Top], grid.cell(align: bottom)[Bot], + grid.cell(inset: (bottom: 0pt))[Bot], grid.cell(inset: (bottom: 0pt))[Bot] +) + +--- +// Test overriding outside alignment +#set align(bottom + right) +#grid( + columns: (1fr, 1fr), + rows: 2em, + align: auto, + fill: green, + [BR], [BR], + grid.cell(align: left, fill: aqua)[BL], grid.cell(align: top, fill: red.lighten(50%))[TR] +) + +--- +// First doc example +#grid( + columns: 2, + fill: red, + align: left, + inset: 5pt, + [ABC], [ABC], + grid.cell(fill: blue)[C], [D], + grid.cell(align: center)[E], [F], + [G], grid.cell(inset: 0pt)[H] +) + +--- +#{ + show grid.cell: emph + grid( + columns: 2, + gutter: 3pt, + [Hello], [World], + [Sweet], [Italics] + ) +} diff --git a/tests/typ/layout/grid-styling.typ b/tests/typ/layout/grid-styling.typ index 577e15c4b..e076d0c4c 100644 --- a/tests/typ/layout/grid-styling.typ +++ b/tests/typ/layout/grid-styling.typ @@ -87,3 +87,48 @@ a [A], [B], ) + +--- +// Test interaction with gutters. +#grid( + columns: (3em, 3em), + fill: (x, y) => (red, blue).at(calc.rem(x, 2)), + align: (x, y) => (left, right).at(calc.rem(y, 2)), + [A], [B], + [C], [D], + [E], [F], + [G], [H] +) + +#grid( + columns: (3em, 3em), + fill: (x, y) => (red, blue).at(calc.rem(x, 2)), + align: (x, y) => (left, right).at(calc.rem(y, 2)), + row-gutter: 5pt, + [A], [B], + [C], [D], + [E], [F], + [G], [H] +) + +#grid( + columns: (3em, 3em), + fill: (x, y) => (red, blue).at(calc.rem(x, 2)), + align: (x, y) => (left, right).at(calc.rem(y, 2)), + column-gutter: 5pt, + [A], [B], + [C], [D], + [E], [F], + [G], [H] +) + +#grid( + columns: (3em, 3em), + fill: (x, y) => (red, blue).at(calc.rem(x, 2)), + align: (x, y) => (left, right).at(calc.rem(y, 2)), + gutter: 5pt, + [A], [B], + [C], [D], + [E], [F], + [G], [H] +) diff --git a/tests/typ/layout/table-cell.typ b/tests/typ/layout/table-cell.typ new file mode 100644 index 000000000..a4d3bba47 --- /dev/null +++ b/tests/typ/layout/table-cell.typ @@ -0,0 +1,102 @@ +// Test basic styling using the table.cell element. + +--- +// Cell override +#table( + align: left, + fill: red, + stroke: blue, + columns: 2, + [AAAAA], [BBBBB], + [A], [B], + table.cell(align: right)[C], [D], + align(right)[E], [F], + align(horizon)[G], [A\ A\ A], + table.cell(align: horizon)[G2], [A\ A\ A], + table.cell(inset: 0pt)[I], [F], + [H], table.cell(fill: blue)[J] +) + +--- +// Cell show rule +#show table.cell: it => [Zz] + +#table( + align: left, + fill: red, + stroke: blue, + columns: 2, + [AAAAA], [BBBBB], + [A], [B], + table.cell(align: right)[C], [D], + align(right)[E], [F], + align(horizon)[G], [A\ A\ A] +) + +--- +#show table.cell: it => (it.align, it.fill) +#table( + align: left, + row-gutter: 5pt, + [A], + table.cell(align: right)[B], + table.cell(fill: aqua)[B], +) + +--- +// Cell set rules +#set table.cell(align: center) +#show table.cell: it => (it.align, it.fill, it.inset) +#set table.cell(inset: 20pt) +#table( + align: left, + row-gutter: 5pt, + [A], + table.cell(align: right)[B], + table.cell(fill: aqua)[B], +) + +--- +// Test folding per-cell properties (align and inset) +#table( + columns: (1fr, 1fr), + rows: (2.5em, auto), + align: right, + fill: (x, y) => (green, aqua).at(calc.rem(x + y, 2)), + [Top], table.cell(align: bottom)[Bot], + table.cell(inset: (bottom: 0pt))[Bot], table.cell(inset: (bottom: 0pt))[Bot] +) + +--- +// Test overriding outside alignment +#set align(bottom + right) +#table( + columns: (1fr, 1fr), + rows: 2em, + align: auto, + fill: green, + [BR], [BR], + table.cell(align: left, fill: aqua)[BL], table.cell(align: top, fill: red.lighten(50%))[TR] +) + +--- +// First doc example +#table( + columns: 2, + fill: green, + align: right, + [*Name*], [*Data*], + table.cell(fill: blue)[J.], [Organizer], + table.cell(align: center)[K.], [Leader], + [M.], table.cell(inset: 0pt)[Player] +) + +--- +#{ + show table.cell: emph + table( + columns: 2, + [Person], [Animal], + [John], [Dog] + ) +} diff --git a/tests/typ/layout/table.typ b/tests/typ/layout/table.typ index 529f27201..1b250aa4c 100644 --- a/tests/typ/layout/table.typ +++ b/tests/typ/layout/table.typ @@ -61,6 +61,51 @@ [B], ) +--- +// Test interaction with gutters. +#table( + columns: (3em, 3em), + fill: (x, y) => (red, blue).at(calc.rem(x, 2)), + align: (x, y) => (left, right).at(calc.rem(y, 2)), + [A], [B], + [C], [D], + [E], [F], + [G], [H] +) + +#table( + columns: (3em, 3em), + fill: (x, y) => (red, blue).at(calc.rem(x, 2)), + align: (x, y) => (left, right).at(calc.rem(y, 2)), + row-gutter: 5pt, + [A], [B], + [C], [D], + [E], [F], + [G], [H] +) + +#table( + columns: (3em, 3em), + fill: (x, y) => (red, blue).at(calc.rem(x, 2)), + align: (x, y) => (left, right).at(calc.rem(y, 2)), + column-gutter: 5pt, + [A], [B], + [C], [D], + [E], [F], + [G], [H] +) + +#table( + columns: (3em, 3em), + fill: (x, y) => (red, blue).at(calc.rem(x, 2)), + align: (x, y) => (left, right).at(calc.rem(y, 2)), + gutter: 5pt, + [A], [B], + [C], [D], + [E], [F], + [G], [H] +) + --- // Ref: false #table()