From 8fa573a9a203084ede03cb2f22a54a4b5973bb88 Mon Sep 17 00:00:00 2001 From: PgBiel <9021226+PgBiel@users.noreply.github.com> Date: Fri, 5 Jan 2024 16:30:07 -0300 Subject: Initial table per-cell customization [More Flexible Tables Pt.2a] (#3037) Co-authored-by: Laurenz --- crates/typst/src/foundations/auto.rs | 16 +- crates/typst/src/layout/grid.rs | 1004 -------------------------------- crates/typst/src/layout/grid/layout.rs | 823 ++++++++++++++++++++++++++ crates/typst/src/layout/grid/mod.rs | 383 ++++++++++++ crates/typst/src/model/bibliography.rs | 8 +- crates/typst/src/model/enum.rs | 26 +- crates/typst/src/model/list.rs | 24 +- crates/typst/src/model/table.rs | 139 ++++- tests/ref/layout/grid-cell.png | Bin 0 -> 39178 bytes tests/ref/layout/grid-styling.png | Bin 18429 -> 27200 bytes tests/ref/layout/table-cell.png | Bin 0 -> 41893 bytes tests/ref/layout/table.png | Bin 12226 -> 22515 bytes tests/typ/layout/grid-cell.typ | 107 ++++ tests/typ/layout/grid-styling.typ | 45 ++ tests/typ/layout/table-cell.typ | 102 ++++ tests/typ/layout/table.typ | 45 ++ 16 files changed, 1665 insertions(+), 1057 deletions(-) delete mode 100644 crates/typst/src/layout/grid.rs create mode 100644 crates/typst/src/layout/grid/layout.rs create mode 100644 crates/typst/src/layout/grid/mod.rs create mode 100644 tests/ref/layout/grid-cell.png create mode 100644 tests/ref/layout/table-cell.png create mode 100644 tests/typ/layout/grid-cell.typ create mode 100644 tests/typ/layout/table-cell.typ diff --git a/crates/typst/src/foundations/auto.rs b/crates/typst/src/foundations/auto.rs index dd36e814..5cb03f7f 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.rs deleted file mode 100644 index 56f8190b..00000000 --- a/crates/typst/src/layout/grid.rs +++ /dev/null @@ -1,1004 +0,0 @@ -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, -}; -use crate::layout::{ - Abs, Align, AlignElem, 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::>()?), -} - -/// A value that can be configured per cell. -#[derive(Debug, Clone, PartialEq, Hash)] -pub enum Celled { - /// A bare value, the same for all cells. - Value(T), - /// A closure mapping from cell coordinates to a value. - Func(Func), - /// An array of alignment values corresponding to each column. - Array(Vec), -} - -impl Celled { - /// Resolve the value based on the cell position. - pub fn resolve(&self, engine: &mut Engine, x: usize, y: usize) -> SourceResult { - Ok(match self { - Self::Value(value) => value.clone(), - Self::Func(func) => func.call(engine, [x, y])?.cast().at(func.span())?, - Self::Array(array) => x - .checked_rem(array.len()) - .and_then(|i| array.get(i)) - .cloned() - .unwrap_or_default(), - }) - } -} - -impl Default for Celled { - fn default() -> Self { - Self::Value(T::default()) - } -} - -impl Reflect for Celled { - fn input() -> CastInfo { - T::input() + Array::input() + Func::input() - } - - fn output() -> CastInfo { - T::output() + Array::output() + Func::output() - } - - fn castable(value: &Value) -> bool { - Array::castable(value) || Func::castable(value) || T::castable(value) - } -} - -impl IntoValue for Celled { - fn into_value(self) -> Value { - match self { - Self::Value(value) => value.into_value(), - Self::Func(func) => func.into_value(), - Self::Array(arr) => arr.into_value(), - } - } -} - -impl FromValue for Celled { - fn from_value(value: Value) -> StrResult { - match value { - Value::Func(v) => Ok(Self::Func(v)), - Value::Array(array) => Ok(Self::Array( - array.into_iter().map(T::from_value).collect::>()?, - )), - v if T::castable(&v) => Ok(Self::Value(T::from_value(v)?)), - v => Err(Self::error(&v)), - } - } -} - -/// Performs grid layout. -pub struct GridLayouter<'a> { - /// The grid cells. - cells: &'a [Content], - /// Whether this is an RTL grid. - is_rtl: bool, - /// Whether this grid has gutters. - has_gutter: bool, - /// 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, -} - -/// 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)] - 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, - ) -> Self { - let mut cols = vec![]; - let mut rows = vec![]; - - // Number of content columns: Always at least one. - let c = tracks.x.len().max(1); - - // Number of content rows: At least as many as given, but also at least - // as many as needed to place each item. - let r = { - let len = cells.len(); - let given = tracks.y.len(); - let needed = len / c + (len % c).clamp(0, 1); - given.max(needed) - }; - - let has_gutter = gutter.any(|tracks| !tracks.is_empty()); - let auto = Sizing::Auto; - let zero = Sizing::Rel(Rel::zero()); - let get_or = |tracks: &[_], idx, default| { - tracks.get(idx).or(tracks.last()).copied().unwrap_or(default) - }; - - // Collect content and gutter columns. - for x in 0..c { - cols.push(get_or(tracks.x, x, auto)); - if has_gutter { - cols.push(get_or(gutter.x, x, zero)); - } - } - - // Collect content and gutter rows. - for y in 0..r { - rows.push(get_or(tracks.y, y, auto)); - if has_gutter { - rows.push(get_or(gutter.y, y, zero)); - } - } - - // Remove superfluous gutter tracks. - if has_gutter { - cols.pop(); - rows.pop(); - } - - // Reverse for RTL. - let is_rtl = TextElem::dir_in(styles) == Dir::RTL; - if is_rtl { - cols.reverse(); - } - - // 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, - stroke, - regions, - styles, - rcols: vec![Abs::zero(); cols.len()], - cols, - width: Abs::zero(), - rrows: vec![], - lrows: vec![], - initial: regions.size, - finished: vec![], - span, - } - } - - /// Determines the columns sizes and then layouts the grid row-by-row. - pub fn layout(mut self, engine: &mut Engine) -> SourceResult { - self.measure_columns(engine)?; - - for y in 0..self.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) { - self.finish_region(engine)?; - } - - match self.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)), - } - } - - self.finish_region(engine)?; - - if self.stroke.is_some() || !matches!(self.fill, Celled::Value(None)) { - self.render_fills_strokes(engine)?; - } - - Ok(GridLayout { - fragment: Fragment::frames(self.finished), - cols: self.rcols, - rows: self.rrows, - }) - } - - /// Add lines and backgrounds. - fn render_fills_strokes(&mut self, engine: &mut Engine) -> SourceResult<()> { - for (frame, rows) in self.finished.iter_mut().zip(&self.rrows) { - if self.rcols.is_empty() || rows.is_empty() { - continue; - } - - // Render table lines. - if let Some(stroke) = self.stroke { - let thickness = stroke.thickness; - let half = thickness / 2.0; - - // Render horizontal lines. - for offset in points(rows.iter().map(|piece| piece.height)) { - let target = Point::with_x(frame.width() + thickness); - let hline = Geometry::Line(target).stroked(stroke.clone()); - frame.prepend( - Point::new(-half, offset), - FrameItem::Shape(hline, self.span), - ); - } - - // Render vertical lines. - for offset in points(self.rcols.iter().copied()) { - let target = Point::with_y(frame.height() + thickness); - let vline = Geometry::Line(target).stroked(stroke.clone()); - frame.prepend( - Point::new(offset, -half), - FrameItem::Shape(vline, self.span), - ); - } - } - - // Render cell backgrounds. - let mut dx = Abs::zero(); - 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 pos = Point::new(dx, dy); - let size = Size::new(col, row.height); - let rect = Geometry::Rect(size).filled(fill); - frame.prepend(pos, FrameItem::Shape(rect, self.span)); - } - dy += row.height; - } - dx += col; - } - } - - Ok(()) - } - - /// Determine all column sizes. - fn measure_columns(&mut self, engine: &mut Engine) -> SourceResult<()> { - // Sum of sizes of resolved relative tracks. - let mut rel = Abs::zero(); - - // Sum of fractions of all fractional tracks. - let mut fr = Fr::zero(); - - // 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) { - match col { - Sizing::Auto => {} - Sizing::Rel(v) => { - let resolved = - v.resolve(self.styles).relative_to(self.regions.base().x); - *rcol = resolved; - rel += resolved; - } - Sizing::Fr(v) => fr += v, - } - } - - // Size that is not used by fixed-size columns. - let available = self.regions.size.x - rel; - if available >= Abs::zero() { - // Determine size of auto columns. - let (auto, count) = self.measure_auto_columns(engine, available)?; - - // If there is remaining space, distribute it to fractional columns, - // otherwise shrink auto columns. - let remaining = available - auto; - if remaining >= Abs::zero() { - self.grow_fractional_columns(remaining, fr); - } else { - self.shrink_auto_columns(available, count); - } - } - - // Sum up the resolved column sizes once here. - self.width = self.rcols.iter().sum(); - - Ok(()) - } - - /// Measure the size that is available to auto columns. - fn measure_auto_columns( - &mut self, - engine: &mut Engine, - available: Abs, - ) -> SourceResult<(Abs, usize)> { - let mut auto = Abs::zero(); - let mut count = 0; - - // 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() { - 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 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] { - Sizing::Rel(v) => { - v.resolve(self.styles).relative_to(self.regions.base().y) - } - _ => self.regions.base().y, - }; - - let size = Size::new(available, height); - let pod = Regions::one(size, Axes::splat(false)); - let frame = cell.measure(engine, self.styles, pod)?.into_frame(); - resolved.set_max(frame.width()); - } - } - - self.rcols[x] = resolved; - auto += resolved; - count += 1; - } - - Ok((auto, count)) - } - - /// Distribute remaining space to fractional columns. - fn grow_fractional_columns(&mut self, remaining: Abs, fr: Fr) { - if fr.is_zero() { - return; - } - - for (&col, rcol) in self.cols.iter().zip(&mut self.rcols) { - if let Sizing::Fr(v) = col { - *rcol = v.share(fr, remaining); - } - } - } - - /// Redistribute space to auto columns so that each gets a fair share. - fn shrink_auto_columns(&mut self, available: Abs, count: usize) { - let mut last; - let mut fair = -Abs::inf(); - let mut redistribute = available; - let mut overlarge = count; - let mut changed = true; - - // Iteratively remove columns that don't need to be shrunk. - while changed && overlarge > 0 { - changed = false; - last = fair; - fair = redistribute / (overlarge as f64); - - for (&col, &rcol) in self.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 { - redistribute -= rcol; - overlarge -= 1; - changed = true; - } - } - } - - // Redistribute space fairly among overlarge columns. - for (&col, rcol) in self.cols.iter().zip(&mut self.rcols) { - if col == Sizing::Auto && *rcol > fair { - *rcol = fair; - } - } - } - - /// Layout a row with automatic height. Such a row may break across multiple - /// regions. - fn layout_auto_row(&mut self, engine: &mut Engine, y: usize) -> SourceResult<()> { - // Determine the size for each region of the row. If the first region - // ends up empty for some column, skip the region and remeasure. - let mut resolved = match self.measure_auto_row(engine, y, true)? { - Some(resolved) => resolved, - None => { - self.finish_region(engine)?; - self.measure_auto_row(engine, y, false)?.unwrap() - } - }; - - // Nothing to layout. - if resolved.is_empty() { - return Ok(()); - } - - // Layout into a single region. - if let &[first] = resolved.as_slice() { - let frame = self.layout_single_row(engine, first, y)?; - self.push_row(frame, y); - return Ok(()); - } - - // Expand all but the last region. - // Skip the first region if the space is eaten up by an fr row. - let len = resolved.len(); - for (region, target) in self - .regions - .iter() - .zip(&mut resolved[..len - 1]) - .skip(self.lrows.iter().any(|row| matches!(row, Row::Fr(..))) as usize) - { - target.set_max(region.y); - } - - // Layout into multiple regions. - let fragment = self.layout_multi_row(engine, &resolved, y)?; - let len = fragment.len(); - for (i, frame) in fragment.into_iter().enumerate() { - self.push_row(frame, y); - if i + 1 < len { - self.finish_region(engine)?; - } - } - - Ok(()) - } - - /// Measure the regions sizes of an auto row. The option is always `Some(_)` - /// if `can_skip` is false. - fn measure_auto_row( - &mut self, - engine: &mut Engine, - y: usize, - can_skip: bool, - ) -> SourceResult>> { - let mut resolved: Vec = vec![]; - - for (x, &rcol) in self.rcols.iter().enumerate() { - if let Some(cell) = self.cell(x, y) { - let mut pod = self.regions; - pod.size.x = rcol; - - let frames = cell.measure(engine, self.styles, pod)?.into_frames(); - - // Skip the first region if one cell in it is empty. Then, - // remeasure. - if let [first, rest @ ..] = frames.as_slice() { - if can_skip - && first.is_empty() - && rest.iter().any(|frame| !frame.is_empty()) - { - return Ok(None); - } - } - - let mut sizes = frames.iter().map(|frame| frame.height()); - for (target, size) in resolved.iter_mut().zip(&mut sizes) { - target.set_max(size); - } - - // New heights are maximal by virtue of being new. Note that - // this extend only uses the rest of the sizes iterator. - resolved.extend(sizes); - } - } - - Ok(Some(resolved)) - } - - /// Layout a row with relative height. Such a row cannot break across - /// multiple regions, but it may force a region break. - fn layout_relative_row( - &mut self, - engine: &mut Engine, - v: Rel, - y: usize, - ) -> SourceResult<()> { - let resolved = v.resolve(self.styles).relative_to(self.regions.base().y); - let frame = self.layout_single_row(engine, resolved, y)?; - - // Skip to fitting region. - let height = frame.height(); - while !self.regions.size.y.fits(height) && !self.regions.in_last() { - self.finish_region(engine)?; - - // Don't skip multiple regions for gutter and don't push a row. - if self.has_gutter && y % 2 == 1 { - return Ok(()); - } - } - - self.push_row(frame, y); - - Ok(()) - } - - /// Layout a row with fixed height and return its frame. - fn layout_single_row( - &mut self, - engine: &mut Engine, - height: Abs, - y: usize, - ) -> SourceResult { - if !height.is_finite() { - bail!(self.span, "cannot create grid with infinite height"); - } - - let mut output = Frame::soft(Size::new(self.width, height)); - let mut pos = Point::zero(); - - for (x, &rcol) in self.rcols.iter().enumerate() { - if let Some(cell) = self.cell(x, y) { - let size = Size::new(rcol, height); - let mut pod = Regions::one(size, Axes::splat(true)); - if self.rows[y] == Sizing::Auto { - pod.full = self.regions.full; - } - let frame = cell.layout(engine, self.styles, pod)?.into_frame(); - output.push_frame(pos, frame); - } - - pos.x += rcol; - } - - Ok(output) - } - - /// Layout a row spanning multiple regions. - fn layout_multi_row( - &mut self, - engine: &mut Engine, - heights: &[Abs], - y: usize, - ) -> SourceResult { - // Prepare frames. - let mut outputs: Vec<_> = heights - .iter() - .map(|&h| Frame::soft(Size::new(self.width, h))) - .collect(); - - // Prepare regions. - let size = Size::new(self.width, heights[0]); - let mut pod = Regions::one(size, Axes::splat(true)); - pod.full = self.regions.full; - pod.backlog = &heights[1..]; - - // Layout the row. - let mut pos = Point::zero(); - for (x, &rcol) in self.rcols.iter().enumerate() { - if let Some(cell) = self.cell(x, y) { - pod.size.x = rcol; - - // Push the layouted frames into the individual output frames. - let fragment = cell.layout(engine, self.styles, pod)?; - for (output, frame) in outputs.iter_mut().zip(fragment) { - output.push_frame(pos, frame); - } - } - - pos.x += rcol; - } - - Ok(Fragment::frames(outputs)) - } - - /// Push a row frame into the current region. - fn push_row(&mut self, frame: Frame, y: usize) { - self.regions.size.y -= frame.height(); - self.lrows.push(Row::Frame(frame, y)); - } - - /// Finish rows for one region. - fn finish_region(&mut self, engine: &mut Engine) -> SourceResult<()> { - // Determine the height of existing rows in the region. - let mut used = Abs::zero(); - let mut fr = Fr::zero(); - for row in &self.lrows { - match row { - Row::Frame(frame, _) => used += frame.height(), - Row::Fr(v, _) => fr += *v, - } - } - - // Determine the size of the grid in this region, expanding fully if - // there are fr rows. - let mut size = Size::new(self.width, used).min(self.initial); - if fr.get() > 0.0 && self.initial.y.is_finite() { - size.y = self.initial.y; - } - - // The frame for the region. - let mut output = Frame::soft(size); - let mut pos = Point::zero(); - let mut rrows = vec![]; - - // Place finished rows and layout fractional rows. - for row in std::mem::take(&mut self.lrows) { - let (frame, y) = match row { - Row::Frame(frame, y) => (frame, y), - Row::Fr(v, y) => { - let remaining = self.regions.full - used; - let height = v.share(fr, remaining); - (self.layout_single_row(engine, height, y)?, y) - } - }; - - let height = frame.height(); - output.push_frame(pos, frame); - rrows.push(RowPiece { height, y }); - pos.y += height; - } - - self.finished.push(output); - self.rrows.push(rrows); - self.regions.next(); - self.initial = self.regions.size; - - 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, -/// and after the extents, e.g. [10mm, 5mm] -> [0mm, 10mm, 15mm]. -fn points(extents: impl IntoIterator) -> impl Iterator { - let mut offset = Abs::zero(); - std::iter::once(Abs::zero()).chain(extents).map(move |extent| { - offset += extent; - offset - }) -} diff --git a/crates/typst/src/layout/grid/layout.rs b/crates/typst/src/layout/grid/layout.rs new file mode 100644 index 00000000..24d641a6 --- /dev/null +++ b/crates/typst/src/layout/grid/layout.rs @@ -0,0 +1,823 @@ +use crate::diag::{bail, At, SourceResult, StrResult}; +use crate::engine::Engine; +use crate::foundations::{ + Array, CastInfo, Content, FromValue, Func, IntoValue, Reflect, Resolve, Smart, + StyleChain, Value, +}; +use crate::layout::{ + 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}; + +/// A value that can be configured per cell. +#[derive(Debug, Clone, PartialEq, Hash)] +pub enum Celled { + /// A bare value, the same for all cells. + Value(T), + /// A closure mapping from cell coordinates to a value. + Func(Func), + /// An array of alignment values corresponding to each column. + Array(Vec), +} + +impl Celled { + /// Resolve the value based on the cell position. + pub fn resolve(&self, engine: &mut Engine, x: usize, y: usize) -> SourceResult { + Ok(match self { + Self::Value(value) => value.clone(), + Self::Func(func) => func.call(engine, [x, y])?.cast().at(func.span())?, + Self::Array(array) => x + .checked_rem(array.len()) + .and_then(|i| array.get(i)) + .cloned() + .unwrap_or_default(), + }) + } +} + +impl Default for Celled { + fn default() -> Self { + Self::Value(T::default()) + } +} + +impl Reflect for Celled { + fn input() -> CastInfo { + T::input() + Array::input() + Func::input() + } + + fn output() -> CastInfo { + T::output() + Array::output() + Func::output() + } + + fn castable(value: &Value) -> bool { + Array::castable(value) || Func::castable(value) || T::castable(value) + } +} + +impl IntoValue for Celled { + fn into_value(self) -> Value { + match self { + Self::Value(value) => value.into_value(), + Self::Func(func) => func.into_value(), + Self::Array(arr) => arr.into_value(), + } + } +} + +impl FromValue for Celled { + fn from_value(value: Value) -> StrResult { + match value { + Value::Func(v) => Ok(Self::Func(v)), + Value::Array(array) => Ok(Self::Array( + array.into_iter().map(T::from_value).collect::>()?, + )), + v if T::castable(&v) => Ok(Self::Value(T::from_value(v)?)), + v => Err(Self::error(&v)), + } + } +} + +/// 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: Vec, + /// The column tracks including gutter tracks. + cols: Vec, + /// The row tracks including gutter tracks. + rows: Vec, + /// Whether this grid has gutters. + has_gutter: bool, + /// Whether this is an RTL grid. + is_rtl: bool, +} + +impl CellGrid { + /// Generates the cell grid, given the tracks and resolved cells. + pub fn new( + tracks: Axes<&[Sizing]>, + gutter: Axes<&[Sizing]>, + cells: Vec, + styles: StyleChain, + ) -> Self { + let mut cols = vec![]; + let mut rows = vec![]; + + // Number of content columns: Always at least one. + let c = tracks.x.len().max(1); + + // Number of content rows: At least as many as given, but also at least + // as many as needed to place each item. + let r = { + let len = cells.len(); + let given = tracks.y.len(); + let needed = len / c + (len % c).clamp(0, 1); + given.max(needed) + }; + + let has_gutter = gutter.any(|tracks| !tracks.is_empty()); + let auto = Sizing::Auto; + let zero = Sizing::Rel(Rel::zero()); + let get_or = |tracks: &[_], idx, default| { + tracks.get(idx).or(tracks.last()).copied().unwrap_or(default) + }; + + // Collect content and gutter columns. + for x in 0..c { + cols.push(get_or(tracks.x, x, auto)); + if has_gutter { + cols.push(get_or(gutter.x, x, zero)); + } + } + + // Collect content and gutter rows. + for y in 0..r { + rows.push(get_or(tracks.y, y, auto)); + if has_gutter { + rows.push(get_or(gutter.y, y, zero)); + } + } + + // Remove superfluous gutter tracks. + if has_gutter { + cols.pop(); + rows.pop(); + } + + // Reverse for RTL. + let is_rtl = TextElem::dir_in(styles) == Dir::RTL; + if is_rtl { + 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 { + grid, + stroke, + regions, + styles, + rcols: vec![Abs::zero(); grid.cols.len()], + width: Abs::zero(), + rrows: vec![], + lrows: vec![], + initial: regions.size, + finished: vec![], + span, + } + } + + /// Determines the columns sizes and then layouts the grid row-by-row. + pub fn layout(mut self, engine: &mut Engine) -> SourceResult { + self.measure_columns(engine)?; + + 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.grid.has_gutter || y % 2 == 0) { + self.finish_region(engine)?; + } + + 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)), + } + } + + self.finish_region(engine)?; + + self.render_fills_strokes()?; + + Ok(Fragment::frames(self.finished)) + } + + /// Add lines and backgrounds. + 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; + } + + // Render table lines. + if let Some(stroke) = self.stroke { + let thickness = stroke.thickness; + let half = thickness / 2.0; + + // Render horizontal lines. + for offset in points(rows.iter().map(|piece| piece.height)) { + let target = Point::with_x(frame.width() + thickness); + let hline = Geometry::Line(target).stroked(stroke.clone()); + frame.prepend( + Point::new(-half, offset), + FrameItem::Shape(hline, self.span), + ); + } + + // Render vertical lines. + for offset in points(self.rcols.iter().copied()) { + let target = Point::with_y(frame.height() + thickness); + let vline = Geometry::Line(target).stroked(stroke.clone()); + frame.prepend( + Point::new(offset, -half), + FrameItem::Shape(vline, self.span), + ); + } + } + + // Render cell backgrounds. + let mut dx = Abs::zero(); + for (x, &col) in self.rcols.iter().enumerate() { + let mut dy = Abs::zero(); + for row in rows { + 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); + frame.prepend(pos, FrameItem::Shape(rect, self.span)); + } + dy += row.height; + } + dx += col; + } + } + + Ok(()) + } + + /// Determine all column sizes. + fn measure_columns(&mut self, engine: &mut Engine) -> SourceResult<()> { + // Sum of sizes of resolved relative tracks. + let mut rel = Abs::zero(); + + // Sum of fractions of all fractional tracks. + let mut fr = Fr::zero(); + + // Resolve the size of all relative columns and compute the sum of all + // fractional tracks. + for (&col, rcol) in self.grid.cols.iter().zip(&mut self.rcols) { + match col { + Sizing::Auto => {} + Sizing::Rel(v) => { + let resolved = + v.resolve(self.styles).relative_to(self.regions.base().x); + *rcol = resolved; + rel += resolved; + } + Sizing::Fr(v) => fr += v, + } + } + + // Size that is not used by fixed-size columns. + let available = self.regions.size.x - rel; + if available >= Abs::zero() { + // Determine size of auto columns. + let (auto, count) = self.measure_auto_columns(engine, available)?; + + // If there is remaining space, distribute it to fractional columns, + // otherwise shrink auto columns. + let remaining = available - auto; + if remaining >= Abs::zero() { + self.grow_fractional_columns(remaining, fr); + } else { + self.shrink_auto_columns(available, count); + } + } + + // Sum up the resolved column sizes once here. + self.width = self.rcols.iter().sum(); + + Ok(()) + } + + /// Measure the size that is available to auto columns. + fn measure_auto_columns( + &mut self, + engine: &mut Engine, + available: Abs, + ) -> SourceResult<(Abs, usize)> { + let mut auto = Abs::zero(); + let mut count = 0; + + // 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.grid.cols.iter().enumerate() { + if col != Sizing::Auto { + continue; + } + + let mut resolved = Abs::zero(); + 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.grid.rows[y] { + Sizing::Rel(v) => { + v.resolve(self.styles).relative_to(self.regions.base().y) + } + _ => self.regions.base().y, + }; + + let size = Size::new(available, height); + let pod = Regions::one(size, Axes::splat(false)); + let frame = cell.measure(engine, self.styles, pod)?.into_frame(); + resolved.set_max(frame.width()); + } + } + + self.rcols[x] = resolved; + auto += resolved; + count += 1; + } + + Ok((auto, count)) + } + + /// Distribute remaining space to fractional columns. + fn grow_fractional_columns(&mut self, remaining: Abs, fr: Fr) { + if fr.is_zero() { + return; + } + + for (&col, rcol) in self.grid.cols.iter().zip(&mut self.rcols) { + if let Sizing::Fr(v) = col { + *rcol = v.share(fr, remaining); + } + } + } + + /// Redistribute space to auto columns so that each gets a fair share. + fn shrink_auto_columns(&mut self, available: Abs, count: usize) { + let mut last; + let mut fair = -Abs::inf(); + let mut redistribute = available; + let mut overlarge = count; + let mut changed = true; + + // Iteratively remove columns that don't need to be shrunk. + while changed && overlarge > 0 { + changed = false; + last = fair; + fair = redistribute / (overlarge as f64); + + 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 { + redistribute -= rcol; + overlarge -= 1; + changed = true; + } + } + } + + // Redistribute space fairly among overlarge columns. + for (&col, rcol) in self.grid.cols.iter().zip(&mut self.rcols) { + if col == Sizing::Auto && *rcol > fair { + *rcol = fair; + } + } + } + + /// Layout a row with automatic height. Such a row may break across multiple + /// regions. + fn layout_auto_row(&mut self, engine: &mut Engine, y: usize) -> SourceResult<()> { + // Determine the size for each region of the row. If the first region + // ends up empty for some column, skip the region and remeasure. + let mut resolved = match self.measure_auto_row(engine, y, true)? { + Some(resolved) => resolved, + None => { + self.finish_region(engine)?; + self.measure_auto_row(engine, y, false)?.unwrap() + } + }; + + // Nothing to layout. + if resolved.is_empty() { + return Ok(()); + } + + // Layout into a single region. + if let &[first] = resolved.as_slice() { + let frame = self.layout_single_row(engine, first, y)?; + self.push_row(frame, y); + return Ok(()); + } + + // Expand all but the last region. + // Skip the first region if the space is eaten up by an fr row. + let len = resolved.len(); + for (region, target) in self + .regions + .iter() + .zip(&mut resolved[..len - 1]) + .skip(self.lrows.iter().any(|row| matches!(row, Row::Fr(..))) as usize) + { + target.set_max(region.y); + } + + // Layout into multiple regions. + let fragment = self.layout_multi_row(engine, &resolved, y)?; + let len = fragment.len(); + for (i, frame) in fragment.into_iter().enumerate() { + self.push_row(frame, y); + if i + 1 < len { + self.finish_region(engine)?; + } + } + + Ok(()) + } + + /// Measure the regions sizes of an auto row. The option is always `Some(_)` + /// if `can_skip` is false. + fn measure_auto_row( + &mut self, + engine: &mut Engine, + y: usize, + can_skip: bool, + ) -> SourceResult>> { + let mut resolved: Vec = vec![]; + + for (x, &rcol) in self.rcols.iter().enumerate() { + if let Some(cell) = self.grid.cell(x, y) { + let mut pod = self.regions; + pod.size.x = rcol; + + let frames = cell.measure(engine, self.styles, pod)?.into_frames(); + + // Skip the first region if one cell in it is empty. Then, + // remeasure. + if let [first, rest @ ..] = frames.as_slice() { + if can_skip + && first.is_empty() + && rest.iter().any(|frame| !frame.is_empty()) + { + return Ok(None); + } + } + + let mut sizes = frames.iter().map(|frame| frame.height()); + for (target, size) in resolved.iter_mut().zip(&mut sizes) { + target.set_max(size); + } + + // New heights are maximal by virtue of being new. Note that + // this extend only uses the rest of the sizes iterator. + resolved.extend(sizes); + } + } + + Ok(Some(resolved)) + } + + /// Layout a row with relative height. Such a row cannot break across + /// multiple regions, but it may force a region break. + fn layout_relative_row( + &mut self, + engine: &mut Engine, + v: Rel, + y: usize, + ) -> SourceResult<()> { + let resolved = v.resolve(self.styles).relative_to(self.regions.base().y); + let frame = self.layout_single_row(engine, resolved, y)?; + + // Skip to fitting region. + let height = frame.height(); + while !self.regions.size.y.fits(height) && !self.regions.in_last() { + self.finish_region(engine)?; + + // Don't skip multiple regions for gutter and don't push a row. + if self.grid.has_gutter && y % 2 == 1 { + return Ok(()); + } + } + + self.push_row(frame, y); + + Ok(()) + } + + /// Layout a row with fixed height and return its frame. + fn layout_single_row( + &mut self, + engine: &mut Engine, + height: Abs, + y: usize, + ) -> SourceResult { + if !height.is_finite() { + bail!(self.span, "cannot create grid with infinite height"); + } + + let mut output = Frame::soft(Size::new(self.width, height)); + let mut pos = Point::zero(); + + for (x, &rcol) in self.rcols.iter().enumerate() { + 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.grid.rows[y] == Sizing::Auto { + pod.full = self.regions.full; + } + let frame = cell.layout(engine, self.styles, pod)?.into_frame(); + output.push_frame(pos, frame); + } + + pos.x += rcol; + } + + Ok(output) + } + + /// Layout a row spanning multiple regions. + fn layout_multi_row( + &mut self, + engine: &mut Engine, + heights: &[Abs], + y: usize, + ) -> SourceResult { + // Prepare frames. + let mut outputs: Vec<_> = heights + .iter() + .map(|&h| Frame::soft(Size::new(self.width, h))) + .collect(); + + // Prepare regions. + let size = Size::new(self.width, heights[0]); + let mut pod = Regions::one(size, Axes::splat(true)); + pod.full = self.regions.full; + pod.backlog = &heights[1..]; + + // Layout the row. + let mut pos = Point::zero(); + for (x, &rcol) in self.rcols.iter().enumerate() { + if let Some(cell) = self.grid.cell(x, y) { + pod.size.x = rcol; + + // Push the layouted frames into the individual output frames. + let fragment = cell.layout(engine, self.styles, pod)?; + for (output, frame) in outputs.iter_mut().zip(fragment) { + output.push_frame(pos, frame); + } + } + + pos.x += rcol; + } + + Ok(Fragment::frames(outputs)) + } + + /// Push a row frame into the current region. + fn push_row(&mut self, frame: Frame, y: usize) { + self.regions.size.y -= frame.height(); + self.lrows.push(Row::Frame(frame, y)); + } + + /// Finish rows for one region. + fn finish_region(&mut self, engine: &mut Engine) -> SourceResult<()> { + // Determine the height of existing rows in the region. + let mut used = Abs::zero(); + let mut fr = Fr::zero(); + for row in &self.lrows { + match row { + Row::Frame(frame, _) => used += frame.height(), + Row::Fr(v, _) => fr += *v, + } + } + + // Determine the size of the grid in this region, expanding fully if + // there are fr rows. + let mut size = Size::new(self.width, used).min(self.initial); + if fr.get() > 0.0 && self.initial.y.is_finite() { + size.y = self.initial.y; + } + + // The frame for the region. + let mut output = Frame::soft(size); + let mut pos = Point::zero(); + let mut rrows = vec![]; + + // Place finished rows and layout fractional rows. + for row in std::mem::take(&mut self.lrows) { + let (frame, y) = match row { + Row::Frame(frame, y) => (frame, y), + Row::Fr(v, y) => { + let remaining = self.regions.full - used; + let height = v.share(fr, remaining); + (self.layout_single_row(engine, height, y)?, y) + } + }; + + let height = frame.height(); + output.push_frame(pos, frame); + rrows.push(RowPiece { height, y }); + pos.y += height; + } + + self.finished.push(output); + self.rrows.push(rrows); + self.regions.next(); + self.initial = self.regions.size; + + Ok(()) + } +} + +/// Turn an iterator of extents into an iterator of offsets before, in between, +/// and after the extents, e.g. [10mm, 5mm] -> [0mm, 10mm, 15mm]. +fn points(extents: impl IntoIterator) -> impl Iterator { + let mut offset = Abs::zero(); + std::iter::once(Abs::zero()).chain(extents).map(move |extent| { + offset += extent; + offset + }) +} diff --git a/crates/typst/src/layout/grid/mod.rs b/crates/typst/src/layout/grid/mod.rs new file mode 100644 index 00000000..4d66fd4a --- /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 fa35936b..62e6d986 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 bb44f438..7f3ffae1 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 48913fc7..520657bb 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 33920445..169289aa 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 00000000..fb683123 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 ae5c0519..c626d2e7 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 00000000..fa3d04cc 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 fe4554d9..b6b31eb1 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 00000000..ced16a97 --- /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 577e15c4..e076d0c4 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 00000000..a4d3bba4 --- /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 529f2720..1b250aa4 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() -- cgit v1.2.3