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,
|
/// 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
|
/// the position after a track is not the same as before the next
|
||||||
/// non-gutter track.
|
/// non-gutter track.
|
||||||
#[derive(PartialEq, Eq)]
|
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||||
pub enum LinePosition {
|
pub enum LinePosition {
|
||||||
/// The line should be drawn before its track (e.g. hline on top of a row).
|
/// The line should be drawn before its track (e.g. hline on top of a row).
|
||||||
Before,
|
Before,
|
||||||
@ -122,7 +122,6 @@ pub(super) fn generate_line_segments<'grid, F, I, L>(
|
|||||||
tracks: I,
|
tracks: I,
|
||||||
index: usize,
|
index: usize,
|
||||||
lines: L,
|
lines: L,
|
||||||
is_max_index: bool,
|
|
||||||
line_stroke_at_track: F,
|
line_stroke_at_track: F,
|
||||||
) -> impl Iterator<Item = LineSegment> + 'grid
|
) -> impl Iterator<Item = LineSegment> + 'grid
|
||||||
where
|
where
|
||||||
@ -154,22 +153,6 @@ where
|
|||||||
// How much to multiply line indices by to account for gutter.
|
// How much to multiply line indices by to account for gutter.
|
||||||
let gutter_factor = if grid.has_gutter { 2 } else { 1 };
|
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,
|
// 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
|
// from start to finish, to create line segments and extend them until they
|
||||||
// are interrupted and thus yielded through the iterator. We then repeat
|
// are interrupted and thus yielded through the iterator. We then repeat
|
||||||
@ -210,9 +193,7 @@ where
|
|||||||
let mut line_strokes = lines
|
let mut line_strokes = lines
|
||||||
.clone()
|
.clone()
|
||||||
.filter(|line| {
|
.filter(|line| {
|
||||||
line.position == expected_line_position
|
line.end
|
||||||
&& line
|
|
||||||
.end
|
|
||||||
.map(|end| {
|
.map(|end| {
|
||||||
// Subtract 1 from end index so we stop at the last
|
// Subtract 1 from end index so we stop at the last
|
||||||
// cell before it (don't cross one extra gutter).
|
// cell before it (don't cross one extra gutter).
|
||||||
@ -554,9 +535,25 @@ pub(super) fn hline_stroke_at_column(
|
|||||||
StrokePriority::GridStroke
|
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) =
|
let (prioritized_cell_stroke, deprioritized_cell_stroke) =
|
||||||
if !use_bottom_border_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
|
// Top border must always be prioritized, even if it did not
|
||||||
// request for that explicitly.
|
// request for that explicitly.
|
||||||
@ -660,6 +657,7 @@ mod test {
|
|||||||
},
|
},
|
||||||
vec![],
|
vec![],
|
||||||
vec![],
|
vec![],
|
||||||
|
None,
|
||||||
entries,
|
entries,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -723,14 +721,7 @@ mod test {
|
|||||||
let tracks = rows.iter().map(|row| (row.y, row.height));
|
let tracks = rows.iter().map(|row| (row.y, row.height));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
expected_splits,
|
expected_splits,
|
||||||
&generate_line_segments(
|
&generate_line_segments(&grid, tracks, x, &[], vline_stroke_at_row)
|
||||||
&grid,
|
|
||||||
tracks,
|
|
||||||
x,
|
|
||||||
&[],
|
|
||||||
x == grid.cols.len(),
|
|
||||||
vline_stroke_at_row
|
|
||||||
)
|
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -955,14 +946,7 @@ mod test {
|
|||||||
let tracks = rows.iter().map(|row| (row.y, row.height));
|
let tracks = rows.iter().map(|row| (row.y, row.height));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
expected_splits,
|
expected_splits,
|
||||||
&generate_line_segments(
|
&generate_line_segments(&grid, tracks, x, &[], vline_stroke_at_row)
|
||||||
&grid,
|
|
||||||
tracks,
|
|
||||||
x,
|
|
||||||
&[],
|
|
||||||
x == grid.cols.len(),
|
|
||||||
vline_stroke_at_row
|
|
||||||
)
|
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1144,7 +1128,6 @@ mod test {
|
|||||||
position: LinePosition::After
|
position: LinePosition::After
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
x == grid.cols.len(),
|
|
||||||
vline_stroke_at_row
|
vline_stroke_at_row
|
||||||
)
|
)
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
@ -1211,6 +1194,7 @@ mod test {
|
|||||||
},
|
},
|
||||||
vec![],
|
vec![],
|
||||||
vec![],
|
vec![],
|
||||||
|
None,
|
||||||
entries,
|
entries,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -1297,22 +1281,17 @@ mod test {
|
|||||||
let tracks = columns.iter().copied().enumerate();
|
let tracks = columns.iter().copied().enumerate();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
expected_splits,
|
expected_splits,
|
||||||
&generate_line_segments(
|
&generate_line_segments(&grid, tracks, y, &[], |grid, y, x, stroke| {
|
||||||
&grid,
|
hline_stroke_at_column(
|
||||||
tracks,
|
|
||||||
y,
|
|
||||||
&[],
|
|
||||||
y == grid.rows.len(),
|
|
||||||
|grid, y, x, stroke| hline_stroke_at_column(
|
|
||||||
grid,
|
grid,
|
||||||
&rows,
|
&rows,
|
||||||
y.checked_sub(1),
|
y.checked_sub(1),
|
||||||
true,
|
true,
|
||||||
y,
|
y,
|
||||||
x,
|
x,
|
||||||
stroke
|
stroke,
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
})
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1496,7 +1475,6 @@ mod test {
|
|||||||
position: LinePosition::After
|
position: LinePosition::After
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
y == grid.rows.len(),
|
|
||||||
|grid, y, x, stroke| hline_stroke_at_column(
|
|grid, y, x, stroke| hline_stroke_at_column(
|
||||||
grid,
|
grid,
|
||||||
&rows,
|
&rows,
|
||||||
@ -1542,7 +1520,6 @@ mod test {
|
|||||||
columns.iter().copied().enumerate(),
|
columns.iter().copied().enumerate(),
|
||||||
4,
|
4,
|
||||||
&[],
|
&[],
|
||||||
4 == grid.rows.len(),
|
|
||||||
|grid, y, x, stroke| hline_stroke_at_column(
|
|grid, y, x, stroke| hline_stroke_at_column(
|
||||||
grid,
|
grid,
|
||||||
&rows,
|
&rows,
|
||||||
|
@ -2,13 +2,16 @@ mod layout;
|
|||||||
mod lines;
|
mod lines;
|
||||||
mod rowspans;
|
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;
|
pub use self::lines::LinePosition;
|
||||||
|
|
||||||
use std::num::NonZeroUsize;
|
use std::num::NonZeroUsize;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use ecow::eco_format;
|
use ecow::{eco_format, EcoString};
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
|
|
||||||
use crate::diag::{bail, SourceResult, StrResult, Trace, Tracepoint};
|
use crate::diag::{bail, SourceResult, StrResult, Trace, Tracepoint};
|
||||||
@ -20,7 +23,7 @@ use crate::layout::{
|
|||||||
Abs, AlignElem, Alignment, Axes, Dir, Fragment, LayoutMultiple, Length,
|
Abs, AlignElem, Alignment, Axes, Dir, Fragment, LayoutMultiple, Length,
|
||||||
OuterHAlignment, OuterVAlignment, Regions, Rel, Sides, Sizing,
|
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::syntax::Span;
|
||||||
use crate::text::TextElem;
|
use crate::text::TextElem;
|
||||||
use crate::util::NonZeroExt;
|
use crate::util::NonZeroExt;
|
||||||
@ -293,6 +296,9 @@ impl GridElem {
|
|||||||
|
|
||||||
#[elem]
|
#[elem]
|
||||||
type GridVLine;
|
type GridVLine;
|
||||||
|
|
||||||
|
#[elem]
|
||||||
|
type GridHeader;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LayoutMultiple for Packed<GridElem> {
|
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());
|
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
|
// Use trace to link back to the grid when a specific cell errors
|
||||||
let tracepoint = || Tracepoint::Call(Some(eco_format!("grid")));
|
let tracepoint = || Tracepoint::Call(Some(eco_format!("grid")));
|
||||||
let items = self.children().iter().map(|child| match child {
|
let children = self.children().iter().map(|child| match child {
|
||||||
GridChild::HLine(hline) => GridItem::HLine {
|
GridChild::Header(header) => ResolvableGridChild::Header {
|
||||||
y: hline.y(styles),
|
repeat: header.repeat(styles),
|
||||||
start: hline.start(styles),
|
span: header.span(),
|
||||||
end: hline.end(styles),
|
items: header.children().iter().map(|child| child.to_resolvable(styles)),
|
||||||
stroke: hline.stroke(styles),
|
|
||||||
span: hline.span(),
|
|
||||||
position: match hline.position(styles) {
|
|
||||||
OuterVAlignment::Top => LinePosition::Before,
|
|
||||||
OuterVAlignment::Bottom => LinePosition::After,
|
|
||||||
},
|
},
|
||||||
},
|
GridChild::Item(item) => {
|
||||||
GridChild::VLine(vline) => GridItem::VLine {
|
ResolvableGridChild::Item(item.to_resolvable(styles))
|
||||||
x: vline.x(styles),
|
|
||||||
start: vline.start(styles),
|
|
||||||
end: vline.end(styles),
|
|
||||||
stroke: vline.stroke(styles),
|
|
||||||
span: vline.span(),
|
|
||||||
position: match vline.position(styles) {
|
|
||||||
OuterHAlignment::Left if TextElem::dir_in(styles) == Dir::RTL => {
|
|
||||||
LinePosition::After
|
|
||||||
}
|
}
|
||||||
OuterHAlignment::Right if TextElem::dir_in(styles) == Dir::RTL => {
|
|
||||||
LinePosition::Before
|
|
||||||
}
|
|
||||||
OuterHAlignment::Start | OuterHAlignment::Left => {
|
|
||||||
LinePosition::Before
|
|
||||||
}
|
|
||||||
OuterHAlignment::End | OuterHAlignment::Right => LinePosition::After,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
GridChild::Cell(cell) => GridItem::Cell(cell.clone()),
|
|
||||||
});
|
});
|
||||||
let grid = CellGrid::resolve(
|
let grid = CellGrid::resolve(
|
||||||
tracks,
|
tracks,
|
||||||
gutter,
|
gutter,
|
||||||
items,
|
children,
|
||||||
fill,
|
fill,
|
||||||
align,
|
align,
|
||||||
&inset,
|
&inset,
|
||||||
@ -385,52 +368,136 @@ cast! {
|
|||||||
/// Any child of a grid element.
|
/// Any child of a grid element.
|
||||||
#[derive(Debug, PartialEq, Clone, Hash)]
|
#[derive(Debug, PartialEq, Clone, Hash)]
|
||||||
pub enum GridChild {
|
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>),
|
HLine(Packed<GridHLine>),
|
||||||
VLine(Packed<GridVLine>),
|
VLine(Packed<GridVLine>),
|
||||||
Cell(Packed<GridCell>),
|
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! {
|
cast! {
|
||||||
GridChild,
|
GridItem,
|
||||||
self => match self {
|
self => match self {
|
||||||
Self::HLine(hline) => hline.into_value(),
|
Self::HLine(hline) => hline.into_value(),
|
||||||
Self::VLine(vline) => vline.into_value(),
|
Self::VLine(vline) => vline.into_value(),
|
||||||
Self::Cell(cell) => cell.into_value(),
|
Self::Cell(cell) => cell.into_value(),
|
||||||
},
|
},
|
||||||
v: Content => {
|
v: Content => {
|
||||||
if v.is::<TableCell>() {
|
v.try_into()?
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Content> for GridChild {
|
impl TryFrom<Content> for GridItem {
|
||||||
fn from(value: Content) -> Self {
|
type Error = EcoString;
|
||||||
value
|
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>()
|
.into_packed::<GridHLine>()
|
||||||
.map(GridChild::HLine)
|
.map(Self::HLine)
|
||||||
.or_else(|value| value.into_packed::<GridVLine>().map(GridChild::VLine))
|
.or_else(|value| value.into_packed::<GridVLine>().map(Self::VLine))
|
||||||
.or_else(|value| value.into_packed::<GridCell>().map(GridChild::Cell))
|
.or_else(|value| value.into_packed::<GridCell>().map(Self::Cell))
|
||||||
.unwrap_or_else(|value| {
|
.unwrap_or_else(|value| {
|
||||||
let span = value.span();
|
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.
|
/// A horizontal line in the grid.
|
||||||
///
|
///
|
||||||
/// Overrides any per-cell stroke, including stroke specified through the
|
/// Overrides any per-cell stroke, including stroke specified through the
|
||||||
|
@ -6,7 +6,7 @@ use crate::layout::{
|
|||||||
};
|
};
|
||||||
use crate::util::MaybeReverseIter;
|
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.
|
/// All information needed to layout a single rowspan.
|
||||||
pub(super) struct Rowspan {
|
pub(super) struct Rowspan {
|
||||||
@ -27,6 +27,13 @@ pub(super) struct Rowspan {
|
|||||||
pub(super) region_full: Abs,
|
pub(super) region_full: Abs,
|
||||||
/// The vertical space available for this rowspan in each region.
|
/// The vertical space available for this rowspan in each region.
|
||||||
pub(super) heights: Vec<Abs>,
|
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.
|
/// 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.
|
/// The available width for the cell across all regions.
|
||||||
pub(super) width: Abs,
|
pub(super) width: Abs,
|
||||||
/// The available height for the cell in its first region.
|
/// The available height for the cell in its first region.
|
||||||
|
/// Infinite when the auto row is unbreakable.
|
||||||
pub(super) height: Abs,
|
pub(super) height: Abs,
|
||||||
/// The backlog of heights available for the cell in later regions.
|
/// The backlog of heights available for the cell in later regions.
|
||||||
|
///
|
||||||
/// When this is `None`, the `custom_backlog` field should be used instead.
|
/// 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]>,
|
pub(super) backlog: Option<&'layouter [Abs]>,
|
||||||
/// If the backlog needs to be built from scratch instead of reusing the
|
/// 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
|
/// 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.
|
/// backlog), then this vector will store the new backlog.
|
||||||
pub(super) custom_backlog: Vec<Abs>,
|
pub(super) custom_backlog: Vec<Abs>,
|
||||||
/// The full height of the first region of the cell.
|
/// The full height of the first region of the cell.
|
||||||
|
/// Infinite when the auto row is unbreakable.
|
||||||
pub(super) full: Abs,
|
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
|
/// The total height of previous rows spanned by the cell in the current
|
||||||
/// region (so far).
|
/// region (so far).
|
||||||
pub(super) height_in_this_region: Abs,
|
pub(super) height_in_this_region: Abs,
|
||||||
@ -65,9 +81,10 @@ pub(super) struct CellMeasurementData<'layouter> {
|
|||||||
|
|
||||||
impl<'a> GridLayouter<'a> {
|
impl<'a> GridLayouter<'a> {
|
||||||
/// Layout a rowspan over the already finished regions, plus the current
|
/// Layout a rowspan over the already finished regions, plus the current
|
||||||
/// region, if it wasn't finished yet (because we're being called from
|
/// region's frame and resolved rows, if it wasn't finished yet (because
|
||||||
/// `finish_region`, but note that this function is also called once after
|
/// we're being called from `finish_region`, but note that this function is
|
||||||
/// all regions are finished, in which case `current_region` is `None`).
|
/// 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
|
/// 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
|
/// 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(
|
pub(super) fn layout_rowspan(
|
||||||
&mut self,
|
&mut self,
|
||||||
rowspan_data: Rowspan,
|
rowspan_data: Rowspan,
|
||||||
current_region: Option<&mut Frame>,
|
current_region_data: Option<(&mut Frame, &[RowPiece])>,
|
||||||
engine: &mut Engine,
|
engine: &mut Engine,
|
||||||
) -> SourceResult<()> {
|
) -> SourceResult<()> {
|
||||||
let Rowspan {
|
let Rowspan {
|
||||||
@ -97,22 +114,42 @@ impl<'a> GridLayouter<'a> {
|
|||||||
pod.backlog = backlog;
|
pod.backlog = backlog;
|
||||||
|
|
||||||
// Push the layouted frames directly into the finished frames.
|
// 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)?;
|
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
|
.finished
|
||||||
.iter_mut()
|
.iter_mut()
|
||||||
.chain(current_region.into_iter())
|
.chain(current_region.into_iter())
|
||||||
.skip(first_region)
|
.skip(first_region)
|
||||||
|
.enumerate()
|
||||||
.zip(fragment)
|
.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
|
header_rows.map(|row| row.height).sum()
|
||||||
// starts at the very top.
|
} else {
|
||||||
pos.y = Abs::zero();
|
// Without a header, start at the very top of the region.
|
||||||
|
Abs::zero()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
finished.push_frame(Point::new(dx, dy), frame);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -141,6 +178,7 @@ impl<'a> GridLayouter<'a> {
|
|||||||
first_region: usize::MAX,
|
first_region: usize::MAX,
|
||||||
region_full: Abs::zero(),
|
region_full: Abs::zero(),
|
||||||
heights: vec![],
|
heights: vec![],
|
||||||
|
max_resolved_row: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -156,11 +194,17 @@ impl<'a> GridLayouter<'a> {
|
|||||||
engine: &mut Engine,
|
engine: &mut Engine,
|
||||||
) -> SourceResult<()> {
|
) -> SourceResult<()> {
|
||||||
if self.unbreakable_rows_left == 0 {
|
if self.unbreakable_rows_left == 0 {
|
||||||
let row_group =
|
let row_group = self.simulate_unbreakable_row_group(
|
||||||
self.simulate_unbreakable_row_group(current_row, &self.regions, engine)?;
|
current_row,
|
||||||
|
None,
|
||||||
|
&self.regions,
|
||||||
|
engine,
|
||||||
|
)?;
|
||||||
|
|
||||||
// Skip to fitting region.
|
// 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.finish_region(engine)?;
|
||||||
}
|
}
|
||||||
self.unbreakable_rows_left = row_group.rows.len();
|
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
|
/// 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
|
/// first row in the group. If `amount_unbreakable_rows` is `None`, keeps
|
||||||
/// unbreakable cells in common.
|
/// 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
|
/// This is used to figure out how much height the next unbreakable row
|
||||||
/// group (if any) needs.
|
/// group (if any) needs.
|
||||||
pub(super) fn simulate_unbreakable_row_group(
|
pub(super) fn simulate_unbreakable_row_group(
|
||||||
&self,
|
&self,
|
||||||
first_row: usize,
|
first_row: usize,
|
||||||
|
amount_unbreakable_rows: Option<usize>,
|
||||||
regions: &Regions<'_>,
|
regions: &Regions<'_>,
|
||||||
engine: &mut Engine,
|
engine: &mut Engine,
|
||||||
) -> SourceResult<UnbreakableRowGroup> {
|
) -> SourceResult<UnbreakableRowGroup> {
|
||||||
let mut row_group = UnbreakableRowGroup::default();
|
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) {
|
for (y, row) in self.grid.rows.iter().enumerate().skip(first_row) {
|
||||||
|
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);
|
let additional_unbreakable_rows = self.check_for_unbreakable_cells(y);
|
||||||
unbreakable_rows_left =
|
unbreakable_rows_left =
|
||||||
unbreakable_rows_left.max(additional_unbreakable_rows);
|
unbreakable_rows_left.max(additional_unbreakable_rows);
|
||||||
|
}
|
||||||
if unbreakable_rows_left == 0 {
|
if unbreakable_rows_left == 0 {
|
||||||
// This check is in case the first row does not have any
|
// This check is in case the first row does not have any
|
||||||
// unbreakable cells. Therefore, no unbreakable row group
|
// unbreakable cells. Therefore, no unbreakable row group
|
||||||
@ -254,10 +305,37 @@ impl<'a> GridLayouter<'a> {
|
|||||||
let rowspan = self.grid.effective_rowspan_of_cell(cell);
|
let rowspan = self.grid.effective_rowspan_of_cell(cell);
|
||||||
|
|
||||||
// This variable is used to construct a custom backlog if the cell
|
// This variable is used to construct a custom backlog if the cell
|
||||||
// is a rowspan. When measuring, we join the heights from previous
|
// is a rowspan, or if headers are used. When measuring, we join
|
||||||
// regions to the current backlog to form the rowspan's expected
|
// the heights from previous regions to the current backlog to form
|
||||||
// backlog.
|
// a rowspan's expected backlog. We also subtract the header's
|
||||||
let mut rowspan_backlog: Vec<Abs> = vec![];
|
// 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:
|
// Each declaration, from top to bottom:
|
||||||
// 1. The height available to the cell in the first region.
|
// 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
|
// 2. The backlog of upcoming region heights to specify as
|
||||||
// available to the cell.
|
// available to the cell.
|
||||||
// 3. The full height of the first region of 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
|
// rows in this region. This is used by rowspans to be able to tell
|
||||||
// how much the auto row needs to expand.
|
// 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.
|
// regions. When the cell isn't a rowspan, this is always zero.
|
||||||
// These frames are skipped after measuring.
|
// 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 {
|
if rowspan == 1 {
|
||||||
// Not a rowspan, so the cell only occupies this row. Therefore:
|
// Not a rowspan, so the cell only occupies this row. Therefore:
|
||||||
// 1. When we measure the cell below, use the available height
|
// 1. When we measure the cell below, use the available height
|
||||||
// remaining in the region as the height it has available.
|
// remaining in the region as the height it has available.
|
||||||
// However, if the auto row is unbreakable, measure with infinite
|
// However, if the auto row is unbreakable, measure with infinite
|
||||||
// height instead to see how much content expands.
|
// 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.
|
// 3. Use the same full region height.
|
||||||
// 4. No height occupied by this cell in this region so far.
|
// 4. No height occupied by this cell in this region so far.
|
||||||
// 5. Yes, this cell started in this region.
|
// 5. Yes, this cell started in this region.
|
||||||
height = if breakable { self.regions.size.y } else { Abs::inf() };
|
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() };
|
full = if breakable { self.regions.full } else { Abs::inf() };
|
||||||
height_in_this_region = Abs::zero();
|
height_in_this_region = Abs::zero();
|
||||||
frames_in_previous_regions = 0;
|
frames_in_previous_regions = 0;
|
||||||
@ -339,21 +426,25 @@ impl<'a> GridLayouter<'a> {
|
|||||||
.iter()
|
.iter()
|
||||||
.copied()
|
.copied()
|
||||||
.chain(std::iter::once(if breakable {
|
.chain(std::iter::once(if breakable {
|
||||||
self.initial.y
|
self.initial.y - self.header_height
|
||||||
} else {
|
} else {
|
||||||
// When measuring unbreakable auto rows, infinite
|
// When measuring unbreakable auto rows, infinite
|
||||||
// height is available for content to expand.
|
// height is available for content to expand.
|
||||||
Abs::inf()
|
Abs::inf()
|
||||||
}));
|
}));
|
||||||
|
|
||||||
rowspan_backlog = if breakable {
|
custom_backlog = if breakable {
|
||||||
// This auto row is breakable. Therefore, join the
|
// This auto row is breakable. Therefore, join the
|
||||||
// rowspan's already laid out heights with the current
|
// rowspan's already laid out heights with the current
|
||||||
// region's height and current backlog to ensure a good
|
// region's height and current backlog to ensure a good
|
||||||
// level of accuracy in the measurements.
|
// level of accuracy in the measurements.
|
||||||
heights_up_to_current_region
|
let backlog = self
|
||||||
.chain(self.regions.backlog.iter().copied())
|
.regions
|
||||||
.collect::<Vec<_>>()
|
.backlog
|
||||||
|
.iter()
|
||||||
|
.map(|&size| size - self.header_height);
|
||||||
|
|
||||||
|
heights_up_to_current_region.chain(backlog).collect::<Vec<_>>()
|
||||||
} else {
|
} else {
|
||||||
// No extra backlog if this is an unbreakable auto row.
|
// No extra backlog if this is an unbreakable auto row.
|
||||||
// Ensure, when measuring, that the rowspan can be laid
|
// Ensure, when measuring, that the rowspan can be laid
|
||||||
@ -365,6 +456,7 @@ impl<'a> GridLayouter<'a> {
|
|||||||
height = *rowspan_height;
|
height = *rowspan_height;
|
||||||
backlog = None;
|
backlog = None;
|
||||||
full = rowspan_full;
|
full = rowspan_full;
|
||||||
|
last = self.regions.last.map(|size| size - self.header_height);
|
||||||
} else {
|
} else {
|
||||||
// The rowspan started in the current region, as its vector
|
// The rowspan started in the current region, as its vector
|
||||||
// of heights in regions is currently empty.
|
// of heights in regions is currently empty.
|
||||||
@ -380,7 +472,7 @@ impl<'a> GridLayouter<'a> {
|
|||||||
} else {
|
} else {
|
||||||
Abs::inf()
|
Abs::inf()
|
||||||
};
|
};
|
||||||
backlog = Some(self.regions.backlog);
|
(backlog, last) = subtract_header_height_from_regions();
|
||||||
full = if breakable { self.regions.full } else { Abs::inf() };
|
full = if breakable { self.regions.full } else { Abs::inf() };
|
||||||
frames_in_previous_regions = 0;
|
frames_in_previous_regions = 0;
|
||||||
}
|
}
|
||||||
@ -391,8 +483,9 @@ impl<'a> GridLayouter<'a> {
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
backlog,
|
backlog,
|
||||||
custom_backlog: rowspan_backlog,
|
custom_backlog,
|
||||||
full,
|
full,
|
||||||
|
last,
|
||||||
height_in_this_region,
|
height_in_this_region,
|
||||||
frames_in_previous_regions,
|
frames_in_previous_regions,
|
||||||
}
|
}
|
||||||
@ -561,7 +654,13 @@ impl<'a> GridLayouter<'a> {
|
|||||||
// expand) because we popped the last resolved size from the
|
// expand) because we popped the last resolved size from the
|
||||||
// resolved vector, above.
|
// resolved vector, above.
|
||||||
simulated_regions.next();
|
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 {
|
if let Some(original_last_resolved_size) = last_resolved_size {
|
||||||
// We're now at the (current) last region of this auto row.
|
// We're now at the (current) last region of this auto row.
|
||||||
// Consider resolved height as already taken space.
|
// 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
|
// which, when used and combined with upcoming spanned rows, covers all
|
||||||
// of the requested rowspan height, we give up.
|
// of the requested rowspan height, we give up.
|
||||||
for _attempt in 0..5 {
|
for _attempt in 0..5 {
|
||||||
let mut regions = simulated_regions;
|
let rowspan_simulator =
|
||||||
let mut total_spanned_height = Abs::zero();
|
RowspanSimulator::new(simulated_regions, self.header_height);
|
||||||
let mut unbreakable_rows_left = unbreakable_rows_left;
|
|
||||||
|
|
||||||
// Height of the latest spanned gutter row.
|
let total_spanned_height = rowspan_simulator.simulate_rowspan_layout(
|
||||||
// Zero if it was removed.
|
y,
|
||||||
let mut latest_spanned_gutter_height = Abs::zero();
|
max_spanned_row,
|
||||||
let spanned_rows = &self.grid.rows[y + 1..=max_spanned_row];
|
amount_to_grow,
|
||||||
for (offset, row) in spanned_rows.iter().enumerate() {
|
requested_rowspan_height,
|
||||||
if (total_spanned_height + amount_to_grow).fits(requested_rowspan_height)
|
unbreakable_rows_left,
|
||||||
{
|
self,
|
||||||
// Stop the simulation, as the combination of upcoming
|
engine,
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the total height spanned by upcoming spanned rows plus the
|
// If the total height spanned by upcoming spanned rows plus the
|
||||||
// current amount we predict the auto row will have to grow (from
|
// 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());
|
extra_amount_to_grow -= simulated_regions.size.y.max(Abs::zero());
|
||||||
simulated_regions.next();
|
simulated_regions.next();
|
||||||
|
simulated_regions.size.y -= self.header_height;
|
||||||
}
|
}
|
||||||
simulated_regions.size.y -= extra_amount_to_grow;
|
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.
|
/// Subtracts some size from the end of a vector of sizes.
|
||||||
/// For example, subtracting 5pt from \[2pt, 1pt, 3pt\] will result in \[1pt\].
|
/// For example, subtracting 5pt from \[2pt, 1pt, 3pt\] will result in \[1pt\].
|
||||||
fn subtract_end_sizes(sizes: &mut Vec<Abs>, mut subtract: Abs) {
|
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::introspection::{Introspector, Locatable, Location};
|
||||||
use crate::layout::{
|
use crate::layout::{
|
||||||
BlockElem, Em, GridCell, GridChild, GridElem, HElem, PadElem, Sizing, TrackSizings,
|
BlockElem, Em, GridCell, GridChild, GridElem, GridItem, HElem, PadElem, Sizing,
|
||||||
VElem,
|
TrackSizings, VElem,
|
||||||
};
|
};
|
||||||
use crate::model::{
|
use crate::model::{
|
||||||
CitationForm, CiteGroup, Destination, FootnoteElem, HeadingElem, LinkElem, ParElem,
|
CitationForm, CiteGroup, Destination, FootnoteElem, HeadingElem, LinkElem, ParElem,
|
||||||
@ -238,13 +238,13 @@ impl Show for Packed<BibliographyElem> {
|
|||||||
if references.iter().any(|(prefix, _)| prefix.is_some()) {
|
if references.iter().any(|(prefix, _)| prefix.is_some()) {
|
||||||
let mut cells = vec![];
|
let mut cells = vec![];
|
||||||
for (prefix, reference) in references {
|
for (prefix, reference) in references {
|
||||||
cells.push(GridChild::Cell(
|
cells.push(GridChild::Item(GridItem::Cell(
|
||||||
Packed::new(GridCell::new(prefix.clone().unwrap_or_default()))
|
Packed::new(GridCell::new(prefix.clone().unwrap_or_default()))
|
||||||
.spanned(span),
|
.spanned(span),
|
||||||
));
|
)));
|
||||||
cells.push(GridChild::Cell(
|
cells.push(GridChild::Item(GridItem::Cell(
|
||||||
Packed::new(GridCell::new(reference.clone())).spanned(span),
|
Packed::new(GridCell::new(reference.clone())).spanned(span),
|
||||||
));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
seq.push(VElem::new(row_gutter).with_weakness(3).pack());
|
seq.push(VElem::new(row_gutter).with_weakness(3).pack());
|
||||||
@ -948,8 +948,12 @@ impl ElemRenderer<'_> {
|
|||||||
if let Some(prefix) = suf_prefix {
|
if let Some(prefix) = suf_prefix {
|
||||||
const COLUMN_GUTTER: Em = Em::new(0.65);
|
const COLUMN_GUTTER: Em = Em::new(0.65);
|
||||||
content = GridElem::new(vec![
|
content = GridElem::new(vec![
|
||||||
GridChild::Cell(Packed::new(GridCell::new(prefix)).spanned(self.span)),
|
GridChild::Item(GridItem::Cell(
|
||||||
GridChild::Cell(Packed::new(GridCell::new(content)).spanned(self.span)),
|
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_columns(TrackSizings(smallvec![Sizing::Auto; 2]))
|
||||||
.with_column_gutter(TrackSizings(smallvec![COLUMN_GUTTER.into()]))
|
.with_column_gutter(TrackSizings(smallvec![COLUMN_GUTTER.into()]))
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
use std::num::NonZeroUsize;
|
use std::num::NonZeroUsize;
|
||||||
use std::sync::Arc;
|
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::engine::Engine;
|
||||||
use crate::foundations::{
|
use crate::foundations::{
|
||||||
cast, elem, scope, Content, Fold, Packed, Show, Smart, StyleChain,
|
cast, elem, scope, Content, Fold, Packed, Show, Smart, StyleChain,
|
||||||
};
|
};
|
||||||
use crate::layout::{
|
use crate::layout::{
|
||||||
show_grid_cell, Abs, Alignment, Axes, Cell, CellGrid, Celled, Dir, Fragment,
|
show_grid_cell, Abs, Alignment, Axes, Cell, CellGrid, Celled, Dir, Fragment,
|
||||||
GridCell, GridHLine, GridItem, GridLayouter, GridVLine, LayoutMultiple, Length,
|
GridCell, GridHLine, GridHeader, GridLayouter, GridVLine, LayoutMultiple, Length,
|
||||||
LinePosition, OuterHAlignment, OuterVAlignment, Regions, Rel, ResolvableCell, Sides,
|
LinePosition, OuterHAlignment, OuterVAlignment, Regions, Rel, ResolvableCell,
|
||||||
TrackSizings,
|
ResolvableGridChild, ResolvableGridItem, Sides, TrackSizings,
|
||||||
};
|
};
|
||||||
use crate::model::Figurable;
|
use crate::model::Figurable;
|
||||||
use crate::syntax::Span;
|
use crate::syntax::Span;
|
||||||
@ -221,6 +221,9 @@ impl TableElem {
|
|||||||
|
|
||||||
#[elem]
|
#[elem]
|
||||||
type TableVLine;
|
type TableVLine;
|
||||||
|
|
||||||
|
#[elem]
|
||||||
|
type TableHeader;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LayoutMultiple for Packed<TableElem> {
|
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());
|
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
|
// Use trace to link back to the table when a specific cell errors
|
||||||
let tracepoint = || Tracepoint::Call(Some(eco_format!("table")));
|
let tracepoint = || Tracepoint::Call(Some(eco_format!("table")));
|
||||||
let items = self.children().iter().map(|child| match child {
|
let children = self.children().iter().map(|child| match child {
|
||||||
TableChild::HLine(hline) => GridItem::HLine {
|
TableChild::Header(header) => ResolvableGridChild::Header {
|
||||||
y: hline.y(styles),
|
repeat: header.repeat(styles),
|
||||||
start: hline.start(styles),
|
span: header.span(),
|
||||||
end: hline.end(styles),
|
items: header.children().iter().map(|child| child.to_resolvable(styles)),
|
||||||
stroke: hline.stroke(styles),
|
|
||||||
span: hline.span(),
|
|
||||||
position: match hline.position(styles) {
|
|
||||||
OuterVAlignment::Top => LinePosition::Before,
|
|
||||||
OuterVAlignment::Bottom => LinePosition::After,
|
|
||||||
},
|
},
|
||||||
},
|
TableChild::Item(item) => {
|
||||||
TableChild::VLine(vline) => GridItem::VLine {
|
ResolvableGridChild::Item(item.to_resolvable(styles))
|
||||||
x: vline.x(styles),
|
|
||||||
start: vline.start(styles),
|
|
||||||
end: vline.end(styles),
|
|
||||||
stroke: vline.stroke(styles),
|
|
||||||
span: vline.span(),
|
|
||||||
position: match vline.position(styles) {
|
|
||||||
OuterHAlignment::Left if TextElem::dir_in(styles) == Dir::RTL => {
|
|
||||||
LinePosition::After
|
|
||||||
}
|
}
|
||||||
OuterHAlignment::Right if TextElem::dir_in(styles) == Dir::RTL => {
|
|
||||||
LinePosition::Before
|
|
||||||
}
|
|
||||||
OuterHAlignment::Start | OuterHAlignment::Left => {
|
|
||||||
LinePosition::Before
|
|
||||||
}
|
|
||||||
OuterHAlignment::End | OuterHAlignment::Right => LinePosition::After,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
TableChild::Cell(cell) => GridItem::Cell(cell.clone()),
|
|
||||||
});
|
});
|
||||||
let grid = CellGrid::resolve(
|
let grid = CellGrid::resolve(
|
||||||
tracks,
|
tracks,
|
||||||
gutter,
|
gutter,
|
||||||
items,
|
children,
|
||||||
fill,
|
fill,
|
||||||
align,
|
align,
|
||||||
&inset,
|
&inset,
|
||||||
@ -338,50 +318,138 @@ impl Figurable for Packed<TableElem> {}
|
|||||||
/// Any child of a table element.
|
/// Any child of a table element.
|
||||||
#[derive(Debug, PartialEq, Clone, Hash)]
|
#[derive(Debug, PartialEq, Clone, Hash)]
|
||||||
pub enum TableChild {
|
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>),
|
HLine(Packed<TableHLine>),
|
||||||
VLine(Packed<TableVLine>),
|
VLine(Packed<TableVLine>),
|
||||||
Cell(Packed<TableCell>),
|
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! {
|
cast! {
|
||||||
TableChild,
|
TableItem,
|
||||||
self => match self {
|
self => match self {
|
||||||
Self::HLine(hline) => hline.into_value(),
|
Self::HLine(hline) => hline.into_value(),
|
||||||
Self::VLine(vline) => vline.into_value(),
|
Self::VLine(vline) => vline.into_value(),
|
||||||
Self::Cell(cell) => cell.into_value(),
|
Self::Cell(cell) => cell.into_value(),
|
||||||
},
|
},
|
||||||
v: Content => {
|
v: Content => {
|
||||||
if v.is::<GridCell>() {
|
v.try_into()?
|
||||||
bail!(
|
},
|
||||||
"cannot use `grid.cell` as a table cell; use `table.cell` instead"
|
}
|
||||||
);
|
|
||||||
|
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>() {
|
if value.is::<TableHeader>() {
|
||||||
bail!(
|
bail!("cannot place a table header within another header");
|
||||||
"cannot use `grid.hline` as a table line; use `table.hline` instead"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if v.is::<GridVLine>() {
|
if value.is::<GridCell>() {
|
||||||
bail!(
|
bail!("cannot use `grid.cell` as a table cell; use `table.cell` instead");
|
||||||
"cannot use `grid.vline` as a table line; use `table.vline` 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 {
|
/// A repeatable table header.
|
||||||
fn from(value: Content) -> Self {
|
#[elem(name = "header", title = "Table Header")]
|
||||||
value
|
pub struct TableHeader {
|
||||||
.into_packed::<TableHLine>()
|
/// Whether this header should be repeated across pages.
|
||||||
.map(TableChild::HLine)
|
#[default(true)]
|
||||||
.or_else(|value| value.into_packed::<TableVLine>().map(TableChild::VLine))
|
pub repeat: bool,
|
||||||
.or_else(|value| value.into_packed::<TableCell>().map(TableChild::Cell))
|
|
||||||
.unwrap_or_else(|value| {
|
/// The cells and lines within the header.
|
||||||
let span = value.span();
|
#[variadic]
|
||||||
TableChild::Cell(Packed::new(TableCell::new(value)).spanned(span))
|
pub children: Vec<TableItem>,
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A horizontal line in the table. See the docs for
|
/// 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: 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]
|
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],
|
[e],
|
||||||
[f]
|
[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()
|
||||||
|
)
|
||||||
|