Table line customization [More Flexible Tables Pt.4] (#3393)
@ -746,6 +746,34 @@ impl<T, const N: usize> Fold for SmallVec<[T; N]> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A variant of fold for foldable optional (`Option<T>`) values where an inner
|
||||
/// `None` value isn't respected (contrary to `Option`'s usual `Fold`
|
||||
/// implementation, with which folding with an inner `None` always returns
|
||||
/// `None`). Instead, when either of the `Option` objects is `None`, the other
|
||||
/// one is necessarily returned by `fold_or`. Normal folding still occurs when
|
||||
/// both values are `Some`, using `T`'s `Fold` implementation.
|
||||
///
|
||||
/// This is useful when `None` in a particular context means "unspecified"
|
||||
/// rather than "absent", in which case a specified value (`Some`) is chosen
|
||||
/// over an unspecified one (`None`), while two specified values are folded
|
||||
/// together.
|
||||
pub trait AlternativeFold {
|
||||
/// Attempts to fold this inner value with an outer value. However, if
|
||||
/// either value is `None`, returns the other one instead of folding.
|
||||
fn fold_or(self, outer: Self) -> Self;
|
||||
}
|
||||
|
||||
impl<T: Fold> AlternativeFold for Option<T> {
|
||||
fn fold_or(self, outer: Self) -> Self {
|
||||
match (self, outer) {
|
||||
(Some(inner), Some(outer)) => Some(inner.fold(outer)),
|
||||
// If one of values is `None`, return the other one instead of
|
||||
// folding.
|
||||
(inner, outer) => inner.or(outer),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A type that accumulates depth when folded.
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Hash)]
|
||||
pub struct Depth(pub usize);
|
||||
|
@ -13,13 +13,14 @@ use crate::diag::StrResult;
|
||||
use crate::eval::ops;
|
||||
use crate::foundations::{
|
||||
fields, repr, Args, Array, AutoValue, Bytes, CastInfo, Content, Datetime, Dict,
|
||||
Duration, FromValue, Func, IntoValue, Label, Module, NativeElement, NativeType,
|
||||
NoneValue, Plugin, Reflect, Repr, Scope, Str, Styles, Type, Version,
|
||||
Duration, Fold, FromValue, Func, IntoValue, Label, Module, NativeElement, NativeType,
|
||||
NoneValue, Plugin, Reflect, Repr, Resolve, Scope, Str, Styles, Type, Version,
|
||||
};
|
||||
use crate::layout::{Abs, Angle, Em, Fr, Length, Ratio, Rel};
|
||||
use crate::symbols::Symbol;
|
||||
use crate::syntax::{ast, Span};
|
||||
use crate::text::{RawElem, TextElem};
|
||||
use crate::util::ArcExt;
|
||||
use crate::visualize::{Color, Gradient, Pattern};
|
||||
|
||||
/// A computational value.
|
||||
@ -668,6 +669,53 @@ primitive! { Type: "type", Type }
|
||||
primitive! { Module: "module", Module }
|
||||
primitive! { Plugin: "plugin", Plugin }
|
||||
|
||||
impl<T: Reflect> Reflect for Arc<T> {
|
||||
fn input() -> CastInfo {
|
||||
T::input()
|
||||
}
|
||||
|
||||
fn output() -> CastInfo {
|
||||
T::output()
|
||||
}
|
||||
|
||||
fn castable(value: &Value) -> bool {
|
||||
T::castable(value)
|
||||
}
|
||||
|
||||
fn error(found: &Value) -> EcoString {
|
||||
T::error(found)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone + IntoValue> IntoValue for Arc<T> {
|
||||
fn into_value(self) -> Value {
|
||||
Arc::take(self).into_value()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: FromValue> FromValue for Arc<T> {
|
||||
fn from_value(value: Value) -> StrResult<Self> {
|
||||
match value {
|
||||
v if T::castable(&v) => Ok(Arc::new(T::from_value(v)?)),
|
||||
_ => Err(Self::error(&value)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone + Resolve> Resolve for Arc<T> {
|
||||
type Output = Arc<T::Output>;
|
||||
|
||||
fn resolve(self, styles: super::StyleChain) -> Self::Output {
|
||||
Arc::new(Arc::take(self).resolve(styles))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone + Fold> Fold for Arc<T> {
|
||||
fn fold(self, outer: Self) -> Self {
|
||||
Arc::new(Arc::take(self).fold(Arc::take(outer)))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
@ -4,7 +4,7 @@ use std::ops::{Add, Div, Mul, Neg, Rem};
|
||||
|
||||
use ecow::EcoString;
|
||||
|
||||
use crate::foundations::{cast, repr, Repr, Value};
|
||||
use crate::foundations::{cast, repr, Fold, Repr, Value};
|
||||
use crate::util::{Numeric, Scalar};
|
||||
|
||||
/// An absolute length.
|
||||
@ -227,6 +227,12 @@ impl<'a> Sum<&'a Self> for Abs {
|
||||
}
|
||||
}
|
||||
|
||||
impl Fold for Abs {
|
||||
fn fold(self, _: Self) -> Self {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
cast! {
|
||||
Abs,
|
||||
self => Value::Length(self.into()),
|
||||
|
@ -321,6 +321,40 @@ cast! {
|
||||
}
|
||||
}
|
||||
|
||||
/// A horizontal alignment which only allows `left`/`right` and `start`/`end`,
|
||||
/// thus excluding `center`.
|
||||
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub enum OuterHAlignment {
|
||||
#[default]
|
||||
Start,
|
||||
Left,
|
||||
Right,
|
||||
End,
|
||||
}
|
||||
|
||||
impl From<OuterHAlignment> for HAlignment {
|
||||
fn from(value: OuterHAlignment) -> Self {
|
||||
match value {
|
||||
OuterHAlignment::Start => Self::Start,
|
||||
OuterHAlignment::Left => Self::Left,
|
||||
OuterHAlignment::Right => Self::Right,
|
||||
OuterHAlignment::End => Self::End,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cast! {
|
||||
OuterHAlignment,
|
||||
self => HAlignment::from(self).into_value(),
|
||||
align: Alignment => match align {
|
||||
Alignment::H(HAlignment::Start) => Self::Start,
|
||||
Alignment::H(HAlignment::Left) => Self::Left,
|
||||
Alignment::H(HAlignment::Right) => Self::Right,
|
||||
Alignment::H(HAlignment::End) => Self::End,
|
||||
v => bail!("expected `start`, `left`, `right`, or `end`, found {}", v.repr()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Where to align something vertically.
|
||||
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||
pub enum VAlignment {
|
||||
@ -383,6 +417,34 @@ cast! {
|
||||
}
|
||||
}
|
||||
|
||||
/// A vertical alignment which only allows `top` and `bottom`, thus excluding
|
||||
/// `horizon`.
|
||||
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||
pub enum OuterVAlignment {
|
||||
#[default]
|
||||
Top,
|
||||
Bottom,
|
||||
}
|
||||
|
||||
impl From<OuterVAlignment> for VAlignment {
|
||||
fn from(value: OuterVAlignment) -> Self {
|
||||
match value {
|
||||
OuterVAlignment::Top => Self::Top,
|
||||
OuterVAlignment::Bottom => Self::Bottom,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cast! {
|
||||
OuterVAlignment,
|
||||
self => VAlignment::from(self).into_value(),
|
||||
align: Alignment => match align {
|
||||
Alignment::V(VAlignment::Top) => Self::Top,
|
||||
Alignment::V(VAlignment::Bottom) => Self::Bottom,
|
||||
v => bail!("expected `top` or `bottom`, found {}", v.repr()),
|
||||
}
|
||||
}
|
||||
|
||||
/// A fixed alignment in the global coordinate space.
|
||||
///
|
||||
/// For horizontal alignment, start is globally left and for vertical alignment
|
||||
|
@ -2,7 +2,8 @@ use std::fmt::{self, Debug, Formatter};
|
||||
|
||||
use crate::diag::StrResult;
|
||||
use crate::foundations::{
|
||||
CastInfo, Dict, Fold, FromValue, IntoValue, Reflect, Resolve, StyleChain, Value,
|
||||
AlternativeFold, CastInfo, Dict, Fold, FromValue, IntoValue, Reflect, Resolve,
|
||||
StyleChain, Value,
|
||||
};
|
||||
use crate::layout::Side;
|
||||
use crate::util::Get;
|
||||
@ -240,13 +241,10 @@ impl<T: Resolve> Resolve for Corners<T> {
|
||||
|
||||
impl<T: Fold> Fold for Corners<Option<T>> {
|
||||
fn fold(self, outer: Self) -> Self {
|
||||
self.zip(outer).map(|(inner, outer)| match (inner, outer) {
|
||||
(Some(inner), Some(outer)) => Some(inner.fold(outer)),
|
||||
// Usually, folding an inner `None` with an `outer` preferres the
|
||||
// explicit `None`. However, here `None` means unspecified and thus
|
||||
// we want `outer`.
|
||||
(inner, outer) => inner.or(outer),
|
||||
})
|
||||
// Usually, folding an inner `None` with an `outer` preferres the
|
||||
// explicit `None`. However, here `None` means unspecified and thus
|
||||
// we want `outer`, so we use `fold_or` to opt into such behavior.
|
||||
self.zip(outer).map(|(inner, outer)| inner.fold_or(outer))
|
||||
}
|
||||
}
|
||||
|
||||
|
1096
crates/typst/src/layout/grid/lines.rs
Normal file
@ -1,8 +1,11 @@
|
||||
mod layout;
|
||||
mod lines;
|
||||
|
||||
pub use self::layout::{Cell, CellGrid, Celled, GridLayouter, ResolvableCell};
|
||||
pub use self::layout::{Cell, CellGrid, Celled, GridItem, GridLayouter, ResolvableCell};
|
||||
pub use self::lines::LinePosition;
|
||||
|
||||
use std::num::NonZeroUsize;
|
||||
use std::sync::Arc;
|
||||
|
||||
use ecow::eco_format;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
@ -13,10 +16,11 @@ use crate::foundations::{
|
||||
cast, elem, scope, Array, Content, Fold, Packed, Show, Smart, StyleChain, Value,
|
||||
};
|
||||
use crate::layout::{
|
||||
AlignElem, Alignment, Axes, Fragment, LayoutMultiple, Length, Regions, Rel, Sides,
|
||||
Sizing,
|
||||
Abs, AlignElem, Alignment, Axes, Dir, Fragment, LayoutMultiple, Length,
|
||||
OuterHAlignment, OuterVAlignment, Regions, Rel, Sides, Sizing,
|
||||
};
|
||||
use crate::syntax::Span;
|
||||
use crate::text::TextElem;
|
||||
use crate::util::NonZeroExt;
|
||||
use crate::visualize::{Paint, Stroke};
|
||||
|
||||
@ -235,12 +239,14 @@ pub struct GridElem {
|
||||
/// 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/).
|
||||
/// If it is necessary to place lines which can cross spacing between cells
|
||||
/// produced by the `gutter` option, or to override the stroke between
|
||||
/// multiple specific cells, consider specifying one or more of
|
||||
/// [`grid.hline`]($grid.hline) and [`grid.vline`]($grid.vline) alongside
|
||||
/// your grid cells.
|
||||
#[resolve]
|
||||
#[fold]
|
||||
pub stroke: Option<Stroke>,
|
||||
pub stroke: Celled<Sides<Option<Option<Arc<Stroke>>>>>,
|
||||
|
||||
/// How much to pad the cells' content.
|
||||
///
|
||||
@ -266,17 +272,25 @@ pub struct GridElem {
|
||||
#[fold]
|
||||
pub inset: Sides<Option<Rel<Length>>>,
|
||||
|
||||
/// The contents of the grid cells.
|
||||
/// The contents of the grid cells, plus any extra grid lines specified
|
||||
/// with the [`grid.hline`]($grid.hline) and [`grid.vline`]($grid.vline)
|
||||
/// elements.
|
||||
///
|
||||
/// The cells are populated in row-major order.
|
||||
#[variadic]
|
||||
pub children: Vec<Packed<GridCell>>,
|
||||
pub children: Vec<GridChild>,
|
||||
}
|
||||
|
||||
#[scope]
|
||||
impl GridElem {
|
||||
#[elem]
|
||||
type GridCell;
|
||||
|
||||
#[elem]
|
||||
type GridHLine;
|
||||
|
||||
#[elem]
|
||||
type GridVLine;
|
||||
}
|
||||
|
||||
impl LayoutMultiple for Packed<GridElem> {
|
||||
@ -294,26 +308,60 @@ impl LayoutMultiple for Packed<GridElem> {
|
||||
let column_gutter = self.column_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 stroke = self.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 items = self.children().iter().map(|child| match child {
|
||||
GridChild::HLine(hline) => GridItem::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,
|
||||
},
|
||||
},
|
||||
GridChild::VLine(vline) => GridItem::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,
|
||||
},
|
||||
},
|
||||
GridChild::Cell(cell) => GridItem::Cell(cell.clone()),
|
||||
});
|
||||
let grid = CellGrid::resolve(
|
||||
tracks,
|
||||
gutter,
|
||||
self.children(),
|
||||
items,
|
||||
fill,
|
||||
align,
|
||||
inset,
|
||||
&stroke,
|
||||
engine,
|
||||
styles,
|
||||
self.span(),
|
||||
)
|
||||
.trace(engine.world, tracepoint, self.span())?;
|
||||
|
||||
let layouter = GridLayouter::new(&grid, &stroke, regions, styles, self.span());
|
||||
let layouter = GridLayouter::new(&grid, regions, styles, self.span());
|
||||
|
||||
// Measure the columns and layout the grid row-by-row.
|
||||
layouter.layout(engine)
|
||||
@ -332,6 +380,151 @@ cast! {
|
||||
values: Array => Self(values.into_iter().map(Value::cast).collect::<StrResult<_>>()?),
|
||||
}
|
||||
|
||||
/// Any child of a grid element.
|
||||
#[derive(Debug, PartialEq, Clone, Hash)]
|
||||
pub enum GridChild {
|
||||
HLine(Packed<GridHLine>),
|
||||
VLine(Packed<GridVLine>),
|
||||
Cell(Packed<GridCell>),
|
||||
}
|
||||
|
||||
cast! {
|
||||
GridChild,
|
||||
self => match self {
|
||||
Self::HLine(hline) => hline.into_value(),
|
||||
Self::VLine(vline) => vline.into_value(),
|
||||
Self::Cell(cell) => cell.into_value(),
|
||||
},
|
||||
v: Content => v.into(),
|
||||
}
|
||||
|
||||
impl From<Content> for GridChild {
|
||||
fn from(value: Content) -> Self {
|
||||
value
|
||||
.into_packed::<GridHLine>()
|
||||
.map(GridChild::HLine)
|
||||
.or_else(|value| value.into_packed::<GridVLine>().map(GridChild::VLine))
|
||||
.or_else(|value| value.into_packed::<GridCell>().map(GridChild::Cell))
|
||||
.unwrap_or_else(|value| {
|
||||
let span = value.span();
|
||||
GridChild::Cell(Packed::new(GridCell::new(value)).spanned(span))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A horizontal line in the grid.
|
||||
///
|
||||
/// Overrides any per-cell stroke, including stroke specified through the
|
||||
/// grid's `stroke` field. Can cross spacing between cells created through
|
||||
/// the grid's `column-gutter` option.
|
||||
#[elem(name = "hline", title = "Grid Horizontal Line")]
|
||||
pub struct GridHLine {
|
||||
/// The row above which the horizontal line is placed (zero-indexed).
|
||||
/// If the `position` field is set to `{bottom}`, the line is placed below
|
||||
/// the row with the given index instead (see that field's docs for
|
||||
/// details).
|
||||
///
|
||||
/// Specifying `{auto}` causes the line to be placed at the row below the
|
||||
/// last automatically positioned cell (that is, cell without coordinate
|
||||
/// overrides) before the line among the grid's children. If there is no
|
||||
/// such cell before the line, it is placed at the top of the grid (row 0).
|
||||
/// Note that specifying for this option exactly the total amount of rows
|
||||
/// in the grid causes this horizontal line to override the bottom border
|
||||
/// of the grid, while a value of 0 overrides the top border.
|
||||
pub y: Smart<usize>,
|
||||
|
||||
/// The column at which the horizontal line starts (zero-indexed, inclusive).
|
||||
pub start: usize,
|
||||
|
||||
/// The column before which the horizontal line ends (zero-indexed,
|
||||
/// exclusive).
|
||||
/// Therefore, the horizontal line will be drawn up to and across column
|
||||
/// `end - 1`.
|
||||
///
|
||||
/// A value equal to `{none}` or to the amount of columns causes it to
|
||||
/// extend all the way towards the end of the grid.
|
||||
pub end: Option<NonZeroUsize>,
|
||||
|
||||
/// The line's stroke.
|
||||
///
|
||||
/// Specifying `{none}` interrupts previous hlines placed across this
|
||||
/// line's range, but does not affect per-cell stroke or vlines.
|
||||
#[resolve]
|
||||
#[fold]
|
||||
#[default(Some(Arc::new(Stroke::default())))]
|
||||
pub stroke: Option<Arc<Stroke>>,
|
||||
|
||||
/// The position at which the line is placed, given its row (`y`) - either
|
||||
/// `{top}` to draw above it or `{bottom}` to draw below it.
|
||||
///
|
||||
/// This setting is only relevant when row gutter is enabled (and
|
||||
/// shouldn't be used otherwise - prefer just increasing the `y` field by
|
||||
/// one instead), since then the position below a row becomes different
|
||||
/// from the position above the next row due to the spacing between both.
|
||||
#[default(OuterVAlignment::Top)]
|
||||
pub position: OuterVAlignment,
|
||||
}
|
||||
|
||||
/// A vertical line in the grid.
|
||||
///
|
||||
/// Overrides any per-cell stroke, including stroke specified through the
|
||||
/// grid's `stroke` field. Can cross spacing between cells created through
|
||||
/// the grid's `row-gutter` option.
|
||||
#[elem(name = "vline", title = "Grid Vertical Line")]
|
||||
pub struct GridVLine {
|
||||
/// The column before which the horizontal line is placed (zero-indexed).
|
||||
/// If the `position` field is set to `{end}`, the line is placed after the
|
||||
/// column with the given index instead (see that field's docs for
|
||||
/// details).
|
||||
///
|
||||
/// Specifying `{auto}` causes the line to be placed at the column after
|
||||
/// the last automatically positioned cell (that is, cell without
|
||||
/// coordinate overrides) before the line among the grid's children. If
|
||||
/// there is no such cell before the line, it is placed before the grid's
|
||||
/// first column (column 0).
|
||||
/// Note that specifying for this option exactly the total amount of
|
||||
/// columns in the grid causes this vertical line to override the end
|
||||
/// border of the grid (right in LTR, left in RTL), while a value of 0
|
||||
/// overrides the start border (left in LTR, right in RTL).
|
||||
pub x: Smart<usize>,
|
||||
|
||||
/// The row at which the vertical line starts (zero-indexed, inclusive).
|
||||
pub start: usize,
|
||||
|
||||
/// The row on top of which the vertical line ends (zero-indexed,
|
||||
/// exclusive).
|
||||
/// Therefore, the vertical line will be drawn up to and across row
|
||||
/// `end - 1`.
|
||||
///
|
||||
/// A value equal to `{none}` or to the amount of rows causes it to extend
|
||||
/// all the way towards the bottom of the grid.
|
||||
pub end: Option<NonZeroUsize>,
|
||||
|
||||
/// The line's stroke.
|
||||
///
|
||||
/// Specifying `{none}` interrupts previous vlines placed across this
|
||||
/// line's range, but does not affect per-cell stroke or hlines.
|
||||
#[resolve]
|
||||
#[fold]
|
||||
#[default(Some(Arc::new(Stroke::default())))]
|
||||
pub stroke: Option<Arc<Stroke>>,
|
||||
|
||||
/// The position at which the line is placed, given its column (`x`) -
|
||||
/// either `{start}` to draw before it or `{end}` to draw after it.
|
||||
///
|
||||
/// The values `{left}` and `{right}` are also accepted, but discouraged as
|
||||
/// they cause your grid to be inconsistent between left-to-right and
|
||||
/// right-to-left documents.
|
||||
///
|
||||
/// This setting is only relevant when column gutter is enabled (and
|
||||
/// shouldn't be used otherwise - prefer just increasing the `x` field by
|
||||
/// one instead), since then the position after a column becomes different
|
||||
/// from the position before the next column due to the spacing between
|
||||
/// both.
|
||||
#[default(OuterHAlignment::Start)]
|
||||
pub position: OuterHAlignment,
|
||||
}
|
||||
|
||||
/// A cell in the grid. Use this to either override grid properties for a
|
||||
/// particular cell, or in show rules to apply certain styles to multiple cells
|
||||
/// at once.
|
||||
@ -383,7 +576,7 @@ cast! {
|
||||
pub struct GridCell {
|
||||
/// The cell's body.
|
||||
#[required]
|
||||
body: Content,
|
||||
pub body: Content,
|
||||
|
||||
/// The cell's column (zero-indexed).
|
||||
/// This field may be used in show rules to style a cell depending on its
|
||||
@ -408,7 +601,7 @@ pub struct GridCell {
|
||||
/// [1], grid.cell(x: 3)[4], [2],
|
||||
/// )
|
||||
/// ```
|
||||
x: Smart<usize>,
|
||||
pub x: Smart<usize>,
|
||||
|
||||
/// The cell's row (zero-indexed).
|
||||
/// This field may be used in show rules to style a cell depending on its
|
||||
@ -427,20 +620,25 @@ pub struct GridCell {
|
||||
/// [A], grid.cell(y: 1)[B], grid.cell(y: 1)[C], grid.cell(y: 2)[D]
|
||||
/// )
|
||||
/// ```
|
||||
y: Smart<usize>,
|
||||
pub y: Smart<usize>,
|
||||
|
||||
/// The amount of columns spanned by this cell.
|
||||
#[default(NonZeroUsize::ONE)]
|
||||
colspan: NonZeroUsize,
|
||||
pub colspan: NonZeroUsize,
|
||||
|
||||
/// The cell's fill override.
|
||||
fill: Smart<Option<Paint>>,
|
||||
pub fill: Smart<Option<Paint>>,
|
||||
|
||||
/// The cell's alignment override.
|
||||
align: Smart<Alignment>,
|
||||
pub align: Smart<Alignment>,
|
||||
|
||||
/// The cell's inset override.
|
||||
inset: Smart<Sides<Option<Rel<Length>>>>,
|
||||
pub inset: Smart<Sides<Option<Rel<Length>>>>,
|
||||
|
||||
/// The cell's stroke override.
|
||||
#[resolve]
|
||||
#[fold]
|
||||
pub stroke: Sides<Option<Option<Arc<Stroke>>>>,
|
||||
}
|
||||
|
||||
cast! {
|
||||
@ -462,11 +660,27 @@ impl ResolvableCell for Packed<GridCell> {
|
||||
fill: &Option<Paint>,
|
||||
align: Smart<Alignment>,
|
||||
inset: Sides<Option<Rel<Length>>>,
|
||||
stroke: Sides<Option<Option<Arc<Stroke<Abs>>>>>,
|
||||
styles: StyleChain,
|
||||
) -> Cell {
|
||||
let cell = &mut *self;
|
||||
let colspan = cell.colspan(styles);
|
||||
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()));
|
||||
@ -482,7 +696,26 @@ impl ResolvableCell for Packed<GridCell> {
|
||||
cell.push_inset(Smart::Custom(
|
||||
cell.inset(styles).map_or(inset, |inner| inner.fold(inset)),
|
||||
));
|
||||
Cell { body: self.pack(), fill, colspan }
|
||||
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 {
|
||||
body: self.pack(),
|
||||
fill,
|
||||
colspan,
|
||||
stroke,
|
||||
stroke_overridden,
|
||||
}
|
||||
}
|
||||
|
||||
fn x(&self, styles: StyleChain) -> Smart<usize> {
|
||||
|
@ -3,7 +3,8 @@ use std::ops::Add;
|
||||
|
||||
use crate::diag::{bail, StrResult};
|
||||
use crate::foundations::{
|
||||
cast, CastInfo, Dict, Fold, FromValue, IntoValue, Reflect, Resolve, StyleChain, Value,
|
||||
cast, AlternativeFold, CastInfo, Dict, Fold, FromValue, IntoValue, Reflect, Resolve,
|
||||
StyleChain, Value,
|
||||
};
|
||||
use crate::layout::{Abs, Alignment, Axes, Axis, Corner, Rel, Size};
|
||||
use crate::util::Get;
|
||||
@ -245,13 +246,10 @@ impl<T: Resolve> Resolve for Sides<T> {
|
||||
|
||||
impl<T: Fold> Fold for Sides<Option<T>> {
|
||||
fn fold(self, outer: Self) -> Self {
|
||||
self.zip(outer).map(|(inner, outer)| match (inner, outer) {
|
||||
(Some(inner), Some(outer)) => Some(inner.fold(outer)),
|
||||
// Usually, folding an inner `None` with an `outer` preferres the
|
||||
// explicit `None`. However, here `None` means unspecified and thus
|
||||
// we want `outer`.
|
||||
(inner, outer) => inner.or(outer),
|
||||
})
|
||||
// Usually, folding an inner `None` with an `outer` preferres the
|
||||
// explicit `None`. However, here `None` means unspecified and thus
|
||||
// we want `outer`, so we use `fold_or` to opt into such behavior.
|
||||
self.zip(outer).map(|(inner, outer)| inner.fold_or(outer))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -29,7 +29,8 @@ use crate::foundations::{
|
||||
};
|
||||
use crate::introspection::{Introspector, Locatable, Location};
|
||||
use crate::layout::{
|
||||
BlockElem, Em, GridCell, GridElem, HElem, PadElem, Sizing, TrackSizings, VElem,
|
||||
BlockElem, Em, GridCell, GridChild, GridElem, HElem, PadElem, Sizing, TrackSizings,
|
||||
VElem,
|
||||
};
|
||||
use crate::model::{
|
||||
CitationForm, CiteGroup, Destination, FootnoteElem, HeadingElem, LinkElem, ParElem,
|
||||
@ -237,11 +238,13 @@ impl Show for Packed<BibliographyElem> {
|
||||
if references.iter().any(|(prefix, _)| prefix.is_some()) {
|
||||
let mut cells = vec![];
|
||||
for (prefix, reference) in references {
|
||||
cells.push(
|
||||
cells.push(GridChild::Cell(
|
||||
Packed::new(GridCell::new(prefix.clone().unwrap_or_default()))
|
||||
.spanned(span),
|
||||
);
|
||||
cells.push(Packed::new(GridCell::new(reference.clone())).spanned(span));
|
||||
));
|
||||
cells.push(GridChild::Cell(
|
||||
Packed::new(GridCell::new(reference.clone())).spanned(span),
|
||||
));
|
||||
}
|
||||
|
||||
seq.push(VElem::new(row_gutter).with_weakness(3).pack());
|
||||
@ -945,8 +948,8 @@ impl ElemRenderer<'_> {
|
||||
if let Some(prefix) = suf_prefix {
|
||||
const COLUMN_GUTTER: Em = Em::new(0.65);
|
||||
content = GridElem::new(vec![
|
||||
Packed::new(GridCell::new(prefix)).spanned(self.span),
|
||||
Packed::new(GridCell::new(content)).spanned(self.span),
|
||||
GridChild::Cell(Packed::new(GridCell::new(prefix)).spanned(self.span)),
|
||||
GridChild::Cell(Packed::new(GridCell::new(content)).spanned(self.span)),
|
||||
])
|
||||
.with_columns(TrackSizings(smallvec![Sizing::Auto; 2]))
|
||||
.with_column_gutter(TrackSizings(smallvec![COLUMN_GUTTER.into()]))
|
||||
|
@ -271,7 +271,6 @@ impl LayoutMultiple for Packed<EnumElem> {
|
||||
number = number.saturating_add(1);
|
||||
}
|
||||
|
||||
let stroke = None;
|
||||
let grid = CellGrid::new(
|
||||
Axes::with_x(&[
|
||||
Sizing::Rel(indent.into()),
|
||||
@ -282,7 +281,7 @@ impl LayoutMultiple for Packed<EnumElem> {
|
||||
Axes::with_y(&[gutter.into()]),
|
||||
cells,
|
||||
);
|
||||
let layouter = GridLayouter::new(&grid, &stroke, regions, styles, self.span());
|
||||
let layouter = GridLayouter::new(&grid, regions, styles, self.span());
|
||||
|
||||
layouter.layout(engine)
|
||||
}
|
||||
|
@ -168,7 +168,6 @@ impl LayoutMultiple for Packed<ListElem> {
|
||||
));
|
||||
}
|
||||
|
||||
let stroke = None;
|
||||
let grid = CellGrid::new(
|
||||
Axes::with_x(&[
|
||||
Sizing::Rel(indent.into()),
|
||||
@ -179,7 +178,7 @@ impl LayoutMultiple for Packed<ListElem> {
|
||||
Axes::with_y(&[gutter.into()]),
|
||||
cells,
|
||||
);
|
||||
let layouter = GridLayouter::new(&grid, &stroke, regions, styles, self.span());
|
||||
let layouter = GridLayouter::new(&grid, regions, styles, self.span());
|
||||
|
||||
layouter.layout(engine)
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
use std::num::NonZeroUsize;
|
||||
use std::sync::Arc;
|
||||
|
||||
use ecow::eco_format;
|
||||
|
||||
@ -8,12 +9,13 @@ use crate::foundations::{
|
||||
cast, elem, scope, Content, Fold, Packed, Show, Smart, StyleChain,
|
||||
};
|
||||
use crate::layout::{
|
||||
show_grid_cell, Abs, Alignment, Axes, Cell, CellGrid, Celled, Fragment, GridLayouter,
|
||||
LayoutMultiple, Length, Regions, Rel, ResolvableCell, Sides, TrackSizings,
|
||||
show_grid_cell, Abs, Alignment, Axes, Cell, CellGrid, Celled, Dir, Fragment,
|
||||
GridItem, GridLayouter, LayoutMultiple, Length, LinePosition, OuterHAlignment,
|
||||
OuterVAlignment, Regions, Rel, ResolvableCell, Sides, TrackSizings,
|
||||
};
|
||||
use crate::model::Figurable;
|
||||
use crate::syntax::Span;
|
||||
use crate::text::{Lang, LocalName, Region};
|
||||
use crate::text::{Lang, LocalName, Region, TextElem};
|
||||
use crate::util::NonZeroExt;
|
||||
use crate::visualize::{Paint, Stroke};
|
||||
|
||||
@ -166,13 +168,17 @@ pub struct TableElem {
|
||||
///
|
||||
/// 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/).
|
||||
/// If it is necessary to place lines which can cross spacing between cells
|
||||
/// produced by the `gutter` option, or to override the stroke between
|
||||
/// multiple specific cells, consider specifying one or more of
|
||||
/// [`table.hline`]($table.hline) and [`table.vline`]($table.vline) alongside
|
||||
/// your table cells.
|
||||
///
|
||||
/// See the [grid documentation]($grid) for more information on stroke.
|
||||
#[resolve]
|
||||
#[fold]
|
||||
#[default(Some(Stroke::default()))]
|
||||
pub stroke: Option<Stroke>,
|
||||
#[default(Celled::Value(Sides::splat(Some(Some(Arc::new(Stroke::default()))))))]
|
||||
pub stroke: Celled<Sides<Option<Option<Arc<Stroke>>>>>,
|
||||
|
||||
/// How much to pad the cells' content.
|
||||
///
|
||||
@ -197,15 +203,23 @@ pub struct TableElem {
|
||||
#[default(Sides::splat(Some(Abs::pt(5.0).into())))]
|
||||
pub inset: Sides<Option<Rel<Length>>>,
|
||||
|
||||
/// The contents of the table cells.
|
||||
/// The contents of the table cells, plus any extra table lines specified
|
||||
/// with the [`table.hline`]($table.hline) and
|
||||
/// [`table.vline`]($table.vline) elements.
|
||||
#[variadic]
|
||||
pub children: Vec<Packed<TableCell>>,
|
||||
pub children: Vec<TableChild>,
|
||||
}
|
||||
|
||||
#[scope]
|
||||
impl TableElem {
|
||||
#[elem]
|
||||
type TableCell;
|
||||
|
||||
#[elem]
|
||||
type TableHLine;
|
||||
|
||||
#[elem]
|
||||
type TableVLine;
|
||||
}
|
||||
|
||||
impl LayoutMultiple for Packed<TableElem> {
|
||||
@ -223,26 +237,60 @@ impl LayoutMultiple for Packed<TableElem> {
|
||||
let column_gutter = self.column_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 stroke = self.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 items = self.children().iter().map(|child| match child {
|
||||
TableChild::HLine(hline) => GridItem::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,
|
||||
},
|
||||
},
|
||||
TableChild::VLine(vline) => GridItem::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,
|
||||
},
|
||||
},
|
||||
TableChild::Cell(cell) => GridItem::Cell(cell.clone()),
|
||||
});
|
||||
let grid = CellGrid::resolve(
|
||||
tracks,
|
||||
gutter,
|
||||
self.children(),
|
||||
items,
|
||||
fill,
|
||||
align,
|
||||
inset,
|
||||
&stroke,
|
||||
engine,
|
||||
styles,
|
||||
self.span(),
|
||||
)
|
||||
.trace(engine.world, tracepoint, self.span())?;
|
||||
|
||||
let layouter = GridLayouter::new(&grid, &stroke, regions, styles, self.span());
|
||||
let layouter = GridLayouter::new(&grid, regions, styles, self.span());
|
||||
layouter.layout(engine)
|
||||
}
|
||||
}
|
||||
@ -286,6 +334,122 @@ impl LocalName for Packed<TableElem> {
|
||||
|
||||
impl Figurable for Packed<TableElem> {}
|
||||
|
||||
/// Any child of a table element.
|
||||
#[derive(Debug, PartialEq, Clone, Hash)]
|
||||
pub enum TableChild {
|
||||
HLine(Packed<TableHLine>),
|
||||
VLine(Packed<TableVLine>),
|
||||
Cell(Packed<TableCell>),
|
||||
}
|
||||
|
||||
cast! {
|
||||
TableChild,
|
||||
self => match self {
|
||||
Self::HLine(hline) => hline.into_value(),
|
||||
Self::VLine(vline) => vline.into_value(),
|
||||
Self::Cell(cell) => cell.into_value(),
|
||||
},
|
||||
v: Content => v.into(),
|
||||
}
|
||||
|
||||
impl From<Content> for TableChild {
|
||||
fn from(value: Content) -> Self {
|
||||
value
|
||||
.into_packed::<TableHLine>()
|
||||
.map(TableChild::HLine)
|
||||
.or_else(|value| value.into_packed::<TableVLine>().map(TableChild::VLine))
|
||||
.or_else(|value| value.into_packed::<TableCell>().map(TableChild::Cell))
|
||||
.unwrap_or_else(|value| {
|
||||
let span = value.span();
|
||||
TableChild::Cell(Packed::new(TableCell::new(value)).spanned(span))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A horizontal line in the table. See the docs for
|
||||
/// [`grid.hline`]($grid.hline) for more information regarding how to use this
|
||||
/// element's fields.
|
||||
///
|
||||
/// Overrides any per-cell stroke, including stroke specified through the
|
||||
/// table's `stroke` field. Can cross spacing between cells created through
|
||||
/// the table's `column-gutter` option.
|
||||
#[elem(name = "hline", title = "Table Horizontal Line")]
|
||||
pub struct TableHLine {
|
||||
/// The row above which the horizontal line is placed (zero-indexed).
|
||||
/// Functions identically to the `y` field in [`grid.hline`]($grid.hline).
|
||||
pub y: Smart<usize>,
|
||||
|
||||
/// The column at which the horizontal line starts (zero-indexed, inclusive).
|
||||
pub start: usize,
|
||||
|
||||
/// The column before which the horizontal line ends (zero-indexed,
|
||||
/// exclusive).
|
||||
pub end: Option<NonZeroUsize>,
|
||||
|
||||
/// The line's stroke.
|
||||
///
|
||||
/// Specifying `{none}` interrupts previous hlines placed across this
|
||||
/// line's range, but does not affect per-cell stroke or vlines.
|
||||
#[resolve]
|
||||
#[fold]
|
||||
#[default(Some(Arc::new(Stroke::default())))]
|
||||
pub stroke: Option<Arc<Stroke>>,
|
||||
|
||||
/// The position at which the line is placed, given its row (`y`) - either
|
||||
/// `{top}` to draw above it or `{bottom}` to draw below it.
|
||||
///
|
||||
/// This setting is only relevant when row gutter is enabled (and
|
||||
/// shouldn't be used otherwise - prefer just increasing the `y` field by
|
||||
/// one instead), since then the position below a row becomes different
|
||||
/// from the position above the next row due to the spacing between both.
|
||||
#[default(OuterVAlignment::Top)]
|
||||
pub position: OuterVAlignment,
|
||||
}
|
||||
|
||||
/// A vertical line in the table. See the docs for [`grid.vline`]($grid.vline)
|
||||
/// for more information regarding how to use this element's fields.
|
||||
///
|
||||
/// Overrides any per-cell stroke, including stroke specified through the
|
||||
/// table's `stroke` field. Can cross spacing between cells created through
|
||||
/// the table's `row-gutter` option.
|
||||
#[elem(name = "vline", title = "Table Vertical Line")]
|
||||
pub struct TableVLine {
|
||||
/// The column before which the horizontal line is placed (zero-indexed).
|
||||
/// Functions identically to the `x` field in [`grid.vline`]($grid.vline).
|
||||
pub x: Smart<usize>,
|
||||
|
||||
/// The row at which the vertical line starts (zero-indexed, inclusive).
|
||||
pub start: usize,
|
||||
|
||||
/// The row on top of which the vertical line ends (zero-indexed,
|
||||
/// exclusive).
|
||||
pub end: Option<NonZeroUsize>,
|
||||
|
||||
/// The line's stroke.
|
||||
///
|
||||
/// Specifying `{none}` interrupts previous vlines placed across this
|
||||
/// line's range, but does not affect per-cell stroke or hlines.
|
||||
#[resolve]
|
||||
#[fold]
|
||||
#[default(Some(Arc::new(Stroke::default())))]
|
||||
pub stroke: Option<Arc<Stroke>>,
|
||||
|
||||
/// The position at which the line is placed, given its column (`x`) -
|
||||
/// either `{start}` to draw before it or `{end}` to draw after it.
|
||||
///
|
||||
/// The values `{left}` and `{right}` are also accepted, but discouraged as
|
||||
/// they cause your table to be inconsistent between left-to-right and
|
||||
/// right-to-left documents.
|
||||
///
|
||||
/// This setting is only relevant when column gutter is enabled (and
|
||||
/// shouldn't be used otherwise - prefer just increasing the `x` field by
|
||||
/// one instead), since then the position after a column becomes different
|
||||
/// from the position before the next column due to the spacing between
|
||||
/// both.
|
||||
#[default(OuterHAlignment::Start)]
|
||||
pub position: OuterHAlignment,
|
||||
}
|
||||
|
||||
/// A cell in the table. Use this to either override table properties for a
|
||||
/// particular cell, or in show rules to apply certain styles to multiple cells
|
||||
/// at once.
|
||||
@ -336,28 +500,33 @@ impl Figurable for Packed<TableElem> {}
|
||||
pub struct TableCell {
|
||||
/// The cell's body.
|
||||
#[required]
|
||||
body: Content,
|
||||
pub body: Content,
|
||||
|
||||
/// The cell's column (zero-indexed).
|
||||
/// Functions identically to the `x` field in [`grid.cell`]($grid.cell).
|
||||
x: Smart<usize>,
|
||||
pub x: Smart<usize>,
|
||||
|
||||
/// The cell's row (zero-indexed).
|
||||
/// Functions identically to the `y` field in [`grid.cell`]($grid.cell).
|
||||
y: Smart<usize>,
|
||||
pub y: Smart<usize>,
|
||||
|
||||
/// The cell's fill override.
|
||||
fill: Smart<Option<Paint>>,
|
||||
pub fill: Smart<Option<Paint>>,
|
||||
|
||||
/// The amount of columns spanned by this cell.
|
||||
#[default(NonZeroUsize::ONE)]
|
||||
colspan: NonZeroUsize,
|
||||
pub colspan: NonZeroUsize,
|
||||
|
||||
/// The cell's alignment override.
|
||||
align: Smart<Alignment>,
|
||||
pub align: Smart<Alignment>,
|
||||
|
||||
/// The cell's inset override.
|
||||
inset: Smart<Sides<Option<Rel<Length>>>>,
|
||||
pub inset: Smart<Sides<Option<Rel<Length>>>>,
|
||||
|
||||
/// The cell's stroke override.
|
||||
#[resolve]
|
||||
#[fold]
|
||||
pub stroke: Sides<Option<Option<Arc<Stroke>>>>,
|
||||
}
|
||||
|
||||
cast! {
|
||||
@ -379,11 +548,27 @@ impl ResolvableCell for Packed<TableCell> {
|
||||
fill: &Option<Paint>,
|
||||
align: Smart<Alignment>,
|
||||
inset: Sides<Option<Rel<Length>>>,
|
||||
stroke: Sides<Option<Option<Arc<Stroke<Abs>>>>>,
|
||||
styles: StyleChain,
|
||||
) -> Cell {
|
||||
let cell = &mut *self;
|
||||
let colspan = cell.colspan(styles);
|
||||
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()));
|
||||
@ -399,7 +584,26 @@ impl ResolvableCell for Packed<TableCell> {
|
||||
cell.push_inset(Smart::Custom(
|
||||
cell.inset(styles).map_or(inset, |inner| inner.fold(inset)),
|
||||
));
|
||||
Cell { body: self.pack(), fill, colspan }
|
||||
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 {
|
||||
body: self.pack(),
|
||||
fill,
|
||||
colspan,
|
||||
stroke,
|
||||
stroke_overridden,
|
||||
}
|
||||
}
|
||||
|
||||
fn x(&self, styles: StyleChain) -> Smart<usize> {
|
||||
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 89 KiB |
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 53 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 44 KiB |
BIN
tests/ref/layout/grid-stroke.png
Normal file
After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
@ -88,3 +88,52 @@
|
||||
[e], [g], grid.cell(colspan: 2)[eee\ e\ e\ e],
|
||||
grid.cell(colspan: 4)[eeee e e e]
|
||||
)
|
||||
|
||||
---
|
||||
// Test left and right for vlines in RTL
|
||||
#set text(dir: rtl)
|
||||
#grid(
|
||||
columns: 3,
|
||||
inset: 5pt,
|
||||
grid.vline(stroke: red, position: left), grid.vline(stroke: green, position: right), [a],
|
||||
grid.vline(stroke: red, position: left), grid.vline(stroke: 2pt, position: right), [b],
|
||||
grid.vline(stroke: red, position: left), grid.vline(stroke: 2pt, position: right), [c],
|
||||
grid.vline(stroke: aqua, position: right)
|
||||
)
|
||||
|
||||
#grid(
|
||||
columns: 3,
|
||||
inset: 5pt,
|
||||
gutter: 3pt,
|
||||
grid.vline(stroke: green, position: left), grid.vline(stroke: red, position: right), [a],
|
||||
grid.vline(stroke: blue, position: left), grid.vline(stroke: red, position: right), [b],
|
||||
grid.vline(stroke: blue, position: left), grid.vline(stroke: red, position: right), [c],
|
||||
grid.vline(stroke: 2pt, position: right)
|
||||
)
|
||||
|
||||
#grid(
|
||||
columns: 3,
|
||||
inset: 5pt,
|
||||
grid.vline(stroke: green, position: start), grid.vline(stroke: red, position: end), [a],
|
||||
grid.vline(stroke: 2pt, position: start), grid.vline(stroke: red, position: end), [b],
|
||||
grid.vline(stroke: 2pt, position: start), grid.vline(stroke: red, position: end), [c],
|
||||
grid.vline(stroke: 2pt, position: start)
|
||||
)
|
||||
|
||||
#grid(
|
||||
columns: 3,
|
||||
inset: 5pt,
|
||||
gutter: 3pt,
|
||||
grid.vline(stroke: green, position: start), grid.vline(stroke: red, position: end), [a],
|
||||
grid.vline(stroke: blue, position: start), grid.vline(stroke: red, position: end), [b],
|
||||
grid.vline(stroke: blue, position: start), grid.vline(stroke: red, position: end), [c],
|
||||
grid.vline(stroke: 2pt, position: start)
|
||||
)
|
||||
|
||||
---
|
||||
// Error: 3:8-3:34 cannot place vertical line at the 'end' position of the end border (x = 1)
|
||||
// Hint: 3:8-3:34 set the line's position to 'start' or place it at a smaller 'x' index
|
||||
#set text(dir: rtl)
|
||||
#grid(
|
||||
[a], grid.vline(position: left)
|
||||
)
|
||||
|
379
tests/typ/layout/grid-stroke.typ
Normal file
@ -0,0 +1,379 @@
|
||||
#let double-line = pattern(size: (1.5pt, 1.5pt), {
|
||||
place(line(stroke: .6pt, start: (0%, 50%), end: (100%, 50%)))
|
||||
})
|
||||
|
||||
#table(
|
||||
stroke: (_, y) => if y != 1 { (bottom: black) },
|
||||
columns: 3,
|
||||
table.cell(colspan: 3, align: center)[*Epic Table*],
|
||||
align(center)[*Name*], align(center)[*Age*], align(center)[*Data*],
|
||||
table.hline(stroke: (paint: double-line, thickness: 2pt)),
|
||||
[John], [30], [None],
|
||||
[Martha], [20], [A],
|
||||
[Joseph], [35], [D]
|
||||
)
|
||||
|
||||
---
|
||||
// Test folding
|
||||
#set grid(stroke: red)
|
||||
#set grid(stroke: 5pt)
|
||||
|
||||
#grid(
|
||||
inset: 10pt,
|
||||
columns: 2,
|
||||
stroke: stroke(dash: "loosely-dotted"),
|
||||
grid.vline(start: 2, end: 3, stroke: (paint: green, dash: none)),
|
||||
[a], [b],
|
||||
grid.hline(end: 1, stroke: blue),
|
||||
[c], [d],
|
||||
[e], grid.cell(stroke: aqua)[f]
|
||||
)
|
||||
|
||||
---
|
||||
// Test set rules on cells and folding
|
||||
#set table.cell(stroke: 4pt)
|
||||
#set table.cell(stroke: blue)
|
||||
#set table.hline(stroke: red)
|
||||
#set table.hline(stroke: 0.75pt)
|
||||
#set table.vline(stroke: 0.75pt)
|
||||
#set table.vline(stroke: aqua)
|
||||
|
||||
#table(
|
||||
columns: 3,
|
||||
gutter: 3pt,
|
||||
inset: 5pt,
|
||||
[a], [b], table.vline(position: end), [c],
|
||||
[d], [e], [f],
|
||||
table.hline(position: bottom),
|
||||
[g], [h], [i],
|
||||
)
|
||||
|
||||
---
|
||||
// Test stroke field on cell show rules
|
||||
#set grid.cell(stroke: (x: 4pt))
|
||||
#set grid.cell(stroke: (x: blue))
|
||||
#show grid.cell: it => {
|
||||
test(it.stroke, (left: stroke(paint: blue, thickness: 4pt, dash: "loosely-dotted"), right: blue + 4pt, top: stroke(thickness: 1pt), bottom: none))
|
||||
it
|
||||
}
|
||||
#grid(
|
||||
stroke: (left: (dash: "loosely-dotted")),
|
||||
inset: 5pt,
|
||||
grid.hline(stroke: red),
|
||||
grid.cell(stroke: (top: 1pt))[a], grid.vline(stroke: yellow),
|
||||
)
|
||||
|
||||
---
|
||||
#table(
|
||||
columns: 3,
|
||||
[a], table.cell(colspan: 2)[b c],
|
||||
table.cell(stroke: blue)[d], [e], [f],
|
||||
[g], [h], table.cell(stroke: (left: yellow, top: green, right: aqua, bottom: red))[i],
|
||||
[j], [k], [l],
|
||||
table.cell(stroke: 3pt)[m], [n], table.cell(stroke: (dash: "loosely-dotted"))[o],
|
||||
)
|
||||
|
||||
---
|
||||
// Test per-column stroke array
|
||||
#let t = table(
|
||||
columns: 3,
|
||||
stroke: (red, blue, green),
|
||||
[a], [b], [c],
|
||||
[d], [e], [f],
|
||||
[h], [i], [j],
|
||||
)
|
||||
#t
|
||||
#set text(dir: rtl)
|
||||
#t
|
||||
|
||||
---
|
||||
#grid(
|
||||
columns: 3,
|
||||
inset: 3pt,
|
||||
stroke: (x, _) => (right: (5pt, (dash: "dotted")).at(calc.rem(x, 2)), bottom: (dash: "densely-dotted")),
|
||||
grid.vline(x: 0, stroke: red),
|
||||
grid.vline(x: 1, stroke: red),
|
||||
grid.vline(x: 2, stroke: red),
|
||||
grid.vline(x: 3, stroke: red),
|
||||
grid.hline(y: 0, end: 1, stroke: blue),
|
||||
grid.hline(y: 1, end: 1, stroke: blue),
|
||||
grid.cell[a],
|
||||
[b], [c]
|
||||
)
|
||||
|
||||
---
|
||||
#set page(height: 5em)
|
||||
#table(
|
||||
columns: 3,
|
||||
inset: 3pt,
|
||||
table.hline(y: 0, end: none, stroke: 3pt + blue),
|
||||
table.vline(x: 0, end: none, stroke: 3pt + green),
|
||||
table.hline(y: 5, end: none, stroke: 3pt + red),
|
||||
table.vline(x: 3, end: none, stroke: 3pt + yellow),
|
||||
[a], [b], [c],
|
||||
[a], [b], [c],
|
||||
[a], [b], [c],
|
||||
[a], [b], [c],
|
||||
[a], [b], [c],
|
||||
)
|
||||
|
||||
---
|
||||
// Automatically positioned lines
|
||||
// Plus stroke thickness ordering
|
||||
#table(
|
||||
columns: 3,
|
||||
table.hline(stroke: red + 5pt),
|
||||
table.vline(stroke: blue + 5pt),
|
||||
table.vline(stroke: 2pt),
|
||||
[a],
|
||||
table.vline(x: 1, stroke: aqua + 5pt),
|
||||
[b],
|
||||
table.vline(stroke: aqua + 5pt),
|
||||
[c],
|
||||
table.vline(stroke: yellow + 5.2pt),
|
||||
table.hline(stroke: green + 5pt),
|
||||
[a], [b], [c],
|
||||
[a], table.hline(stroke: green + 2pt), table.vline(stroke: 2pt), [b], [c],
|
||||
)
|
||||
|
||||
---
|
||||
// Line specification order priority
|
||||
// The last line should be blue, not red.
|
||||
// The middle line should have disappeared.
|
||||
#grid(
|
||||
columns: 2,
|
||||
inset: 2pt,
|
||||
grid.hline(y: 2, stroke: red + 5pt),
|
||||
grid.vline(),
|
||||
[a], [b],
|
||||
grid.hline(stroke: red),
|
||||
grid.hline(stroke: none),
|
||||
[c], grid.cell(stroke: (top: aqua))[d],
|
||||
grid.hline(stroke: blue),
|
||||
)
|
||||
|
||||
---
|
||||
// Position: bottom and position: end with gutter should have a visible effect
|
||||
// of moving the lines after the next track.
|
||||
#table(
|
||||
columns: 3,
|
||||
gutter: 3pt,
|
||||
stroke: blue,
|
||||
table.hline(end: 2, stroke: red),
|
||||
table.hline(end: 2, stroke: aqua, position: bottom),
|
||||
table.vline(end: 2, stroke: green), [a], table.vline(end: 2, stroke: green), table.vline(end: 2, position: end, stroke: orange), [b], table.vline(end: 2, stroke: aqua, position: end), table.vline(end: 2, stroke: green), [c], table.vline(end: 2, stroke: green),
|
||||
[d], [e], [f],
|
||||
table.hline(end: 2, stroke: red),
|
||||
[g], [h], [ie],
|
||||
table.hline(end: 2, stroke: green),
|
||||
)
|
||||
|
||||
---
|
||||
// Using position: bottom and position: end without gutter should be the same
|
||||
// as placing a line after the next track.
|
||||
#table(
|
||||
columns: 3,
|
||||
stroke: blue,
|
||||
table.hline(end: 2, stroke: red),
|
||||
table.hline(end: 2, stroke: aqua, position: bottom),
|
||||
table.vline(end: 2, stroke: green), [a], table.vline(end: 2, stroke: green), [b], table.vline(end: 2, stroke: aqua, position: end), table.vline(end: 2, stroke: green), [c], table.vline(end: 2, stroke: green),
|
||||
table.hline(end: 2, stroke: 5pt),
|
||||
[d], [e], [f],
|
||||
table.hline(end: 2, stroke: red),
|
||||
[g], [h], [i],
|
||||
table.hline(end: 2, stroke: red),
|
||||
)
|
||||
|
||||
---
|
||||
// Test left and right for grid vlines.
|
||||
#grid(
|
||||
columns: 3,
|
||||
inset: 5pt,
|
||||
grid.vline(stroke: green, position: left), grid.vline(stroke: red, position: right), [a],
|
||||
grid.vline(stroke: 2pt, position: left), grid.vline(stroke: red, position: right), [b],
|
||||
grid.vline(stroke: 2pt, position: left), grid.vline(stroke: red, position: right), [c],
|
||||
grid.vline(stroke: 2pt, position: left)
|
||||
)
|
||||
|
||||
#grid(
|
||||
columns: 3,
|
||||
inset: 5pt,
|
||||
gutter: 3pt,
|
||||
grid.vline(stroke: green, position: left), grid.vline(stroke: red, position: right), [a],
|
||||
grid.vline(stroke: blue, position: left), grid.vline(stroke: red, position: right), [b],
|
||||
grid.vline(stroke: blue, position: left), grid.vline(stroke: red, position: right), [c],
|
||||
grid.vline(stroke: 2pt, position: left)
|
||||
)
|
||||
|
||||
---
|
||||
// Test left and right for table vlines.
|
||||
#table(
|
||||
columns: 3,
|
||||
inset: 5pt,
|
||||
table.vline(stroke: green, position: left), table.vline(stroke: red, position: right), [a],
|
||||
table.vline(stroke: 2pt, position: left), table.vline(stroke: red, position: right), [b],
|
||||
table.vline(stroke: 2pt, position: left), table.vline(stroke: red, position: right), [c],
|
||||
table.vline(stroke: 2pt, position: left)
|
||||
)
|
||||
|
||||
#table(
|
||||
columns: 3,
|
||||
inset: 5pt,
|
||||
gutter: 3pt,
|
||||
table.vline(stroke: green, position: left), table.vline(stroke: red, position: right), [a],
|
||||
table.vline(stroke: blue, position: left), table.vline(stroke: red, position: right), [b],
|
||||
table.vline(stroke: blue, position: left), table.vline(stroke: red, position: right), [c],
|
||||
table.vline(stroke: 2pt, position: left)
|
||||
)
|
||||
|
||||
---
|
||||
// Hlines and vlines should always appear on top of cell strokes.
|
||||
#table(
|
||||
columns: 3,
|
||||
stroke: aqua,
|
||||
table.vline(stroke: red, position: end), [a], table.vline(stroke: red), [b], [c],
|
||||
table.cell(stroke: blue)[d], [e], [f],
|
||||
table.hline(stroke: red),
|
||||
[g], table.cell(stroke: blue)[h], [i],
|
||||
)
|
||||
|
||||
#table(
|
||||
columns: 3,
|
||||
gutter: 3pt,
|
||||
stroke: aqua,
|
||||
table.vline(stroke: red, position: end), [a], table.vline(stroke: red), [b], [c],
|
||||
table.cell(stroke: blue)[d], [e], [f],
|
||||
table.hline(stroke: red),
|
||||
[g], table.cell(stroke: blue)[h], [i],
|
||||
)
|
||||
|
||||
---
|
||||
// Ensure cell stroke overrides always appear on top.
|
||||
#table(
|
||||
columns: 2,
|
||||
stroke: black,
|
||||
table.cell(stroke: red)[a], [b],
|
||||
[c], [d],
|
||||
)
|
||||
|
||||
#table(
|
||||
columns: 2,
|
||||
table.cell(stroke: red)[a], [b],
|
||||
[c], [d],
|
||||
)
|
||||
|
||||
---
|
||||
// Error: 7:3-7:32 cannot place horizontal line at the 'bottom' position of the bottom border (y = 2)
|
||||
// Hint: 7:3-7:32 set the line's position to 'top' or place it at a smaller 'y' index
|
||||
#table(
|
||||
columns: 2,
|
||||
[a], [b],
|
||||
[c], [d],
|
||||
table.hline(stroke: aqua),
|
||||
table.hline(position: top),
|
||||
table.hline(position: bottom)
|
||||
)
|
||||
|
||||
---
|
||||
// Error: 8:3-8:32 cannot place horizontal line at the 'bottom' position of the bottom border (y = 2)
|
||||
// Hint: 8:3-8:32 set the line's position to 'top' or place it at a smaller 'y' index
|
||||
#table(
|
||||
columns: 2,
|
||||
gutter: 3pt,
|
||||
[a], [b],
|
||||
[c], [d], table.vline(stroke: red),
|
||||
table.hline(stroke: aqua),
|
||||
table.hline(position: top),
|
||||
table.hline(position: bottom)
|
||||
)
|
||||
|
||||
---
|
||||
// Error: 6:3-6:28 cannot place vertical line at the 'end' position of the end border (x = 2)
|
||||
// Hint: 6:3-6:28 set the line's position to 'start' or place it at a smaller 'x' index
|
||||
#grid(
|
||||
columns: 2,
|
||||
[a], [b],
|
||||
grid.vline(stroke: aqua),
|
||||
grid.vline(position: start),
|
||||
grid.vline(position: end)
|
||||
)
|
||||
|
||||
---
|
||||
// Error: 7:3-7:28 cannot place vertical line at the 'end' position of the end border (x = 2)
|
||||
// Hint: 7:3-7:28 set the line's position to 'start' or place it at a smaller 'x' index
|
||||
#grid(
|
||||
columns: 2,
|
||||
gutter: 3pt,
|
||||
[a], [b],
|
||||
grid.vline(stroke: aqua),
|
||||
grid.vline(position: start),
|
||||
grid.vline(position: end)
|
||||
)
|
||||
|
||||
---
|
||||
// Error: 4:3-4:19 cannot place horizontal line at invalid row 3
|
||||
#grid(
|
||||
[a],
|
||||
[b],
|
||||
grid.hline(y: 3)
|
||||
)
|
||||
|
||||
---
|
||||
// Error: 5:3-5:19 cannot place horizontal line at invalid row 3
|
||||
#grid(
|
||||
gutter: 3pt,
|
||||
[a],
|
||||
[b],
|
||||
grid.hline(y: 3)
|
||||
)
|
||||
|
||||
---
|
||||
// Error: 4:3-4:20 cannot place vertical line at invalid column 3
|
||||
#table(
|
||||
columns: 2,
|
||||
[a], [b],
|
||||
table.vline(x: 3)
|
||||
)
|
||||
|
||||
---
|
||||
// Error: 5:3-5:20 cannot place vertical line at invalid column 3
|
||||
#table(
|
||||
columns: 2,
|
||||
gutter: 3pt,
|
||||
[a], [b],
|
||||
table.vline(x: 3)
|
||||
)
|
||||
|
||||
---
|
||||
// Error: 3:3-3:31 line cannot end before it starts
|
||||
#grid(
|
||||
columns: 3,
|
||||
grid.hline(start: 2, end: 1),
|
||||
[a], [b], [c],
|
||||
)
|
||||
|
||||
---
|
||||
// Error: 3:3-3:32 line cannot end before it starts
|
||||
#table(
|
||||
columns: 3,
|
||||
table.vline(start: 2, end: 1),
|
||||
[a], [b], [c],
|
||||
[d], [e], [f],
|
||||
[g], [h], [i],
|
||||
)
|
||||
|
||||
---
|
||||
// Error: 24-31 expected `top` or `bottom`, found horizon
|
||||
#table.hline(position: horizon)
|
||||
|
||||
---
|
||||
// Error: 24-30 expected `start`, `left`, `right`, or `end`, found center
|
||||
#table.vline(position: center)
|
||||
|
||||
---
|
||||
// Error: 24-29 expected `top` or `bottom`, found right
|
||||
#table.hline(position: right)
|
||||
|
||||
---
|
||||
// Error: 24-27 expected `start`, `left`, `right`, or `end`, found top
|
||||
#table.vline(position: top)
|