Initial table per-cell customization [More Flexible Tables Pt.2a] (#3037)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
This commit is contained in:
parent
a124694f08
commit
8fa573a9a2
@ -128,8 +128,20 @@ impl<T> Smart<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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<F>(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<F, U>(self, f: F) -> Smart<U>
|
||||
where
|
||||
F: FnOnce(T) -> Smart<U>,
|
||||
|
@ -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<Option<Paint>>,
|
||||
|
||||
/// 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<Smart<Align>>,
|
||||
|
||||
/// 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<Stroke>,
|
||||
|
||||
/// 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<Option<Rel<Length>>>,
|
||||
|
||||
/// The contents of the grid cells.
|
||||
///
|
||||
/// The cells are populated in row-major order.
|
||||
#[variadic]
|
||||
pub children: Vec<Content>,
|
||||
}
|
||||
|
||||
impl Layout for GridElem {
|
||||
#[typst_macros::time(name = "grid", span = self.span())]
|
||||
fn layout(
|
||||
&self,
|
||||
engine: &mut Engine,
|
||||
styles: StyleChain,
|
||||
regions: Regions,
|
||||
) -> SourceResult<Fragment> {
|
||||
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<Smart<Align>>,
|
||||
inset: Sides<Rel<Length>>,
|
||||
) -> SourceResult<Vec<Content>> {
|
||||
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::<StrResult<_>>()?),
|
||||
}
|
||||
use crate::visualize::{FixedStroke, Geometry, Paint};
|
||||
|
||||
/// A value that can be configured per cell.
|
||||
#[derive(Debug, Clone, PartialEq, Hash)]
|
||||
@ -347,87 +82,70 @@ impl<T: FromValue> FromValue for Celled<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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<Paint>,
|
||||
}
|
||||
|
||||
impl From<Content> 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<Fragment> {
|
||||
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<Paint>,
|
||||
align: Smart<Align>,
|
||||
inset: Sides<Rel<Length>>,
|
||||
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<Cell>,
|
||||
/// The column tracks including gutter tracks.
|
||||
cols: Vec<Sizing>,
|
||||
/// The row tracks including gutter tracks.
|
||||
rows: Vec<Sizing>,
|
||||
// How to fill the cells.
|
||||
#[allow(dead_code)]
|
||||
fill: &'a Celled<Option<Paint>>,
|
||||
// How to stroke the cells.
|
||||
#[allow(dead_code)]
|
||||
stroke: &'a Option<FixedStroke>,
|
||||
/// The regions to layout children into.
|
||||
regions: Regions<'a>,
|
||||
/// The inherited styles.
|
||||
styles: StyleChain<'a>,
|
||||
/// Resolved column sizes.
|
||||
rcols: Vec<Abs>,
|
||||
/// The sum of `rcols`.
|
||||
width: Abs,
|
||||
/// Resolve row sizes, by region.
|
||||
rrows: Vec<Vec<RowPiece>>,
|
||||
/// Rows in the current region.
|
||||
lrows: Vec<Row>,
|
||||
/// The initial size of the current region before we started subtracting.
|
||||
initial: Size,
|
||||
/// Frames for finished regions.
|
||||
finished: Vec<Frame>,
|
||||
/// 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<Abs>,
|
||||
/// The heights of the resulting rows segments, by region.
|
||||
pub rows: Vec<Vec<RowPiece>>,
|
||||
}
|
||||
|
||||
/// 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<Option<Paint>>,
|
||||
stroke: &'a Option<FixedStroke>,
|
||||
regions: Regions<'a>,
|
||||
styles: StyleChain<'a>,
|
||||
span: Span,
|
||||
cells: Vec<Cell>,
|
||||
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<T: ResolvableCell + Clone + Default>(
|
||||
tracks: Axes<&[Sizing]>,
|
||||
gutter: Axes<&[Sizing]>,
|
||||
cells: &[T],
|
||||
fill: &Celled<Option<Paint>>,
|
||||
align: &Celled<Smart<Align>>,
|
||||
inset: Sides<Rel<Length>>,
|
||||
engine: &mut Engine,
|
||||
styles: StyleChain,
|
||||
) -> SourceResult<Self> {
|
||||
// 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::<SourceResult<Vec<_>>>()?;
|
||||
|
||||
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<FixedStroke>,
|
||||
/// The regions to layout children into.
|
||||
regions: Regions<'a>,
|
||||
/// The inherited styles.
|
||||
styles: StyleChain<'a>,
|
||||
/// Resolved column sizes.
|
||||
rcols: Vec<Abs>,
|
||||
/// The sum of `rcols`.
|
||||
width: Abs,
|
||||
/// Resolve row sizes, by region.
|
||||
rrows: Vec<Vec<RowPiece>>,
|
||||
/// Rows in the current region.
|
||||
lrows: Vec<Row>,
|
||||
/// The initial size of the current region before we started subtracting.
|
||||
initial: Size,
|
||||
/// Frames for finished regions.
|
||||
finished: Vec<Frame>,
|
||||
/// 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<FixedStroke>,
|
||||
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<GridLayout> {
|
||||
pub fn layout(mut self, engine: &mut Engine) -> SourceResult<Fragment> {
|
||||
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<Abs> = 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,
|
383
crates/typst/src/layout/grid/mod.rs
Normal file
383
crates/typst/src/layout/grid/mod.rs
Normal file
@ -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<Option<Paint>>,
|
||||
|
||||
/// 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<Smart<Align>>,
|
||||
|
||||
/// 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<Stroke>,
|
||||
|
||||
/// 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<Option<Rel<Length>>>,
|
||||
|
||||
/// The contents of the grid cells.
|
||||
///
|
||||
/// The cells are populated in row-major order.
|
||||
#[variadic]
|
||||
pub children: Vec<GridCell>,
|
||||
}
|
||||
|
||||
#[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<Fragment> {
|
||||
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::<StrResult<_>>()?),
|
||||
}
|
||||
|
||||
/// 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<Option<Paint>>,
|
||||
|
||||
/// The cell's alignment override.
|
||||
align: Smart<Align>,
|
||||
|
||||
/// The cell's inset override.
|
||||
inset: Smart<Sides<Option<Rel<Length>>>>,
|
||||
}
|
||||
|
||||
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<Paint>,
|
||||
align: Smart<Align>,
|
||||
inset: Sides<Rel<Length>>,
|
||||
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<Content> {
|
||||
show_grid_cell(self.body().clone(), self.inset(styles), self.align(styles))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Content> for GridCell {
|
||||
fn from(value: Content) -> Self {
|
||||
value
|
||||
.to::<Self>()
|
||||
.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<Sides<Option<Rel<Length>>>>,
|
||||
align: Smart<Align>,
|
||||
) -> SourceResult<Content> {
|
||||
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)
|
||||
}
|
@ -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()]))
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<Content>,
|
||||
pub children: Vec<TableCell>,
|
||||
}
|
||||
|
||||
#[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<Option<Paint>>,
|
||||
|
||||
/// The cell's alignment override.
|
||||
align: Smart<Align>,
|
||||
|
||||
/// The cell's inset override.
|
||||
inset: Smart<Sides<Option<Rel<Length>>>>,
|
||||
}
|
||||
|
||||
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<Paint>,
|
||||
align: Smart<Align>,
|
||||
inset: Sides<Rel<Length>>,
|
||||
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<Content> {
|
||||
show_grid_cell(self.body().clone(), self.inset(styles), self.align(styles))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Content> for TableCell {
|
||||
fn from(value: Content) -> Self {
|
||||
value
|
||||
.to::<Self>()
|
||||
.cloned()
|
||||
.unwrap_or_else(|| Self::new(value.clone()))
|
||||
}
|
||||
}
|
||||
|
BIN
tests/ref/layout/grid-cell.png
Normal file
BIN
tests/ref/layout/grid-cell.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 38 KiB |
Binary file not shown.
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 27 KiB |
BIN
tests/ref/layout/table-cell.png
Normal file
BIN
tests/ref/layout/table-cell.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 41 KiB |
Binary file not shown.
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 22 KiB |
107
tests/typ/layout/grid-cell.typ
Normal file
107
tests/typ/layout/grid-cell.typ
Normal file
@ -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]
|
||||
)
|
||||
}
|
@ -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]
|
||||
)
|
||||
|
102
tests/typ/layout/table-cell.typ
Normal file
102
tests/typ/layout/table-cell.typ
Normal file
@ -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]
|
||||
)
|
||||
}
|
@ -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()
|
||||
|
Loading…
Reference in New Issue
Block a user