mod cells;
mod layout;
mod lines;
mod repeated;
mod rowspans;
pub use self::cells::{
Cell, CellGrid, Celled, ResolvableCell, ResolvableGridChild, ResolvableGridItem,
};
pub use self::layout::GridLayouter;
pub use self::lines::LinePosition;
use std::num::NonZeroUsize;
use std::sync::Arc;
use ecow::eco_format;
use smallvec::{smallvec, SmallVec};
use crate::diag::{bail, HintedStrResult, HintedString, SourceResult, Trace, Tracepoint};
use crate::engine::Engine;
use crate::foundations::{
cast, elem, scope, Array, Content, Fold, NativeElement, Packed, Show, Smart,
StyleChain, Value,
};
use crate::introspection::Locator;
use crate::layout::{
Abs, Alignment, Axes, BlockElem, Dir, Fragment, Length, OuterHAlignment,
OuterVAlignment, Regions, Rel, Sides, Sizing,
};
use crate::model::{TableCell, TableFooter, TableHLine, TableHeader, TableVLine};
use crate::syntax::Span;
use crate::text::TextElem;
use crate::utils::NonZeroExt;
use crate::visualize::{Paint, Stroke};
#[elem(scope, Show)]
pub struct GridElem {
#[borrowed]
pub columns: TrackSizings,
#[borrowed]
pub rows: TrackSizings,
#[external]
pub gutter: TrackSizings,
#[parse(
let gutter = args.named("gutter")?;
args.named("column-gutter")?.or_else(|| gutter.clone())
)]
#[borrowed]
pub column_gutter: TrackSizings,
#[parse(args.named("row-gutter")?.or_else(|| gutter.clone()))]
#[borrowed]
pub row_gutter: TrackSizings,
#[borrowed]
pub fill: Celled<Option<Paint>>,
#[borrowed]
pub align: Celled<Smart<Alignment>>,
#[resolve]
#[fold]
pub stroke: Celled<Sides<Option<Option<Arc<Stroke>>>>>,
#[fold]
pub inset: Celled<Sides<Option<Rel<Length>>>>,
#[variadic]
pub children: Vec<GridChild>,
}
#[scope]
impl GridElem {
#[elem]
type GridCell;
#[elem]
type GridHLine;
#[elem]
type GridVLine;
#[elem]
type GridHeader;
#[elem]
type GridFooter;
}
impl Show for Packed<GridElem> {
fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
Ok(BlockElem::multi_layouter(self.clone(), layout_grid)
.pack()
.spanned(self.span()))
}
}
#[typst_macros::time(span = elem.span())]
fn layout_grid(
elem: &Packed<GridElem>,
engine: &mut Engine,
locator: Locator,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment> {
let inset = elem.inset(styles);
let align = elem.align(styles);
let columns = elem.columns(styles);
let rows = elem.rows(styles);
let column_gutter = elem.column_gutter(styles);
let row_gutter = elem.row_gutter(styles);
let fill = elem.fill(styles);
let stroke = elem.stroke(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 tracepoint = || Tracepoint::Call(Some(eco_format!("grid")));
let resolve_item = |item: &GridItem| item.to_resolvable(styles);
let children = elem.children().iter().map(|child| match child {
GridChild::Header(header) => ResolvableGridChild::Header {
repeat: header.repeat(styles),
span: header.span(),
items: header.children().iter().map(resolve_item),
},
GridChild::Footer(footer) => ResolvableGridChild::Footer {
repeat: footer.repeat(styles),
span: footer.span(),
items: footer.children().iter().map(resolve_item),
},
GridChild::Item(item) => ResolvableGridChild::Item(item.to_resolvable(styles)),
});
let grid = CellGrid::resolve(
tracks,
gutter,
locator,
children,
fill,
align,
&inset,
&stroke,
engine,
styles,
elem.span(),
)
.trace(engine.world, tracepoint, elem.span())?;
let layouter = GridLayouter::new(&grid, regions, styles, elem.span());
layouter.layout(engine)
}
#[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::<HintedStrResult<_>>()?),
}
#[derive(Debug, PartialEq, Clone, Hash)]
pub enum GridChild {
Header(Packed<GridHeader>),
Footer(Packed<GridFooter>),
Item(GridItem),
}
cast! {
GridChild,
self => match self {
Self::Header(header) => header.into_value(),
Self::Footer(footer) => footer.into_value(),
Self::Item(item) => item.into_value(),
},
v: Content => {
v.try_into()?
},
}
impl TryFrom<Content> for GridChild {
type Error = HintedString;
fn try_from(value: Content) -> HintedStrResult<Self> {
if value.is::<TableHeader>() {
bail!(
"cannot use `table.header` as a grid header";
hint: "use `grid.header` instead"
)
}
if value.is::<TableFooter>() {
bail!(
"cannot use `table.footer` as a grid footer";
hint: "use `grid.footer` instead"
)
}
value
.into_packed::<GridHeader>()
.map(Self::Header)
.or_else(|value| value.into_packed::<GridFooter>().map(Self::Footer))
.or_else(|value| GridItem::try_from(value).map(Self::Item))
}
}
#[derive(Debug, PartialEq, Clone, Hash)]
pub enum GridItem {
HLine(Packed<GridHLine>),
VLine(Packed<GridVLine>),
Cell(Packed<GridCell>),
}
impl GridItem {
fn to_resolvable(&self, styles: StyleChain) -> ResolvableGridItem<Packed<GridCell>> {
match self {
Self::HLine(hline) => ResolvableGridItem::HLine {
y: hline.y(styles),
start: hline.start(styles),
end: hline.end(styles),
stroke: hline.stroke(styles),
span: hline.span(),
position: match hline.position(styles) {
OuterVAlignment::Top => LinePosition::Before,
OuterVAlignment::Bottom => LinePosition::After,
},
},
Self::VLine(vline) => ResolvableGridItem::VLine {
x: vline.x(styles),
start: vline.start(styles),
end: vline.end(styles),
stroke: vline.stroke(styles),
span: vline.span(),
position: match vline.position(styles) {
OuterHAlignment::Left if TextElem::dir_in(styles) == Dir::RTL => {
LinePosition::After
}
OuterHAlignment::Right if TextElem::dir_in(styles) == Dir::RTL => {
LinePosition::Before
}
OuterHAlignment::Start | OuterHAlignment::Left => {
LinePosition::Before
}
OuterHAlignment::End | OuterHAlignment::Right => LinePosition::After,
},
},
Self::Cell(cell) => ResolvableGridItem::Cell(cell.clone()),
}
}
}
cast! {
GridItem,
self => match self {
Self::HLine(hline) => hline.into_value(),
Self::VLine(vline) => vline.into_value(),
Self::Cell(cell) => cell.into_value(),
},
v: Content => {
v.try_into()?
}
}
impl TryFrom<Content> for GridItem {
type Error = HintedString;
fn try_from(value: Content) -> HintedStrResult<Self> {
if value.is::<GridHeader>() {
bail!("cannot place a grid header within another header or footer");
}
if value.is::<TableHeader>() {
bail!("cannot place a table header within another header or footer");
}
if value.is::<GridFooter>() {
bail!("cannot place a grid footer within another footer or header");
}
if value.is::<TableFooter>() {
bail!("cannot place a table footer within another footer or header");
}
if value.is::<TableCell>() {
bail!(
"cannot use `table.cell` as a grid cell";
hint: "use `grid.cell` instead"
);
}
if value.is::<TableHLine>() {
bail!(
"cannot use `table.hline` as a grid line";
hint: "use `grid.hline` instead"
);
}
if value.is::<TableVLine>() {
bail!(
"cannot use `table.vline` as a grid line";
hint: "use `grid.vline` instead"
);
}
Ok(value
.into_packed::<GridHLine>()
.map(Self::HLine)
.or_else(|value| value.into_packed::<GridVLine>().map(Self::VLine))
.or_else(|value| value.into_packed::<GridCell>().map(Self::Cell))
.unwrap_or_else(|value| {
let span = value.span();
Self::Cell(Packed::new(GridCell::new(value)).spanned(span))
}))
}
}
#[elem(name = "header", title = "Grid Header")]
pub struct GridHeader {
#[default(true)]
pub repeat: bool,
#[variadic]
pub children: Vec<GridItem>,
}
#[elem(name = "footer", title = "Grid Footer")]
pub struct GridFooter {
#[default(true)]
pub repeat: bool,
#[variadic]
pub children: Vec<GridItem>,
}
#[elem(name = "hline", title = "Grid Horizontal Line")]
pub struct GridHLine {
pub y: Smart<usize>,
pub start: usize,
pub end: Option<NonZeroUsize>,
#[resolve]
#[fold]
#[default(Some(Arc::new(Stroke::default())))]
pub stroke: Option<Arc<Stroke>>,
#[default(OuterVAlignment::Top)]
pub position: OuterVAlignment,
}
#[elem(name = "vline", title = "Grid Vertical Line")]
pub struct GridVLine {
pub x: Smart<usize>,
pub start: usize,
pub end: Option<NonZeroUsize>,
#[resolve]
#[fold]
#[default(Some(Arc::new(Stroke::default())))]
pub stroke: Option<Arc<Stroke>>,
#[default(OuterHAlignment::Start)]
pub position: OuterHAlignment,
}
#[elem(name = "cell", title = "Grid Cell", Show)]
pub struct GridCell {
#[required]
pub body: Content,
pub x: Smart<usize>,
pub y: Smart<usize>,
#[default(NonZeroUsize::ONE)]
pub colspan: NonZeroUsize,
#[default(NonZeroUsize::ONE)]
pub rowspan: NonZeroUsize,
pub fill: Smart<Option<Paint>>,
pub align: Smart<Alignment>,
pub inset: Smart<Sides<Option<Rel<Length>>>>,
#[resolve]
#[fold]
pub stroke: Sides<Option<Option<Arc<Stroke>>>>,
pub breakable: Smart<bool>,
}
cast! {
GridCell,
v: Content => v.into(),
}
impl Default for Packed<GridCell> {
fn default() -> Self {
Packed::new(GridCell::new(Content::default()))
}
}
impl ResolvableCell for Packed<GridCell> {
fn resolve_cell<'a>(
mut self,
x: usize,
y: usize,
fill: &Option<Paint>,
align: Smart<Alignment>,
inset: Sides<Option<Rel<Length>>>,
stroke: Sides<Option<Option<Arc<Stroke<Abs>>>>>,
breakable: bool,
locator: Locator<'a>,
styles: StyleChain,
) -> Cell<'a> {
let cell = &mut *self;
let colspan = cell.colspan(styles);
let rowspan = cell.rowspan(styles);
let breakable = cell.breakable(styles).unwrap_or(breakable);
let fill = cell.fill(styles).unwrap_or_else(|| fill.clone());
let cell_stroke = cell.stroke(styles);
let stroke_overridden =
cell_stroke.as_ref().map(|side| matches!(side, Some(Some(_))));
let stroke = cell_stroke.fold(stroke).map(Option::flatten);
cell.push_x(Smart::Custom(x));
cell.push_y(Smart::Custom(y));
cell.push_fill(Smart::Custom(fill.clone()));
cell.push_align(match align {
Smart::Custom(align) => {
Smart::Custom(cell.align(styles).map_or(align, |inner| inner.fold(align)))
}
Smart::Auto => cell.align(styles),
});
cell.push_inset(Smart::Custom(
cell.inset(styles).map_or(inset, |inner| inner.fold(inset)),
));
cell.push_stroke(
stroke.as_ref().map(|side| {
Some(side.as_ref().map(|cell_stroke| {
Arc::new((**cell_stroke).clone().map(Length::from))
}))
}),
);
cell.push_breakable(Smart::Custom(breakable));
Cell {
body: self.pack(),
locator,
fill,
colspan,
rowspan,
stroke,
stroke_overridden,
breakable,
}
}
fn x(&self, styles: StyleChain) -> Smart<usize> {
(**self).x(styles)
}
fn y(&self, styles: StyleChain) -> Smart<usize> {
(**self).y(styles)
}
fn colspan(&self, styles: StyleChain) -> NonZeroUsize {
(**self).colspan(styles)
}
fn rowspan(&self, styles: StyleChain) -> NonZeroUsize {
(**self).rowspan(styles)
}
fn span(&self) -> Span {
Packed::span(self)
}
}
impl Show for Packed<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 {
#[allow(clippy::unwrap_or_default)]
value.unpack::<Self>().unwrap_or_else(Self::new)
}
}
pub(crate) fn show_grid_cell(
mut body: Content,
inset: Smart<Sides<Option<Rel<Length>>>>,
align: Smart<Alignment>,
) -> SourceResult<Content> {
let inset = inset.unwrap_or_default().map(Option::unwrap_or_default);
if inset != Sides::default() {
body = body.padded(inset);
}
if let Smart::Custom(alignment) = align {
body = body.aligned(alignment);
}
Ok(body)
}