summaryrefslogtreecommitdiff
path: root/crates
diff options
context:
space:
mode:
authorPgBiel <9021226+PgBiel@users.noreply.github.com>2025-03-24 17:42:48 -0300
committerGitHub <noreply@github.com>2025-03-24 20:42:48 +0000
commit1f1c1338785dc09a43292cf106b4a23b4e1bd86e (patch)
treea537fd755a8b69039156b40225523d3831340771 /crates
parent1e591ac8dcfb7160bd401e76b4ff39aec80db219 (diff)
Refactor grid header and footer resolving (#5919)
Diffstat (limited to 'crates')
-rw-r--r--crates/typst-library/src/layout/grid/resolve.rs1932
1 files changed, 1181 insertions, 751 deletions
diff --git a/crates/typst-library/src/layout/grid/resolve.rs b/crates/typst-library/src/layout/grid/resolve.rs
index 08d0130d..bad25b47 100644
--- a/crates/typst-library/src/layout/grid/resolve.rs
+++ b/crates/typst-library/src/layout/grid/resolve.rs
@@ -1,4 +1,5 @@
use std::num::NonZeroUsize;
+use std::ops::Range;
use std::sync::Arc;
use ecow::eco_format;
@@ -20,6 +21,8 @@ use typst_library::Dir;
use typst_syntax::Span;
use typst_utils::NonZeroExt;
+use crate::introspection::SplitLocator;
+
/// Convert a grid to a cell grid.
#[typst_macros::time(span = elem.span())]
pub fn grid_to_cellgrid<'a>(
@@ -57,7 +60,7 @@ pub fn grid_to_cellgrid<'a>(
ResolvableGridChild::Item(grid_item_to_resolvable(item, styles))
}
});
- CellGrid::resolve(
+ resolve_cellgrid(
tracks,
gutter,
locator,
@@ -110,7 +113,7 @@ pub fn table_to_cellgrid<'a>(
ResolvableGridChild::Item(table_item_to_resolvable(item, styles))
}
});
- CellGrid::resolve(
+ resolve_cellgrid(
tracks,
gutter,
locator,
@@ -421,12 +424,14 @@ pub struct Line {
}
/// A repeatable grid header. Starts at the first row.
+#[derive(Debug)]
pub struct Header {
/// The index after the last row included in this header.
pub end: usize,
}
/// A repeatable grid footer. Stops at the last row.
+#[derive(Debug)]
pub struct Footer {
/// The first row included in this footer.
pub start: usize,
@@ -652,36 +657,318 @@ impl<'a> CellGrid<'a> {
Self::new_internal(tracks, gutter, vec![], vec![], None, None, entries)
}
- /// Resolves and positions all cells in the grid before creating it.
- /// Allows them to keep track of their final properties and positions
- /// and adjust their fields accordingly.
- /// Cells must implement Clone as they will be owned. Additionally, they
- /// must implement Default in order to fill positions in the grid which
- /// weren't explicitly specified by the user with empty cells.
- #[allow(clippy::too_many_arguments)]
- pub fn resolve<T, C, I>(
+ /// Generates the cell grid, given the tracks and resolved entries.
+ pub fn new_internal(
tracks: Axes<&[Sizing]>,
gutter: Axes<&[Sizing]>,
- locator: Locator<'a>,
- children: C,
- fill: &Celled<Option<Paint>>,
- align: &Celled<Smart<Alignment>>,
- inset: &Celled<Sides<Option<Rel<Length>>>>,
- stroke: &ResolvedCelled<Sides<Option<Option<Arc<Stroke>>>>>,
- engine: &mut Engine,
- styles: StyleChain,
- span: Span,
- ) -> SourceResult<Self>
+ vlines: Vec<Vec<Line>>,
+ hlines: Vec<Vec<Line>>,
+ header: Option<Repeatable<Header>>,
+ footer: Option<Repeatable<Footer>>,
+ entries: Vec<Entry<'a>>,
+ ) -> Self {
+ let mut cols = vec![];
+ let mut rows = vec![];
+
+ // Number of content columns: Always at least one.
+ let num_cols = tracks.x.len().max(1);
+
+ // Number of content rows: At least as many as given, but also at least
+ // as many as needed to place each item.
+ let num_rows = {
+ let len = entries.len();
+ let given = tracks.y.len();
+ let needed = len / num_cols + (len % num_cols).clamp(0, 1);
+ given.max(needed)
+ };
+
+ let has_gutter = gutter.any(|tracks| !tracks.is_empty());
+ let auto = Sizing::Auto;
+ let zero = Sizing::Rel(Rel::zero());
+ let get_or = |tracks: &[_], idx, default| {
+ tracks.get(idx).or(tracks.last()).copied().unwrap_or(default)
+ };
+
+ // Collect content and gutter columns.
+ for x in 0..num_cols {
+ cols.push(get_or(tracks.x, x, auto));
+ if has_gutter {
+ cols.push(get_or(gutter.x, x, zero));
+ }
+ }
+
+ // Collect content and gutter rows.
+ for y in 0..num_rows {
+ rows.push(get_or(tracks.y, y, auto));
+ if has_gutter {
+ rows.push(get_or(gutter.y, y, zero));
+ }
+ }
+
+ // Remove superfluous gutter tracks.
+ if has_gutter {
+ cols.pop();
+ rows.pop();
+ }
+
+ Self {
+ cols,
+ rows,
+ entries,
+ vlines,
+ hlines,
+ header,
+ footer,
+ has_gutter,
+ }
+ }
+
+ /// Get the grid entry in column `x` and row `y`.
+ ///
+ /// Returns `None` if it's a gutter cell.
+ #[track_caller]
+ pub fn entry(&self, x: usize, y: usize) -> Option<&Entry<'a>> {
+ assert!(x < self.cols.len());
+ assert!(y < self.rows.len());
+
+ if self.has_gutter {
+ // Even columns and rows are children, odd ones are gutter.
+ if x % 2 == 0 && y % 2 == 0 {
+ let c = 1 + self.cols.len() / 2;
+ self.entries.get((y / 2) * c + x / 2)
+ } else {
+ None
+ }
+ } else {
+ let c = self.cols.len();
+ self.entries.get(y * c + x)
+ }
+ }
+
+ /// Get the content of the cell in column `x` and row `y`.
+ ///
+ /// Returns `None` if it's a gutter cell or merged position.
+ #[track_caller]
+ pub fn cell(&self, x: usize, y: usize) -> Option<&Cell<'a>> {
+ self.entry(x, y).and_then(Entry::as_cell)
+ }
+
+ /// Returns the position of the parent cell of the grid entry at the given
+ /// position. It is guaranteed to have a non-gutter, non-merged cell at
+ /// the returned position, due to how the grid is built.
+ /// - If the entry at the given position is a cell, returns the given
+ /// position.
+ /// - If it is a merged cell, returns the parent cell's position.
+ /// - If it is a gutter cell, returns None.
+ #[track_caller]
+ pub fn parent_cell_position(&self, x: usize, y: usize) -> Option<Axes<usize>> {
+ self.entry(x, y).map(|entry| match entry {
+ Entry::Cell(_) => Axes::new(x, y),
+ Entry::Merged { parent } => {
+ let c = self.non_gutter_column_count();
+ let factor = if self.has_gutter { 2 } else { 1 };
+ Axes::new(factor * (*parent % c), factor * (*parent / c))
+ }
+ })
+ }
+
+ /// Returns the position of the actual parent cell of a merged position,
+ /// even if the given position is gutter, in which case we return the
+ /// parent of the nearest adjacent content cell which could possibly span
+ /// the given gutter position. If the given position is not a gutter cell,
+ /// then this function will return the same as `parent_cell_position` would.
+ /// If the given position is a gutter cell, but no cell spans it, returns
+ /// `None`.
+ ///
+ /// This is useful for lines. A line needs to check if a cell next to it
+ /// has a stroke override - even at a gutter position there could be a
+ /// stroke override, since a cell could be merged with two cells at both
+ /// ends of the gutter cell (e.g. to its left and to its right), and thus
+ /// that cell would impose a stroke under the gutter. This function allows
+ /// getting the position of that cell (which spans the given gutter
+ /// position, if it is gutter), if it exists; otherwise returns None (it's
+ /// gutter and no cell spans it).
+ #[track_caller]
+ pub fn effective_parent_cell_position(
+ &self,
+ x: usize,
+ y: usize,
+ ) -> Option<Axes<usize>> {
+ if self.has_gutter {
+ // If (x, y) is a gutter cell, we skip it (skip a gutter column and
+ // row) to the nearest adjacent content cell, in the direction
+ // which merged cells grow toward (increasing x and increasing y),
+ // such that we can verify if that adjacent cell is merged with the
+ // gutter cell by checking if its parent would come before (x, y).
+ // Otherwise, no cell is merged with this gutter cell, and we
+ // return None.
+ self.parent_cell_position(x + x % 2, y + y % 2)
+ .filter(|&parent| parent.x <= x && parent.y <= y)
+ } else {
+ self.parent_cell_position(x, y)
+ }
+ }
+
+ /// Checks if the track with the given index is gutter.
+ /// Does not check if the index is a valid track.
+ #[inline]
+ pub fn is_gutter_track(&self, index: usize) -> bool {
+ self.has_gutter && index % 2 == 1
+ }
+
+ /// Returns the effective colspan of a cell, considering the gutters it
+ /// might span if the grid has gutters.
+ #[inline]
+ pub fn effective_colspan_of_cell(&self, cell: &Cell) -> usize {
+ if self.has_gutter {
+ 2 * cell.colspan.get() - 1
+ } else {
+ cell.colspan.get()
+ }
+ }
+
+ /// Returns the effective rowspan of a cell, considering the gutters it
+ /// might span if the grid has gutters.
+ #[inline]
+ pub fn effective_rowspan_of_cell(&self, cell: &Cell) -> usize {
+ if self.has_gutter {
+ 2 * cell.rowspan.get() - 1
+ } else {
+ cell.rowspan.get()
+ }
+ }
+
+ #[inline]
+ pub fn non_gutter_column_count(&self) -> usize {
+ if self.has_gutter {
+ // Calculation: With gutters, we have
+ // 'cols = 2 * (non-gutter cols) - 1', since there is a gutter
+ // column between each regular column. Therefore,
+ // 'floor(cols / 2)' will be equal to
+ // 'floor(non-gutter cols - 1/2) = non-gutter-cols - 1',
+ // so 'non-gutter cols = 1 + floor(cols / 2)'.
+ 1 + self.cols.len() / 2
+ } else {
+ self.cols.len()
+ }
+ }
+}
+
+/// Resolves and positions all cells in the grid before creating it.
+/// Allows them to keep track of their final properties and positions
+/// and adjust their fields accordingly.
+/// Cells must implement Clone as they will be owned. Additionally, they
+/// must implement Default in order to fill positions in the grid which
+/// weren't explicitly specified by the user with empty cells.
+#[allow(clippy::too_many_arguments)]
+pub fn resolve_cellgrid<'a, 'x, T, C, I>(
+ tracks: Axes<&'a [Sizing]>,
+ gutter: Axes<&'a [Sizing]>,
+ locator: Locator<'x>,
+ children: C,
+ fill: &'a Celled<Option<Paint>>,
+ align: &'a Celled<Smart<Alignment>>,
+ inset: &'a Celled<Sides<Option<Rel<Length>>>>,
+ stroke: &'a ResolvedCelled<Sides<Option<Option<Arc<Stroke>>>>>,
+ engine: &'a mut Engine,
+ styles: StyleChain<'a>,
+ span: Span,
+) -> SourceResult<CellGrid<'x>>
+where
+ T: ResolvableCell + Default,
+ I: Iterator<Item = ResolvableGridItem<T>>,
+ C: IntoIterator<Item = ResolvableGridChild<T, I>>,
+ C::IntoIter: ExactSizeIterator,
+{
+ CellGridResolver {
+ tracks,
+ gutter,
+ locator: locator.split(),
+ fill,
+ align,
+ inset,
+ stroke,
+ engine,
+ styles,
+ span,
+ }
+ .resolve(children)
+}
+
+struct CellGridResolver<'a, 'b, 'x> {
+ tracks: Axes<&'a [Sizing]>,
+ gutter: Axes<&'a [Sizing]>,
+ locator: SplitLocator<'x>,
+ fill: &'a Celled<Option<Paint>>,
+ align: &'a Celled<Smart<Alignment>>,
+ inset: &'a Celled<Sides<Option<Rel<Length>>>>,
+ stroke: &'a ResolvedCelled<Sides<Option<Option<Arc<Stroke>>>>>,
+ engine: &'a mut Engine<'b>,
+ styles: StyleChain<'a>,
+ span: Span,
+}
+
+#[derive(Debug, Clone, Copy)]
+enum RowGroupKind {
+ Header,
+ Footer,
+}
+
+impl RowGroupKind {
+ fn name(self) -> &'static str {
+ match self {
+ Self::Header => "header",
+ Self::Footer => "footer",
+ }
+ }
+}
+
+struct RowGroupData {
+ /// The range of rows of cells inside this grid row group. The
+ /// first and last rows are guaranteed to have cells (an exception
+ /// is made when there is gutter, in which case the group range may
+ /// be expanded to include an additional gutter row when there is a
+ /// repeatable header or footer). This is `None` until the first
+ /// cell of the row group is placed, then it is continually adjusted
+ /// to fit the cells inside the row group.
+ ///
+ /// This stays as `None` for fully empty headers and footers.
+ range: Option<Range<usize>>,
+ span: Span,
+ kind: RowGroupKind,
+
+ /// Start of the range of indices of hlines at the top of the row group.
+ /// This is always the first index after the last hline before we started
+ /// building the row group - any upcoming hlines would appear at least at
+ /// this index.
+ ///
+ /// These hlines were auto-positioned and appeared before any auto-pos
+ /// cells, so they will appear at the first possible row (above the
+ /// first row spanned by the row group).
+ top_hlines_start: usize,
+
+ /// End of the range of indices of hlines at the top of the row group.
+ ///
+ /// This starts as `None`, meaning that, if we stop the loop before we find
+ /// any auto-pos cells, all auto-pos hlines after the last hline (after the
+ /// index `top_hlines_start`) should be moved to the top of the row group.
+ ///
+ /// It becomes `Some(index of last hline at the top)` when an auto-pos cell
+ /// is found, as auto-pos hlines after any auto-pos cells appear below
+ /// them, not at the top of the row group.
+ top_hlines_end: Option<usize>,
+}
+
+impl<'x> CellGridResolver<'_, '_, 'x> {
+ fn resolve<T, C, I>(mut self, children: C) -> SourceResult<CellGrid<'x>>
where
T: ResolvableCell + Default,
I: Iterator<Item = ResolvableGridItem<T>>,
C: IntoIterator<Item = ResolvableGridChild<T, I>>,
C::IntoIter: ExactSizeIterator,
{
- let mut locator = locator.split();
-
// Number of content columns: Always at least one.
- let c = tracks.x.len().max(1);
+ let columns = self.tracks.x.len().max(1);
// Lists of lines.
// Horizontal lines are only pushed later to be able to check for row
@@ -695,7 +982,7 @@ impl<'a> CellGrid<'a> {
// For consistency, only push vertical lines later as well.
let mut pending_vlines: Vec<(Span, Line)> = vec![];
- let has_gutter = gutter.any(|tracks| !tracks.is_empty());
+ let has_gutter = self.gutter.any(|tracks| !tracks.is_empty());
let mut header: Option<Header> = None;
let mut repeat_header = false;
@@ -705,27 +992,6 @@ impl<'a> CellGrid<'a> {
let mut footer: Option<(usize, Span, Footer)> = None;
let mut repeat_footer = false;
- // Resolves the breakability of a cell. Cells that span at least one
- // auto-sized row or gutter are considered breakable.
- let resolve_breakable = |y, rowspan| {
- let auto = Sizing::Auto;
- let zero = Sizing::Rel(Rel::zero());
- tracks
- .y
- .iter()
- .chain(std::iter::repeat(tracks.y.last().unwrap_or(&auto)))
- .skip(y)
- .take(rowspan)
- .any(|row| row == &Sizing::Auto)
- || gutter
- .y
- .iter()
- .chain(std::iter::repeat(gutter.y.last().unwrap_or(&zero)))
- .skip(y)
- .take(rowspan - 1)
- .any(|row_gutter| row_gutter == &Sizing::Auto)
- };
-
// We can't just use the cell's index in the 'cells' vector to
// determine its automatic position, since cells could have arbitrary
// positions, so the position of a cell in 'cells' can differ from its
@@ -733,532 +999,605 @@ impl<'a> CellGrid<'a> {
// Therefore, we use a counter, 'auto_index', to determine the position
// of the next cell with (x: auto, y: auto). It is only stepped when
// a cell with (x: auto, y: auto), usually the vast majority, is found.
+ //
+ // Note that a separate counter ('local_auto_index') is used within
+ // headers and footers, as explained above its definition. Outside of
+ // those (when the table child being processed is a single cell),
+ // 'local_auto_index' will simply be an alias for 'auto_index', which
+ // will be updated after that cell is placed, if it is an
+ // automatically-positioned cell.
let mut auto_index: usize = 0;
- // We have to rebuild the grid to account for arbitrary positions.
+ // We have to rebuild the grid to account for fixed cell positions.
+ //
// Create at least 'children.len()' positions, since there could be at
// least 'children.len()' cells (if no explicit lines were specified),
- // even though some of them might be placed in arbitrary positions and
- // thus cause the grid to expand.
- // Additionally, make sure we allocate up to the next multiple of 'c',
- // since each row will have 'c' cells, even if the last few cells
- // weren't explicitly specified by the user.
- // We apply '% c' twice so that the amount of cells potentially missing
- // is zero when 'children.len()' is already a multiple of 'c' (thus
- // 'children.len() % c' would be zero).
+ // even though some of them might be placed in fixed positions and thus
+ // cause the grid to expand.
+ //
+ // Additionally, make sure we allocate up to the next multiple of
+ // 'columns', since each row will have 'columns' cells, even if the
+ // last few cells weren't explicitly specified by the user.
let children = children.into_iter();
- let Some(child_count) = children.len().checked_add((c - children.len() % c) % c)
- else {
- bail!(span, "too many cells or lines were given")
+ let Some(child_count) = children.len().checked_next_multiple_of(columns) else {
+ bail!(self.span, "too many cells or lines were given")
};
let mut resolved_cells: Vec<Option<Entry>> = Vec::with_capacity(child_count);
for child in children {
- let mut is_header = false;
- let mut is_footer = false;
- let mut child_start = usize::MAX;
- let mut child_end = 0;
- let mut child_span = Span::detached();
- let mut start_new_row = false;
- let mut first_index_of_top_hlines = usize::MAX;
- let mut first_index_of_non_top_hlines = usize::MAX;
-
- let (header_footer_items, simple_item) = match child {
- ResolvableGridChild::Header { repeat, span, items, .. } => {
- if header.is_some() {
- bail!(span, "cannot have more than one header");
- }
+ self.resolve_grid_child(
+ columns,
+ &mut pending_hlines,
+ &mut pending_vlines,
+ &mut header,
+ &mut repeat_header,
+ &mut footer,
+ &mut repeat_footer,
+ &mut auto_index,
+ &mut resolved_cells,
+ child,
+ )?;
+ }
- is_header = true;
- child_span = span;
- repeat_header = repeat;
+ let resolved_cells = self.fixup_cells::<T>(resolved_cells, columns)?;
- // If any cell in the header is automatically positioned,
- // have it skip to the next row. This is to avoid having a
- // header after a partially filled row just add cells to
- // that row instead of starting a new one.
- // FIXME: Revise this approach when headers can start from
- // arbitrary rows.
- start_new_row = true;
+ let row_amount = resolved_cells.len().div_ceil(columns);
+ let (hlines, vlines) = self.collect_lines(
+ pending_hlines,
+ pending_vlines,
+ has_gutter,
+ columns,
+ row_amount,
+ )?;
+
+ let (header, footer) = self.finalize_headers_and_footers(
+ has_gutter,
+ header,
+ repeat_header,
+ footer,
+ repeat_footer,
+ row_amount,
+ )?;
- // Any hlines at the top of the header will start at this
- // index.
- first_index_of_top_hlines = pending_hlines.len();
+ Ok(CellGrid::new_internal(
+ self.tracks,
+ self.gutter,
+ vlines,
+ hlines,
+ header,
+ footer,
+ resolved_cells,
+ ))
+ }
- (Some(items), None)
+ /// Resolve a grid child, which can be a header, a footer (both of which
+ /// are row groups, and thus contain multiple grid items inside them), or
+ /// a grid item - a cell, an hline or a vline.
+ ///
+ /// This process consists of placing the child and any sub-items into
+ /// appropriate positions in the resolved grid. This is mostly relevant for
+ /// items without fixed positions, such that they must be placed after the
+ /// previous one, perhaps skipping existing cells along the way.
+ #[allow(clippy::too_many_arguments)]
+ fn resolve_grid_child<T, I>(
+ &mut self,
+ columns: usize,
+ pending_hlines: &mut Vec<(Span, Line, bool)>,
+ pending_vlines: &mut Vec<(Span, Line)>,
+ header: &mut Option<Header>,
+ repeat_header: &mut bool,
+ footer: &mut Option<(usize, Span, Footer)>,
+ repeat_footer: &mut bool,
+ auto_index: &mut usize,
+ resolved_cells: &mut Vec<Option<Entry<'x>>>,
+ child: ResolvableGridChild<T, I>,
+ ) -> SourceResult<()>
+ where
+ T: ResolvableCell + Default,
+ I: Iterator<Item = ResolvableGridItem<T>>,
+ {
+ // Data for the row group in this iteration.
+ //
+ // Note that cells outside headers and footers are grid children
+ // with a single cell inside, and thus not considered row groups,
+ // in which case this variable remains 'None'.
+ let mut row_group_data: Option<RowGroupData> = None;
+
+ // The normal auto index should only be stepped (upon placing an
+ // automatically-positioned cell, to indicate the position of the
+ // next) outside of headers or footers, in which case the auto
+ // index will be updated with the local auto index. Inside headers
+ // and footers, however, cells can only start after the first empty
+ // row (as determined by 'first_available_row' below), meaning that
+ // the next automatically-positioned cell will be in a different
+ // position than it would usually be if it would be in a non-empty
+ // row, so we must step a local index inside headers and footers
+ // instead, and use a separate counter outside them.
+ let mut local_auto_index = *auto_index;
+
+ // The first row in which this table group can fit.
+ //
+ // Within headers and footers, this will correspond to the first
+ // fully empty row available in the grid. This is because headers
+ // and footers always occupy entire rows, so they cannot occupy
+ // a non-empty row.
+ let mut first_available_row = 0;
+
+ let (header_footer_items, simple_item) = match child {
+ ResolvableGridChild::Header { repeat, span, items, .. } => {
+ if header.is_some() {
+ bail!(span, "cannot have more than one header");
}
- ResolvableGridChild::Footer { repeat, span, items, .. } => {
- if footer.is_some() {
- bail!(span, "cannot have more than one footer");
- }
- is_footer = true;
- child_span = span;
- repeat_footer = repeat;
+ row_group_data = Some(RowGroupData {
+ range: None,
+ span,
+ kind: RowGroupKind::Header,
+ top_hlines_start: pending_hlines.len(),
+ top_hlines_end: None,
+ });
+
+ *repeat_header = repeat;
- // If any cell in the footer is automatically positioned,
- // have it skip to the next row. This is to avoid having a
- // footer after a partially filled row just add cells to
- // that row instead of starting a new one.
- start_new_row = true;
+ first_available_row =
+ find_next_empty_row(resolved_cells, local_auto_index, columns);
- // Any hlines at the top of the footer will start at this
- // index.
- first_index_of_top_hlines = pending_hlines.len();
+ // If any cell in the header is automatically positioned,
+ // have it skip to the next empty row. This is to avoid
+ // having a header after a partially filled row just add
+ // cells to that row instead of starting a new one.
+ //
+ // Note that the first fully empty row is always after the
+ // latest auto-position cell, since each auto-position cell
+ // always occupies the first available position after the
+ // previous one. Therefore, this will be >= auto_index.
+ local_auto_index = first_available_row * columns;
- (Some(items), None)
+ (Some(items), None)
+ }
+ ResolvableGridChild::Footer { repeat, span, items, .. } => {
+ if footer.is_some() {
+ bail!(span, "cannot have more than one footer");
}
- ResolvableGridChild::Item(item) => (None, Some(item)),
- };
- let items = header_footer_items
- .into_iter()
- .flatten()
- .chain(simple_item.into_iter());
- for item in items {
- let cell = match item {
- ResolvableGridItem::HLine {
- y,
- start,
- end,
- stroke,
- span,
- position,
- } => {
- let has_auto_y = y.is_auto();
- let y = y.unwrap_or_else(|| {
- // Avoid placing the hline inside consecutive
- // rowspans occupying all columns, as it'd just
- // disappear, at least when there's no column
- // gutter.
- skip_auto_index_through_fully_merged_rows(
- &resolved_cells,
- &mut auto_index,
- c,
- );
-
- // When no 'y' is specified for the hline, we place
- // it under the latest automatically positioned
- // cell.
- // The current value of the auto index is always
- // the index of the latest automatically positioned
- // cell placed plus one (that's what we do in
- // 'resolve_cell_position'), so we subtract 1 to
- // get that cell's index, and place the hline below
- // its row. The exception is when the auto_index is
- // 0, meaning no automatically positioned cell was
- // placed yet. In that case, we place the hline at
- // the top of the table.
- //
- // Exceptionally, the hline will be placed before
- // the minimum auto index if the current auto index
- // from previous iterations is smaller than the
- // minimum it should have for the current grid
- // child. Effectively, this means that a hline at
- // the start of a header will always appear above
- // that header's first row. Similarly for footers.
- auto_index
- .checked_sub(1)
- .map_or(0, |last_auto_index| last_auto_index / c + 1)
- });
- if end.is_some_and(|end| end.get() < start) {
- bail!(span, "line cannot end before it starts");
- }
- let line = Line { index: y, start, end, stroke, position };
-
- // Since the amount of rows is dynamic, delay placing
- // hlines until after all cells were placed so we can
- // properly verify if they are valid. Note that we
- // can't place hlines even if we already know they
- // would be in a valid row, since it's possible that we
- // pushed pending hlines in the same row as this one in
- // previous iterations, and we need to ensure that
- // hlines from previous iterations are pushed to the
- // final vector of hlines first - the order of hlines
- // must be kept, as this matters when determining which
- // one "wins" in case of conflict. Pushing the current
- // hline before we push pending hlines later would
- // change their order!
- pending_hlines.push((span, line, has_auto_y));
- continue;
+ row_group_data = Some(RowGroupData {
+ range: None,
+ span,
+ kind: RowGroupKind::Footer,
+ top_hlines_start: pending_hlines.len(),
+ top_hlines_end: None,
+ });
+
+ *repeat_footer = repeat;
+
+ first_available_row =
+ find_next_empty_row(resolved_cells, local_auto_index, columns);
+
+ local_auto_index = first_available_row * columns;
+
+ (Some(items), None)
+ }
+ ResolvableGridChild::Item(item) => (None, Some(item)),
+ };
+
+ let items = header_footer_items.into_iter().flatten().chain(simple_item);
+ for item in items {
+ let cell = match item {
+ ResolvableGridItem::HLine { y, start, end, stroke, span, position } => {
+ let has_auto_y = y.is_auto();
+ let y = y.unwrap_or_else(|| {
+ // Avoid placing the hline inside consecutive
+ // rowspans occupying all columns, as it'd just
+ // disappear, at least when there's no column
+ // gutter.
+ skip_auto_index_through_fully_merged_rows(
+ resolved_cells,
+ &mut local_auto_index,
+ columns,
+ );
+
+ // When no 'y' is specified for the hline, we place
+ // it under the latest automatically positioned
+ // cell.
+ // The current value of the auto index is always
+ // the index of the latest automatically positioned
+ // cell placed plus one (that's what we do in
+ // 'resolve_cell_position'), so we subtract 1 to
+ // get that cell's index, and place the hline below
+ // its row. The exception is when the auto_index is
+ // 0, meaning no automatically positioned cell was
+ // placed yet. In that case, we place the hline at
+ // the top of the table.
+ //
+ // Exceptionally, the hline will be placed before
+ // the minimum auto index if the current auto index
+ // from previous iterations is smaller than the
+ // minimum it should have for the current grid
+ // child. Effectively, this means that a hline at
+ // the start of a header will always appear above
+ // that header's first row. Similarly for footers.
+ local_auto_index
+ .checked_sub(1)
+ .map_or(0, |last_auto_index| last_auto_index / columns + 1)
+ });
+ if end.is_some_and(|end| end.get() < start) {
+ bail!(span, "line cannot end before it starts");
}
- ResolvableGridItem::VLine {
- x,
- start,
- end,
- stroke,
- span,
- position,
- } => {
- let x = x.unwrap_or_else(|| {
- // When no 'x' is specified for the vline, we place
- // it after the latest automatically positioned
- // cell.
- // The current value of the auto index is always
- // the index of the latest automatically positioned
- // cell placed plus one (that's what we do in
- // 'resolve_cell_position'), so we subtract 1 to
- // get that cell's index, and place the vline after
- // its column. The exception is when the auto_index
- // is 0, meaning no automatically positioned cell
- // was placed yet. In that case, we place the vline
- // to the left of the table.
- //
- // Exceptionally, a vline is also placed to the
- // left of the table if we should start a new row
- // for the next automatically positioned cell.
- // For example, this means that a vline at
- // the beginning of a header will be placed to its
- // left rather than after the previous
- // automatically positioned cell. Same for footers.
- auto_index
- .checked_sub(1)
- .filter(|_| !start_new_row)
- .map_or(0, |last_auto_index| last_auto_index % c + 1)
- });
- if end.is_some_and(|end| end.get() < start) {
- bail!(span, "line cannot end before it starts");
- }
- let line = Line { index: x, start, end, stroke, position };
-
- // For consistency with hlines, we only push vlines to
- // the final vector of vlines after processing every
+ let line = Line { index: y, start, end, stroke, position };
+
+ // Since the amount of rows is dynamic, delay placing
+ // hlines until after all cells were placed so we can
+ // properly verify if they are valid. Note that we
+ // can't place hlines even if we already know they
+ // would be in a valid row, since it's possible that we
+ // pushed pending hlines in the same row as this one in
+ // previous iterations, and we need to ensure that
+ // hlines from previous iterations are pushed to the
+ // final vector of hlines first - the order of hlines
+ // must be kept, as this matters when determining which
+ // one "wins" in case of conflict. Pushing the current
+ // hline before we push pending hlines later would
+ // change their order!
+ pending_hlines.push((span, line, has_auto_y));
+ continue;
+ }
+ ResolvableGridItem::VLine { x, start, end, stroke, span, position } => {
+ let x = x.unwrap_or_else(|| {
+ // When no 'x' is specified for the vline, we place
+ // it after the latest automatically positioned
// cell.
- pending_vlines.push((span, line));
- continue;
+ // The current value of the auto index is always
+ // the index of the latest automatically positioned
+ // cell placed plus one (that's what we do in
+ // 'resolve_cell_position'), so we subtract 1 to
+ // get that cell's index, and place the vline after
+ // its column. The exception is when the auto_index
+ // is 0, meaning no automatically positioned cell
+ // was placed yet. In that case, we place the vline
+ // to the left of the table.
+ //
+ // Exceptionally, a vline is also placed to the
+ // left of the table when specified at the start
+ // of a row group, such as a header or footer, that
+ // is, when no automatically-positioned cells have
+ // been specified for that group yet.
+ // For example, this means that a vline at
+ // the beginning of a header will be placed to its
+ // left rather than after the previous
+ // automatically positioned cell. Same for footers.
+ local_auto_index
+ .checked_sub(1)
+ .filter(|_| local_auto_index > first_available_row * columns)
+ .map_or(0, |last_auto_index| last_auto_index % columns + 1)
+ });
+ if end.is_some_and(|end| end.get() < start) {
+ bail!(span, "line cannot end before it starts");
}
- ResolvableGridItem::Cell(cell) => cell,
- };
- let cell_span = cell.span();
- let colspan = cell.colspan(styles).get();
- let rowspan = cell.rowspan(styles).get();
- // Let's calculate the cell's final position based on its
- // requested position.
- let resolved_index = {
- let cell_x = cell.x(styles);
- let cell_y = cell.y(styles);
- resolve_cell_position(
- cell_x,
- cell_y,
- colspan,
+ let line = Line { index: x, start, end, stroke, position };
+
+ // For consistency with hlines, we only push vlines to
+ // the final vector of vlines after processing every
+ // cell.
+ pending_vlines.push((span, line));
+ continue;
+ }
+ ResolvableGridItem::Cell(cell) => cell,
+ };
+ let cell_span = cell.span();
+ let colspan = cell.colspan(self.styles).get();
+ let rowspan = cell.rowspan(self.styles).get();
+ // Let's calculate the cell's final position based on its
+ // requested position.
+ let resolved_index = {
+ let cell_x = cell.x(self.styles);
+ let cell_y = cell.y(self.styles);
+ resolve_cell_position(
+ cell_x,
+ cell_y,
+ colspan,
+ rowspan,
+ header.as_ref(),
+ footer.as_ref(),
+ resolved_cells,
+ &mut local_auto_index,
+ first_available_row,
+ columns,
+ row_group_data.is_some(),
+ )
+ .at(cell_span)?
+ };
+ let x = resolved_index % columns;
+ let y = resolved_index / columns;
+
+ if colspan > columns - x {
+ bail!(
+ cell_span,
+ "cell's colspan would cause it to exceed the available column(s)";
+ hint: "try placing the cell in another position or reducing its colspan"
+ )
+ }
+
+ let Some(largest_index) = columns
+ .checked_mul(rowspan - 1)
+ .and_then(|full_rowspan_offset| {
+ resolved_index.checked_add(full_rowspan_offset)
+ })
+ .and_then(|last_row_pos| last_row_pos.checked_add(colspan - 1))
+ else {
+ bail!(
+ cell_span,
+ "cell would span an exceedingly large position";
+ hint: "try reducing the cell's rowspan or colspan"
+ )
+ };
+
+ // Cell's header or footer must expand to include the cell's
+ // occupied positions, if possible.
+ if let Some(RowGroupData {
+ range: group_range, kind, top_hlines_end, ..
+ }) = &mut row_group_data
+ {
+ *group_range = Some(
+ expand_row_group(
+ resolved_cells,
+ group_range.as_ref(),
+ *kind,
+ first_available_row,
+ y,
rowspan,
- &resolved_cells,
- &mut auto_index,
- &mut start_new_row,
- c,
- )
- .at(cell_span)?
- };
- let x = resolved_index % c;
- let y = resolved_index / c;
-
- if colspan > c - x {
- bail!(
- cell_span,
- "cell's colspan would cause it to exceed the available column(s)";
- hint: "try placing the cell in another position or reducing its colspan"
+ columns,
)
+ .at(cell_span)?,
+ );
+
+ if top_hlines_end.is_none()
+ && local_auto_index > first_available_row * columns
+ {
+ // Auto index was moved, so upcoming auto-pos hlines should
+ // no longer appear at the top.
+ *top_hlines_end = Some(pending_hlines.len());
}
+ }
- let Some(largest_index) = c
- .checked_mul(rowspan - 1)
- .and_then(|full_rowspan_offset| {
- resolved_index.checked_add(full_rowspan_offset)
- })
- .and_then(|last_row_pos| last_row_pos.checked_add(colspan - 1))
+ // Let's resolve the cell so it can determine its own fields
+ // based on its final position.
+ let cell = self.resolve_cell(cell, x, y, rowspan, cell_span)?;
+
+ if largest_index >= resolved_cells.len() {
+ // Ensure the length of the vector of resolved cells is
+ // always a multiple of 'columns' by pushing full rows every
+ // time. Here, we add enough absent positions (later
+ // converted to empty cells) to ensure the last row in the
+ // new vector length is completely filled. This is
+ // necessary so that those positions, even if not
+ // explicitly used at the end, are eventually susceptible
+ // to show rules and receive grid styling, as they will be
+ // resolved as empty cells in a second loop below.
+ let Some(new_len) = largest_index
+ .checked_add(1)
+ .and_then(|new_len| new_len.checked_next_multiple_of(columns))
else {
- bail!(
- cell_span,
- "cell would span an exceedingly large position";
- hint: "try reducing the cell's rowspan or colspan"
- )
+ bail!(cell_span, "cell position too large")
};
- // Let's resolve the cell so it can determine its own fields
- // based on its final position.
- let cell = cell.resolve_cell(
- x,
- y,
- &fill.resolve(engine, styles, x, y)?,
- align.resolve(engine, styles, x, y)?,
- inset.resolve(engine, styles, x, y)?,
- stroke.resolve(engine, styles, x, y)?,
- resolve_breakable(y, rowspan),
- locator.next(&cell_span),
- styles,
- );
-
- if largest_index >= resolved_cells.len() {
- // Ensure the length of the vector of resolved cells is
- // always a multiple of 'c' by pushing full rows every
- // time. Here, we add enough absent positions (later
- // converted to empty cells) to ensure the last row in the
- // new vector length is completely filled. This is
- // necessary so that those positions, even if not
- // explicitly used at the end, are eventually susceptible
- // to show rules and receive grid styling, as they will be
- // resolved as empty cells in a second loop below.
- let Some(new_len) = largest_index
- .checked_add(1)
- .and_then(|new_len| new_len.checked_add((c - new_len % c) % c))
- else {
- bail!(cell_span, "cell position too large")
- };
-
- // Here, the cell needs to be placed in a position which
- // doesn't exist yet in the grid (out of bounds). We will
- // add enough absent positions for this to be possible.
- // They must be absent as no cells actually occupy them
- // (they can be overridden later); however, if no cells
- // occupy them as we finish building the grid, then such
- // positions will be replaced by empty cells.
- resolved_cells.resize_with(new_len, || None);
- }
+ // Here, the cell needs to be placed in a position which
+ // doesn't exist yet in the grid (out of bounds). We will
+ // add enough absent positions for this to be possible.
+ // They must be absent as no cells actually occupy them
+ // (they can be overridden later); however, if no cells
+ // occupy them as we finish building the grid, then such
+ // positions will be replaced by empty cells.
+ resolved_cells.resize_with(new_len, || None);
+ }
- // The vector is large enough to contain the cell, so we can
- // just index it directly to access the position it will be
- // placed in. However, we still need to ensure we won't try to
- // place a cell where there already is one.
- let slot = &mut resolved_cells[resolved_index];
- if slot.is_some() {
- bail!(
- cell_span,
- "attempted to place a second cell at column {x}, row {y}";
- hint: "try specifying your cells in a different order"
- );
- }
+ // The vector is large enough to contain the cell, so we can
+ // just index it directly to access the position it will be
+ // placed in. However, we still need to ensure we won't try to
+ // place a cell where there already is one.
+ let slot = &mut resolved_cells[resolved_index];
+ if slot.is_some() {
+ bail!(
+ cell_span,
+ "attempted to place a second cell at column {x}, row {y}";
+ hint: "try specifying your cells in a different order"
+ );
+ }
- *slot = Some(Entry::Cell(cell));
-
- // Now, if the cell spans more than one row or column, we fill
- // the spanned positions in the grid with Entry::Merged
- // pointing to the original cell as its parent.
- for rowspan_offset in 0..rowspan {
- let spanned_y = y + rowspan_offset;
- let first_row_index = resolved_index + c * rowspan_offset;
- for (colspan_offset, slot) in resolved_cells[first_row_index..]
- [..colspan]
- .iter_mut()
- .enumerate()
- {
- let spanned_x = x + colspan_offset;
- if spanned_x == x && spanned_y == y {
- // This is the parent cell.
- continue;
- }
- if slot.is_some() {
- bail!(
- cell_span,
- "cell would span a previously placed cell at column {spanned_x}, row {spanned_y}";
- hint: "try specifying your cells in a different order or reducing the cell's rowspan or colspan"
- )
- }
- *slot = Some(Entry::Merged { parent: resolved_index });
- }
- }
+ *slot = Some(Entry::Cell(cell));
- if is_header || is_footer {
- // Ensure each cell in a header or footer is fully
- // contained within it.
- child_start = child_start.min(y);
- child_end = child_end.max(y + rowspan);
-
- if start_new_row && child_start <= auto_index.div_ceil(c) {
- // No need to start a new row as we already include
- // the row of the next automatically positioned cell in
- // the header or footer.
- start_new_row = false;
+ // Now, if the cell spans more than one row or column, we fill
+ // the spanned positions in the grid with Entry::Merged
+ // pointing to the original cell as its parent.
+ for rowspan_offset in 0..rowspan {
+ let spanned_y = y + rowspan_offset;
+ let first_row_index = resolved_index + columns * rowspan_offset;
+ for (colspan_offset, slot) in
+ resolved_cells[first_row_index..][..colspan].iter_mut().enumerate()
+ {
+ let spanned_x = x + colspan_offset;
+ if spanned_x == x && spanned_y == y {
+ // This is the parent cell.
+ continue;
}
-
- if !start_new_row {
- // From now on, upcoming hlines won't be at the top of
- // the child, as the first automatically positioned
- // cell was placed.
- first_index_of_non_top_hlines =
- first_index_of_non_top_hlines.min(pending_hlines.len());
+ if slot.is_some() {
+ bail!(
+ cell_span,
+ "cell would span a previously placed cell at column {spanned_x}, row {spanned_y}";
+ hint: "try specifying your cells in a different order or reducing the cell's rowspan or colspan"
+ )
}
+ *slot = Some(Entry::Merged { parent: resolved_index });
}
}
+ }
- if (is_header || is_footer) && child_start == usize::MAX {
- // Empty header/footer: consider the header/footer to be
- // at the next empty row after the latest auto index.
- auto_index = find_next_empty_row(&resolved_cells, auto_index, c);
- child_start = auto_index.div_ceil(c);
- child_end = child_start + 1;
+ if let Some(row_group) = row_group_data {
+ let group_range = match row_group.range {
+ Some(group_range) => group_range,
- if resolved_cells.len() <= c * child_start {
- // Ensure the automatically chosen row actually exists.
- resolved_cells.resize_with(c * (child_start + 1), || None);
- }
- }
+ None => {
+ // Empty header/footer: consider the header/footer to be
+ // at the next empty row after the latest auto index.
+ local_auto_index = first_available_row * columns;
+ let group_start = first_available_row;
+ let group_end = group_start + 1;
- if is_header {
- if child_start != 0 {
- bail!(
- child_span,
- "header must start at the first row";
- hint: "remove any rows before the header"
- );
- }
+ if resolved_cells.len() <= columns * group_start {
+ // Ensure the automatically chosen row actually exists.
+ resolved_cells.resize_with(columns * (group_start + 1), || None);
+ }
- header = Some(Header {
- // Later on, we have to correct this number in case there
- // is gutter. But only once all cells have been analyzed
- // and the header has fully expanded in the fixup loop
- // below.
- end: child_end,
- });
- }
+ // Even though this header or footer is fully empty, we add one
+ // default cell to maintain the invariant that each header and
+ // footer has at least one 'Some(...)' cell at its first row
+ // and at least one at its last row (here they are the same
+ // row, of course). This invariant is important to ensure
+ // 'find_next_empty_row' will skip through any existing headers
+ // and footers without having to loop through them each time.
+ // Cells themselves, unfortunately, still have to.
+ assert!(resolved_cells[local_auto_index].is_none());
+ resolved_cells[local_auto_index] =
+ Some(Entry::Cell(self.resolve_cell(
+ T::default(),
+ 0,
+ first_available_row,
+ 1,
+ Span::detached(),
+ )?));
+
+ group_start..group_end
+ }
+ };
- if is_footer {
- // Only check if the footer is at the end later, once we know
- // the final amount of rows.
- footer = Some((
- child_end,
- child_span,
- Footer {
- // Later on, we have to correct this number in case there
- // is gutter, but only once all cells have been analyzed
- // and the header's and footer's exact boundaries are
- // known. That is because the gutter row immediately
- // before the footer might not be included as part of
- // the footer if it is contained within the header.
- start: child_start,
- },
- ));
+ let top_hlines_end = row_group.top_hlines_end.unwrap_or(pending_hlines.len());
+ for (_, top_hline, has_auto_y) in pending_hlines
+ .get_mut(row_group.top_hlines_start..top_hlines_end)
+ .unwrap_or(&mut [])
+ {
+ if *has_auto_y {
+ // Move this hline to the top of the child, as it was
+ // placed before the first automatically positioned cell
+ // and had an automatic index.
+ top_hline.index = group_range.start;
+ }
}
- if is_header || is_footer {
- let amount_hlines = pending_hlines.len();
- for (_, top_hline, has_auto_y) in pending_hlines
- .get_mut(
- first_index_of_top_hlines
- ..first_index_of_non_top_hlines.min(amount_hlines),
- )
- .unwrap_or(&mut [])
- {
- if *has_auto_y {
- // Move this hline to the top of the child, as it was
- // placed before the first automatically positioned cell
- // and had an automatic index.
- top_hline.index = child_start;
+ match row_group.kind {
+ RowGroupKind::Header => {
+ if group_range.start != 0 {
+ bail!(
+ row_group.span,
+ "header must start at the first row";
+ hint: "remove any rows before the header"
+ );
}
+
+ *header = Some(Header {
+ // Later on, we have to correct this number in case there
+ // is gutter. But only once all cells have been analyzed
+ // and the header has fully expanded in the fixup loop
+ // below.
+ end: group_range.end,
+ });
}
- // Next automatically positioned cell goes under this header.
- // FIXME: Consider only doing this if the header has any fully
- // automatically positioned cells. Otherwise,
- // `resolve_cell_position` should be smart enough to skip
- // upcoming headers.
- // Additionally, consider that cells with just an 'x' override
- // could end up going too far back and making previous
- // non-header rows into header rows (maybe they should be
- // placed at the first row that is fully empty or something).
- // Nothing we can do when both 'x' and 'y' were overridden, of
- // course.
- // None of the above are concerns for now, as headers must
- // start at the first row.
- auto_index = auto_index.max(c * child_end);
+ RowGroupKind::Footer => {
+ // Only check if the footer is at the end later, once we know
+ // the final amount of rows.
+ *footer = Some((
+ group_range.end,
+ row_group.span,
+ Footer {
+ // Later on, we have to correct this number in case there
+ // is gutter, but only once all cells have been analyzed
+ // and the header's and footer's exact boundaries are
+ // known. That is because the gutter row immediately
+ // before the footer might not be included as part of
+ // the footer if it is contained within the header.
+ start: group_range.start,
+ },
+ ));
+ }
}
+ } else {
+ // The child was a single cell outside headers or footers.
+ // Therefore, 'local_auto_index' for this table child was
+ // simply an alias for 'auto_index', so we update it as needed.
+ *auto_index = local_auto_index;
}
- // If the user specified cells occupying less rows than the given rows,
- // we shall expand the grid so that it has at least the given amount of
- // rows.
- let Some(expected_total_cells) = c.checked_mul(tracks.y.len()) else {
- bail!(span, "too many rows were specified");
+ Ok(())
+ }
+
+ /// Fixup phase (final step in cell grid generation):
+ ///
+ /// 1. Replace absent entries by resolved empty cells, producing a vector
+ /// of `Entry` from `Option<Entry>`.
+ ///
+ /// 2. Add enough empty cells to the end of the grid such that it has at
+ /// least the given amount of rows (must be a multiple of `columns`,
+ /// and all rows before the last cell must have cells, empty or not,
+ /// even if the user didn't specify those cells).
+ ///
+ /// That is necessary, for example, to ensure even unspecified cells
+ /// can be affected by show rules and grid-wide styling.
+ fn fixup_cells<T>(
+ &mut self,
+ resolved_cells: Vec<Option<Entry<'x>>>,
+ columns: usize,
+ ) -> SourceResult<Vec<Entry<'x>>>
+ where
+ T: ResolvableCell + Default,
+ {
+ let Some(expected_total_cells) = columns.checked_mul(self.tracks.y.len()) else {
+ bail!(self.span, "too many rows were specified");
};
let missing_cells = expected_total_cells.saturating_sub(resolved_cells.len());
- // Fixup phase (final step in cell grid generation):
- // 1. Replace absent entries by resolved empty cells, and produce a
- // vector of 'Entry' from 'Option<Entry>'.
- // 2. Add enough empty cells to the end of the grid such that it has at
- // least the given amount of rows.
- // 3. If any cells were added to the header's rows after the header's
- // creation, ensure the header expands enough to accommodate them
- // across all of their spanned rows. Same for the footer.
- // 4. If any cells before the footer try to span it, error.
- let resolved_cells = resolved_cells
+ resolved_cells
.into_iter()
.chain(std::iter::repeat_with(|| None).take(missing_cells))
.enumerate()
.map(|(i, cell)| {
if let Some(cell) = cell {
- if let Some(parent_cell) = cell.as_cell() {
- if let Some(header) = &mut header
- {
- let y = i / c;
- if y < header.end {
- // Ensure the header expands enough such that
- // all cells inside it, even those added later,
- // are fully contained within the header.
- // FIXME: check if start < y < end when start can
- // be != 0.
- // FIXME: when start can be != 0, decide what
- // happens when a cell after the header placed
- // above it tries to span the header (either
- // error or expand upwards).
- header.end = header.end.max(y + parent_cell.rowspan.get());
- }
- }
-
- if let Some((end, footer_span, footer)) = &mut footer {
- let x = i % c;
- let y = i / c;
- let cell_end = y + parent_cell.rowspan.get();
- if y < footer.start && cell_end > footer.start {
- // Don't allow a cell before the footer to span
- // it. Surely, we could move the footer to
- // start at where this cell starts, so this is
- // more of a design choice, as it's unlikely
- // for the user to intentionally include a cell
- // before the footer spanning it but not
- // being repeated with it.
- bail!(
- *footer_span,
- "footer would conflict with a cell placed before it at column {x} row {y}";
- hint: "try reducing that cell's rowspan or moving the footer"
- );
- }
- if y >= footer.start && y < *end {
- // Expand the footer to include all rows
- // spanned by this cell, as it is inside the
- // footer.
- *end = (*end).max(cell_end);
- }
- }
- }
-
Ok(cell)
} else {
- let x = i % c;
- let y = i / c;
+ let x = i % columns;
+ let y = i / columns;
- // Ensure all absent entries are affected by show rules and
- // grid styling by turning them into resolved empty cells.
- let new_cell = T::default().resolve_cell(
+ Ok(Entry::Cell(self.resolve_cell(
+ T::default(),
x,
y,
- &fill.resolve(engine, styles, x, y)?,
- align.resolve(engine, styles, x, y)?,
- inset.resolve(engine, styles, x, y)?,
- stroke.resolve(engine, styles, x, y)?,
- resolve_breakable(y, 1),
- locator.next(&()),
- styles,
- );
- Ok(Entry::Cell(new_cell))
+ 1,
+ Span::detached(),
+ )?))
}
})
- .collect::<SourceResult<Vec<Entry>>>()?;
+ .collect::<SourceResult<Vec<Entry>>>()
+ }
- // Populate the final lists of lines.
- // For each line type (horizontal or vertical), we keep a vector for
- // every group of lines with the same index.
- let mut vlines: Vec<Vec<Line>> = vec![];
+ /// Takes the list of pending lines and evaluates a final list of hlines
+ /// and vlines (in that order in the returned tuple), detecting invalid
+ /// line positions in the process.
+ ///
+ /// For each line type (horizontal and vertical respectively), returns a
+ /// vector containing one inner vector for every group of lines with the
+ /// same index.
+ ///
+ /// For example, an hline above the second row (y = 1) is inside the inner
+ /// vector at position 1 of the first vector (hlines) returned by this
+ /// function.
+ #[allow(clippy::type_complexity)]
+ fn collect_lines(
+ &self,
+ pending_hlines: Vec<(Span, Line, bool)>,
+ pending_vlines: Vec<(Span, Line)>,
+ has_gutter: bool,
+ columns: usize,
+ row_amount: usize,
+ ) -> SourceResult<(Vec<Vec<Line>>, Vec<Vec<Line>>)> {
let mut hlines: Vec<Vec<Line>> = vec![];
- let row_amount = resolved_cells.len().div_ceil(c);
+ let mut vlines: Vec<Vec<Line>> = vec![];
for (line_span, line, _) in pending_hlines {
let y = line.index;
@@ -1300,33 +1639,34 @@ impl<'a> CellGrid<'a> {
for (line_span, line) in pending_vlines {
let x = line.index;
- if x > c {
+ if x > columns {
bail!(line_span, "cannot place vertical line at invalid column {x}");
}
- if x == c && line.position == LinePosition::After {
+ if x == columns && line.position == LinePosition::After {
bail!(
line_span,
- "cannot place vertical line at the 'end' position of the end border (x = {c})";
+ "cannot place vertical line at the 'end' position of the end border (x = {columns})";
hint: "set the line's position to 'start' or place it at a smaller 'x' index"
);
}
- let line =
- if line.position == LinePosition::After && (!has_gutter || x + 1 == c) {
- // Just place the line before the next column if
- // there's no gutter and the line should be placed
- // after the one with given index.
- //
- // Note that placing after the last column is also the
- // same as just placing on the grid's end border, even
- // with gutter.
- Line {
- index: x + 1,
- position: LinePosition::Before,
- ..line
- }
- } else {
- line
- };
+ let line = if line.position == LinePosition::After
+ && (!has_gutter || x + 1 == columns)
+ {
+ // Just place the line before the next column if
+ // there's no gutter and the line should be placed
+ // after the one with given index.
+ //
+ // Note that placing after the last column is also the
+ // same as just placing on the grid's end border, even
+ // with gutter.
+ Line {
+ index: x + 1,
+ position: LinePosition::Before,
+ ..line
+ }
+ } else {
+ line
+ };
let x = line.index;
if vlines.len() <= x {
@@ -1335,6 +1675,26 @@ impl<'a> CellGrid<'a> {
vlines[x].push(line);
}
+ Ok((hlines, vlines))
+ }
+
+ /// Generate the final headers and footers:
+ ///
+ /// 1. Convert gutter-ignorant to gutter-aware indices if necessary;
+ /// 2. Expand the header downwards (or footer upwards) to also include
+ /// an adjacent gutter row to be repeated alongside that header or
+ /// footer, if there is gutter;
+ /// 3. Wrap headers and footers in the correct [`Repeatable`] variant.
+ #[allow(clippy::type_complexity)]
+ fn finalize_headers_and_footers(
+ &self,
+ has_gutter: bool,
+ header: Option<Header>,
+ repeat_header: bool,
+ footer: Option<(usize, Span, Footer)>,
+ repeat_footer: bool,
+ row_amount: usize,
+ ) -> SourceResult<(Option<Repeatable<Header>>, Option<Repeatable<Footer>>)> {
let header = header
.map(|mut header| {
// Repeat the gutter below a header (hence why we don't
@@ -1392,10 +1752,6 @@ impl<'a> CellGrid<'a> {
}
}
- if header_end.is_some_and(|header_end| header_end > footer.start) {
- bail!(footer_span, "header and footer must not have common rows");
- }
-
Ok(footer)
})
.transpose()?
@@ -1407,212 +1763,207 @@ impl<'a> CellGrid<'a> {
}
});
- Ok(Self::new_internal(
- tracks,
- gutter,
- vlines,
- hlines,
- header,
- footer,
- resolved_cells,
- ))
+ Ok((header, footer))
}
- /// Generates the cell grid, given the tracks and resolved entries.
- pub fn new_internal(
- tracks: Axes<&[Sizing]>,
- gutter: Axes<&[Sizing]>,
- vlines: Vec<Vec<Line>>,
- hlines: Vec<Vec<Line>>,
- header: Option<Repeatable<Header>>,
- footer: Option<Repeatable<Footer>>,
- entries: Vec<Entry<'a>>,
- ) -> Self {
- let mut cols = vec![];
- let mut rows = vec![];
-
- // Number of content columns: Always at least one.
- let c = tracks.x.len().max(1);
-
- // Number of content rows: At least as many as given, but also at least
- // as many as needed to place each item.
- let r = {
- let len = entries.len();
- let given = tracks.y.len();
- let needed = len / c + (len % c).clamp(0, 1);
- given.max(needed)
- };
-
- let has_gutter = gutter.any(|tracks| !tracks.is_empty());
- let auto = Sizing::Auto;
- let zero = Sizing::Rel(Rel::zero());
- let get_or = |tracks: &[_], idx, default| {
- tracks.get(idx).or(tracks.last()).copied().unwrap_or(default)
+ /// Resolves the cell's fields based on grid-wide properties.
+ fn resolve_cell<T>(
+ &mut self,
+ cell: T,
+ x: usize,
+ y: usize,
+ rowspan: usize,
+ cell_span: Span,
+ ) -> SourceResult<Cell<'x>>
+ where
+ T: ResolvableCell + Default,
+ {
+ // Resolve the breakability of a cell. Cells that span at least one
+ // auto-sized row or gutter are considered breakable.
+ let breakable = {
+ let auto = Sizing::Auto;
+ let zero = Sizing::Rel(Rel::zero());
+ self.tracks
+ .y
+ .iter()
+ .chain(std::iter::repeat(self.tracks.y.last().unwrap_or(&auto)))
+ .skip(y)
+ .take(rowspan)
+ .any(|row| row == &Sizing::Auto)
+ || self
+ .gutter
+ .y
+ .iter()
+ .chain(std::iter::repeat(self.gutter.y.last().unwrap_or(&zero)))
+ .skip(y)
+ .take(rowspan - 1)
+ .any(|row_gutter| row_gutter == &Sizing::Auto)
};
- // Collect content and gutter columns.
- for x in 0..c {
- cols.push(get_or(tracks.x, x, auto));
- if has_gutter {
- cols.push(get_or(gutter.x, x, zero));
- }
- }
-
- // Collect content and gutter rows.
- for y in 0..r {
- rows.push(get_or(tracks.y, y, auto));
- if has_gutter {
- rows.push(get_or(gutter.y, y, zero));
- }
- }
-
- // Remove superfluous gutter tracks.
- if has_gutter {
- cols.pop();
- rows.pop();
- }
-
- Self {
- cols,
- rows,
- entries,
- vlines,
- hlines,
- header,
- footer,
- has_gutter,
- }
+ Ok(cell.resolve_cell(
+ x,
+ y,
+ &self.fill.resolve(self.engine, self.styles, x, y)?,
+ self.align.resolve(self.engine, self.styles, x, y)?,
+ self.inset.resolve(self.engine, self.styles, x, y)?,
+ self.stroke.resolve(self.engine, self.styles, x, y)?,
+ breakable,
+ self.locator.next(&cell_span),
+ self.styles,
+ ))
}
+}
- /// Get the grid entry in column `x` and row `y`.
- ///
- /// Returns `None` if it's a gutter cell.
- #[track_caller]
- pub fn entry(&self, x: usize, y: usize) -> Option<&Entry<'a>> {
- assert!(x < self.cols.len());
- assert!(y < self.rows.len());
+/// Given the existing range of a row group (header or footer), tries to expand
+/// it to fit the new cell placed inside it. If the newly-expanded row group
+/// would conflict with existing cells or other row groups, an error is
+/// returned. Otherwise, the new `start..end` range of rows in the row group is
+/// returned.
+fn expand_row_group(
+ resolved_cells: &[Option<Entry<'_>>],
+ group_range: Option<&Range<usize>>,
+ group_kind: RowGroupKind,
+ first_available_row: usize,
+ cell_y: usize,
+ rowspan: usize,
+ columns: usize,
+) -> HintedStrResult<Range<usize>> {
+ // Ensure each cell in a header or footer is fully contained within it by
+ // expanding the header or footer towards this new cell.
+ let (new_group_start, new_group_end) = group_range
+ .map_or((cell_y, cell_y + rowspan), |r| {
+ (r.start.min(cell_y), r.end.max(cell_y + rowspan))
+ });
- if self.has_gutter {
- // Even columns and rows are children, odd ones are gutter.
- if x % 2 == 0 && y % 2 == 0 {
- let c = 1 + self.cols.len() / 2;
- self.entries.get((y / 2) * c + x / 2)
- } else {
- None
- }
- } else {
- let c = self.cols.len();
- self.entries.get(y * c + x)
- }
+ // This check might be unnecessary with the loop below, but let's keep it
+ // here for full correctness.
+ //
+ // Quickly detect the case:
+ // y = 0 => occupied
+ // y = 1 => empty
+ // y = 2 => header
+ // and header tries to expand to y = 0 - invalid, as
+ // 'y = 1' is the earliest row it can occupy.
+ if new_group_start < first_available_row {
+ bail!(
+ "cell would cause {} to expand to non-empty row {}",
+ group_kind.name(),
+ first_available_row.saturating_sub(1);
+ hint: "try moving its cells to available rows"
+ );
}
- /// Get the content of the cell in column `x` and row `y`.
- ///
- /// Returns `None` if it's a gutter cell or merged position.
- #[track_caller]
- pub fn cell(&self, x: usize, y: usize) -> Option<&Cell<'a>> {
- self.entry(x, y).and_then(Entry::as_cell)
- }
+ let new_rows =
+ group_range.map_or((new_group_start..new_group_end).chain(0..0), |r| {
+ // NOTE: 'r.end' is one row AFTER the row group's last row, so it
+ // makes sense to check it if 'new_group_end > r.end', that is, if
+ // the row group is going to expand. It is NOT a duplicate check,
+ // as we hadn't checked it before (in a previous run, it was
+ // 'new_group_end' at the exclusive end of the range)!
+ //
+ // NOTE: To keep types the same, we have to always return
+ // '(range).chain(range)', which justifies chaining an empty
+ // range above.
+ (new_group_start..r.start).chain(r.end..new_group_end)
+ });
- /// Returns the position of the parent cell of the grid entry at the given
- /// position. It is guaranteed to have a non-gutter, non-merged cell at
- /// the returned position, due to how the grid is built.
- /// - If the entry at the given position is a cell, returns the given
- /// position.
- /// - If it is a merged cell, returns the parent cell's position.
- /// - If it is a gutter cell, returns None.
- #[track_caller]
- pub fn parent_cell_position(&self, x: usize, y: usize) -> Option<Axes<usize>> {
- self.entry(x, y).map(|entry| match entry {
- Entry::Cell(_) => Axes::new(x, y),
- Entry::Merged { parent } => {
- let c = self.non_gutter_column_count();
- let factor = if self.has_gutter { 2 } else { 1 };
- Axes::new(factor * (*parent % c), factor * (*parent / c))
+ // The check above isn't enough, however, even when the header is expanding
+ // upwards, as it might expand upwards towards an occupied row after the
+ // first empty row, e.g.
+ //
+ // y = 0 => occupied
+ // y = 1 => empty (first_available_row = 1)
+ // y = 2 => occupied
+ // y = 3 => header
+ //
+ // Here, we should bail if the header tries to expand upwards, regardless
+ // of the fact that the conflicting row (y = 2) comes after the first
+ // available row.
+ //
+ // Note that expanding upwards is only possible when row-positioned cells
+ // are specified, in one of the following cases:
+ //
+ // 1. We place e.g. 'table.cell(y: 3)' followed by 'table.cell(y: 2)'
+ // (earlier row => upwards);
+ //
+ // 2. We place e.g. 'table.cell(y: 3)' followed by '[a]' (auto-pos cell
+ // favors 'first_available_row', so the header tries to expand upwards to
+ // place the cell at 'y = 1' and conflicts at 'y = 2') or
+ // 'table.cell(x: 1)' (same deal).
+ //
+ // Of course, we also need to check for downward expansion as usual as
+ // there could be a non-empty row below the header, but the upward case is
+ // highlighted as it was checked separately before (and also to explain
+ // what kind of situation we are preventing with this check).
+ //
+ // Note that simply checking for non-empty rows like below not only
+ // prevents conflicts with top-level cells (outside of headers and
+ // footers), but also prevents conflicts with other headers or footers,
+ // since we have an invariant that even empty headers and footers must
+ // contain at least one 'Some(...)' position in 'resolved_cells'. More
+ // precisely, each header and footer has at least one 'Some(...)' cell at
+ // 'group_range.start' and at 'group_range.end - 1' - non-empty headers and
+ // footers don't span any unnecessary rows. Therefore, we don't have to
+ // loop over headers and footers, only check if the new rows are empty.
+ for new_y in new_rows {
+ if let Some(new_row @ [_non_empty, ..]) = resolved_cells
+ .get(new_y * columns..)
+ .map(|cells| &cells[..columns.min(cells.len())])
+ {
+ if new_row.iter().any(Option::is_some) {
+ bail!(
+ "cell would cause {} to expand to non-empty row {new_y}",
+ group_kind.name();
+ hint: "try moving its cells to available rows",
+ )
}
- })
- }
-
- /// Returns the position of the actual parent cell of a merged position,
- /// even if the given position is gutter, in which case we return the
- /// parent of the nearest adjacent content cell which could possibly span
- /// the given gutter position. If the given position is not a gutter cell,
- /// then this function will return the same as `parent_cell_position` would.
- /// If the given position is a gutter cell, but no cell spans it, returns
- /// `None`.
- ///
- /// This is useful for lines. A line needs to check if a cell next to it
- /// has a stroke override - even at a gutter position there could be a
- /// stroke override, since a cell could be merged with two cells at both
- /// ends of the gutter cell (e.g. to its left and to its right), and thus
- /// that cell would impose a stroke under the gutter. This function allows
- /// getting the position of that cell (which spans the given gutter
- /// position, if it is gutter), if it exists; otherwise returns None (it's
- /// gutter and no cell spans it).
- #[track_caller]
- pub fn effective_parent_cell_position(
- &self,
- x: usize,
- y: usize,
- ) -> Option<Axes<usize>> {
- if self.has_gutter {
- // If (x, y) is a gutter cell, we skip it (skip a gutter column and
- // row) to the nearest adjacent content cell, in the direction
- // which merged cells grow toward (increasing x and increasing y),
- // such that we can verify if that adjacent cell is merged with the
- // gutter cell by checking if its parent would come before (x, y).
- // Otherwise, no cell is merged with this gutter cell, and we
- // return None.
- self.parent_cell_position(x + x % 2, y + y % 2)
- .filter(|&parent| parent.x <= x && parent.y <= y)
} else {
- self.parent_cell_position(x, y)
+ // Received 'None' or an empty slice, so we are expanding the
+ // header or footer into new rows, which is always valid and cannot
+ // conflict with existing cells. (Note that we only resize
+ // 'resolved_cells' after this function is called, so, if this
+ // header or footer is at the bottom of the table so far, this loop
+ // will end quite early, regardless of where this cell was placed
+ // or of its rowspan value.)
+ break;
}
}
- /// Checks if the track with the given index is gutter.
- /// Does not check if the index is a valid track.
- #[inline]
- pub fn is_gutter_track(&self, index: usize) -> bool {
- self.has_gutter && index % 2 == 1
- }
+ Ok(new_group_start..new_group_end)
+}
- /// Returns the effective colspan of a cell, considering the gutters it
- /// might span if the grid has gutters.
- #[inline]
- pub fn effective_colspan_of_cell(&self, cell: &Cell) -> usize {
- if self.has_gutter {
- 2 * cell.colspan.get() - 1
- } else {
- cell.colspan.get()
+/// Check if a cell's fixed row would conflict with a header or footer.
+fn check_for_conflicting_cell_row(
+ header: Option<&Header>,
+ footer: Option<&(usize, Span, Footer)>,
+ cell_y: usize,
+ rowspan: usize,
+) -> HintedStrResult<()> {
+ if let Some(header) = header {
+ // TODO: check start (right now zero, always satisfied)
+ if cell_y < header.end {
+ bail!(
+ "cell would conflict with header spanning the same position";
+ hint: "try moving the cell or the header"
+ );
}
}
- /// Returns the effective rowspan of a cell, considering the gutters it
- /// might span if the grid has gutters.
- #[inline]
- pub fn effective_rowspan_of_cell(&self, cell: &Cell) -> usize {
- if self.has_gutter {
- 2 * cell.rowspan.get() - 1
- } else {
- cell.rowspan.get()
+ if let Some((footer_end, _, footer)) = footer {
+ // NOTE: y + rowspan >, not >=, footer.start, to check if the rowspan
+ // enters the footer. For example, consider a rowspan of 1: if
+ // `y + 1 = footer.start` holds, that means `y < footer.start`, and it
+ // only occupies one row (`y`), so the cell is actually not in
+ // conflict.
+ if cell_y < *footer_end && cell_y + rowspan > footer.start {
+ bail!(
+ "cell would conflict with footer spanning the same position";
+ hint: "try reducing the cell's rowspan or moving the footer"
+ );
}
}
- #[inline]
- pub fn non_gutter_column_count(&self) -> usize {
- if self.has_gutter {
- // Calculation: With gutters, we have
- // 'cols = 2 * (non-gutter cols) - 1', since there is a gutter
- // column between each regular column. Therefore,
- // 'floor(cols / 2)' will be equal to
- // 'floor(non-gutter cols - 1/2) = non-gutter-cols - 1',
- // so 'non-gutter cols = 1 + floor(cols / 2)'.
- 1 + self.cols.len() / 2
- } else {
- self.cols.len()
- }
- }
+ Ok(())
}
/// Given a cell's requested x and y, the vector with the resolved cell
@@ -1620,20 +1971,23 @@ impl<'a> CellGrid<'a> {
/// `(auto, auto)` cell) and the amount of columns in the grid, returns the
/// final index of this cell in the vector of resolved cells.
///
-/// The `start_new_row` parameter is used to ensure that, if this cell is
-/// fully automatically positioned, it should start a new, empty row. This is
-/// useful for headers and footers, which must start at their own rows, without
-/// interference from previous cells.
+/// The `first_available_row` parameter is used by headers and footers to
+/// indicate the first empty row available. Any rows before those should
+/// not be picked by cells with `auto` row positioning, since headers and
+/// footers occupy entire rows, and may not conflict with cells outside them.
#[allow(clippy::too_many_arguments)]
fn resolve_cell_position(
cell_x: Smart<usize>,
cell_y: Smart<usize>,
colspan: usize,
rowspan: usize,
+ header: Option<&Header>,
+ footer: Option<&(usize, Span, Footer)>,
resolved_cells: &[Option<Entry>],
auto_index: &mut usize,
- start_new_row: &mut bool,
+ first_available_row: usize,
columns: usize,
+ in_row_group: bool,
) -> HintedStrResult<usize> {
// Translates a (x, y) position to the equivalent index in the final cell vector.
// Errors if the position would be too large.
@@ -1648,29 +2002,24 @@ fn resolve_cell_position(
(Smart::Auto, Smart::Auto) => {
// Let's find the first available position starting from the
// automatic position counter, searching in row-major order.
- let mut resolved_index = *auto_index;
- if *start_new_row {
- resolved_index =
- find_next_empty_row(resolved_cells, resolved_index, columns);
-
- // Next cell won't have to start a new row if we just did that,
- // in principle.
- *start_new_row = false;
- } else {
- while let Some(Some(_)) = resolved_cells.get(resolved_index) {
- // Skip any non-absent cell positions (`Some(None)`) to
- // determine where this cell will be placed. An out of
- // bounds position (thus `None`) is also a valid new
- // position (only requires expanding the vector).
- resolved_index += 1;
- }
- }
+ // Note that the counter ignores any cells with fixed positions,
+ // but automatically-positioned cells will avoid conflicts by
+ // simply skipping existing cells, headers and footers.
+ let resolved_index = find_next_available_position::<false>(
+ header,
+ footer,
+ resolved_cells,
+ columns,
+ *auto_index,
+ )?;
// Ensure the next cell with automatic position will be
// placed after this one (maybe not immediately after).
//
// The calculation below also affects the position of the upcoming
- // automatically-positioned lines.
+ // automatically-positioned lines, as they are placed below
+ // (horizontal lines) or to the right (vertical lines) of the cell
+ // that would be placed at 'auto_index'.
*auto_index = if colspan == columns {
// The cell occupies all columns, so no cells can be placed
// after it until all of its rows have been spanned.
@@ -1692,24 +2041,46 @@ fn resolve_cell_position(
}
if let Smart::Custom(cell_y) = cell_y {
// Cell has chosen its exact position.
+ //
+ // Ensure it doesn't conflict with an existing header or
+ // footer (but only if it isn't already in one, otherwise there
+ // will already be a separate check).
+ if !in_row_group {
+ check_for_conflicting_cell_row(header, footer, cell_y, rowspan)?;
+ }
+
cell_index(cell_x, cell_y)
} else {
// Cell has only chosen its column.
// Let's find the first row which has that column available.
- let mut resolved_y = 0;
- while let Some(Some(_)) =
- resolved_cells.get(cell_index(cell_x, resolved_y)?)
- {
- // Try each row until either we reach an absent position
- // (`Some(None)`) or an out of bounds position (`None`),
- // in which case we'd create a new row to place this cell in.
- resolved_y += 1;
- }
- cell_index(cell_x, resolved_y)
+ // If in a header or footer, start searching by the first empty
+ // row / the header or footer's first row (specified through
+ // 'first_available_row'). Otherwise, start searching at the
+ // first row.
+ let initial_index = cell_index(cell_x, first_available_row)?;
+
+ // Try each row until either we reach an absent position at the
+ // requested column ('Some(None)') or an out of bounds position
+ // ('None'), in which case we'd create a new row to place this
+ // cell in.
+ find_next_available_position::<true>(
+ header,
+ footer,
+ resolved_cells,
+ columns,
+ initial_index,
+ )
}
}
// Cell has only chosen its row, not its column.
(Smart::Auto, Smart::Custom(cell_y)) => {
+ // Ensure it doesn't conflict with an existing header or
+ // footer (but only if it isn't already in one, otherwise there
+ // will already be a separate check).
+ if !in_row_group {
+ check_for_conflicting_cell_row(header, footer, cell_y, rowspan)?;
+ }
+
// Let's find the first column which has that row available.
let first_row_pos = cell_index(0, cell_y)?;
let last_row_pos = first_row_pos
@@ -1736,14 +2107,73 @@ fn resolve_cell_position(
}
}
-/// Computes the index of the first cell in the next empty row in the grid,
-/// starting with the given initial index.
+/// Finds the first available position after the initial index in the resolved
+/// grid of cells. Skips any non-absent positions (positions which already
+/// have cells specified by the user) as well as any headers and footers.
+#[inline]
+fn find_next_available_position<const SKIP_ROWS: bool>(
+ header: Option<&Header>,
+ footer: Option<&(usize, Span, Footer)>,
+ resolved_cells: &[Option<Entry<'_>>],
+ columns: usize,
+ initial_index: usize,
+) -> HintedStrResult<usize> {
+ let mut resolved_index = initial_index;
+
+ loop {
+ if let Some(Some(_)) = resolved_cells.get(resolved_index) {
+ // Skip any non-absent cell positions (`Some(None)`) to
+ // determine where this cell will be placed. An out of
+ // bounds position (thus `None`) is also a valid new
+ // position (only requires expanding the vector).
+ if SKIP_ROWS {
+ // Skip one row at a time (cell chose its column, so we don't
+ // change it).
+ resolved_index =
+ resolved_index.checked_add(columns).ok_or_else(|| {
+ HintedString::from(eco_format!("cell position too large"))
+ })?;
+ } else {
+ // Ensure we don't run unnecessary checks in the hot path
+ // (for fully automatically-positioned cells). Memory usage
+ // would become impractically large before this overflows.
+ resolved_index += 1;
+ }
+ } else if let Some(header) =
+ header.filter(|header| resolved_index < header.end * columns)
+ {
+ // Skip header (can't place a cell inside it from outside it).
+ resolved_index = header.end * columns;
+
+ if SKIP_ROWS {
+ // Ensure the cell's chosen column is kept after the
+ // header.
+ resolved_index += initial_index % columns;
+ }
+ } else if let Some((footer_end, _, _)) = footer.filter(|(end, _, footer)| {
+ resolved_index >= footer.start * columns && resolved_index < *end * columns
+ }) {
+ // Skip footer, for the same reason.
+ resolved_index = *footer_end * columns;
+
+ if SKIP_ROWS {
+ resolved_index += initial_index % columns;
+ }
+ } else {
+ return Ok(resolved_index);
+ }
+ }
+}
+
+/// Computes the `y` of the next available empty row, given the auto index as
+/// an initial index for search, since we know that there are no empty rows
+/// before automatically-positioned cells, as they are placed sequentially.
fn find_next_empty_row(
resolved_cells: &[Option<Entry>],
- initial_index: usize,
+ auto_index: usize,
columns: usize,
) -> usize {
- let mut resolved_index = initial_index.next_multiple_of(columns);
+ let mut resolved_index = auto_index.next_multiple_of(columns);
while resolved_cells
.get(resolved_index..resolved_index + columns)
.is_some_and(|row| row.iter().any(Option::is_some))
@@ -1752,7 +2182,7 @@ fn find_next_empty_row(
resolved_index += columns;
}
- resolved_index
+ resolved_index / columns
}
/// Fully merged rows under the cell of latest auto index indicate rowspans