2024-10-27 18:04:55 +00:00

417 lines
15 KiB
Rust

mod cells;
mod layouter;
mod lines;
mod repeated;
mod rowspans;
pub use self::cells::{Cell, CellGrid};
pub use self::layouter::GridLayouter;
use std::num::NonZeroUsize;
use std::sync::Arc;
use ecow::eco_format;
use typst_library::diag::{SourceResult, Trace, Tracepoint};
use typst_library::engine::Engine;
use typst_library::foundations::{Fold, Packed, Smart, StyleChain};
use typst_library::introspection::Locator;
use typst_library::layout::{
Abs, Alignment, Axes, Dir, Fragment, GridCell, GridChild, GridElem, GridItem, Length,
OuterHAlignment, OuterVAlignment, Regions, Rel, Sides,
};
use typst_library::model::{TableCell, TableChild, TableElem, TableItem};
use typst_library::text::TextElem;
use typst_library::visualize::{Paint, Stroke};
use typst_syntax::Span;
use self::cells::{
LinePosition, ResolvableCell, ResolvableGridChild, ResolvableGridItem,
};
use self::layouter::RowPiece;
use self::lines::{
generate_line_segments, hline_stroke_at_column, vline_stroke_at_row, Line,
LineSegment,
};
use self::repeated::{Footer, Header, Repeatable};
use self::rowspans::{Rowspan, UnbreakableRowGroup};
/// Layout the grid.
#[typst_macros::time(span = elem.span())]
pub 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());
// Use trace to link back to the grid when a specific cell errors
let tracepoint = || Tracepoint::Call(Some(eco_format!("grid")));
let resolve_item = |item: &GridItem| grid_item_to_resolvable(item, 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(grid_item_to_resolvable(item, 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());
// Measure the columns and layout the grid row-by-row.
layouter.layout(engine)
}
/// Layout the table.
#[typst_macros::time(span = elem.span())]
pub 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());
// Use trace to link back to the table when a specific cell errors
let tracepoint = || Tracepoint::Call(Some(eco_format!("table")));
let resolve_item = |item: &TableItem| table_item_to_resolvable(item, 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(table_item_to_resolvable(item, 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)
}
fn grid_item_to_resolvable(
item: &GridItem,
styles: StyleChain,
) -> ResolvableGridItem<Packed<GridCell>> {
match item {
GridItem::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,
},
},
GridItem::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,
},
},
GridItem::Cell(cell) => ResolvableGridItem::Cell(cell.clone()),
}
}
fn table_item_to_resolvable(
item: &TableItem,
styles: StyleChain,
) -> ResolvableGridItem<Packed<TableCell>> {
match item {
TableItem::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,
},
},
TableItem::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,
},
},
TableItem::Cell(cell) => ResolvableGridItem::Cell(cell.clone()),
}
}
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(_))));
// Using a typical 'Sides' fold, an unspecified side loses to a
// specified side. Additionally, when both are specified, an inner
// None wins over the outer Some, and vice-versa. When both are
// specified and Some, fold occurs, which, remarkably, leads to an Arc
// clone.
//
// In the end, we flatten because, for layout purposes, an unspecified
// cell stroke is the same as specifying 'none', so we equate the two
// concepts.
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)))
}
// 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 => cell.align(styles),
});
cell.push_inset(Smart::Custom(
cell.inset(styles).map_or(inset, |inner| inner.fold(inset)),
));
cell.push_stroke(
// Here we convert the resolved stroke to a regular stroke, however
// with resolved units (that is, 'em' converted to absolute units).
// We also convert any stroke unspecified by both the cell and the
// outer stroke ('None' in the folded stroke) to 'none', that is,
// all sides are present in the resulting Sides object accessible
// by show rules on table cells.
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 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(_))));
// Using a typical 'Sides' fold, an unspecified side loses to a
// specified side. Additionally, when both are specified, an inner
// None wins over the outer Some, and vice-versa. When both are
// specified and Some, fold occurs, which, remarkably, leads to an Arc
// clone.
//
// In the end, we flatten because, for layout purposes, an unspecified
// cell stroke is the same as specifying 'none', so we equate the two
// concepts.
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)))
}
// 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 => cell.align(styles),
});
cell.push_inset(Smart::Custom(
cell.inset(styles).map_or(inset, |inner| inner.fold(inset)),
));
cell.push_stroke(
// Here we convert the resolved stroke to a regular stroke, however
// with resolved units (that is, 'em' converted to absolute units).
// We also convert any stroke unspecified by both the cell and the
// outer stroke ('None' in the folded stroke) to 'none', that is,
// all sides are present in the resulting Sides object accessible
// by show rules on grid cells.
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)
}
}