mirror of
https://github.com/typst/typst
synced 2025-05-15 17:45:27 +08:00
337 lines
11 KiB
Rust
337 lines
11 KiB
Rust
use typst::eval::{CastInfo, Reflect};
|
|
|
|
use crate::layout::{AlignElem, GridLayouter, TrackSizings};
|
|
use crate::meta::{Figurable, LocalName};
|
|
use crate::prelude::*;
|
|
|
|
/// A table of items.
|
|
///
|
|
/// 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.
|
|
///
|
|
/// To give a table a caption and make it [referenceable]($ref), put it into a
|
|
/// [figure]($figure).
|
|
///
|
|
/// # Example
|
|
/// ```example
|
|
/// #table(
|
|
/// columns: (1fr, auto, auto),
|
|
/// inset: 10pt,
|
|
/// align: horizon,
|
|
/// [], [*Area*], [*Parameters*],
|
|
/// image("cylinder.svg"),
|
|
/// $ pi h (D^2 - d^2) / 4 $,
|
|
/// [
|
|
/// $h$: height \
|
|
/// $D$: outer radius \
|
|
/// $d$: inner radius
|
|
/// ],
|
|
/// image("tetrahedron.svg"),
|
|
/// $ sqrt(2) / 12 a^3 $,
|
|
/// [$a$: edge length]
|
|
/// )
|
|
/// ```
|
|
#[elem(Layout, LocalName, Figurable)]
|
|
pub struct TableElem {
|
|
/// The column sizes. See the [grid documentation]($grid) for more
|
|
/// information on track sizing.
|
|
pub columns: TrackSizings,
|
|
|
|
/// The row sizes. See the [grid documentation]($grid) for more information
|
|
/// on track sizing.
|
|
pub rows: TrackSizings,
|
|
|
|
/// The gaps between rows & columns. See the [grid documentation]($grid) for
|
|
/// more information on gutters.
|
|
#[external]
|
|
pub gutter: TrackSizings,
|
|
|
|
/// The gaps between columns. Takes precedence over `gutter`. See the
|
|
/// [grid documentation]($grid) for more information on gutters.
|
|
#[parse(
|
|
let gutter = args.named("gutter")?;
|
|
args.named("column-gutter")?.or_else(|| gutter.clone())
|
|
)]
|
|
pub column_gutter: TrackSizings,
|
|
|
|
/// The gaps between rows. Takes precedence over `gutter`. See the
|
|
/// [grid documentation]($grid) for more information on gutters.
|
|
#[parse(args.named("row-gutter")?.or_else(|| gutter.clone()))]
|
|
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 tables.
|
|
///
|
|
/// ```example
|
|
/// #table(
|
|
/// fill: (col, _) => if calc.odd(col) { luma(240) } else { white },
|
|
/// align: (col, row) =>
|
|
/// if row == 0 { center }
|
|
/// else if col == 0 { left }
|
|
/// else { right },
|
|
/// columns: 4,
|
|
/// [], [*Q1*], [*Q2*], [*Q3*],
|
|
/// [Revenue:], [1000 €], [2000 €], [3000 €],
|
|
/// [Expenses:], [500 €], [1000 €], [1500 €],
|
|
/// [Profit:], [500 €], [1000 €], [1500 €],
|
|
/// )
|
|
/// ```
|
|
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
|
|
/// #table(
|
|
/// columns: 3,
|
|
/// align: (x, y) => (left, center, right).at(x),
|
|
/// [Hello], [Hello], [Hello],
|
|
/// [A], [B], [C],
|
|
/// )
|
|
/// ```
|
|
pub align: Celled<Smart<Align>>,
|
|
|
|
/// How to [stroke]($stroke) the cells.
|
|
///
|
|
/// Strokes can be disabled by setting this to `{none}`.
|
|
///
|
|
/// _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]
|
|
#[default(Some(Stroke::default()))]
|
|
pub stroke: Option<Stroke>,
|
|
|
|
/// How much to pad the cells' content.
|
|
#[default(Abs::pt(5.0).into())]
|
|
pub inset: Rel<Length>,
|
|
|
|
/// The contents of the table cells.
|
|
#[variadic]
|
|
pub children: Vec<Content>,
|
|
}
|
|
|
|
impl Layout for TableElem {
|
|
#[tracing::instrument(name = "TableElem::layout", skip_all)]
|
|
fn layout(
|
|
&self,
|
|
vt: &mut Vt,
|
|
styles: StyleChain,
|
|
regions: Regions,
|
|
) -> SourceResult<Fragment> {
|
|
let inset = self.inset(styles);
|
|
let align = self.align(styles);
|
|
|
|
let tracks = Axes::new(self.columns(styles).0, self.rows(styles).0);
|
|
let gutter = Axes::new(self.column_gutter(styles).0, self.row_gutter(styles).0);
|
|
let cols = tracks.x.len().max(1);
|
|
let cells: Vec<_> = self
|
|
.children()
|
|
.into_iter()
|
|
.enumerate()
|
|
.map(|(i, child)| {
|
|
let mut child = child.padded(Sides::splat(inset));
|
|
|
|
let x = i % cols;
|
|
let y = i / cols;
|
|
if let Smart::Custom(alignment) = align.resolve(vt, x, y)? {
|
|
child = child.styled(AlignElem::set_alignment(alignment));
|
|
}
|
|
|
|
Ok(child)
|
|
})
|
|
.collect::<SourceResult<_>>()?;
|
|
|
|
let fill = self.fill(styles);
|
|
let stroke = self.stroke(styles).map(Stroke::unwrap_or_default);
|
|
|
|
// Prepare grid layout by unifying content and gutter tracks.
|
|
let layouter = GridLayouter::new(
|
|
tracks.as_deref(),
|
|
gutter.as_deref(),
|
|
&cells,
|
|
regions,
|
|
styles,
|
|
);
|
|
|
|
// Measure the columns and layout the grid row-by-row.
|
|
let mut layout = layouter.layout(vt)?;
|
|
|
|
// Add lines and backgrounds.
|
|
for (frame, rows) in layout.fragment.iter_mut().zip(&layout.rows) {
|
|
if layout.cols.is_empty() || rows.is_empty() {
|
|
continue;
|
|
}
|
|
|
|
// Render table lines.
|
|
if let Some(stroke) = &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(layout.cols.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 layout.cols.iter().enumerate() {
|
|
let mut dy = Abs::zero();
|
|
for row in rows {
|
|
if let Some(fill) = fill.resolve(vt, 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(layout.fragment)
|
|
}
|
|
}
|
|
|
|
/// 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<Item = Abs>) -> impl Iterator<Item = Abs> {
|
|
let mut offset = Abs::zero();
|
|
std::iter::once(Abs::zero()).chain(extents).map(move |extent| {
|
|
offset += extent;
|
|
offset
|
|
})
|
|
}
|
|
|
|
/// A value that can be configured per cell.
|
|
#[derive(Debug, Clone, PartialEq, Hash)]
|
|
pub enum Celled<T> {
|
|
/// 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<T>),
|
|
}
|
|
|
|
impl<T: Default + Clone + FromValue> Celled<T> {
|
|
/// Resolve the value based on the cell position.
|
|
pub fn resolve(&self, vt: &mut Vt, x: usize, y: usize) -> SourceResult<T> {
|
|
Ok(match self {
|
|
Self::Value(value) => value.clone(),
|
|
Self::Func(func) => func.call_vt(vt, [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<T: Default> Default for Celled<T> {
|
|
fn default() -> Self {
|
|
Self::Value(T::default())
|
|
}
|
|
}
|
|
|
|
impl<T: Reflect> Reflect for Celled<T> {
|
|
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<T: IntoValue> IntoValue for Celled<T> {
|
|
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<T: FromValue> FromValue for Celled<T> {
|
|
fn from_value(value: Value) -> StrResult<Self> {
|
|
match value {
|
|
Value::Func(v) => Ok(Self::Func(v)),
|
|
Value::Array(array) => Ok(Self::Array(
|
|
array.into_iter().map(T::from_value).collect::<StrResult<_>>()?,
|
|
)),
|
|
v if T::castable(&v) => Ok(Self::Value(T::from_value(v)?)),
|
|
v => Err(Self::error(&v)),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl LocalName for TableElem {
|
|
fn local_name(&self, lang: Lang, _: Option<Region>) -> &'static str {
|
|
match lang {
|
|
Lang::ALBANIAN => "Tabel",
|
|
Lang::ARABIC => "جدول",
|
|
Lang::BOKMÅL => "Tabell",
|
|
Lang::CHINESE => "表",
|
|
Lang::CZECH => "Tabulka",
|
|
Lang::DANISH => "Tabel",
|
|
Lang::DUTCH => "Tabel",
|
|
Lang::FILIPINO => "Talaan",
|
|
Lang::FINNISH => "Taulukko",
|
|
Lang::FRENCH => "Tableau",
|
|
Lang::GERMAN => "Tabelle",
|
|
Lang::HUNGARIAN => "Táblázat",
|
|
Lang::ITALIAN => "Tabella",
|
|
Lang::NYNORSK => "Tabell",
|
|
Lang::POLISH => "Tabela",
|
|
Lang::PORTUGUESE => "Tabela",
|
|
Lang::ROMANIAN => "Tabelul",
|
|
Lang::RUSSIAN => "Таблица",
|
|
Lang::SLOVENIAN => "Tabela",
|
|
Lang::SPANISH => "Tabla",
|
|
Lang::SWEDISH => "Tabell",
|
|
Lang::TURKISH => "Tablo",
|
|
Lang::UKRAINIAN => "Таблиця",
|
|
Lang::VIETNAMESE => "Bảng",
|
|
Lang::JAPANESE => "表",
|
|
Lang::ENGLISH | _ => "Table",
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Figurable for TableElem {}
|