summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPgBiel <9021226+PgBiel@users.noreply.github.com>2025-05-24 04:12:06 -0300
committerPgBiel <9021226+PgBiel@users.noreply.github.com>2025-06-28 22:39:35 -0300
commit0951fe13fd96e91620434dd19fb33f2993d6624f (patch)
tree33ffd9f81293a344be6cd983be9b9a54ae6ac0f0
parentf9b1bfd1b034b12e57d892034b8caf9c34661803 (diff)
resolve multiple footers
-rw-r--r--crates/typst-layout/src/grid/lines.rs4
-rw-r--r--crates/typst-library/src/layout/grid/resolve.rs237
-rw-r--r--tests/suite/layout/grid/footers.typ3
3 files changed, 122 insertions, 122 deletions
diff --git a/crates/typst-layout/src/grid/lines.rs b/crates/typst-layout/src/grid/lines.rs
index 973f0d21..0ea113ac 100644
--- a/crates/typst-layout/src/grid/lines.rs
+++ b/crates/typst-layout/src/grid/lines.rs
@@ -641,7 +641,7 @@ mod test {
vec![],
vec![],
vec![],
- None,
+ vec![],
entries,
)
}
@@ -1179,7 +1179,7 @@ mod test {
vec![],
vec![],
vec![],
- None,
+ vec![],
entries,
)
}
diff --git a/crates/typst-library/src/layout/grid/resolve.rs b/crates/typst-library/src/layout/grid/resolve.rs
index 78f5cd42..1e607ccc 100644
--- a/crates/typst-library/src/layout/grid/resolve.rs
+++ b/crates/typst-library/src/layout/grid/resolve.rs
@@ -697,7 +697,7 @@ impl<'a> CellGrid<'a> {
cells: impl IntoIterator<Item = Cell<'a>>,
) -> Self {
let entries = cells.into_iter().map(Entry::Cell).collect();
- Self::new_internal(tracks, gutter, vec![], vec![], vec![], None, entries)
+ Self::new_internal(tracks, gutter, vec![], vec![], vec![], vec![], entries)
}
/// Generates the cell grid, given the tracks and resolved entries.
@@ -707,7 +707,7 @@ impl<'a> CellGrid<'a> {
vlines: Vec<Vec<Line>>,
hlines: Vec<Vec<Line>>,
headers: Vec<Repeatable<Header>>,
- footer: Option<Repeatable<Footer>>,
+ footers: Vec<Repeatable<Footer>>,
entries: Vec<Entry<'a>>,
) -> Self {
let mut cols = vec![];
@@ -754,7 +754,6 @@ impl<'a> CellGrid<'a> {
rows.pop();
}
- let footers: Vec<Repeatable<Footer>> = footer.into_iter().collect();
let sorted_footers = simulate_footer_repetition(&footers);
Self {
@@ -991,6 +990,7 @@ struct RowGroupData {
///
/// This stays as `None` for fully empty headers and footers.
range: Option<Range<usize>>,
+ #[allow(dead_code)] // TODO: should we remove this?
span: Span,
kind: RowGroupKind,
@@ -1048,11 +1048,7 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
let has_gutter = self.gutter.any(|tracks| !tracks.is_empty());
let mut headers: Vec<Repeatable<Header>> = vec![];
-
- // Stores where the footer is supposed to end, its span, and the
- // actual footer structure.
- let mut footer: Option<(usize, Span, Footer)> = None;
- let mut repeat_footer = false;
+ let mut footers: Vec<Repeatable<Footer>> = vec![];
// If true, there has been at least one cell besides headers and
// footers. When false, footers at the end are forced to not repeat.
@@ -1074,10 +1070,11 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
// automatically-positioned cell.
let mut auto_index: usize = 0;
- // The next header after the latest auto-positioned cell. This is used
- // to avoid checking for collision with headers that were already
- // skipped.
+ // The next header and footer after the latest auto-positioned cell.
+ // These are used to avoid checking for collision with headers that
+ // were already skipped.
let mut next_header = 0;
+ let mut next_footer = 0;
// We have to rebuild the grid to account for fixed cell positions.
//
@@ -1100,10 +1097,10 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
&mut pending_hlines,
&mut pending_vlines,
&mut headers,
- &mut footer,
- &mut repeat_footer,
+ &mut footers,
&mut auto_index,
&mut next_header,
+ &mut next_footer,
&mut resolved_cells,
&mut at_least_one_cell,
child,
@@ -1121,11 +1118,10 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
row_amount,
)?;
- let footer = self.finalize_headers_and_footers(
+ self.finalize_headers_and_footers(
has_gutter,
&mut headers,
- footer,
- repeat_footer,
+ &mut footers,
row_amount,
at_least_one_cell,
)?;
@@ -1136,7 +1132,7 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
vlines,
hlines,
headers,
- footer,
+ footers,
resolved_cells,
))
}
@@ -1156,10 +1152,10 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
pending_hlines: &mut Vec<(Span, Line, bool)>,
pending_vlines: &mut Vec<(Span, Line)>,
headers: &mut Vec<Repeatable<Header>>,
- footer: &mut Option<(usize, Span, Footer)>,
- repeat_footer: &mut bool,
+ footers: &mut Vec<Repeatable<Footer>>,
auto_index: &mut usize,
next_header: &mut usize,
+ next_footer: &mut usize,
resolved_cells: &mut Vec<Option<Entry<'x>>>,
at_least_one_cell: &mut bool,
child: ResolvableGridChild<T, I>,
@@ -1212,6 +1208,12 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
&mut (*next_header).clone()
};
+ let local_next_footer = if matches!(child, ResolvableGridChild::Item(_)) {
+ next_footer
+ } else {
+ &mut (*next_footer).clone()
+ };
+
// The first row in which this table group can fit.
//
// Within headers and footers, this will correspond to the first
@@ -1249,10 +1251,6 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
(Some(items), None)
}
ResolvableGridChild::Footer { repeat, span, items, .. } => {
- if footer.is_some() {
- bail!(span, "cannot have more than one footer");
- }
-
row_group_data = Some(RowGroupData {
range: None,
span,
@@ -1396,10 +1394,11 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
colspan,
rowspan,
headers,
- footer.as_ref(),
+ footers,
resolved_cells,
local_auto_index,
local_next_header,
+ local_next_footer,
first_available_row,
columns,
row_group_data.is_some(),
@@ -1621,23 +1620,23 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
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,
- end: group_range.end,
- level: 1,
- },
- ));
-
- *repeat_footer = row_group.repeat;
+ let data = 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,
+ end: group_range.end,
+ level: row_group.repeatable_level.get(),
+ };
+
+ footers.push(if row_group.repeat {
+ Repeatable::Repeated(data)
+ } else {
+ Repeatable::NotRepeated(data)
+ });
}
}
}
@@ -1807,24 +1806,30 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
&self,
has_gutter: bool,
headers: &mut [Repeatable<Header>],
- footer: Option<(usize, Span, Footer)>,
- repeat_footer: bool,
+ footers: &mut [Repeatable<Footer>],
row_amount: usize,
at_least_one_cell: bool,
- ) -> SourceResult<Option<Repeatable<Footer>>> {
+ ) -> SourceResult<()> {
// Mark consecutive headers right before the end of the table, or the
// final footer, as short lived, given that there are no normal rows
// after them, so repeating them is pointless.
//
- // It is important to do this BEFORE we update header and footer ranges
- // due to gutter below as 'row_amount' doesn't consider gutter.
- //
// TODO(subfooters): take the last footer if it is at the end and
// backtrack through consecutive footers until the first one in the
// sequence is found. If there is no footer at the end, there are no
// haeders to turn short-lived.
- let mut consecutive_header_start =
- footer.as_ref().map(|(_, _, f)| f.start).unwrap_or(row_amount);
+ //
+ // TODO: interleaved headers and footers?
+ let mut first_end_footer = row_amount;
+ for end_footer in footers.iter().rev() {
+ if end_footer.end != first_end_footer {
+ break;
+ }
+
+ first_end_footer = end_footer.start;
+ }
+
+ let mut consecutive_header_start = first_end_footer;
for header_at_the_end in headers.iter_mut().rev().take_while(move |h| {
let at_the_end = h.range.end == consecutive_header_start;
@@ -1863,13 +1868,9 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
let row_amount = (2 * row_amount).saturating_sub(1);
header.range.end = header.range.end.min(row_amount);
}
- }
- let footer = footer
- .map(|(footer_end, footer_span, mut footer)| {
- if footer_end != row_amount {
- bail!(footer_span, "footer must end at the last row");
- }
+ for repeatable_footer in &mut *footers {
+ let footer = repeatable_footer.unwrap_mut();
// TODO(subfooters): will need a global slice of headers and
// footers for when we have multiple footers
@@ -1883,45 +1884,37 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
// out the footer for real, the mechanism can be disabled.
let last_header_end = headers.last().map(|header| header.range.end);
- if has_gutter {
- // Convert the footer's start index to post-gutter coordinates.
- footer.start *= 2;
-
- // Include the gutter right before the footer, unless there is
- // none, or the gutter is already included in the header (no
- // rows between the header and the footer).
- if last_header_end != Some(footer.start) {
- footer.start = footer.start.saturating_sub(1);
- }
+ // Convert the footer's start index to post-gutter coordinates.
+ footer.start *= 2;
- // Adapt footer end but DO NOT include the gutter below it,
- // if it exists. Calculation:
- // - Starts as 'last y + 1'.
- // - The result will be
- // 2 * (last_y + 1) - 1 = 2 * last_y + 1,
- // which is the new index of the last footer row plus one,
- // meaning we do exclude any gutter below this way.
- //
- // It also keeps us within the total amount of rows, so we
- // don't need to '.min()' later.
- footer.end = (2 * footer.end).saturating_sub(1);
+ // TODO: this probably has to change
+ // Include the gutter right before the footer, unless there is
+ // none, or the gutter is already included in the header (no
+ // rows between the header and the footer).
+ if last_header_end != Some(footer.start) {
+ footer.start = footer.start.saturating_sub(1);
}
- Ok(footer)
- })
- .transpose()?
- .map(|footer| {
- // Don't repeat footers when the table only has headers and
- // footers.
- // TODO(subfooters): Switch this to marking the last N
- // consecutive footers as short lived.
- Repeatable {
- inner: footer,
- repeated: repeat_footer && at_least_one_cell,
+ // Adapt footer end but DO NOT include the gutter below it,
+ // if it exists. Calculation:
+ // - Starts as 'last y + 1'.
+ // - The result will be
+ // 2 * (last_y + 1) - 1 = 2 * last_y + 1,
+ // which is the new index of the last footer row plus one,
+ // meaning we do exclude any gutter below this way.
+ //
+ // It also keeps us within the total amount of rows, so we
+ // don't need to '.min()' later.
+ footer.end = (2 * footer.end).saturating_sub(1);
+
+ if !at_least_one_cell {
+ // TODO: short-lived (and remove this?)
+ repeatable_footer.repeated = false;
}
- });
+ }
+ }
- Ok(footer)
+ Ok(())
}
/// Resolves the cell's fields based on grid-wide properties.
@@ -2093,7 +2086,7 @@ fn expand_row_group(
/// Check if a cell's fixed row would conflict with a header or footer.
fn check_for_conflicting_cell_row(
headers: &[Repeatable<Header>],
- footer: Option<&(usize, Span, Footer)>,
+ footers: &[Repeatable<Footer>],
cell_y: usize,
rowspan: usize,
) -> HintedStrResult<()> {
@@ -2112,13 +2105,14 @@ fn check_for_conflicting_cell_row(
);
}
- if let Some((_, _, footer)) = footer {
- 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"
- );
- }
+ if footers
+ .iter()
+ .any(|footer| 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"
+ );
}
Ok(())
@@ -2140,10 +2134,11 @@ fn resolve_cell_position(
colspan: usize,
rowspan: usize,
headers: &[Repeatable<Header>],
- footer: Option<&(usize, Span, Footer)>,
+ footers: &[Repeatable<Footer>],
resolved_cells: &[Option<Entry>],
auto_index: &mut usize,
next_header: &mut usize,
+ next_footer: &mut usize,
first_available_row: usize,
columns: usize,
in_row_group: bool,
@@ -2166,11 +2161,12 @@ fn resolve_cell_position(
// simply skipping existing cells, headers and footers.
let resolved_index = find_next_available_position(
headers,
- footer,
+ footers,
resolved_cells,
columns,
*auto_index,
next_header,
+ next_footer,
false,
)?;
@@ -2207,7 +2203,7 @@ fn resolve_cell_position(
// 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(headers, footer, cell_y, rowspan)?;
+ check_for_conflicting_cell_row(headers, footers, cell_y, rowspan)?;
}
cell_index(cell_x, cell_y)
@@ -2226,25 +2222,26 @@ fn resolve_cell_position(
// cell in.
find_next_available_position(
headers,
- footer,
+ footers,
resolved_cells,
columns,
initial_index,
- // Make our own copy of the 'next_header' counter, since it
- // should only be updated by auto cells. However, we cannot
- // start with the same value as we are searching from the
- // start, and not from 'auto_index', so auto cells might
- // have skipped some headers already which this cell will
- // also need to skip.
+ // Make new copies of the 'next_header/footer' counters,
+ // since they should only be updated by auto cells.
+ // However, we cannot start with the same values as we are
+ // searching from the start, and not from 'auto_index', so
+ // auto cells might have skipped some headers and footers
+ // already which this cell will also need to skip.
//
- // We could, in theory, keep a separate 'next_header'
- // counter for cells with fixed columns. But then we would
+ // We could, in theory, keep separate 'next_header/footer'
+ // counters for cells with fixed columns. But then we would
// need one for every column, and much like how there isn't
// an index counter for each column either, the potential
// speed gain seems less relevant for a less used feature.
// Still, it is something to consider for the future if
// this turns out to be a bottleneck in important cases.
&mut 0,
+ &mut 0,
true,
)
}
@@ -2255,7 +2252,7 @@ fn resolve_cell_position(
// 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(headers, footer, cell_y, rowspan)?;
+ check_for_conflicting_cell_row(headers, footers, cell_y, rowspan)?;
}
// Let's find the first column which has that row available.
@@ -2293,11 +2290,12 @@ fn resolve_cell_position(
#[inline]
fn find_next_available_position(
headers: &[Repeatable<Header>],
- footer: Option<&(usize, Span, Footer)>,
+ footers: &[Repeatable<Footer>],
resolved_cells: &[Option<Entry<'_>>],
columns: usize,
initial_index: usize,
next_header: &mut usize,
+ next_footer: &mut usize,
skip_rows: bool,
) -> HintedStrResult<usize> {
let mut resolved_index = initial_index;
@@ -2341,15 +2339,20 @@ fn find_next_available_position(
// From now on, only check the headers afterwards.
*next_header += 1;
- } else if let Some((footer_end, _, _)) = footer.filter(|(end, _, footer)| {
- resolved_index >= footer.start * columns && resolved_index < *end * columns
- }) {
+ } else if let Some(footer) = footers
+ .get(*next_footer)
+ .filter(|footer| resolved_index >= footer.start * columns)
+ {
// Skip footer, for the same reason.
- resolved_index = *footer_end * columns;
+ if resolved_index < footer.end * columns {
+ resolved_index = footer.end * columns;
- if skip_rows {
- resolved_index += initial_index % columns;
+ if skip_rows {
+ resolved_index += initial_index % columns;
+ }
}
+
+ *next_footer += 1;
} else {
return Ok(resolved_index);
}
diff --git a/tests/suite/layout/grid/footers.typ b/tests/suite/layout/grid/footers.typ
index c0b03f50..f37f63ae 100644
--- a/tests/suite/layout/grid/footers.typ
+++ b/tests/suite/layout/grid/footers.typ
@@ -89,7 +89,6 @@
stroke: black,
inset: 5pt,
grid.cell(x: 1)[a],
- // Error: 3-56 footer must end at the last row
grid.footer(grid.cell(x: 0)[b1], grid.cell(x: 0)[b2]),
// This should skip the footer
grid.cell(x: 1)[c]
@@ -141,14 +140,12 @@
)
--- grid-footer-not-at-last-row ---
-// Error: 2:3-2:19 footer must end at the last row
#grid(
grid.footer([a]),
[b],
)
--- grid-footer-not-at-last-row-two-columns ---
-// Error: 3:3-3:19 footer must end at the last row
#grid(
columns: 2,
grid.footer([a]),