use std::num::NonZeroUsize;
use std::sync::Arc;
use ecow::eco_format;
use crate::diag::{bail, HintedStrResult, HintedString, SourceResult, Trace, Tracepoint};
use crate::engine::Engine;
use crate::foundations::{
cast, elem, scope, Content, Fold, NativeElement, Packed, Show, Smart, StyleChain,
};
use crate::introspection::Locator;
use crate::layout::{
show_grid_cell, Abs, Alignment, Axes, BlockElem, Cell, CellGrid, Celled, Dir,
Fragment, GridCell, GridFooter, GridHLine, GridHeader, GridLayouter, GridVLine,
Length, LinePosition, OuterHAlignment, OuterVAlignment, Regions, Rel, ResolvableCell,
ResolvableGridChild, ResolvableGridItem, Sides, TrackSizings,
};
use crate::model::Figurable;
use crate::syntax::Span;
use crate::text::{LocalName, TextElem};
use crate::utils::NonZeroExt;
use crate::visualize::{Paint, Stroke};
#[elem(scope, Show, LocalName, Figurable)]
pub struct TableElem {
#[borrowed]
pub columns: TrackSizings,
#[borrowed]
pub rows: TrackSizings,
#[external]
pub gutter: TrackSizings,
#[borrowed]
#[parse(
let gutter = args.named("gutter")?;
args.named("column-gutter")?.or_else(|| gutter.clone())
)]
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]
#[default(Celled::Value(Sides::splat(Some(Some(Arc::new(Stroke::default()))))))]
pub stroke: Celled<Sides<Option<Option<Arc<Stroke>>>>>,
#[fold]
#[default(Celled::Value(Sides::splat(Some(Abs::pt(5.0).into()))))]
pub inset: Celled<Sides<Option<Rel<Length>>>>,
#[variadic]
pub children: Vec<TableChild>,
}
#[scope]
impl TableElem {
#[elem]
type TableCell;
#[elem]
type TableHLine;
#[elem]
type TableVLine;
#[elem]
type TableHeader;
#[elem]
type TableFooter;
}
impl Show for Packed<TableElem> {
fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
Ok(BlockElem::multi_layouter(self.clone(), layout_table)
.pack()
.spanned(self.span()))
}
}
#[typst_macros::time(span = elem.span())]
fn layout_table(
elem: &Packed<TableElem>,
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!("table")));
let resolve_item = |item: &TableItem| item.to_resolvable(styles);
let children = elem.children().iter().map(|child| match child {
TableChild::Header(header) => ResolvableGridChild::Header {
repeat: header.repeat(styles),
span: header.span(),
items: header.children().iter().map(resolve_item),
},
TableChild::Footer(footer) => ResolvableGridChild::Footer {
repeat: footer.repeat(styles),
span: footer.span(),
items: footer.children().iter().map(resolve_item),
},
TableChild::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)
}
impl LocalName for Packed<TableElem> {
const KEY: &'static str = "table";
}
impl Figurable for Packed<TableElem> {}
#[derive(Debug, PartialEq, Clone, Hash)]
pub enum TableChild {
Header(Packed<TableHeader>),
Footer(Packed<TableFooter>),
Item(TableItem),
}
cast! {
TableChild,
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 TableChild {
type Error = HintedString;
fn try_from(value: Content) -> HintedStrResult<Self> {
if value.is::<GridHeader>() {
bail!(
"cannot use `grid.header` as a table header";
hint: "use `table.header` instead"
)
}
if value.is::<GridFooter>() {
bail!(
"cannot use `grid.footer` as a table footer";
hint: "use `table.footer` instead"
)
}
value
.into_packed::<TableHeader>()
.map(Self::Header)
.or_else(|value| value.into_packed::<TableFooter>().map(Self::Footer))
.or_else(|value| TableItem::try_from(value).map(Self::Item))
}
}
#[derive(Debug, PartialEq, Clone, Hash)]
pub enum TableItem {
HLine(Packed<TableHLine>),
VLine(Packed<TableVLine>),
Cell(Packed<TableCell>),
}
impl TableItem {
fn to_resolvable(&self, styles: StyleChain) -> ResolvableGridItem<Packed<TableCell>> {
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! {
TableItem,
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 TableItem {
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::<GridCell>() {
bail!(
"cannot use `grid.cell` as a table cell";
hint: "use `table.cell` instead"
);
}
if value.is::<GridHLine>() {
bail!(
"cannot use `grid.hline` as a table line";
hint: "use `table.hline` instead"
);
}
if value.is::<GridVLine>() {
bail!(
"cannot use `grid.vline` as a table line";
hint: "use `table.vline` instead"
);
}
Ok(value
.into_packed::<TableHLine>()
.map(Self::HLine)
.or_else(|value| value.into_packed::<TableVLine>().map(Self::VLine))
.or_else(|value| value.into_packed::<TableCell>().map(Self::Cell))
.unwrap_or_else(|value| {
let span = value.span();
Self::Cell(Packed::new(TableCell::new(value)).spanned(span))
}))
}
}
#[elem(name = "header", title = "Table Header")]
pub struct TableHeader {
#[default(true)]
pub repeat: bool,
#[variadic]
pub children: Vec<TableItem>,
}
#[elem(name = "footer", title = "Table Footer")]
pub struct TableFooter {
#[default(true)]
pub repeat: bool,
#[variadic]
pub children: Vec<TableItem>,
}
#[elem(name = "hline", title = "Table Horizontal Line")]
pub struct TableHLine {
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 = "Table Vertical Line")]
pub struct TableVLine {
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 = "Table Cell", Show)]
pub struct TableCell {
#[required]
pub body: Content,
pub x: Smart<usize>,
pub y: Smart<usize>,
#[default(NonZeroUsize::ONE)]
pub colspan: NonZeroUsize,
#[default(NonZeroUsize::ONE)]
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! {
TableCell,
v: Content => v.into(),
}
impl Default for Packed<TableCell> {
fn default() -> Self {
Packed::new(TableCell::new(Content::default()))
}
}
impl ResolvableCell for Packed<TableCell> {
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<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 {
#[allow(clippy::unwrap_or_default)]
value.unpack::<Self>().unwrap_or_else(Self::new)
}
}