Repeatable Table Headers [More Flexible Tables Pt.5a] (#3545)
@ -40,7 +40,7 @@ pub struct Line {
|
||||
/// its index. This is mostly only relevant when gutter is used, since, then,
|
||||
/// the position after a track is not the same as before the next
|
||||
/// non-gutter track.
|
||||
#[derive(PartialEq, Eq)]
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
pub enum LinePosition {
|
||||
/// The line should be drawn before its track (e.g. hline on top of a row).
|
||||
Before,
|
||||
@ -122,7 +122,6 @@ pub(super) fn generate_line_segments<'grid, F, I, L>(
|
||||
tracks: I,
|
||||
index: usize,
|
||||
lines: L,
|
||||
is_max_index: bool,
|
||||
line_stroke_at_track: F,
|
||||
) -> impl Iterator<Item = LineSegment> + 'grid
|
||||
where
|
||||
@ -154,22 +153,6 @@ where
|
||||
// How much to multiply line indices by to account for gutter.
|
||||
let gutter_factor = if grid.has_gutter { 2 } else { 1 };
|
||||
|
||||
// Which line position to look for in the given list of lines.
|
||||
//
|
||||
// If the index represents a gutter track, this means the list of lines
|
||||
// parameter will actually correspond to the list of lines in the previous
|
||||
// index, so we must look for lines positioned after the previous index,
|
||||
// and not before, to determine which lines should be placed in gutter.
|
||||
//
|
||||
// Note that the maximum index is always an odd number when there's gutter,
|
||||
// so we must check for it to ensure we don't give it the same treatment as
|
||||
// a line before a gutter track.
|
||||
let expected_line_position = if grid.is_gutter_track(index) && !is_max_index {
|
||||
LinePosition::After
|
||||
} else {
|
||||
LinePosition::Before
|
||||
};
|
||||
|
||||
// Create an iterator of line segments, which will go through each track,
|
||||
// from start to finish, to create line segments and extend them until they
|
||||
// are interrupted and thus yielded through the iterator. We then repeat
|
||||
@ -210,20 +193,18 @@ where
|
||||
let mut line_strokes = lines
|
||||
.clone()
|
||||
.filter(|line| {
|
||||
line.position == expected_line_position
|
||||
&& line
|
||||
.end
|
||||
.map(|end| {
|
||||
// Subtract 1 from end index so we stop at the last
|
||||
// cell before it (don't cross one extra gutter).
|
||||
let end = if grid.has_gutter {
|
||||
2 * end.get() - 1
|
||||
} else {
|
||||
end.get()
|
||||
};
|
||||
(gutter_factor * line.start..end).contains(&track)
|
||||
})
|
||||
.unwrap_or_else(|| track >= gutter_factor * line.start)
|
||||
line.end
|
||||
.map(|end| {
|
||||
// Subtract 1 from end index so we stop at the last
|
||||
// cell before it (don't cross one extra gutter).
|
||||
let end = if grid.has_gutter {
|
||||
2 * end.get() - 1
|
||||
} else {
|
||||
end.get()
|
||||
};
|
||||
(gutter_factor * line.start..end).contains(&track)
|
||||
})
|
||||
.unwrap_or_else(|| track >= gutter_factor * line.start)
|
||||
})
|
||||
.map(|line| line.stroke.clone());
|
||||
|
||||
@ -554,9 +535,25 @@ pub(super) fn hline_stroke_at_column(
|
||||
StrokePriority::GridStroke
|
||||
};
|
||||
|
||||
// Top border stroke and header stroke are generally prioritized, unless
|
||||
// they don't have explicit hline overrides and one or more user-provided
|
||||
// hlines would appear at the same position, which then are prioritized.
|
||||
let top_stroke_comes_from_header =
|
||||
grid.header
|
||||
.as_ref()
|
||||
.zip(local_top_y)
|
||||
.is_some_and(|(header, local_top_y)| {
|
||||
// Ensure the row above us is a repeated header.
|
||||
// FIXME: Make this check more robust when headers at arbitrary
|
||||
// positions are added.
|
||||
local_top_y + 1 == header.end && y != header.end
|
||||
});
|
||||
|
||||
let (prioritized_cell_stroke, deprioritized_cell_stroke) =
|
||||
if !use_bottom_border_stroke
|
||||
&& (use_top_border_stroke || top_cell_prioritized && !bottom_cell_prioritized)
|
||||
&& (use_top_border_stroke
|
||||
|| top_stroke_comes_from_header
|
||||
|| top_cell_prioritized && !bottom_cell_prioritized)
|
||||
{
|
||||
// Top border must always be prioritized, even if it did not
|
||||
// request for that explicitly.
|
||||
@ -660,6 +657,7 @@ mod test {
|
||||
},
|
||||
vec![],
|
||||
vec![],
|
||||
None,
|
||||
entries,
|
||||
)
|
||||
}
|
||||
@ -723,15 +721,8 @@ mod test {
|
||||
let tracks = rows.iter().map(|row| (row.y, row.height));
|
||||
assert_eq!(
|
||||
expected_splits,
|
||||
&generate_line_segments(
|
||||
&grid,
|
||||
tracks,
|
||||
x,
|
||||
&[],
|
||||
x == grid.cols.len(),
|
||||
vline_stroke_at_row
|
||||
)
|
||||
.collect::<Vec<_>>(),
|
||||
&generate_line_segments(&grid, tracks, x, &[], vline_stroke_at_row)
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -955,15 +946,8 @@ mod test {
|
||||
let tracks = rows.iter().map(|row| (row.y, row.height));
|
||||
assert_eq!(
|
||||
expected_splits,
|
||||
&generate_line_segments(
|
||||
&grid,
|
||||
tracks,
|
||||
x,
|
||||
&[],
|
||||
x == grid.cols.len(),
|
||||
vline_stroke_at_row
|
||||
)
|
||||
.collect::<Vec<_>>(),
|
||||
&generate_line_segments(&grid, tracks, x, &[], vline_stroke_at_row)
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1144,7 +1128,6 @@ mod test {
|
||||
position: LinePosition::After
|
||||
},
|
||||
],
|
||||
x == grid.cols.len(),
|
||||
vline_stroke_at_row
|
||||
)
|
||||
.collect::<Vec<_>>(),
|
||||
@ -1211,6 +1194,7 @@ mod test {
|
||||
},
|
||||
vec![],
|
||||
vec![],
|
||||
None,
|
||||
entries,
|
||||
)
|
||||
}
|
||||
@ -1297,22 +1281,17 @@ mod test {
|
||||
let tracks = columns.iter().copied().enumerate();
|
||||
assert_eq!(
|
||||
expected_splits,
|
||||
&generate_line_segments(
|
||||
&grid,
|
||||
tracks,
|
||||
y,
|
||||
&[],
|
||||
y == grid.rows.len(),
|
||||
|grid, y, x, stroke| hline_stroke_at_column(
|
||||
&generate_line_segments(&grid, tracks, y, &[], |grid, y, x, stroke| {
|
||||
hline_stroke_at_column(
|
||||
grid,
|
||||
&rows,
|
||||
y.checked_sub(1),
|
||||
true,
|
||||
y,
|
||||
x,
|
||||
stroke
|
||||
stroke,
|
||||
)
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
}
|
||||
@ -1496,7 +1475,6 @@ mod test {
|
||||
position: LinePosition::After
|
||||
},
|
||||
],
|
||||
y == grid.rows.len(),
|
||||
|grid, y, x, stroke| hline_stroke_at_column(
|
||||
grid,
|
||||
&rows,
|
||||
@ -1542,7 +1520,6 @@ mod test {
|
||||
columns.iter().copied().enumerate(),
|
||||
4,
|
||||
&[],
|
||||
4 == grid.rows.len(),
|
||||
|grid, y, x, stroke| hline_stroke_at_column(
|
||||
grid,
|
||||
&rows,
|
||||
|
@ -2,13 +2,16 @@ mod layout;
|
||||
mod lines;
|
||||
mod rowspans;
|
||||
|
||||
pub use self::layout::{Cell, CellGrid, Celled, GridItem, GridLayouter, ResolvableCell};
|
||||
pub use self::layout::{
|
||||
Cell, CellGrid, Celled, GridLayouter, ResolvableCell, ResolvableGridChild,
|
||||
ResolvableGridItem,
|
||||
};
|
||||
pub use self::lines::LinePosition;
|
||||
|
||||
use std::num::NonZeroUsize;
|
||||
use std::sync::Arc;
|
||||
|
||||
use ecow::eco_format;
|
||||
use ecow::{eco_format, EcoString};
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
|
||||
use crate::diag::{bail, SourceResult, StrResult, Trace, Tracepoint};
|
||||
@ -20,7 +23,7 @@ use crate::layout::{
|
||||
Abs, AlignElem, Alignment, Axes, Dir, Fragment, LayoutMultiple, Length,
|
||||
OuterHAlignment, OuterVAlignment, Regions, Rel, Sides, Sizing,
|
||||
};
|
||||
use crate::model::{TableCell, TableHLine, TableVLine};
|
||||
use crate::model::{TableCell, TableHLine, TableHeader, TableVLine};
|
||||
use crate::syntax::Span;
|
||||
use crate::text::TextElem;
|
||||
use crate::util::NonZeroExt;
|
||||
@ -293,6 +296,9 @@ impl GridElem {
|
||||
|
||||
#[elem]
|
||||
type GridVLine;
|
||||
|
||||
#[elem]
|
||||
type GridHeader;
|
||||
}
|
||||
|
||||
impl LayoutMultiple for Packed<GridElem> {
|
||||
@ -316,43 +322,20 @@ impl LayoutMultiple for Packed<GridElem> {
|
||||
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,
|
||||
},
|
||||
let children = self.children().iter().map(|child| match child {
|
||||
GridChild::Header(header) => ResolvableGridChild::Header {
|
||||
repeat: header.repeat(styles),
|
||||
span: header.span(),
|
||||
items: header.children().iter().map(|child| child.to_resolvable(styles)),
|
||||
},
|
||||
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()),
|
||||
GridChild::Item(item) => {
|
||||
ResolvableGridChild::Item(item.to_resolvable(styles))
|
||||
}
|
||||
});
|
||||
let grid = CellGrid::resolve(
|
||||
tracks,
|
||||
gutter,
|
||||
items,
|
||||
children,
|
||||
fill,
|
||||
align,
|
||||
&inset,
|
||||
@ -385,52 +368,136 @@ cast! {
|
||||
/// Any child of a grid element.
|
||||
#[derive(Debug, PartialEq, Clone, Hash)]
|
||||
pub enum GridChild {
|
||||
Header(Packed<GridHeader>),
|
||||
Item(GridItem),
|
||||
}
|
||||
|
||||
cast! {
|
||||
GridChild,
|
||||
self => match self {
|
||||
Self::Header(header) => header.into_value(),
|
||||
Self::Item(item) => item.into_value(),
|
||||
},
|
||||
v: Content => {
|
||||
v.try_into()?
|
||||
},
|
||||
}
|
||||
|
||||
impl TryFrom<Content> for GridChild {
|
||||
type Error = EcoString;
|
||||
fn try_from(value: Content) -> StrResult<Self> {
|
||||
if value.is::<TableHeader>() {
|
||||
bail!("cannot use `table.header` as a grid header; use `grid.header` instead")
|
||||
}
|
||||
|
||||
value
|
||||
.into_packed::<GridHeader>()
|
||||
.map(Self::Header)
|
||||
.or_else(|value| GridItem::try_from(value).map(Self::Item))
|
||||
}
|
||||
}
|
||||
|
||||
/// A grid item, which is the basic unit of grid specification.
|
||||
#[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! {
|
||||
GridChild,
|
||||
GridItem,
|
||||
self => match self {
|
||||
Self::HLine(hline) => hline.into_value(),
|
||||
Self::VLine(vline) => vline.into_value(),
|
||||
Self::Cell(cell) => cell.into_value(),
|
||||
},
|
||||
v: Content => {
|
||||
if v.is::<TableCell>() {
|
||||
bail!(
|
||||
"cannot use `table.cell` as a grid cell; use `grid.cell` instead"
|
||||
);
|
||||
}
|
||||
if v.is::<TableHLine>() {
|
||||
bail!(
|
||||
"cannot use `table.hline` as a grid line; use `grid.hline` instead"
|
||||
);
|
||||
}
|
||||
if v.is::<TableVLine>() {
|
||||
bail!(
|
||||
"cannot use `table.vline` as a grid line; use `grid.vline` instead"
|
||||
);
|
||||
}
|
||||
v.into()
|
||||
v.try_into()?
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Content> for GridChild {
|
||||
fn from(value: Content) -> Self {
|
||||
value
|
||||
impl TryFrom<Content> for GridItem {
|
||||
type Error = EcoString;
|
||||
fn try_from(value: Content) -> StrResult<Self> {
|
||||
if value.is::<GridHeader>() {
|
||||
bail!("cannot place a grid header within another header");
|
||||
}
|
||||
if value.is::<TableHeader>() {
|
||||
bail!("cannot place a table header within another header");
|
||||
}
|
||||
if value.is::<TableCell>() {
|
||||
bail!("cannot use `table.cell` as a grid cell; use `grid.cell` instead");
|
||||
}
|
||||
if value.is::<TableHLine>() {
|
||||
bail!("cannot use `table.hline` as a grid line; use `grid.hline` instead");
|
||||
}
|
||||
if value.is::<TableVLine>() {
|
||||
bail!("cannot use `table.vline` as a grid line; use `grid.vline` instead");
|
||||
}
|
||||
|
||||
Ok(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))
|
||||
.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();
|
||||
GridChild::Cell(Packed::new(GridCell::new(value)).spanned(span))
|
||||
})
|
||||
Self::Cell(Packed::new(GridCell::new(value)).spanned(span))
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/// A repeatable grid header.
|
||||
#[elem(name = "header", title = "Grid Header")]
|
||||
pub struct GridHeader {
|
||||
/// Whether this header should be repeated across pages.
|
||||
#[default(true)]
|
||||
pub repeat: bool,
|
||||
|
||||
/// The cells and lines within the header.
|
||||
#[variadic]
|
||||
pub children: Vec<GridItem>,
|
||||
}
|
||||
|
||||
/// A horizontal line in the grid.
|
||||
///
|
||||
/// Overrides any per-cell stroke, including stroke specified through the
|
||||
|
@ -6,7 +6,7 @@ use crate::layout::{
|
||||
};
|
||||
use crate::util::MaybeReverseIter;
|
||||
|
||||
use super::layout::{points, Row};
|
||||
use super::layout::{in_last_with_offset, points, Row, RowPiece};
|
||||
|
||||
/// All information needed to layout a single rowspan.
|
||||
pub(super) struct Rowspan {
|
||||
@ -27,6 +27,13 @@ pub(super) struct Rowspan {
|
||||
pub(super) region_full: Abs,
|
||||
/// The vertical space available for this rowspan in each region.
|
||||
pub(super) heights: Vec<Abs>,
|
||||
/// The index of the largest resolved spanned row so far.
|
||||
/// Once a spanned row is resolved and its height added to `heights`, this
|
||||
/// number is increased. Older rows, even if repeated through e.g. a
|
||||
/// header, will no longer contribute height to this rowspan.
|
||||
///
|
||||
/// This is `None` if no spanned rows were resolved in `finish_region` yet.
|
||||
pub(super) max_resolved_row: Option<usize>,
|
||||
}
|
||||
|
||||
/// The output of the simulation of an unbreakable row group.
|
||||
@ -44,9 +51,14 @@ pub(super) struct CellMeasurementData<'layouter> {
|
||||
/// The available width for the cell across all regions.
|
||||
pub(super) width: Abs,
|
||||
/// The available height for the cell in its first region.
|
||||
/// Infinite when the auto row is unbreakable.
|
||||
pub(super) height: Abs,
|
||||
/// The backlog of heights available for the cell in later regions.
|
||||
///
|
||||
/// When this is `None`, the `custom_backlog` field should be used instead.
|
||||
/// That's because, otherwise, this field would have to contain a reference
|
||||
/// to the `custom_backlog` field, which isn't possible in Rust without
|
||||
/// resorting to unsafe hacks.
|
||||
pub(super) backlog: Option<&'layouter [Abs]>,
|
||||
/// If the backlog needs to be built from scratch instead of reusing the
|
||||
/// one at the current region, which is the case of a multi-region rowspan
|
||||
@ -54,7 +66,11 @@ pub(super) struct CellMeasurementData<'layouter> {
|
||||
/// backlog), then this vector will store the new backlog.
|
||||
pub(super) custom_backlog: Vec<Abs>,
|
||||
/// The full height of the first region of the cell.
|
||||
/// Infinite when the auto row is unbreakable.
|
||||
pub(super) full: Abs,
|
||||
/// The height of the last repeated region to use in the measurement pod,
|
||||
/// if any.
|
||||
pub(super) last: Option<Abs>,
|
||||
/// The total height of previous rows spanned by the cell in the current
|
||||
/// region (so far).
|
||||
pub(super) height_in_this_region: Abs,
|
||||
@ -65,9 +81,10 @@ pub(super) struct CellMeasurementData<'layouter> {
|
||||
|
||||
impl<'a> GridLayouter<'a> {
|
||||
/// Layout a rowspan over the already finished regions, plus the current
|
||||
/// region, if it wasn't finished yet (because we're being called from
|
||||
/// `finish_region`, but note that this function is also called once after
|
||||
/// all regions are finished, in which case `current_region` is `None`).
|
||||
/// region's frame and resolved rows, if it wasn't finished yet (because
|
||||
/// we're being called from `finish_region`, but note that this function is
|
||||
/// also called once after all regions are finished, in which case
|
||||
/// `current_region_data` is `None`).
|
||||
///
|
||||
/// We need to do this only once we already know the heights of all
|
||||
/// spanned rows, which is only possible after laying out the last row
|
||||
@ -75,7 +92,7 @@ impl<'a> GridLayouter<'a> {
|
||||
pub(super) fn layout_rowspan(
|
||||
&mut self,
|
||||
rowspan_data: Rowspan,
|
||||
current_region: Option<&mut Frame>,
|
||||
current_region_data: Option<(&mut Frame, &[RowPiece])>,
|
||||
engine: &mut Engine,
|
||||
) -> SourceResult<()> {
|
||||
let Rowspan {
|
||||
@ -97,22 +114,42 @@ impl<'a> GridLayouter<'a> {
|
||||
pod.backlog = backlog;
|
||||
|
||||
// Push the layouted frames directly into the finished frames.
|
||||
// At first, we draw the rowspan starting at its expected offset
|
||||
// in the first region.
|
||||
let mut pos = Point::new(dx, dy);
|
||||
let fragment = cell.layout(engine, self.styles, pod)?;
|
||||
for (finished, frame) in self
|
||||
let (current_region, current_rrows) = current_region_data.unzip();
|
||||
for ((i, finished), frame) in self
|
||||
.finished
|
||||
.iter_mut()
|
||||
.chain(current_region.into_iter())
|
||||
.skip(first_region)
|
||||
.enumerate()
|
||||
.zip(fragment)
|
||||
{
|
||||
finished.push_frame(pos, frame);
|
||||
let dy = if i == 0 {
|
||||
// At first, we draw the rowspan starting at its expected
|
||||
// vertical offset in the first region.
|
||||
dy
|
||||
} else {
|
||||
// The rowspan continuation starts after the header (thus,
|
||||
// at a position after the sum of the laid out header
|
||||
// rows).
|
||||
if let Some(header) = &self.grid.header {
|
||||
let header_rows = self
|
||||
.rrows
|
||||
.get(i)
|
||||
.map(Vec::as_slice)
|
||||
.or(current_rrows)
|
||||
.unwrap_or(&[])
|
||||
.iter()
|
||||
.take_while(|row| row.y < header.end);
|
||||
|
||||
// From the second region onwards, the rowspan's continuation
|
||||
// starts at the very top.
|
||||
pos.y = Abs::zero();
|
||||
header_rows.map(|row| row.height).sum()
|
||||
} else {
|
||||
// Without a header, start at the very top of the region.
|
||||
Abs::zero()
|
||||
}
|
||||
};
|
||||
|
||||
finished.push_frame(Point::new(dx, dy), frame);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@ -141,6 +178,7 @@ impl<'a> GridLayouter<'a> {
|
||||
first_region: usize::MAX,
|
||||
region_full: Abs::zero(),
|
||||
heights: vec![],
|
||||
max_resolved_row: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -156,11 +194,17 @@ impl<'a> GridLayouter<'a> {
|
||||
engine: &mut Engine,
|
||||
) -> SourceResult<()> {
|
||||
if self.unbreakable_rows_left == 0 {
|
||||
let row_group =
|
||||
self.simulate_unbreakable_row_group(current_row, &self.regions, engine)?;
|
||||
let row_group = self.simulate_unbreakable_row_group(
|
||||
current_row,
|
||||
None,
|
||||
&self.regions,
|
||||
engine,
|
||||
)?;
|
||||
|
||||
// Skip to fitting region.
|
||||
while !self.regions.size.y.fits(row_group.height) && !self.regions.in_last() {
|
||||
while !self.regions.size.y.fits(row_group.height)
|
||||
&& !in_last_with_offset(self.regions, self.header_height)
|
||||
{
|
||||
self.finish_region(engine)?;
|
||||
}
|
||||
self.unbreakable_rows_left = row_group.rows.len();
|
||||
@ -170,23 +214,30 @@ impl<'a> GridLayouter<'a> {
|
||||
}
|
||||
|
||||
/// Simulates a group of unbreakable rows, starting with the index of the
|
||||
/// first row in the group. Keeps adding rows to the group until none have
|
||||
/// unbreakable cells in common.
|
||||
/// first row in the group. If `amount_unbreakable_rows` is `None`, keeps
|
||||
/// adding rows to the group until none have unbreakable cells in common.
|
||||
/// Otherwise, adds specifically the given amount of rows to the group.
|
||||
///
|
||||
/// This is used to figure out how much height the next unbreakable row
|
||||
/// group (if any) needs.
|
||||
pub(super) fn simulate_unbreakable_row_group(
|
||||
&self,
|
||||
first_row: usize,
|
||||
amount_unbreakable_rows: Option<usize>,
|
||||
regions: &Regions<'_>,
|
||||
engine: &mut Engine,
|
||||
) -> SourceResult<UnbreakableRowGroup> {
|
||||
let mut row_group = UnbreakableRowGroup::default();
|
||||
let mut unbreakable_rows_left = 0;
|
||||
let mut unbreakable_rows_left = amount_unbreakable_rows.unwrap_or(0);
|
||||
for (y, row) in self.grid.rows.iter().enumerate().skip(first_row) {
|
||||
let additional_unbreakable_rows = self.check_for_unbreakable_cells(y);
|
||||
unbreakable_rows_left =
|
||||
unbreakable_rows_left.max(additional_unbreakable_rows);
|
||||
if amount_unbreakable_rows.is_none() {
|
||||
// When we don't set a fixed amount of unbreakable rows,
|
||||
// determine the amount based on the rowspan of unbreakable
|
||||
// cells in rows.
|
||||
let additional_unbreakable_rows = self.check_for_unbreakable_cells(y);
|
||||
unbreakable_rows_left =
|
||||
unbreakable_rows_left.max(additional_unbreakable_rows);
|
||||
}
|
||||
if unbreakable_rows_left == 0 {
|
||||
// This check is in case the first row does not have any
|
||||
// unbreakable cells. Therefore, no unbreakable row group
|
||||
@ -254,10 +305,37 @@ impl<'a> GridLayouter<'a> {
|
||||
let rowspan = self.grid.effective_rowspan_of_cell(cell);
|
||||
|
||||
// This variable is used to construct a custom backlog if the cell
|
||||
// is a rowspan. When measuring, we join the heights from previous
|
||||
// regions to the current backlog to form the rowspan's expected
|
||||
// backlog.
|
||||
let mut rowspan_backlog: Vec<Abs> = vec![];
|
||||
// is a rowspan, or if headers are used. When measuring, we join
|
||||
// the heights from previous regions to the current backlog to form
|
||||
// a rowspan's expected backlog. We also subtract the header's
|
||||
// height from all regions.
|
||||
let mut custom_backlog: Vec<Abs> = vec![];
|
||||
|
||||
// This function is used to subtract the expected header height from
|
||||
// each upcoming region size in the current backlog and last region.
|
||||
let mut subtract_header_height_from_regions = || {
|
||||
// Only breakable auto rows need to update their backlogs based
|
||||
// on the presence of a header, given that unbreakable auto
|
||||
// rows don't depend on the backlog, as they only span one
|
||||
// region.
|
||||
if breakable && self.grid.header.is_some() {
|
||||
// Subtract header height from all upcoming regions when
|
||||
// measuring the cell, including the last repeated region.
|
||||
//
|
||||
// This will update the 'custom_backlog' vector with the
|
||||
// updated heights of the upcoming regions.
|
||||
let mapped_regions = self.regions.map(&mut custom_backlog, |size| {
|
||||
Size::new(size.x, size.y - self.header_height)
|
||||
});
|
||||
|
||||
// Callees must use the custom backlog instead of the current
|
||||
// backlog, so we return 'None'.
|
||||
return (None, mapped_regions.last);
|
||||
}
|
||||
|
||||
// No need to change the backlog or last region.
|
||||
(Some(self.regions.backlog), self.regions.last)
|
||||
};
|
||||
|
||||
// Each declaration, from top to bottom:
|
||||
// 1. The height available to the cell in the first region.
|
||||
@ -266,25 +344,34 @@ impl<'a> GridLayouter<'a> {
|
||||
// 2. The backlog of upcoming region heights to specify as
|
||||
// available to the cell.
|
||||
// 3. The full height of the first region of the cell.
|
||||
// 4. The total height of the cell covered by previously spanned
|
||||
// 4. Height of the last repeated region to use in the measurement pod.
|
||||
// 5. The total height of the cell covered by previously spanned
|
||||
// rows in this region. This is used by rowspans to be able to tell
|
||||
// how much the auto row needs to expand.
|
||||
// 5. The amount of frames laid out by this cell in previous
|
||||
// 6. The amount of frames laid out by this cell in previous
|
||||
// regions. When the cell isn't a rowspan, this is always zero.
|
||||
// These frames are skipped after measuring.
|
||||
let (height, backlog, full, height_in_this_region, frames_in_previous_regions);
|
||||
let height;
|
||||
let backlog;
|
||||
let full;
|
||||
let last;
|
||||
let height_in_this_region;
|
||||
let frames_in_previous_regions;
|
||||
|
||||
if rowspan == 1 {
|
||||
// Not a rowspan, so the cell only occupies this row. Therefore:
|
||||
// 1. When we measure the cell below, use the available height
|
||||
// remaining in the region as the height it has available.
|
||||
// However, if the auto row is unbreakable, measure with infinite
|
||||
// height instead to see how much content expands.
|
||||
// 2. Also use the region's backlog when measuring.
|
||||
// 2. Use the region's backlog and last region when measuring,
|
||||
// however subtract the expected header height from each upcoming
|
||||
// size, if there is a header.
|
||||
// 3. Use the same full region height.
|
||||
// 4. No height occupied by this cell in this region so far.
|
||||
// 5. Yes, this cell started in this region.
|
||||
height = if breakable { self.regions.size.y } else { Abs::inf() };
|
||||
backlog = Some(self.regions.backlog);
|
||||
(backlog, last) = subtract_header_height_from_regions();
|
||||
full = if breakable { self.regions.full } else { Abs::inf() };
|
||||
height_in_this_region = Abs::zero();
|
||||
frames_in_previous_regions = 0;
|
||||
@ -339,21 +426,25 @@ impl<'a> GridLayouter<'a> {
|
||||
.iter()
|
||||
.copied()
|
||||
.chain(std::iter::once(if breakable {
|
||||
self.initial.y
|
||||
self.initial.y - self.header_height
|
||||
} else {
|
||||
// When measuring unbreakable auto rows, infinite
|
||||
// height is available for content to expand.
|
||||
Abs::inf()
|
||||
}));
|
||||
|
||||
rowspan_backlog = if breakable {
|
||||
custom_backlog = if breakable {
|
||||
// This auto row is breakable. Therefore, join the
|
||||
// rowspan's already laid out heights with the current
|
||||
// region's height and current backlog to ensure a good
|
||||
// level of accuracy in the measurements.
|
||||
heights_up_to_current_region
|
||||
.chain(self.regions.backlog.iter().copied())
|
||||
.collect::<Vec<_>>()
|
||||
let backlog = self
|
||||
.regions
|
||||
.backlog
|
||||
.iter()
|
||||
.map(|&size| size - self.header_height);
|
||||
|
||||
heights_up_to_current_region.chain(backlog).collect::<Vec<_>>()
|
||||
} else {
|
||||
// No extra backlog if this is an unbreakable auto row.
|
||||
// Ensure, when measuring, that the rowspan can be laid
|
||||
@ -365,6 +456,7 @@ impl<'a> GridLayouter<'a> {
|
||||
height = *rowspan_height;
|
||||
backlog = None;
|
||||
full = rowspan_full;
|
||||
last = self.regions.last.map(|size| size - self.header_height);
|
||||
} else {
|
||||
// The rowspan started in the current region, as its vector
|
||||
// of heights in regions is currently empty.
|
||||
@ -380,7 +472,7 @@ impl<'a> GridLayouter<'a> {
|
||||
} else {
|
||||
Abs::inf()
|
||||
};
|
||||
backlog = Some(self.regions.backlog);
|
||||
(backlog, last) = subtract_header_height_from_regions();
|
||||
full = if breakable { self.regions.full } else { Abs::inf() };
|
||||
frames_in_previous_regions = 0;
|
||||
}
|
||||
@ -391,8 +483,9 @@ impl<'a> GridLayouter<'a> {
|
||||
width,
|
||||
height,
|
||||
backlog,
|
||||
custom_backlog: rowspan_backlog,
|
||||
custom_backlog,
|
||||
full,
|
||||
last,
|
||||
height_in_this_region,
|
||||
frames_in_previous_regions,
|
||||
}
|
||||
@ -561,7 +654,13 @@ impl<'a> GridLayouter<'a> {
|
||||
// expand) because we popped the last resolved size from the
|
||||
// resolved vector, above.
|
||||
simulated_regions.next();
|
||||
|
||||
// Subtract the initial header height, since that's the height we
|
||||
// used when subtracting from the region backlog's heights while
|
||||
// measuring cells.
|
||||
simulated_regions.size.y -= self.header_height;
|
||||
}
|
||||
|
||||
if let Some(original_last_resolved_size) = last_resolved_size {
|
||||
// We're now at the (current) last region of this auto row.
|
||||
// Consider resolved height as already taken space.
|
||||
@ -689,87 +788,18 @@ impl<'a> GridLayouter<'a> {
|
||||
// which, when used and combined with upcoming spanned rows, covers all
|
||||
// of the requested rowspan height, we give up.
|
||||
for _attempt in 0..5 {
|
||||
let mut regions = simulated_regions;
|
||||
let mut total_spanned_height = Abs::zero();
|
||||
let mut unbreakable_rows_left = unbreakable_rows_left;
|
||||
let rowspan_simulator =
|
||||
RowspanSimulator::new(simulated_regions, self.header_height);
|
||||
|
||||
// Height of the latest spanned gutter row.
|
||||
// Zero if it was removed.
|
||||
let mut latest_spanned_gutter_height = Abs::zero();
|
||||
let spanned_rows = &self.grid.rows[y + 1..=max_spanned_row];
|
||||
for (offset, row) in spanned_rows.iter().enumerate() {
|
||||
if (total_spanned_height + amount_to_grow).fits(requested_rowspan_height)
|
||||
{
|
||||
// Stop the simulation, as the combination of upcoming
|
||||
// spanned rows (so far) and the current amount the auto
|
||||
// row expands by has already fully covered the height the
|
||||
// rowspans need.
|
||||
break;
|
||||
}
|
||||
let spanned_y = y + 1 + offset;
|
||||
let is_gutter = self.grid.is_gutter_track(spanned_y);
|
||||
|
||||
if unbreakable_rows_left == 0 {
|
||||
// Simulate unbreakable row groups, and skip regions until
|
||||
// they fit. There is no risk of infinite recursion, as
|
||||
// no auto rows participate in the simulation, so the
|
||||
// unbreakable row group simulator won't recursively call
|
||||
// 'measure_auto_row' or (consequently) this function.
|
||||
let row_group =
|
||||
self.simulate_unbreakable_row_group(spanned_y, ®ions, engine)?;
|
||||
while !regions.size.y.fits(row_group.height) && !regions.in_last() {
|
||||
total_spanned_height -= latest_spanned_gutter_height;
|
||||
latest_spanned_gutter_height = Abs::zero();
|
||||
regions.next();
|
||||
}
|
||||
|
||||
unbreakable_rows_left = row_group.rows.len();
|
||||
}
|
||||
|
||||
match row {
|
||||
// Fixed-size spanned rows are what we are interested in.
|
||||
// They contribute a fixed amount of height to our rowspan.
|
||||
Sizing::Rel(v) => {
|
||||
let height = v.resolve(self.styles).relative_to(regions.base().y);
|
||||
total_spanned_height += height;
|
||||
if is_gutter {
|
||||
latest_spanned_gutter_height = height;
|
||||
}
|
||||
|
||||
let mut skipped_region = false;
|
||||
while unbreakable_rows_left == 0
|
||||
&& !regions.size.y.fits(height)
|
||||
&& !regions.in_last()
|
||||
{
|
||||
// A row was pushed to the next region. Therefore,
|
||||
// the immediately preceding gutter row is removed.
|
||||
total_spanned_height -= latest_spanned_gutter_height;
|
||||
latest_spanned_gutter_height = Abs::zero();
|
||||
skipped_region = true;
|
||||
regions.next();
|
||||
}
|
||||
|
||||
if !skipped_region || !is_gutter {
|
||||
// No gutter at the top of a new region, so don't
|
||||
// account for it if we just skipped a region.
|
||||
regions.size.y -= height;
|
||||
}
|
||||
}
|
||||
Sizing::Auto => {
|
||||
// We only simulate for rowspans which end at the
|
||||
// current auto row. Therefore, there won't be any
|
||||
// further auto rows.
|
||||
unreachable!();
|
||||
}
|
||||
// For now, we ignore fractional rows on simulation.
|
||||
Sizing::Fr(_) if is_gutter => {
|
||||
latest_spanned_gutter_height = Abs::zero();
|
||||
}
|
||||
Sizing::Fr(_) => {}
|
||||
}
|
||||
|
||||
unbreakable_rows_left = unbreakable_rows_left.saturating_sub(1);
|
||||
}
|
||||
let total_spanned_height = rowspan_simulator.simulate_rowspan_layout(
|
||||
y,
|
||||
max_spanned_row,
|
||||
amount_to_grow,
|
||||
requested_rowspan_height,
|
||||
unbreakable_rows_left,
|
||||
self,
|
||||
engine,
|
||||
)?;
|
||||
|
||||
// If the total height spanned by upcoming spanned rows plus the
|
||||
// current amount we predict the auto row will have to grow (from
|
||||
@ -841,6 +871,7 @@ impl<'a> GridLayouter<'a> {
|
||||
{
|
||||
extra_amount_to_grow -= simulated_regions.size.y.max(Abs::zero());
|
||||
simulated_regions.next();
|
||||
simulated_regions.size.y -= self.header_height;
|
||||
}
|
||||
simulated_regions.size.y -= extra_amount_to_grow;
|
||||
}
|
||||
@ -850,6 +881,189 @@ impl<'a> GridLayouter<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Auxiliary structure holding state during rowspan simulation.
|
||||
struct RowspanSimulator<'a> {
|
||||
/// The state of regions during the simulation.
|
||||
regions: Regions<'a>,
|
||||
/// The height of the header in the currently simulated region.
|
||||
header_height: Abs,
|
||||
/// The total spanned height so far in the simulation.
|
||||
total_spanned_height: Abs,
|
||||
/// Height of the latest spanned gutter row in the simulation.
|
||||
/// Zero if it was removed.
|
||||
latest_spanned_gutter_height: Abs,
|
||||
}
|
||||
|
||||
impl<'a> RowspanSimulator<'a> {
|
||||
/// Creates new rowspan simulation state with the given regions and initial
|
||||
/// header height. Other fields should always start as zero.
|
||||
fn new(regions: Regions<'a>, header_height: Abs) -> Self {
|
||||
Self {
|
||||
regions,
|
||||
header_height,
|
||||
total_spanned_height: Abs::zero(),
|
||||
latest_spanned_gutter_height: Abs::zero(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculates the total spanned height of the rowspan.
|
||||
/// Stops calculating if, at any point in the simulation, the value of
|
||||
/// `total_spanned_height + amount_to_grow` becomes larger than
|
||||
/// `requested_rowspan_height`, as the results are not going to become any
|
||||
/// more useful after that point.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn simulate_rowspan_layout(
|
||||
mut self,
|
||||
y: usize,
|
||||
max_spanned_row: usize,
|
||||
amount_to_grow: Abs,
|
||||
requested_rowspan_height: Abs,
|
||||
mut unbreakable_rows_left: usize,
|
||||
layouter: &GridLayouter<'_>,
|
||||
engine: &mut Engine,
|
||||
) -> SourceResult<Abs> {
|
||||
let spanned_rows = &layouter.grid.rows[y + 1..=max_spanned_row];
|
||||
for (offset, row) in spanned_rows.iter().enumerate() {
|
||||
if (self.total_spanned_height + amount_to_grow).fits(requested_rowspan_height)
|
||||
{
|
||||
// Stop the simulation, as the combination of upcoming
|
||||
// spanned rows (so far) and the current amount the auto
|
||||
// row expands by has already fully covered the height the
|
||||
// rowspans need.
|
||||
return Ok(self.total_spanned_height);
|
||||
}
|
||||
let spanned_y = y + 1 + offset;
|
||||
let is_gutter = layouter.grid.is_gutter_track(spanned_y);
|
||||
|
||||
if unbreakable_rows_left == 0 {
|
||||
// Simulate unbreakable row groups, and skip regions until
|
||||
// they fit. There is no risk of infinite recursion, as
|
||||
// no auto rows participate in the simulation, so the
|
||||
// unbreakable row group simulator won't recursively call
|
||||
// 'measure_auto_row' or (consequently) this function.
|
||||
let row_group = layouter.simulate_unbreakable_row_group(
|
||||
spanned_y,
|
||||
None,
|
||||
&self.regions,
|
||||
engine,
|
||||
)?;
|
||||
while !self.regions.size.y.fits(row_group.height)
|
||||
&& !in_last_with_offset(self.regions, self.header_height)
|
||||
{
|
||||
self.finish_region(layouter, engine)?;
|
||||
}
|
||||
|
||||
unbreakable_rows_left = row_group.rows.len();
|
||||
}
|
||||
|
||||
match row {
|
||||
// Fixed-size spanned rows are what we are interested in.
|
||||
// They contribute a fixed amount of height to our rowspan.
|
||||
Sizing::Rel(v) => {
|
||||
let height =
|
||||
v.resolve(layouter.styles).relative_to(self.regions.base().y);
|
||||
self.total_spanned_height += height;
|
||||
if is_gutter {
|
||||
self.latest_spanned_gutter_height = height;
|
||||
}
|
||||
|
||||
let mut skipped_region = false;
|
||||
while unbreakable_rows_left == 0
|
||||
&& !self.regions.size.y.fits(height)
|
||||
&& !in_last_with_offset(self.regions, self.header_height)
|
||||
{
|
||||
self.finish_region(layouter, engine)?;
|
||||
|
||||
skipped_region = true;
|
||||
}
|
||||
|
||||
if !skipped_region || !is_gutter {
|
||||
// No gutter at the top of a new region, so don't
|
||||
// account for it if we just skipped a region.
|
||||
self.regions.size.y -= height;
|
||||
}
|
||||
}
|
||||
Sizing::Auto => {
|
||||
// We only simulate for rowspans which end at the
|
||||
// current auto row. Therefore, there won't be any
|
||||
// further auto rows.
|
||||
unreachable!();
|
||||
}
|
||||
// For now, we ignore fractional rows on simulation.
|
||||
Sizing::Fr(_) if is_gutter => {
|
||||
self.latest_spanned_gutter_height = Abs::zero();
|
||||
}
|
||||
Sizing::Fr(_) => {}
|
||||
}
|
||||
|
||||
unbreakable_rows_left = unbreakable_rows_left.saturating_sub(1);
|
||||
}
|
||||
|
||||
Ok(self.total_spanned_height)
|
||||
}
|
||||
|
||||
fn simulate_header_layout(
|
||||
&mut self,
|
||||
layouter: &GridLayouter<'_>,
|
||||
engine: &mut Engine,
|
||||
) -> SourceResult<()> {
|
||||
if let Some(header) = &layouter.grid.header {
|
||||
// We can't just use the initial header height on each
|
||||
// region, because header height might vary depending
|
||||
// on region size if it contains rows with relative
|
||||
// lengths. Therefore, we re-simulate headers on each
|
||||
// new region.
|
||||
// It's true that, when measuring cells, we reduce each
|
||||
// height in the backlog to consider the initial header
|
||||
// height; however, our simulation checks what happens
|
||||
// AFTER the auto row, so we can just use the original
|
||||
// backlog from `self.regions`.
|
||||
let header_row_group =
|
||||
layouter.simulate_header(header, &self.regions, engine)?;
|
||||
let mut skipped_region = false;
|
||||
|
||||
// Skip until we reach a fitting region for this header.
|
||||
while !self.regions.size.y.fits(header_row_group.height)
|
||||
&& !self.regions.in_last()
|
||||
{
|
||||
self.regions.next();
|
||||
skipped_region = true;
|
||||
}
|
||||
|
||||
self.header_height = if skipped_region {
|
||||
// Simulate headers again, at the new region, as
|
||||
// the full region height may change.
|
||||
layouter.simulate_header(header, &self.regions, engine)?.height
|
||||
} else {
|
||||
header_row_group.height
|
||||
};
|
||||
|
||||
// Consume the header's height from the new region,
|
||||
// but don't consider it spanned. The rowspan
|
||||
// does not go over the header (as an invariant,
|
||||
// any rowspans spanning a header row are fully
|
||||
// contained within that header's rows).
|
||||
self.regions.size.y -= self.header_height;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn finish_region(
|
||||
&mut self,
|
||||
layouter: &GridLayouter<'_>,
|
||||
engine: &mut Engine,
|
||||
) -> SourceResult<()> {
|
||||
// If a row was pushed to the next region, the immediately
|
||||
// preceding gutter row is removed.
|
||||
self.total_spanned_height -= self.latest_spanned_gutter_height;
|
||||
self.latest_spanned_gutter_height = Abs::zero();
|
||||
self.regions.next();
|
||||
|
||||
self.simulate_header_layout(layouter, engine)
|
||||
}
|
||||
}
|
||||
|
||||
/// Subtracts some size from the end of a vector of sizes.
|
||||
/// For example, subtracting 5pt from \[2pt, 1pt, 3pt\] will result in \[1pt\].
|
||||
fn subtract_end_sizes(sizes: &mut Vec<Abs>, mut subtract: Abs) {
|
||||
|
@ -29,8 +29,8 @@ use crate::foundations::{
|
||||
};
|
||||
use crate::introspection::{Introspector, Locatable, Location};
|
||||
use crate::layout::{
|
||||
BlockElem, Em, GridCell, GridChild, GridElem, HElem, PadElem, Sizing, TrackSizings,
|
||||
VElem,
|
||||
BlockElem, Em, GridCell, GridChild, GridElem, GridItem, HElem, PadElem, Sizing,
|
||||
TrackSizings, VElem,
|
||||
};
|
||||
use crate::model::{
|
||||
CitationForm, CiteGroup, Destination, FootnoteElem, HeadingElem, LinkElem, ParElem,
|
||||
@ -238,13 +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(GridChild::Cell(
|
||||
cells.push(GridChild::Item(GridItem::Cell(
|
||||
Packed::new(GridCell::new(prefix.clone().unwrap_or_default()))
|
||||
.spanned(span),
|
||||
));
|
||||
cells.push(GridChild::Cell(
|
||||
)));
|
||||
cells.push(GridChild::Item(GridItem::Cell(
|
||||
Packed::new(GridCell::new(reference.clone())).spanned(span),
|
||||
));
|
||||
)));
|
||||
}
|
||||
|
||||
seq.push(VElem::new(row_gutter).with_weakness(3).pack());
|
||||
@ -948,8 +948,12 @@ impl ElemRenderer<'_> {
|
||||
if let Some(prefix) = suf_prefix {
|
||||
const COLUMN_GUTTER: Em = Em::new(0.65);
|
||||
content = GridElem::new(vec![
|
||||
GridChild::Cell(Packed::new(GridCell::new(prefix)).spanned(self.span)),
|
||||
GridChild::Cell(Packed::new(GridCell::new(content)).spanned(self.span)),
|
||||
GridChild::Item(GridItem::Cell(
|
||||
Packed::new(GridCell::new(prefix)).spanned(self.span),
|
||||
)),
|
||||
GridChild::Item(GridItem::Cell(
|
||||
Packed::new(GridCell::new(content)).spanned(self.span),
|
||||
)),
|
||||
])
|
||||
.with_columns(TrackSizings(smallvec![Sizing::Auto; 2]))
|
||||
.with_column_gutter(TrackSizings(smallvec![COLUMN_GUTTER.into()]))
|
||||
|
@ -1,18 +1,18 @@
|
||||
use std::num::NonZeroUsize;
|
||||
use std::sync::Arc;
|
||||
|
||||
use ecow::eco_format;
|
||||
use ecow::{eco_format, EcoString};
|
||||
|
||||
use crate::diag::{bail, SourceResult, Trace, Tracepoint};
|
||||
use crate::diag::{bail, SourceResult, StrResult, Trace, Tracepoint};
|
||||
use crate::engine::Engine;
|
||||
use crate::foundations::{
|
||||
cast, elem, scope, Content, Fold, Packed, Show, Smart, StyleChain,
|
||||
};
|
||||
use crate::layout::{
|
||||
show_grid_cell, Abs, Alignment, Axes, Cell, CellGrid, Celled, Dir, Fragment,
|
||||
GridCell, GridHLine, GridItem, GridLayouter, GridVLine, LayoutMultiple, Length,
|
||||
LinePosition, OuterHAlignment, OuterVAlignment, Regions, Rel, ResolvableCell, Sides,
|
||||
TrackSizings,
|
||||
GridCell, GridHLine, GridHeader, GridLayouter, GridVLine, LayoutMultiple, Length,
|
||||
LinePosition, OuterHAlignment, OuterVAlignment, Regions, Rel, ResolvableCell,
|
||||
ResolvableGridChild, ResolvableGridItem, Sides, TrackSizings,
|
||||
};
|
||||
use crate::model::Figurable;
|
||||
use crate::syntax::Span;
|
||||
@ -221,6 +221,9 @@ impl TableElem {
|
||||
|
||||
#[elem]
|
||||
type TableVLine;
|
||||
|
||||
#[elem]
|
||||
type TableHeader;
|
||||
}
|
||||
|
||||
impl LayoutMultiple for Packed<TableElem> {
|
||||
@ -244,43 +247,20 @@ impl LayoutMultiple for Packed<TableElem> {
|
||||
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,
|
||||
},
|
||||
let children = self.children().iter().map(|child| match child {
|
||||
TableChild::Header(header) => ResolvableGridChild::Header {
|
||||
repeat: header.repeat(styles),
|
||||
span: header.span(),
|
||||
items: header.children().iter().map(|child| child.to_resolvable(styles)),
|
||||
},
|
||||
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()),
|
||||
TableChild::Item(item) => {
|
||||
ResolvableGridChild::Item(item.to_resolvable(styles))
|
||||
}
|
||||
});
|
||||
let grid = CellGrid::resolve(
|
||||
tracks,
|
||||
gutter,
|
||||
items,
|
||||
children,
|
||||
fill,
|
||||
align,
|
||||
&inset,
|
||||
@ -338,50 +318,138 @@ impl Figurable for Packed<TableElem> {}
|
||||
/// Any child of a table element.
|
||||
#[derive(Debug, PartialEq, Clone, Hash)]
|
||||
pub enum TableChild {
|
||||
Header(Packed<TableHeader>),
|
||||
Item(TableItem),
|
||||
}
|
||||
|
||||
cast! {
|
||||
TableChild,
|
||||
self => match self {
|
||||
Self::Header(header) => header.into_value(),
|
||||
Self::Item(item) => item.into_value(),
|
||||
},
|
||||
v: Content => {
|
||||
v.try_into()?
|
||||
},
|
||||
}
|
||||
|
||||
impl TryFrom<Content> for TableChild {
|
||||
type Error = EcoString;
|
||||
|
||||
fn try_from(value: Content) -> StrResult<Self> {
|
||||
if value.is::<GridHeader>() {
|
||||
bail!(
|
||||
"cannot use `grid.header` as a table header; use `table.header` instead"
|
||||
)
|
||||
}
|
||||
|
||||
value
|
||||
.into_packed::<TableHeader>()
|
||||
.map(Self::Header)
|
||||
.or_else(|value| TableItem::try_from(value).map(Self::Item))
|
||||
}
|
||||
}
|
||||
|
||||
/// A table item, which is the basic unit of table specification.
|
||||
#[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! {
|
||||
TableChild,
|
||||
TableItem,
|
||||
self => match self {
|
||||
Self::HLine(hline) => hline.into_value(),
|
||||
Self::VLine(vline) => vline.into_value(),
|
||||
Self::Cell(cell) => cell.into_value(),
|
||||
},
|
||||
v: Content => {
|
||||
if v.is::<GridCell>() {
|
||||
bail!(
|
||||
"cannot use `grid.cell` as a table cell; use `table.cell` instead"
|
||||
);
|
||||
v.try_into()?
|
||||
},
|
||||
}
|
||||
|
||||
impl TryFrom<Content> for TableItem {
|
||||
type Error = EcoString;
|
||||
|
||||
fn try_from(value: Content) -> StrResult<Self> {
|
||||
if value.is::<GridHeader>() {
|
||||
bail!("cannot place a grid header within another header");
|
||||
}
|
||||
if v.is::<GridHLine>() {
|
||||
bail!(
|
||||
"cannot use `grid.hline` as a table line; use `table.hline` instead"
|
||||
);
|
||||
if value.is::<TableHeader>() {
|
||||
bail!("cannot place a table header within another header");
|
||||
}
|
||||
if v.is::<GridVLine>() {
|
||||
bail!(
|
||||
"cannot use `grid.vline` as a table line; use `table.vline` instead"
|
||||
);
|
||||
if value.is::<GridCell>() {
|
||||
bail!("cannot use `grid.cell` as a table cell; use `table.cell` instead");
|
||||
}
|
||||
v.into()
|
||||
if value.is::<GridHLine>() {
|
||||
bail!("cannot use `grid.hline` as a table line; use `table.hline` instead");
|
||||
}
|
||||
if value.is::<GridVLine>() {
|
||||
bail!("cannot use `grid.vline` as a table line; 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))
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
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 repeatable table header.
|
||||
#[elem(name = "header", title = "Table Header")]
|
||||
pub struct TableHeader {
|
||||
/// Whether this header should be repeated across pages.
|
||||
#[default(true)]
|
||||
pub repeat: bool,
|
||||
|
||||
/// The cells and lines within the header.
|
||||
#[variadic]
|
||||
pub children: Vec<TableItem>,
|
||||
}
|
||||
|
||||
/// A horizontal line in the table. See the docs for
|
||||
|
BIN
tests/ref/layout/grid-headers-1.png
Normal file
After Width: | Height: | Size: 123 KiB |
BIN
tests/ref/layout/grid-headers-2.png
Normal file
After Width: | Height: | Size: 110 KiB |
BIN
tests/ref/layout/grid-headers-3.png
Normal file
After Width: | Height: | Size: 100 KiB |
BIN
tests/ref/layout/grid-headers-4.png
Normal file
After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 95 KiB |
162
tests/typ/layout/grid-headers-1.typ
Normal file
@ -0,0 +1,162 @@
|
||||
#set page(width: auto, height: 12em)
|
||||
#table(
|
||||
columns: 5,
|
||||
align: center + horizon,
|
||||
table.header(
|
||||
table.cell(colspan: 5)[*Cool Zone*],
|
||||
table.cell(stroke: red)[*Name*], table.cell(stroke: aqua)[*Number*], [*Data 1*], [*Data 2*], [*Etc*],
|
||||
table.hline(start: 2, end: 3, stroke: yellow)
|
||||
),
|
||||
..range(0, 6).map(i => ([John \##i], table.cell(stroke: green)[123], table.cell(stroke: blue)[456], [789], [?], table.hline(start: 4, end: 5, stroke: red))).flatten()
|
||||
)
|
||||
|
||||
---
|
||||
// Disable repetition
|
||||
#set page(width: auto, height: 12em)
|
||||
#table(
|
||||
columns: 5,
|
||||
align: center + horizon,
|
||||
table.header(
|
||||
table.cell(colspan: 5)[*Cool Zone*],
|
||||
table.cell(stroke: red)[*Name*], table.cell(stroke: aqua)[*Number*], [*Data 1*], [*Data 2*], [*Etc*],
|
||||
table.hline(start: 2, end: 3, stroke: yellow),
|
||||
repeat: false
|
||||
),
|
||||
..range(0, 6).map(i => ([John \##i], table.cell(stroke: green)[123], table.cell(stroke: blue)[456], [789], [?], table.hline(start: 4, end: 5, stroke: red))).flatten()
|
||||
)
|
||||
|
||||
---
|
||||
#set page(width: auto, height: 12em)
|
||||
#table(
|
||||
columns: 5,
|
||||
align: center + horizon,
|
||||
gutter: 3pt,
|
||||
table.header(
|
||||
table.cell(colspan: 5)[*Cool Zone*],
|
||||
table.cell(stroke: red)[*Name*], table.cell(stroke: aqua)[*Number*], [*Data 1*], [*Data 2*], [*Etc*],
|
||||
table.hline(start: 2, end: 3, stroke: yellow),
|
||||
),
|
||||
..range(0, 6).map(i => ([John \##i], table.cell(stroke: green)[123], table.cell(stroke: blue)[456], [789], [?], table.hline(start: 4, end: 5, stroke: red))).flatten()
|
||||
)
|
||||
|
||||
---
|
||||
// Relative lengths
|
||||
#set page(height: 10em)
|
||||
#table(
|
||||
rows: (30%, 30%, auto),
|
||||
table.header(
|
||||
[*A*],
|
||||
[*B*]
|
||||
),
|
||||
[C],
|
||||
[C]
|
||||
)
|
||||
|
||||
---
|
||||
#grid(
|
||||
grid.cell(y: 1)[a],
|
||||
grid.header(grid.cell(y: 0)[b]),
|
||||
grid.cell(y: 2)[c]
|
||||
)
|
||||
|
||||
---
|
||||
// When the header is the last grid child, it shouldn't include the gutter row
|
||||
// after it, because there is none.
|
||||
#grid(
|
||||
columns: 2,
|
||||
gutter: 3pt,
|
||||
grid.header(
|
||||
[a], [b],
|
||||
[c], [d]
|
||||
)
|
||||
)
|
||||
|
||||
---
|
||||
#set page(height: 14em)
|
||||
#let t(n) = table(
|
||||
columns: 3,
|
||||
align: center + horizon,
|
||||
gutter: 3pt,
|
||||
table.header(
|
||||
table.cell(colspan: 3)[*Cool Zone #n*],
|
||||
[*Name*], [*Num*], [*Data*]
|
||||
),
|
||||
..range(0, 5).map(i => ([\##i], table.cell(stroke: green)[123], table.cell(stroke: blue)[456])).flatten()
|
||||
)
|
||||
#grid(
|
||||
gutter: 3pt,
|
||||
t(0),
|
||||
t(1)
|
||||
)
|
||||
|
||||
---
|
||||
// Test line positioning in header
|
||||
#table(
|
||||
columns: 3,
|
||||
stroke: none,
|
||||
table.hline(stroke: red, end: 2),
|
||||
table.vline(stroke: red, end: 3),
|
||||
table.header(
|
||||
table.hline(stroke: aqua, start: 2),
|
||||
table.vline(stroke: aqua, start: 3), [*A*], table.hline(stroke: orange), table.vline(stroke: orange), [*B*],
|
||||
[*C*], [*D*]
|
||||
),
|
||||
[a], [b],
|
||||
[c], [d],
|
||||
[e], [f]
|
||||
)
|
||||
|
||||
---
|
||||
// Error: 3:3-3:19 header must start at the first row
|
||||
// Hint: 3:3-3:19 remove any rows before the header
|
||||
#grid(
|
||||
[a],
|
||||
grid.header([b])
|
||||
)
|
||||
|
||||
---
|
||||
// Error: 4:3-4:19 header must start at the first row
|
||||
// Hint: 4:3-4:19 remove any rows before the header
|
||||
#grid(
|
||||
columns: 2,
|
||||
[a],
|
||||
grid.header([b])
|
||||
)
|
||||
|
||||
---
|
||||
// Error: 3:3-3:19 cannot have more than one header
|
||||
#grid(
|
||||
grid.header([a]),
|
||||
grid.header([b]),
|
||||
[a],
|
||||
)
|
||||
|
||||
---
|
||||
// Error: 2:3-2:20 cannot use `table.header` as a grid header; use `grid.header` instead
|
||||
#grid(
|
||||
table.header([a]),
|
||||
[a],
|
||||
)
|
||||
|
||||
---
|
||||
// Error: 2:3-2:19 cannot use `grid.header` as a table header; use `table.header` instead
|
||||
#table(
|
||||
grid.header([a]),
|
||||
[a],
|
||||
)
|
||||
|
||||
---
|
||||
// Error: 14-28 cannot place a grid header within another header
|
||||
#grid.header(grid.header[a])
|
||||
|
||||
---
|
||||
// Error: 14-29 cannot place a table header within another header
|
||||
#grid.header(table.header[a])
|
||||
|
||||
---
|
||||
// Error: 15-29 cannot place a grid header within another header
|
||||
#table.header(grid.header[a])
|
||||
|
||||
---
|
||||
// Error: 15-30 cannot place a table header within another header
|
||||
#table.header(table.header[a])
|
52
tests/typ/layout/grid-headers-2.typ
Normal file
@ -0,0 +1,52 @@
|
||||
#set page(height: 15em)
|
||||
#table(
|
||||
rows: (auto, 2.5em, auto),
|
||||
table.header(
|
||||
[*Hello*],
|
||||
[*World*]
|
||||
),
|
||||
block(width: 2em, height: 20em, fill: red)
|
||||
)
|
||||
|
||||
---
|
||||
// Rowspan sizing algorithm doesn't do the best job at non-contiguous content
|
||||
// ATM.
|
||||
#set page(height: 15em)
|
||||
|
||||
#table(
|
||||
rows: (auto, 2.5em, 2em, auto, 5em),
|
||||
table.header(
|
||||
[*Hello*],
|
||||
[*World*]
|
||||
),
|
||||
table.cell(rowspan: 3, lorem(40))
|
||||
)
|
||||
|
||||
---
|
||||
// Rowspan sizing algorithm doesn't do the best job at non-contiguous content
|
||||
// ATM.
|
||||
#set page(height: 15em)
|
||||
|
||||
#table(
|
||||
rows: (auto, 2.5em, 2em, auto, 5em),
|
||||
gutter: 3pt,
|
||||
table.header(
|
||||
[*Hello*],
|
||||
[*World*]
|
||||
),
|
||||
table.cell(rowspan: 3, lorem(40))
|
||||
)
|
||||
|
||||
---
|
||||
// This should look right
|
||||
#set page(height: 15em)
|
||||
|
||||
#table(
|
||||
rows: (auto, 2.5em, 2em, auto),
|
||||
gutter: 3pt,
|
||||
table.header(
|
||||
[*Hello*],
|
||||
[*World*]
|
||||
),
|
||||
table.cell(rowspan: 3, lorem(40))
|
||||
)
|
35
tests/typ/layout/grid-headers-3.typ
Normal file
@ -0,0 +1,35 @@
|
||||
// Test lack of space for header + text.
|
||||
#set page(height: 9em)
|
||||
|
||||
#table(
|
||||
rows: (auto, 2.5em, auto, auto, 10em),
|
||||
gutter: 3pt,
|
||||
table.header(
|
||||
[*Hello*],
|
||||
[*World*]
|
||||
),
|
||||
table.cell(rowspan: 3, lorem(80))
|
||||
)
|
||||
|
||||
---
|
||||
// Orphan header prevention test
|
||||
#set page(height: 12em)
|
||||
#v(8em)
|
||||
#grid(
|
||||
columns: 3,
|
||||
grid.header(
|
||||
[*Mui*], [*A*], grid.cell(rowspan: 2, fill: orange)[*B*],
|
||||
[*Header*], [*Header* #v(0.1em)]
|
||||
),
|
||||
..([Test], [Test], [Test]) * 20
|
||||
)
|
||||
|
||||
---
|
||||
// Empty header should just be a repeated blank row
|
||||
#set page(height: 12em)
|
||||
#table(
|
||||
columns: 4,
|
||||
align: center + horizon,
|
||||
table.header(),
|
||||
..range(0, 4).map(i => ([John \##i], table.cell(stroke: green)[123], table.cell(stroke: blue)[456], [789])).flatten()
|
||||
)
|
58
tests/typ/layout/grid-headers-4.typ
Normal file
@ -0,0 +1,58 @@
|
||||
// When a header has a rowspan with an empty row, it should be displayed
|
||||
// properly
|
||||
#set page(height: 10em)
|
||||
|
||||
#let count = counter("g")
|
||||
#table(
|
||||
rows: (auto, 2em, auto, auto),
|
||||
table.header(
|
||||
[eeec],
|
||||
table.cell(rowspan: 2, count.step() + count.display()),
|
||||
),
|
||||
[d],
|
||||
block(width: 5em, fill: yellow, lorem(15)),
|
||||
[d]
|
||||
)
|
||||
#count.display()
|
||||
|
||||
---
|
||||
// Ensure header expands to fit cell placed in it after its declaration
|
||||
#set page(height: 10em)
|
||||
#table(
|
||||
columns: 2,
|
||||
table.header(
|
||||
[a], [b],
|
||||
[c],
|
||||
),
|
||||
table.cell(x: 1, y: 1, rowspan: 2, lorem(80))
|
||||
)
|
||||
|
||||
---
|
||||
// Nested table with header should repeat both headers
|
||||
#set page(height: 10em)
|
||||
#table(
|
||||
table.header(
|
||||
[a]
|
||||
),
|
||||
table(
|
||||
table.header(
|
||||
[b]
|
||||
),
|
||||
[a\ b\ c\ d]
|
||||
)
|
||||
)
|
||||
|
||||
---
|
||||
#set page(height: 12em)
|
||||
#table(
|
||||
table.header(
|
||||
table(
|
||||
table.header(
|
||||
[b]
|
||||
),
|
||||
[c],
|
||||
[d]
|
||||
)
|
||||
),
|
||||
[a\ b]
|
||||
)
|
@ -209,3 +209,24 @@
|
||||
grid.cell(y: 6, breakable: false)[m], grid.cell(y: 6, breakable: true)[n],
|
||||
grid.cell(y: 7, breakable: false)[o], grid.cell(y: 7, breakable: true)[p], grid.cell(y: 7, breakable: auto)[q]
|
||||
)
|
||||
|
||||
---
|
||||
#table(
|
||||
columns: 2,
|
||||
table.cell(stroke: (bottom: red))[a], [b],
|
||||
table.hline(stroke: green),
|
||||
table.cell(stroke: (top: yellow, left: green, right: aqua, bottom: blue), colspan: 1, rowspan: 2)[d], table.cell(colspan: 1, rowspan: 2)[e],
|
||||
[f],
|
||||
[g]
|
||||
)
|
||||
|
||||
---
|
||||
#table(
|
||||
columns: 2,
|
||||
gutter: 3pt,
|
||||
table.cell(stroke: (bottom: red))[a], [b],
|
||||
table.hline(stroke: green),
|
||||
table.cell(stroke: (top: yellow, left: green, right: aqua, bottom: blue), colspan: 1, rowspan: 2)[d], table.cell(colspan: 1, rowspan: 2)[e],
|
||||
[f],
|
||||
[g]
|
||||
)
|
||||
|
@ -178,3 +178,18 @@
|
||||
[e],
|
||||
[f]
|
||||
)
|
||||
|
||||
---
|
||||
// Headers
|
||||
#set page(height: 15em)
|
||||
#set text(dir: rtl)
|
||||
#table(
|
||||
columns: 5,
|
||||
align: center + horizon,
|
||||
table.header(
|
||||
table.cell(colspan: 5)[*Cool Zone*],
|
||||
table.cell(stroke: red)[*N1*], table.cell(stroke: aqua)[*N2*], [*D1*], [*D2*], [*Etc*],
|
||||
table.hline(start: 2, end: 3, stroke: yellow)
|
||||
),
|
||||
..range(0, 10).map(i => ([\##i], table.cell(stroke: green)[123], table.cell(stroke: blue)[456], [789], [?], table.hline(start: 4, end: 5, stroke: red))).flatten()
|
||||
)
|
||||
|