mirror of
https://github.com/typst/typst
synced 2025-05-20 03:55:29 +08:00
Grid and Table API Unification [More Flexible Tables Pt.1] (#3009)
This commit is contained in:
parent
c361566cb2
commit
111a69f6aa
@ -2,18 +2,20 @@ use std::num::NonZeroUsize;
|
|||||||
|
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
|
|
||||||
use crate::diag::{bail, SourceResult, StrResult};
|
use crate::diag::{bail, At, SourceResult, StrResult};
|
||||||
use crate::engine::Engine;
|
use crate::engine::Engine;
|
||||||
use crate::foundations::{
|
use crate::foundations::{
|
||||||
cast, elem, Array, Content, NativeElement, Resolve, StyleChain, Value,
|
cast, elem, Array, CastInfo, Content, FromValue, Func, IntoValue, NativeElement,
|
||||||
|
Reflect, Resolve, Smart, StyleChain, Value,
|
||||||
};
|
};
|
||||||
use crate::layout::{
|
use crate::layout::{
|
||||||
Abs, Axes, Dir, Fr, Fragment, Frame, Layout, Length, Point, Regions, Rel, Size,
|
Abs, Align, AlignElem, Axes, Dir, Fr, Fragment, Frame, FrameItem, Layout, Length,
|
||||||
Sizing,
|
Point, Regions, Rel, Sides, Size, Sizing,
|
||||||
};
|
};
|
||||||
use crate::syntax::Span;
|
use crate::syntax::Span;
|
||||||
use crate::text::TextElem;
|
use crate::text::TextElem;
|
||||||
use crate::util::Numeric;
|
use crate::util::Numeric;
|
||||||
|
use crate::visualize::{FixedStroke, Geometry, Paint, Stroke};
|
||||||
|
|
||||||
/// Arranges content in a grid.
|
/// Arranges content in a grid.
|
||||||
///
|
///
|
||||||
@ -118,6 +120,81 @@ pub struct GridElem {
|
|||||||
#[borrowed]
|
#[borrowed]
|
||||||
pub row_gutter: TrackSizings,
|
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 contents of the grid cells.
|
||||||
///
|
///
|
||||||
/// The cells are populated in row-major order.
|
/// The cells are populated in row-major order.
|
||||||
@ -133,16 +210,27 @@ impl Layout for GridElem {
|
|||||||
styles: StyleChain,
|
styles: StyleChain,
|
||||||
regions: Regions,
|
regions: Regions,
|
||||||
) -> SourceResult<Fragment> {
|
) -> SourceResult<Fragment> {
|
||||||
|
let inset = self.inset(styles);
|
||||||
|
let align = self.align(styles);
|
||||||
let columns = self.columns(styles);
|
let columns = self.columns(styles);
|
||||||
let rows = self.rows(styles);
|
let rows = self.rows(styles);
|
||||||
let column_gutter = self.column_gutter(styles);
|
let column_gutter = self.column_gutter(styles);
|
||||||
let row_gutter = self.row_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.
|
// Prepare grid layout by unifying content and gutter tracks.
|
||||||
let layouter = GridLayouter::new(
|
let layouter = GridLayouter::new(
|
||||||
Axes::new(&columns.0, &rows.0),
|
tracks,
|
||||||
Axes::new(&column_gutter.0, &row_gutter.0),
|
gutter,
|
||||||
&self.children,
|
&cells,
|
||||||
|
fill,
|
||||||
|
&stroke,
|
||||||
regions,
|
regions,
|
||||||
styles,
|
styles,
|
||||||
self.span(),
|
self.span(),
|
||||||
@ -153,6 +241,31 @@ impl Layout for GridElem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.
|
/// Track sizing definitions.
|
||||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||||
pub struct TrackSizings(pub SmallVec<[Sizing; 4]>);
|
pub struct TrackSizings(pub SmallVec<[Sizing; 4]>);
|
||||||
@ -165,6 +278,75 @@ cast! {
|
|||||||
values: Array => Self(values.into_iter().map(Value::cast).collect::<StrResult<_>>()?),
|
values: Array => Self(values.into_iter().map(Value::cast).collect::<StrResult<_>>()?),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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, engine: &mut Engine, x: usize, y: usize) -> SourceResult<T> {
|
||||||
|
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<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)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Performs grid layout.
|
/// Performs grid layout.
|
||||||
pub struct GridLayouter<'a> {
|
pub struct GridLayouter<'a> {
|
||||||
/// The grid cells.
|
/// The grid cells.
|
||||||
@ -177,6 +359,12 @@ pub struct GridLayouter<'a> {
|
|||||||
cols: Vec<Sizing>,
|
cols: Vec<Sizing>,
|
||||||
/// The row tracks including gutter tracks.
|
/// The row tracks including gutter tracks.
|
||||||
rows: Vec<Sizing>,
|
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.
|
/// The regions to layout children into.
|
||||||
regions: Regions<'a>,
|
regions: Regions<'a>,
|
||||||
/// The inherited styles.
|
/// The inherited styles.
|
||||||
@ -230,10 +418,13 @@ impl<'a> GridLayouter<'a> {
|
|||||||
/// Create a new grid layouter.
|
/// Create a new grid layouter.
|
||||||
///
|
///
|
||||||
/// This prepares grid layout by unifying content and gutter tracks.
|
/// This prepares grid layout by unifying content and gutter tracks.
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn new(
|
pub fn new(
|
||||||
tracks: Axes<&[Sizing]>,
|
tracks: Axes<&[Sizing]>,
|
||||||
gutter: Axes<&[Sizing]>,
|
gutter: Axes<&[Sizing]>,
|
||||||
cells: &'a [Content],
|
cells: &'a [Content],
|
||||||
|
fill: &'a Celled<Option<Paint>>,
|
||||||
|
stroke: &'a Option<FixedStroke>,
|
||||||
regions: Regions<'a>,
|
regions: Regions<'a>,
|
||||||
styles: StyleChain<'a>,
|
styles: StyleChain<'a>,
|
||||||
span: Span,
|
span: Span,
|
||||||
@ -298,6 +489,8 @@ impl<'a> GridLayouter<'a> {
|
|||||||
is_rtl,
|
is_rtl,
|
||||||
has_gutter,
|
has_gutter,
|
||||||
rows,
|
rows,
|
||||||
|
fill,
|
||||||
|
stroke,
|
||||||
regions,
|
regions,
|
||||||
styles,
|
styles,
|
||||||
rcols: vec![Abs::zero(); cols.len()],
|
rcols: vec![Abs::zero(); cols.len()],
|
||||||
@ -331,6 +524,10 @@ impl<'a> GridLayouter<'a> {
|
|||||||
|
|
||||||
self.finish_region(engine)?;
|
self.finish_region(engine)?;
|
||||||
|
|
||||||
|
if self.stroke.is_some() || !matches!(self.fill, Celled::Value(None)) {
|
||||||
|
self.render_fills_strokes(engine)?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(GridLayout {
|
Ok(GridLayout {
|
||||||
fragment: Fragment::frames(self.finished),
|
fragment: Fragment::frames(self.finished),
|
||||||
cols: self.rcols,
|
cols: self.rcols,
|
||||||
@ -338,6 +535,59 @@ impl<'a> GridLayouter<'a> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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.
|
/// Determine all column sizes.
|
||||||
#[tracing::instrument(name = "GridLayouter::measure_columns", skip_all)]
|
#[tracing::instrument(name = "GridLayouter::measure_columns", skip_all)]
|
||||||
fn measure_columns(&mut self, engine: &mut Engine) -> SourceResult<()> {
|
fn measure_columns(&mut self, engine: &mut Engine) -> SourceResult<()> {
|
||||||
@ -743,3 +993,13 @@ impl<'a> GridLayouter<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -6,8 +6,8 @@ use crate::foundations::{
|
|||||||
cast, elem, scope, Array, Content, Fold, NativeElement, Smart, StyleChain,
|
cast, elem, scope, Array, Content, Fold, NativeElement, Smart, StyleChain,
|
||||||
};
|
};
|
||||||
use crate::layout::{
|
use crate::layout::{
|
||||||
Align, Axes, BlockElem, Em, Fragment, GridLayouter, HAlign, Layout, Length, Regions,
|
Align, Axes, BlockElem, Celled, Em, Fragment, GridLayouter, HAlign, Layout, Length,
|
||||||
Sizing, Spacing, VAlign,
|
Regions, Sizing, Spacing, VAlign,
|
||||||
};
|
};
|
||||||
use crate::model::{Numbering, NumberingPattern, ParElem};
|
use crate::model::{Numbering, NumberingPattern, ParElem};
|
||||||
use crate::text::TextElem;
|
use crate::text::TextElem;
|
||||||
@ -266,6 +266,8 @@ impl Layout for EnumElem {
|
|||||||
number = number.saturating_add(1);
|
number = number.saturating_add(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let fill = Celled::Value(None);
|
||||||
|
let stroke = None;
|
||||||
let layouter = GridLayouter::new(
|
let layouter = GridLayouter::new(
|
||||||
Axes::with_x(&[
|
Axes::with_x(&[
|
||||||
Sizing::Rel(indent.into()),
|
Sizing::Rel(indent.into()),
|
||||||
@ -275,6 +277,8 @@ impl Layout for EnumElem {
|
|||||||
]),
|
]),
|
||||||
Axes::with_y(&[gutter.into()]),
|
Axes::with_y(&[gutter.into()]),
|
||||||
&cells,
|
&cells,
|
||||||
|
&fill,
|
||||||
|
&stroke,
|
||||||
regions,
|
regions,
|
||||||
styles,
|
styles,
|
||||||
self.span(),
|
self.span(),
|
||||||
|
@ -5,8 +5,8 @@ use crate::foundations::{
|
|||||||
Value,
|
Value,
|
||||||
};
|
};
|
||||||
use crate::layout::{
|
use crate::layout::{
|
||||||
Axes, BlockElem, Em, Fragment, GridLayouter, HAlign, Layout, Length, Regions, Sizing,
|
Axes, BlockElem, Celled, Em, Fragment, GridLayouter, HAlign, Layout, Length, Regions,
|
||||||
Spacing, VAlign,
|
Sizing, Spacing, VAlign,
|
||||||
};
|
};
|
||||||
use crate::model::ParElem;
|
use crate::model::ParElem;
|
||||||
use crate::text::TextElem;
|
use crate::text::TextElem;
|
||||||
@ -166,6 +166,8 @@ impl Layout for ListElem {
|
|||||||
cells.push(item.body().clone().styled(Self::set_depth(Depth)));
|
cells.push(item.body().clone().styled(Self::set_depth(Depth)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let fill = Celled::Value(None);
|
||||||
|
let stroke = None;
|
||||||
let layouter = GridLayouter::new(
|
let layouter = GridLayouter::new(
|
||||||
Axes::with_x(&[
|
Axes::with_x(&[
|
||||||
Sizing::Rel(indent.into()),
|
Sizing::Rel(indent.into()),
|
||||||
@ -175,6 +177,8 @@ impl Layout for ListElem {
|
|||||||
]),
|
]),
|
||||||
Axes::with_y(&[gutter.into()]),
|
Axes::with_y(&[gutter.into()]),
|
||||||
&cells,
|
&cells,
|
||||||
|
&fill,
|
||||||
|
&stroke,
|
||||||
regions,
|
regions,
|
||||||
styles,
|
styles,
|
||||||
self.span(),
|
self.span(),
|
||||||
|
@ -1,16 +1,13 @@
|
|||||||
use crate::diag::{At, SourceResult, StrResult};
|
use crate::diag::SourceResult;
|
||||||
use crate::engine::Engine;
|
use crate::engine::Engine;
|
||||||
use crate::foundations::{
|
use crate::foundations::{elem, Content, NativeElement, Smart, StyleChain};
|
||||||
elem, Array, CastInfo, Content, FromValue, Func, IntoValue, NativeElement, Reflect,
|
|
||||||
Smart, StyleChain, Value,
|
|
||||||
};
|
|
||||||
use crate::layout::{
|
use crate::layout::{
|
||||||
Abs, Align, AlignElem, Axes, Fragment, FrameItem, GridLayouter, Layout, Length,
|
apply_align_inset_to_cells, Abs, Align, Axes, Celled, Fragment, GridLayouter, Layout,
|
||||||
Point, Regions, Rel, Sides, Size, TrackSizings,
|
Length, Regions, Rel, Sides, TrackSizings,
|
||||||
};
|
};
|
||||||
use crate::model::Figurable;
|
use crate::model::Figurable;
|
||||||
use crate::text::{Lang, LocalName, Region};
|
use crate::text::{Lang, LocalName, Region};
|
||||||
use crate::visualize::{Geometry, Paint, Stroke};
|
use crate::visualize::{Paint, Stroke};
|
||||||
|
|
||||||
/// A table of items.
|
/// A table of items.
|
||||||
///
|
///
|
||||||
@ -169,166 +166,27 @@ impl Layout for TableElem {
|
|||||||
let rows = self.rows(styles);
|
let rows = self.rows(styles);
|
||||||
let column_gutter = self.column_gutter(styles);
|
let column_gutter = self.column_gutter(styles);
|
||||||
let row_gutter = self.row_gutter(styles);
|
let row_gutter = self.row_gutter(styles);
|
||||||
|
|
||||||
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 cols = tracks.x.len().max(1);
|
|
||||||
let cells: Vec<_> = self
|
|
||||||
.children()
|
|
||||||
.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::<SourceResult<_>>()?;
|
|
||||||
|
|
||||||
let fill = self.fill(styles);
|
let fill = self.fill(styles);
|
||||||
let stroke = self.stroke(styles).map(Stroke::unwrap_or_default);
|
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.
|
// Prepare grid layout by unifying content and gutter tracks.
|
||||||
let layouter =
|
let layouter = GridLayouter::new(
|
||||||
GridLayouter::new(tracks, gutter, &cells, regions, styles, self.span());
|
tracks,
|
||||||
|
gutter,
|
||||||
// Measure the columns and layout the grid row-by-row.
|
&cells,
|
||||||
let mut layout = layouter.layout(engine)?;
|
fill,
|
||||||
|
&stroke,
|
||||||
// Add lines and backgrounds.
|
regions,
|
||||||
for (frame, rows) in layout.fragment.iter_mut().zip(&layout.rows) {
|
styles,
|
||||||
if layout.cols.is_empty() || rows.is_empty() {
|
self.span(),
|
||||||
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.
|
Ok(layouter.layout(engine)?.fragment)
|
||||||
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(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(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, engine: &mut Engine, x: usize, y: usize) -> SourceResult<T> {
|
|
||||||
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<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)),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BIN
tests/ref/layout/grid-styling.png
Normal file
BIN
tests/ref/layout/grid-styling.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
89
tests/typ/layout/grid-styling.typ
Normal file
89
tests/typ/layout/grid-styling.typ
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
// Test grid styling options.
|
||||||
|
|
||||||
|
---
|
||||||
|
#set page(height: 70pt)
|
||||||
|
#set grid(fill: (x, y) => if calc.even(x + y) { rgb("aaa") })
|
||||||
|
|
||||||
|
#grid(
|
||||||
|
columns: (1fr,) * 3,
|
||||||
|
stroke: 2pt + rgb("333"),
|
||||||
|
[A], [B], [C], [], [], [D \ E \ F \ \ \ G], [H],
|
||||||
|
)
|
||||||
|
|
||||||
|
---
|
||||||
|
#grid(columns: 3, stroke: none, fill: green, [A], [B], [C])
|
||||||
|
|
||||||
|
---
|
||||||
|
// Test general alignment.
|
||||||
|
#grid(
|
||||||
|
columns: 3,
|
||||||
|
align: left,
|
||||||
|
[Hello], [Hello], [Hello],
|
||||||
|
[A], [B], [C],
|
||||||
|
)
|
||||||
|
|
||||||
|
// Test alignment with a function.
|
||||||
|
#grid(
|
||||||
|
columns: 3,
|
||||||
|
align: (x, y) => (left, center, right).at(x),
|
||||||
|
[Hello], [Hello], [Hello],
|
||||||
|
[A], [B], [C],
|
||||||
|
)
|
||||||
|
|
||||||
|
// Test alignment with array.
|
||||||
|
#grid(
|
||||||
|
columns: (1fr, 1fr, 1fr),
|
||||||
|
align: (left, center, right),
|
||||||
|
[A], [B], [C]
|
||||||
|
)
|
||||||
|
|
||||||
|
// Test empty array.
|
||||||
|
#set align(center)
|
||||||
|
#grid(
|
||||||
|
columns: (1fr, 1fr, 1fr),
|
||||||
|
align: (),
|
||||||
|
[A], [B], [C]
|
||||||
|
)
|
||||||
|
|
||||||
|
a
|
||||||
|
|
||||||
|
---
|
||||||
|
// Test inset.
|
||||||
|
#grid(
|
||||||
|
columns: (1fr,) * 3,
|
||||||
|
stroke: 2pt + rgb("333"),
|
||||||
|
inset: 5pt,
|
||||||
|
[A], [B], [C], [], [], [D \ E \ F \ \ \ G], [H],
|
||||||
|
)
|
||||||
|
|
||||||
|
#grid(
|
||||||
|
columns: 3,
|
||||||
|
inset: 10pt,
|
||||||
|
fill: blue,
|
||||||
|
[A], [B], [C]
|
||||||
|
)
|
||||||
|
|
||||||
|
#grid(
|
||||||
|
columns: 3,
|
||||||
|
inset: (y: 10pt),
|
||||||
|
[A], [B], [C]
|
||||||
|
)
|
||||||
|
|
||||||
|
#grid(
|
||||||
|
columns: 3,
|
||||||
|
inset: (left: 20pt, rest: 10pt),
|
||||||
|
stroke: 3pt + red,
|
||||||
|
[A], [B], [C]
|
||||||
|
)
|
||||||
|
|
||||||
|
#grid(
|
||||||
|
columns: 2,
|
||||||
|
inset: (
|
||||||
|
left: 20pt,
|
||||||
|
right: 5pt,
|
||||||
|
top: 10pt,
|
||||||
|
bottom: 3pt,
|
||||||
|
),
|
||||||
|
[A],
|
||||||
|
[B],
|
||||||
|
)
|
Loading…
x
Reference in New Issue
Block a user