Repeatable Table Headers [More Flexible Tables Pt.5a] (#3545)

This commit is contained in:
PgBiel 2024-03-06 05:41:16 -03:00 committed by GitHub
parent 5b2ffd9dd0
commit 898367f096
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 1737 additions and 595 deletions

File diff suppressed because it is too large Load Diff

View File

@ -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,

View File

@ -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

View File

@ -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, &regions, 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) {

View File

@ -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()]))

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 95 KiB

View 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])

View 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))
)

View 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()
)

View 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]
)

View File

@ -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]
)

View File

@ -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()
)