Compare commits
29 Commits
98e23c1370
...
1c9f242638
Author | SHA1 | Date | |
---|---|---|---|
|
1c9f242638 | ||
|
d13617ed9b | ||
|
315612b1f7 | ||
|
f3cc3bdae7 | ||
|
a2f5593174 | ||
|
c346fb8589 | ||
|
8f434146d8 | ||
|
40ae2324d1 | ||
|
858e620ef7 | ||
|
8c416b88f2 | ||
|
eae79440b0 | ||
|
7ee5dfaa89 | ||
|
183f47ecc0 | ||
|
bd7e403a6d | ||
|
b3fd4676c4 | ||
|
0951fe13fd | ||
|
f9b1bfd1b0 | ||
|
b26e004be9 | ||
|
9422ecc74a | ||
|
58db042ff3 | ||
|
e89e3066a4 | ||
|
3de1237f54 | ||
|
b63f6c99df | ||
|
db2ac385a9 | ||
|
5f663a8da4 | ||
|
3bf0f2b48c | ||
|
0a27b50551 | ||
|
5292c5b198 | ||
|
cce5fe739a |
@ -6,6 +6,7 @@ use typst_library::foundations::{Resolve, StyleChain};
|
||||
use typst_library::layout::grid::resolve::{
|
||||
Cell, CellGrid, Header, LinePosition, Repeatable,
|
||||
};
|
||||
use typst_library::layout::resolve::Footer;
|
||||
use typst_library::layout::{
|
||||
Abs, Axes, Dir, Fr, Fragment, Frame, FrameItem, Length, Point, Region, Regions, Rel,
|
||||
Size, Sizing,
|
||||
@ -60,6 +61,16 @@ pub struct GridLayouter<'a> {
|
||||
pub(super) pending_headers: &'a [Repeatable<Header>],
|
||||
/// Next headers to be processed.
|
||||
pub(super) upcoming_headers: &'a [Repeatable<Header>],
|
||||
/// Currently repeating footers, one per level. Sorted by increasing
|
||||
/// levels.
|
||||
///
|
||||
/// Note that some levels may be absent, in particular level 0, which does
|
||||
/// not exist (so all levels are >= 1).
|
||||
pub(super) repeating_footers: Vec<&'a Footer>,
|
||||
/// Next footers to be processed.
|
||||
pub(super) upcoming_footers: &'a [Repeatable<Footer>],
|
||||
/// Next footers sorted by when they start repeating.
|
||||
pub(super) upcoming_sorted_footers: &'a [Repeatable<Footer>],
|
||||
/// State of the row being currently laid out.
|
||||
///
|
||||
/// This is kept as a field to avoid passing down too many parameters from
|
||||
@ -155,6 +166,12 @@ pub(super) struct Current {
|
||||
/// when finding a new header and causing existing repeating headers to
|
||||
/// stop.
|
||||
pub(super) repeating_header_heights: Vec<Abs>,
|
||||
/// The height for each repeating footer that will be placed in this region.
|
||||
///
|
||||
/// This is used to know how much to update `repeating_footer_height` by
|
||||
/// when finding a footer and causing existing repeating footers to
|
||||
/// stop (and new ones to start).
|
||||
pub(super) repeating_footer_heights: Vec<Abs>,
|
||||
/// The simulated footer height for this region.
|
||||
///
|
||||
/// The simulation occurs before any rows are laid out for a region.
|
||||
@ -215,7 +232,7 @@ pub(super) enum Row {
|
||||
|
||||
impl Row {
|
||||
/// Returns the `y` index of this row.
|
||||
fn index(&self) -> usize {
|
||||
pub(super) fn index(&self) -> usize {
|
||||
match self {
|
||||
Self::Frame(_, y, _) => *y,
|
||||
Self::Fr(_, y, _) => *y,
|
||||
@ -253,6 +270,10 @@ impl<'a> GridLayouter<'a> {
|
||||
repeating_headers: vec![],
|
||||
upcoming_headers: &grid.headers,
|
||||
pending_headers: Default::default(),
|
||||
// This is updated on layout
|
||||
repeating_footers: vec![],
|
||||
upcoming_footers: &grid.footers,
|
||||
upcoming_sorted_footers: &grid.sorted_footers,
|
||||
row_state: RowState::default(),
|
||||
current: Current {
|
||||
initial: regions.size,
|
||||
@ -264,6 +285,7 @@ impl<'a> GridLayouter<'a> {
|
||||
lrows_orphan_snapshot: None,
|
||||
repeating_header_height: Abs::zero(),
|
||||
repeating_header_heights: vec![],
|
||||
repeating_footer_heights: vec![],
|
||||
footer_height: Abs::zero(),
|
||||
},
|
||||
span,
|
||||
@ -274,15 +296,7 @@ impl<'a> GridLayouter<'a> {
|
||||
pub fn layout(mut self, engine: &mut Engine) -> SourceResult<Fragment> {
|
||||
self.measure_columns(engine)?;
|
||||
|
||||
if let Some(footer) = &self.grid.footer {
|
||||
if footer.repeated {
|
||||
// Ensure rows in the first region will be aware of the
|
||||
// possible presence of the footer.
|
||||
self.prepare_footer(footer, engine, 0)?;
|
||||
self.regions.size.y -= self.current.footer_height;
|
||||
self.current.initial_after_repeats = self.regions.size.y;
|
||||
}
|
||||
}
|
||||
self.prepare_next_repeating_footers(true, engine)?;
|
||||
|
||||
let mut y = 0;
|
||||
let mut consecutive_header_count = 0;
|
||||
@ -298,13 +312,15 @@ impl<'a> GridLayouter<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(footer) = &self.grid.footer {
|
||||
if footer.repeated && y >= footer.start {
|
||||
if y == footer.start {
|
||||
self.layout_footer(footer, engine, self.finished.len())?;
|
||||
self.flush_orphans();
|
||||
}
|
||||
y = footer.end;
|
||||
if let [next_footer, other_footers @ ..] = self.upcoming_footers {
|
||||
// TODO(subfooters): effective range (consider gutter before
|
||||
// if it was removed)
|
||||
if next_footer.range.contains(&y) {
|
||||
self.upcoming_footers = other_footers;
|
||||
self.place_new_footer(engine, next_footer)?;
|
||||
self.flush_orphans();
|
||||
y = next_footer.range.end;
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@ -1566,26 +1582,34 @@ impl<'a> GridLayouter<'a> {
|
||||
// TODO(subfooters): explicitly check for short-lived footers.
|
||||
// TODO(subfooters): widow prevention for non-repeated footers with a
|
||||
// similar mechanism / when implementing multiple footers.
|
||||
let footer_would_be_widow = matches!(&self.grid.footer, Some(footer) if footer.repeated)
|
||||
&& self.current.lrows.is_empty()
|
||||
&& self.current.could_progress_at_top;
|
||||
// TODO(subfooters): could progress check must be replaced to consider
|
||||
// the presence of non-repeating footer (then always true).
|
||||
let may_place_footers = !self.repeating_footers.is_empty()
|
||||
&& (!self.current.lrows.is_empty() || !self.current.could_progress_at_top);
|
||||
|
||||
let mut laid_out_footer_start = None;
|
||||
if !footer_would_be_widow {
|
||||
if let Some(footer) = &self.grid.footer {
|
||||
// Don't layout the footer if it would be alone with the header
|
||||
// in the page (hence the widow check), and don't layout it
|
||||
// twice (check below).
|
||||
//
|
||||
// TODO(subfooters): this check can be replaced by a vector of
|
||||
// repeating footers in the future, and/or some "pending
|
||||
// footers" vector for footers we're about to place.
|
||||
if footer.repeated
|
||||
&& self.current.lrows.iter().all(|row| row.index() < footer.start)
|
||||
{
|
||||
laid_out_footer_start = Some(footer.start);
|
||||
self.layout_footer(footer, engine, self.finished.len())?;
|
||||
}
|
||||
if may_place_footers {
|
||||
// Don't layout the footer if it would be alone with the header
|
||||
// in the page (hence the widow check), and don't layout it
|
||||
// twice (it is removed from repeating_footers once it is
|
||||
// reached).
|
||||
//
|
||||
// Use index for iteration to avoid borrow conflict.
|
||||
//
|
||||
// Note that repeating footers are in reverse order.
|
||||
//
|
||||
// TODO(subfooters): "pending footers" vector for footers we're
|
||||
// about to place. Needed for widow prevention of non-repeated
|
||||
// footers.
|
||||
let mut i = 0;
|
||||
while let Some(footer_index) = self.repeating_footers.len().checked_sub(1 + i)
|
||||
{
|
||||
self.layout_footer(
|
||||
self.repeating_footers[footer_index],
|
||||
false,
|
||||
engine,
|
||||
self.finished.len(),
|
||||
)?;
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1684,12 +1708,24 @@ impl<'a> GridLayouter<'a> {
|
||||
// laid out at the first frame of the row).
|
||||
// Any rowspans ending before this row are laid out even
|
||||
// on this row's first frame.
|
||||
if laid_out_footer_start.is_none_or(|footer_start| {
|
||||
// If this is a footer row, then only lay out this rowspan
|
||||
// if the rowspan is contained within the footer.
|
||||
y < footer_start || rowspan.y >= footer_start
|
||||
}) && (rowspan.y + rowspan.rowspan < y + 1
|
||||
|| rowspan.y + rowspan.rowspan == y + 1 && is_last)
|
||||
if (!may_place_footers
|
||||
|| self.repeating_footers.iter().all(|footer| {
|
||||
// If this is a footer row, then only lay out this rowspan
|
||||
// if the rowspan is contained within the footer.
|
||||
// Since the footer is a row from "the future", it
|
||||
// always has a larger Y than all active rowspans,
|
||||
// so we must not interpret a rowspan before it to have
|
||||
// already ended because we saw a repeated footer.
|
||||
//
|
||||
// Of course, not a concern for non-repeated or
|
||||
// short-lived footers as they only appear once.
|
||||
//
|
||||
// TODO(subfooters): use effective range
|
||||
// (what about the gutter?).
|
||||
!footer.range.contains(&y) || footer.range.contains(&rowspan.y)
|
||||
}))
|
||||
&& (rowspan.y + rowspan.rowspan < y + 1
|
||||
|| rowspan.y + rowspan.rowspan == y + 1 && is_last)
|
||||
{
|
||||
// Rowspan ends at this or an earlier row, so we take
|
||||
// it from the rowspans vector and lay it out.
|
||||
@ -1732,25 +1768,18 @@ impl<'a> GridLayouter<'a> {
|
||||
);
|
||||
|
||||
if !last {
|
||||
self.current.repeated_header_rows = 0;
|
||||
self.current.last_repeated_header_end = 0;
|
||||
self.current.repeating_header_height = Abs::zero();
|
||||
self.current.repeating_header_heights.clear();
|
||||
|
||||
let disambiguator = self.finished.len();
|
||||
if let Some(footer) =
|
||||
self.grid.footer.as_ref().and_then(Repeatable::as_repeated)
|
||||
{
|
||||
self.prepare_footer(footer, engine, disambiguator)?;
|
||||
if !self.repeating_footers.is_empty() {
|
||||
// TODO(subfooters): let's not...
|
||||
let footers = self.repeating_footers.clone();
|
||||
self.prepare_repeating_footers(
|
||||
footers.iter().copied(),
|
||||
true,
|
||||
engine,
|
||||
disambiguator,
|
||||
)?;
|
||||
}
|
||||
|
||||
// Ensure rows don't try to overrun the footer.
|
||||
// Note that header layout will only subtract this again if it has
|
||||
// to skip regions to fit headers, so there is no risk of
|
||||
// subtracting this twice.
|
||||
self.regions.size.y -= self.current.footer_height;
|
||||
self.current.initial_after_repeats = self.regions.size.y;
|
||||
|
||||
if !self.repeating_headers.is_empty() || !self.pending_headers.is_empty() {
|
||||
// Add headers to the new region.
|
||||
self.layout_active_headers(engine)?;
|
||||
@ -1780,6 +1809,13 @@ impl<'a> GridLayouter<'a> {
|
||||
|
||||
self.current.could_progress_at_top = self.regions.may_progress();
|
||||
|
||||
self.current.repeated_header_rows = 0;
|
||||
self.current.last_repeated_header_end = 0;
|
||||
self.current.repeating_header_height = Abs::zero();
|
||||
self.current.repeating_header_heights.clear();
|
||||
self.current.footer_height = Abs::zero();
|
||||
self.current.repeating_footer_heights.clear();
|
||||
|
||||
if !self.grid.headers.is_empty() {
|
||||
self.finished_header_rows.push(header_row_info);
|
||||
}
|
||||
|
@ -512,15 +512,18 @@ pub fn hline_stroke_at_column(
|
||||
);
|
||||
|
||||
// Prioritize the footer's top stroke as well where applicable.
|
||||
// TODO(subfooters): do this properly (store footer rows)
|
||||
let bottom_stroke_comes_from_footer = grid
|
||||
.footer
|
||||
.as_ref()
|
||||
.footers
|
||||
.last()
|
||||
.and_then(Repeatable::as_repeated)
|
||||
.is_some_and(|footer| {
|
||||
// Ensure the row below us is a repeated footer.
|
||||
// FIXME: Make this check more robust when footers at arbitrary
|
||||
// positions are added.
|
||||
local_top_y.unwrap_or(0) + 1 < footer.start && y >= footer.start
|
||||
footer.range.end == grid.rows.len()
|
||||
&& local_top_y.unwrap_or(0) + 1 < footer.range.start
|
||||
&& y >= footer.range.start
|
||||
});
|
||||
|
||||
let (prioritized_cell_stroke, deprioritized_cell_stroke) =
|
||||
@ -638,7 +641,7 @@ mod test {
|
||||
vec![],
|
||||
vec![],
|
||||
vec![],
|
||||
None,
|
||||
vec![],
|
||||
entries,
|
||||
)
|
||||
}
|
||||
@ -1176,7 +1179,7 @@ mod test {
|
||||
vec![],
|
||||
vec![],
|
||||
vec![],
|
||||
None,
|
||||
vec![],
|
||||
entries,
|
||||
)
|
||||
}
|
||||
|
@ -240,16 +240,18 @@ impl<'a> GridLayouter<'a> {
|
||||
self.current.initial_after_repeats = self.regions.size.y;
|
||||
}
|
||||
|
||||
if let Some(footer) = &self.grid.footer {
|
||||
if footer.repeated && skipped_region {
|
||||
// Simulate the footer again; the region's 'full' might have
|
||||
// changed.
|
||||
self.regions.size.y += self.current.footer_height;
|
||||
self.current.footer_height = self
|
||||
.simulate_footer(footer, &self.regions, engine, disambiguator)?
|
||||
.height;
|
||||
self.regions.size.y -= self.current.footer_height;
|
||||
}
|
||||
if !self.repeating_footers.is_empty() && skipped_region {
|
||||
// Simulate the footer again; the region's 'full' might have
|
||||
// changed.
|
||||
let (footer_height, footer_heights) = self.simulate_footer_heights(
|
||||
self.repeating_footers.iter().copied(),
|
||||
&self.regions,
|
||||
engine,
|
||||
disambiguator,
|
||||
)?;
|
||||
|
||||
self.current.footer_height = footer_height;
|
||||
self.current.repeating_footer_heights.extend(footer_heights);
|
||||
}
|
||||
|
||||
let repeating_header_rows =
|
||||
@ -463,74 +465,243 @@ impl<'a> GridLayouter<'a> {
|
||||
)
|
||||
}
|
||||
|
||||
/// Updates `self.footer_height` by simulating the footer, and skips to fitting region.
|
||||
pub fn prepare_footer(
|
||||
/// Place a footer we have reached through normal row layout.
|
||||
pub fn place_new_footer(
|
||||
&mut self,
|
||||
footer: &Footer,
|
||||
engine: &mut Engine,
|
||||
footer: &Repeatable<Footer>,
|
||||
) -> SourceResult<()> {
|
||||
// TODO(subfooters): short-lived check
|
||||
if !footer.repeated {
|
||||
// TODO(subfooters): widow prevention for this.
|
||||
// Will need some lookahead. For now, act as short-lived.
|
||||
let footer_height =
|
||||
self.simulate_footer(footer, &self.regions, engine, 0)?.height;
|
||||
|
||||
// Skip to fitting region where only this footer fits.
|
||||
while self.unbreakable_rows_left == 0
|
||||
&& !self.regions.size.y.fits(footer_height)
|
||||
&& self.may_progress_with_repeats()
|
||||
{
|
||||
// Advance regions until we can place the footer.
|
||||
// Treat as a normal row group.
|
||||
self.finish_region(engine, false)?;
|
||||
}
|
||||
|
||||
self.layout_footer(footer, true, engine, 0)?;
|
||||
} else {
|
||||
// Placing a non-short-lived repeating footer, so it must be
|
||||
// the latest one in the repeating footers vector.
|
||||
let latest_repeating_footer = self.repeating_footers.pop().unwrap();
|
||||
assert_eq!(latest_repeating_footer.range.start, footer.range.start);
|
||||
|
||||
let expected_footer_height =
|
||||
self.current.repeating_footer_heights.pop().unwrap();
|
||||
|
||||
// Ensure upcoming rows won't see that this footer will occupy
|
||||
// any space in future regions anymore.
|
||||
self.current.footer_height -= expected_footer_height;
|
||||
|
||||
// Ensure footer rows have their own expected height
|
||||
// available. While not that relevant for them, as they will be
|
||||
// laid out as an unbreakable row group, it's relevant for any
|
||||
// further rows in the same region.
|
||||
self.regions.size.y += expected_footer_height;
|
||||
|
||||
self.layout_footer(footer, false, engine, self.finished.len())?;
|
||||
}
|
||||
|
||||
// If the next group of footers would conflict with other repeating
|
||||
// footers, wait for them to finish repeating before adding more to
|
||||
// repeat.
|
||||
if self.repeating_footers.is_empty()
|
||||
|| self
|
||||
.upcoming_sorted_footers
|
||||
.first()
|
||||
.is_some_and(|f| f.level >= footer.level)
|
||||
{
|
||||
self.prepare_next_repeating_footers(false, engine)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Takes all non-conflicting consecutive footers which are about to start
|
||||
/// repeating, skips to the first region where they all fit, and pushes
|
||||
/// them to `repeating_footers`, sorted by ascending levels.
|
||||
pub fn prepare_next_repeating_footers(
|
||||
&mut self,
|
||||
first_footers: bool,
|
||||
engine: &mut Engine,
|
||||
) -> SourceResult<()> {
|
||||
let [next_footer, other_footers @ ..] = self.upcoming_sorted_footers else {
|
||||
// No footers to take.
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
// TODO(subfooters): also ignore short-lived footers.
|
||||
if !next_footer.repeated {
|
||||
// Skip this footer and don't do anything until we get to it.
|
||||
//
|
||||
// TODO(subfooters): grouping and laying out non-repeated with
|
||||
// repeated, with widow prevention.
|
||||
self.upcoming_sorted_footers = other_footers;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Collect upcoming consecutive footers, they will start repeating with
|
||||
// this one if compatible
|
||||
let mut min_level = next_footer.level;
|
||||
let first_conflicting_index = other_footers
|
||||
.iter()
|
||||
.take_while(|f| {
|
||||
// TODO(subfooters): check for short-lived
|
||||
let compatible = f.repeated && f.level > min_level;
|
||||
min_level = f.level;
|
||||
compatible
|
||||
})
|
||||
.count()
|
||||
+ 1;
|
||||
|
||||
let (next_repeating_footers, new_upcoming_footers) =
|
||||
self.upcoming_sorted_footers.split_at(first_conflicting_index);
|
||||
|
||||
self.upcoming_sorted_footers = new_upcoming_footers;
|
||||
self.prepare_repeating_footers(
|
||||
next_repeating_footers.iter().map(Repeatable::deref),
|
||||
first_footers,
|
||||
engine,
|
||||
0,
|
||||
)?;
|
||||
|
||||
self.repeating_footers
|
||||
.extend(next_repeating_footers.iter().filter_map(Repeatable::as_repeated));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Updates `self.current.repeating_footer_height` by simulating repeating
|
||||
/// footers, and skips to fitting region.
|
||||
pub fn prepare_repeating_footers(
|
||||
&mut self,
|
||||
footers: impl ExactSizeIterator<Item = &'a Footer> + Clone,
|
||||
at_region_top: bool,
|
||||
engine: &mut Engine,
|
||||
disambiguator: usize,
|
||||
) -> SourceResult<()> {
|
||||
let footer_height = self
|
||||
.simulate_footer(footer, &self.regions, engine, disambiguator)?
|
||||
.height;
|
||||
let (mut expected_footer_height, mut expected_footer_heights) = self
|
||||
.simulate_footer_heights(
|
||||
footers.clone(),
|
||||
&self.regions,
|
||||
engine,
|
||||
disambiguator,
|
||||
)?;
|
||||
|
||||
// Skip to fitting region where all of them fit at once.
|
||||
//
|
||||
// Can't be widows: they are assumed to not be short-lived, so
|
||||
// there is at least one non-footer before them, and this
|
||||
// function is called right after placing a new footer, but
|
||||
// before the next non-footer, or at the top of the region,
|
||||
// at which point we haven't reached the row before the highest
|
||||
// level footer yet since the footer itself won't cause a
|
||||
// region break.
|
||||
let mut skipped_region = false;
|
||||
while self.unbreakable_rows_left == 0
|
||||
&& !self.regions.size.y.fits(footer_height)
|
||||
&& !self.regions.size.y.fits(expected_footer_height)
|
||||
&& self.regions.may_progress()
|
||||
{
|
||||
// Advance regions without any output until we can place the
|
||||
// footer.
|
||||
self.finish_region_internal(
|
||||
Frame::soft(Axes::splat(Abs::zero())),
|
||||
vec![],
|
||||
Default::default(),
|
||||
);
|
||||
if at_region_top {
|
||||
self.finish_region_internal(
|
||||
Frame::soft(Axes::splat(Abs::zero())),
|
||||
vec![],
|
||||
Default::default(),
|
||||
);
|
||||
} else {
|
||||
self.finish_region(engine, false)?;
|
||||
}
|
||||
skipped_region = true;
|
||||
}
|
||||
|
||||
// TODO(subfooters): Consider resetting header height etc. if we skip
|
||||
// region. (Maybe move that step to `finish_region_internal`.)
|
||||
//
|
||||
// That is unnecessary at the moment as 'prepare_footers' is only
|
||||
// called at the start of the region, so header height is always zero
|
||||
// and no headers were placed so far, but what about when we can have
|
||||
// footers in the middle of the region? Let's think about this then.
|
||||
self.current.footer_height = if skipped_region {
|
||||
if skipped_region {
|
||||
// Simulate the footer again; the region's 'full' might have
|
||||
// changed.
|
||||
self.simulate_footer(footer, &self.regions, engine, disambiguator)?
|
||||
.height
|
||||
} else {
|
||||
footer_height
|
||||
};
|
||||
// changed, and the vector of heights was cleared.
|
||||
(expected_footer_height, expected_footer_heights) = self
|
||||
.simulate_footer_heights(footers, &self.regions, engine, disambiguator)?;
|
||||
}
|
||||
|
||||
// Ensure rows don't try to overrun the new footers.
|
||||
// Note that header layout will only subtract this again if it has
|
||||
// to skip regions to fit headers, so there is no risk of
|
||||
// subtracting this twice.
|
||||
self.regions.size.y -= expected_footer_height;
|
||||
self.current.footer_height += expected_footer_height;
|
||||
self.current.repeating_footer_heights.extend(expected_footer_heights);
|
||||
|
||||
if at_region_top {
|
||||
self.current.initial_after_repeats = self.regions.size.y;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn simulate_footer_heights(
|
||||
&self,
|
||||
footers: impl ExactSizeIterator<Item = &'a Footer>,
|
||||
regions: &Regions<'_>,
|
||||
engine: &mut Engine,
|
||||
disambiguator: usize,
|
||||
) -> SourceResult<(Abs, Vec<Abs>)> {
|
||||
let mut total_footer_height = Abs::zero();
|
||||
let mut footer_heights = Vec::with_capacity(footers.len());
|
||||
for footer in footers {
|
||||
let footer_height =
|
||||
self.simulate_footer(footer, regions, engine, disambiguator)?.height;
|
||||
|
||||
total_footer_height += footer_height;
|
||||
footer_heights.push(footer_height);
|
||||
}
|
||||
|
||||
Ok((total_footer_height, footer_heights))
|
||||
}
|
||||
|
||||
/// Lays out all rows in the footer.
|
||||
/// They are unbreakable.
|
||||
pub fn layout_footer(
|
||||
&mut self,
|
||||
footer: &Footer,
|
||||
as_short_lived: bool,
|
||||
engine: &mut Engine,
|
||||
disambiguator: usize,
|
||||
) -> SourceResult<()> {
|
||||
// Ensure footer rows have their own height available.
|
||||
// Won't change much as we're creating an unbreakable row group
|
||||
// anyway, so this is mostly for correctness.
|
||||
self.regions.size.y += self.current.footer_height;
|
||||
|
||||
let repeats = self.grid.footer.as_ref().is_some_and(|f| f.repeated);
|
||||
let footer_len = self.grid.rows.len() - footer.start;
|
||||
let footer_len = footer.range.end - footer.range.start;
|
||||
self.unbreakable_rows_left += footer_len;
|
||||
|
||||
for y in footer.start..self.grid.rows.len() {
|
||||
let footer_start = if self.grid.is_gutter_track(footer.range.start)
|
||||
&& self
|
||||
.current
|
||||
.lrows
|
||||
.last()
|
||||
.is_none_or(|r| self.grid.is_gutter_track(r.index()))
|
||||
{
|
||||
// Skip gutter at the top of footer if there's already a gutter
|
||||
// from a repeated header right before it in the current region.
|
||||
// Normally, that shouldn't happen as it indicates we have a widow,
|
||||
// but we can't fully prevent widows anyway.
|
||||
footer.range.start + 1
|
||||
} else {
|
||||
footer.range.start
|
||||
};
|
||||
|
||||
for y in footer_start..footer.range.end {
|
||||
self.layout_row_with_state(
|
||||
y,
|
||||
engine,
|
||||
disambiguator,
|
||||
RowState {
|
||||
in_active_repeatable: repeats,
|
||||
in_active_repeatable: !as_short_lived,
|
||||
..Default::default()
|
||||
},
|
||||
)?;
|
||||
@ -553,8 +724,8 @@ impl<'a> GridLayouter<'a> {
|
||||
// assume that the amount of unbreakable rows following the first row
|
||||
// in the footer will be precisely the rows in the footer.
|
||||
self.simulate_unbreakable_row_group(
|
||||
footer.start,
|
||||
Some(footer.end - footer.start),
|
||||
footer.range.start,
|
||||
Some(footer.range.end - footer.range.start),
|
||||
regions,
|
||||
engine,
|
||||
disambiguator,
|
||||
|
@ -234,24 +234,12 @@ impl GridLayouter<'_> {
|
||||
engine: &mut Engine,
|
||||
) -> SourceResult<()> {
|
||||
if self.unbreakable_rows_left == 0 {
|
||||
// By default, the amount of unbreakable rows starting at the
|
||||
// current row is dynamic and depends on the amount of upcoming
|
||||
// unbreakable cells (with or without a rowspan setting).
|
||||
let mut amount_unbreakable_rows = None;
|
||||
if let Some(footer) = &self.grid.footer {
|
||||
if !footer.repeated && current_row >= footer.start {
|
||||
// Non-repeated footer, so keep it unbreakable.
|
||||
//
|
||||
// TODO(subfooters): This will become unnecessary
|
||||
// once non-repeated footers are treated differently and
|
||||
// have widow prevention.
|
||||
amount_unbreakable_rows = Some(self.grid.rows.len() - footer.start);
|
||||
}
|
||||
}
|
||||
|
||||
let row_group = self.simulate_unbreakable_row_group(
|
||||
current_row,
|
||||
amount_unbreakable_rows,
|
||||
// By default, the amount of unbreakable rows starting at the
|
||||
// current row is dynamic and depends on the amount of upcoming
|
||||
// unbreakable cells (with or without a rowspan setting).
|
||||
None,
|
||||
&self.regions,
|
||||
engine,
|
||||
0,
|
||||
@ -400,7 +388,8 @@ impl GridLayouter<'_> {
|
||||
if breakable
|
||||
&& (!self.repeating_headers.is_empty()
|
||||
|| !self.pending_headers.is_empty()
|
||||
|| matches!(&self.grid.footer, Some(footer) if footer.repeated))
|
||||
// TODO(subfooters): pending footers
|
||||
|| !self.repeating_footers.is_empty())
|
||||
{
|
||||
// Subtract header and footer height from all upcoming regions
|
||||
// when measuring the cell, including the last repeated region.
|
||||
@ -1176,14 +1165,23 @@ impl<'a> RowspanSimulator<'a> {
|
||||
(None, Abs::zero())
|
||||
};
|
||||
|
||||
let footer_height = if let Some(footer) =
|
||||
layouter.grid.footer.as_ref().and_then(Repeatable::as_repeated)
|
||||
let (repeating_footers, footer_height) = if layouter.repeating_footers.is_empty()
|
||||
{
|
||||
layouter
|
||||
.simulate_footer(footer, &self.regions, engine, disambiguator)?
|
||||
.height
|
||||
(None, Abs::zero())
|
||||
} else {
|
||||
Abs::zero()
|
||||
// Only repeating footers have survived after the first region
|
||||
// break.
|
||||
// TODO(subfooters): consider pending footers
|
||||
let repeating_footers = layouter.repeating_footers.iter().copied();
|
||||
|
||||
let (footer_height, _) = layouter.simulate_footer_heights(
|
||||
repeating_footers.clone(),
|
||||
&self.regions,
|
||||
engine,
|
||||
disambiguator,
|
||||
)?;
|
||||
|
||||
(Some(repeating_footers), footer_height)
|
||||
};
|
||||
|
||||
let mut skipped_region = false;
|
||||
@ -1212,15 +1210,18 @@ impl<'a> RowspanSimulator<'a> {
|
||||
};
|
||||
}
|
||||
|
||||
if let Some(footer) =
|
||||
layouter.grid.footer.as_ref().and_then(Repeatable::as_repeated)
|
||||
{
|
||||
if let Some(repeating_footers) = repeating_footers {
|
||||
self.footer_height = if skipped_region {
|
||||
// Simulate footers again, at the new region, as
|
||||
// the full region height may change.
|
||||
layouter
|
||||
.simulate_footer(footer, &self.regions, engine, disambiguator)?
|
||||
.height
|
||||
.simulate_footer_heights(
|
||||
repeating_footers,
|
||||
&self.regions,
|
||||
engine,
|
||||
disambiguator,
|
||||
)?
|
||||
.0
|
||||
} else {
|
||||
footer_height
|
||||
};
|
||||
|
@ -496,6 +496,16 @@ pub struct GridFooter {
|
||||
#[default(true)]
|
||||
pub repeat: bool,
|
||||
|
||||
/// The level of the footer. Must not be zero.
|
||||
///
|
||||
/// This allows repeating multiple footers at once. Footers with different
|
||||
/// levels can repeat together, as long as they have descending levels.
|
||||
///
|
||||
/// Notably, when a footer with a lower level stops repeating, all higher
|
||||
/// or equal level headers start repeating, replacing the previous footer.
|
||||
#[default(NonZeroU32::ONE)]
|
||||
pub level: NonZeroU32,
|
||||
|
||||
/// The cells and lines within the footer.
|
||||
#[variadic]
|
||||
pub children: Vec<GridItem>,
|
||||
|
@ -54,6 +54,7 @@ pub fn grid_to_cellgrid<'a>(
|
||||
},
|
||||
GridChild::Footer(footer) => ResolvableGridChild::Footer {
|
||||
repeat: footer.repeat(styles),
|
||||
level: footer.level(styles),
|
||||
span: footer.span(),
|
||||
items: footer.children.iter().map(resolve_item),
|
||||
},
|
||||
@ -108,6 +109,7 @@ pub fn table_to_cellgrid<'a>(
|
||||
},
|
||||
TableChild::Footer(footer) => ResolvableGridChild::Footer {
|
||||
repeat: footer.repeat(styles),
|
||||
level: footer.level(styles),
|
||||
span: footer.span(),
|
||||
items: footer.children.iter().map(resolve_item),
|
||||
},
|
||||
@ -445,31 +447,22 @@ pub struct Header {
|
||||
}
|
||||
|
||||
/// A repeatable grid footer. Stops at the last row.
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Footer {
|
||||
/// The first row included in this footer.
|
||||
pub start: usize,
|
||||
/// The index after the last row included in this footer.
|
||||
pub end: usize,
|
||||
/// The range of rows included in this footer.
|
||||
pub range: Range<usize>,
|
||||
/// The footer's level.
|
||||
///
|
||||
/// Used similarly to header level.
|
||||
pub level: u32,
|
||||
}
|
||||
|
||||
impl Footer {
|
||||
/// The footer's range of included rows.
|
||||
#[inline]
|
||||
pub fn range(&self) -> Range<usize> {
|
||||
self.start..self.end
|
||||
}
|
||||
}
|
||||
|
||||
/// A possibly repeatable grid child (header or footer).
|
||||
///
|
||||
/// It still exists even when not repeatable, but must not have additional
|
||||
/// considerations by grid layout, other than for consistency (such as making
|
||||
/// a certain group of rows unbreakable).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Repeatable<T> {
|
||||
inner: T,
|
||||
|
||||
@ -656,7 +649,7 @@ impl<'a> Entry<'a> {
|
||||
/// Any grid child, which can be either a header or an item.
|
||||
pub enum ResolvableGridChild<T: ResolvableCell, I> {
|
||||
Header { repeat: bool, level: NonZeroU32, span: Span, items: I },
|
||||
Footer { repeat: bool, span: Span, items: I },
|
||||
Footer { repeat: bool, level: NonZeroU32, span: Span, items: I },
|
||||
Item(ResolvableGridItem<T>),
|
||||
}
|
||||
|
||||
@ -678,8 +671,12 @@ pub struct CellGrid<'a> {
|
||||
pub hlines: Vec<Vec<Line>>,
|
||||
/// The repeatable headers of this grid.
|
||||
pub headers: Vec<Repeatable<Header>>,
|
||||
/// The repeatable footer of this grid.
|
||||
pub footer: Option<Repeatable<Footer>>,
|
||||
/// The repeatable footers of this grid.
|
||||
pub footers: Vec<Repeatable<Footer>>,
|
||||
/// Footers sorted by order of when they start repeating, or should
|
||||
/// otherwise be laid out for the first time (even if only once, for
|
||||
/// non-repeating footers).
|
||||
pub sorted_footers: Vec<Repeatable<Footer>>,
|
||||
/// Whether this grid has gutters.
|
||||
pub has_gutter: bool,
|
||||
}
|
||||
@ -692,7 +689,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.
|
||||
@ -702,7 +699,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![];
|
||||
@ -749,6 +746,8 @@ impl<'a> CellGrid<'a> {
|
||||
rows.pop();
|
||||
}
|
||||
|
||||
let sorted_footers = simulate_footer_repetition(&footers);
|
||||
|
||||
Self {
|
||||
cols,
|
||||
rows,
|
||||
@ -756,7 +755,8 @@ impl<'a> CellGrid<'a> {
|
||||
vlines,
|
||||
hlines,
|
||||
headers,
|
||||
footer,
|
||||
footers,
|
||||
sorted_footers,
|
||||
has_gutter,
|
||||
}
|
||||
}
|
||||
@ -895,6 +895,11 @@ impl<'a> CellGrid<'a> {
|
||||
pub fn has_repeated_headers(&self) -> bool {
|
||||
self.headers.iter().any(|h| h.repeated)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn has_repeated_footers(&self) -> bool {
|
||||
self.footers.iter().any(|f| f.repeated)
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolves and positions all cells in the grid before creating it.
|
||||
@ -977,6 +982,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,
|
||||
|
||||
@ -1034,15 +1040,13 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
|
||||
let has_gutter = self.gutter.any(|tracks| !tracks.is_empty());
|
||||
|
||||
let mut headers: Vec<Repeatable<Header>> = vec![];
|
||||
let mut footers: Vec<Repeatable<Footer>> = 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;
|
||||
|
||||
// If true, there has been at least one cell besides headers and
|
||||
// footers. When false, footers at the end are forced to not repeat.
|
||||
let mut at_least_one_cell = false;
|
||||
// The first and last rows containing a cell outside a row group, that
|
||||
// is, outside a header or footer. Headers after the last such row and
|
||||
// footers before the first such row have no "children" cells and thus
|
||||
// are not repeated.
|
||||
let mut first_last_cell_rows = None;
|
||||
|
||||
// We can't just use the cell's index in the 'cells' vector to
|
||||
// determine its automatic position, since cells could have arbitrary
|
||||
@ -1060,10 +1064,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.
|
||||
//
|
||||
@ -1086,12 +1091,12 @@ 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,
|
||||
&mut first_last_cell_rows,
|
||||
child,
|
||||
)?;
|
||||
}
|
||||
@ -1107,13 +1112,12 @@ 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,
|
||||
first_last_cell_rows,
|
||||
)?;
|
||||
|
||||
Ok(CellGrid::new_internal(
|
||||
@ -1122,7 +1126,7 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
|
||||
vlines,
|
||||
hlines,
|
||||
headers,
|
||||
footer,
|
||||
footers,
|
||||
resolved_cells,
|
||||
))
|
||||
}
|
||||
@ -1142,12 +1146,12 @@ 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,
|
||||
first_last_cell_rows: &mut Option<(usize, usize)>,
|
||||
child: ResolvableGridChild<T, I>,
|
||||
) -> SourceResult<()>
|
||||
where
|
||||
@ -1198,6 +1202,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
|
||||
@ -1207,7 +1217,7 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
|
||||
let mut first_available_row = 0;
|
||||
|
||||
let (header_footer_items, simple_item) = match child {
|
||||
ResolvableGridChild::Header { repeat, level, span, items, .. } => {
|
||||
ResolvableGridChild::Header { repeat, level, span, items } => {
|
||||
row_group_data = Some(RowGroupData {
|
||||
range: None,
|
||||
span,
|
||||
@ -1234,17 +1244,13 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
|
||||
|
||||
(Some(items), None)
|
||||
}
|
||||
ResolvableGridChild::Footer { repeat, span, items, .. } => {
|
||||
if footer.is_some() {
|
||||
bail!(span, "cannot have more than one footer");
|
||||
}
|
||||
|
||||
ResolvableGridChild::Footer { repeat, level, span, items } => {
|
||||
row_group_data = Some(RowGroupData {
|
||||
range: None,
|
||||
span,
|
||||
repeat,
|
||||
kind: RowGroupKind::Footer,
|
||||
repeatable_level: NonZeroU32::ONE,
|
||||
repeatable_level: level,
|
||||
top_hlines_start: pending_hlines.len(),
|
||||
top_hlines_end: None,
|
||||
});
|
||||
@ -1256,13 +1262,7 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
|
||||
|
||||
(Some(items), None)
|
||||
}
|
||||
ResolvableGridChild::Item(item) => {
|
||||
if matches!(item, ResolvableGridItem::Cell(_)) {
|
||||
*at_least_one_cell = true;
|
||||
}
|
||||
|
||||
(None, Some(item))
|
||||
}
|
||||
ResolvableGridChild::Item(item) => (None, Some(item)),
|
||||
};
|
||||
|
||||
let items = header_footer_items.into_iter().flatten().chain(simple_item);
|
||||
@ -1382,10 +1382,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(),
|
||||
@ -1443,6 +1444,13 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
|
||||
// no longer appear at the top.
|
||||
*top_hlines_end = Some(pending_hlines.len());
|
||||
}
|
||||
} else {
|
||||
// This is a cell outside a row group.
|
||||
*first_last_cell_rows = Some(
|
||||
first_last_cell_rows
|
||||
.map(|(first, last)| (first.min(y), last.max(y)))
|
||||
.unwrap_or((y, y)),
|
||||
);
|
||||
}
|
||||
|
||||
// Let's resolve the cell so it can determine its own fields
|
||||
@ -1607,23 +1615,18 @@ 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,
|
||||
},
|
||||
));
|
||||
let data = Footer {
|
||||
// Later on, we have to correct this range 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.
|
||||
range: group_range,
|
||||
level: row_group.repeatable_level.get(),
|
||||
};
|
||||
|
||||
*repeat_footer = row_group.repeat;
|
||||
footers.push(Repeatable { inner: data, repeated: row_group.repeat });
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1788,36 +1791,41 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
|
||||
/// 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,
|
||||
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>>> {
|
||||
first_last_cell_rows: Option<(usize, usize)>,
|
||||
) -> 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.
|
||||
// footers at the end, 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);
|
||||
for header_at_the_end in headers.iter_mut().rev().take_while(move |h| {
|
||||
let at_the_end = h.range.end == consecutive_header_start;
|
||||
// Same for consecutive footers right after the start of the table or
|
||||
// any initial headers.
|
||||
if let Some((first_cell_row, last_cell_row)) = first_last_cell_rows {
|
||||
for header in
|
||||
headers.iter_mut().rev().take_while(|h| h.range.start > last_cell_row)
|
||||
{
|
||||
header.short_lived = true;
|
||||
}
|
||||
|
||||
consecutive_header_start = h.range.start;
|
||||
at_the_end
|
||||
}) {
|
||||
header_at_the_end.short_lived = true;
|
||||
for footer in footers.iter_mut().take_while(|f| f.range.end <= first_cell_row)
|
||||
{
|
||||
// TODO(subfooters): short lived
|
||||
footer.repeated = false;
|
||||
}
|
||||
} else {
|
||||
// No cells outside headers or footers, so nobody repeats!
|
||||
for header in &mut *headers {
|
||||
header.short_lived = true;
|
||||
}
|
||||
for footer in &mut *footers {
|
||||
// TODO(subfooters): short lived
|
||||
footer.repeated = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Repeat the gutter below a header (hence why we don't
|
||||
@ -1849,14 +1857,8 @@ 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 footer in &mut *footers {
|
||||
// TODO(subfooters): will need a global slice of headers and
|
||||
// footers for when we have multiple footers
|
||||
// Alternatively, never include the gutter in the footer's
|
||||
@ -1869,45 +1871,32 @@ 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;
|
||||
// Convert the footer's start index to post-gutter coordinates.
|
||||
footer.range.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);
|
||||
}
|
||||
|
||||
// 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.range.start) {
|
||||
footer.range.start = footer.range.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.range.end = (2 * footer.range.end).saturating_sub(1);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(footer)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Resolves the cell's fields based on grid-wide properties.
|
||||
@ -2079,7 +2068,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<()> {
|
||||
@ -2098,13 +2087,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.range.end && cell_y + rowspan > footer.range.start)
|
||||
{
|
||||
bail!(
|
||||
"cell would conflict with footer spanning the same position";
|
||||
hint: "try reducing the cell's rowspan or moving the footer"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@ -2126,10 +2116,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,
|
||||
@ -2152,11 +2143,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,
|
||||
)?;
|
||||
|
||||
@ -2193,7 +2185,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)
|
||||
@ -2212,25 +2204,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,
|
||||
)
|
||||
}
|
||||
@ -2241,7 +2234,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.
|
||||
@ -2276,14 +2269,16 @@ fn resolve_cell_position(
|
||||
///
|
||||
/// When `skip_rows` is true, one row is skipped on each iteration, preserving
|
||||
/// the column. That is used to find a position for a fixed column cell.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[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;
|
||||
@ -2327,15 +2322,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.range.start * columns)
|
||||
{
|
||||
// Skip footer, for the same reason.
|
||||
resolved_index = *footer_end * columns;
|
||||
if resolved_index < footer.range.end * columns {
|
||||
resolved_index = footer.range.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);
|
||||
}
|
||||
@ -2389,3 +2389,50 @@ fn skip_auto_index_through_fully_merged_rows(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a vector where all footers are sorted ahead of time by the points
|
||||
/// at which they start repeating. When a new footer is about to be laid out,
|
||||
/// conflicting footers which come before it in this vector must stop
|
||||
/// repeating.
|
||||
fn simulate_footer_repetition(footers: &[Repeatable<Footer>]) -> Vec<Repeatable<Footer>> {
|
||||
if footers.len() <= 1 {
|
||||
return footers.to_vec();
|
||||
}
|
||||
|
||||
let mut ordered_footers = Vec::with_capacity(footers.len());
|
||||
let mut repeating_footers: Vec<&Repeatable<Footer>> = vec![];
|
||||
|
||||
// Read footers in reverse, using the same algorithm as headers to
|
||||
// determine when a footer starts and stops repeating, but going from grid
|
||||
// end to start. When it stops repeating, that's when it will start
|
||||
// repeating in proper layout (from start to end), whereas it starts
|
||||
// repeating here when it should stop repeating in practice. So,
|
||||
// effectively, repeated footer layout is the same as for headers, but
|
||||
// reversed, which we take advantage of by doing it reversed and then
|
||||
// reversing it all back later.
|
||||
for footer in footers.iter().rev() {
|
||||
// Keep only lower level footers. Assume sorted by increasing levels.
|
||||
let stopped_repeating = repeating_footers
|
||||
.drain(repeating_footers.partition_point(|f| f.level < footer.level)..);
|
||||
|
||||
// If they stopped repeating here, that's when they will start
|
||||
// repeating. We save them in reverse of the reverse order so they stay
|
||||
// sorted by increasing levels when we reverse `ordered_footers` later.
|
||||
ordered_footers.extend(stopped_repeating.rev().cloned());
|
||||
|
||||
if footer.repeated {
|
||||
// Start repeating now. Vector stays sorted by increasing levels,
|
||||
// as any higher-level footers stopped repeating now.
|
||||
repeating_footers.push(footer);
|
||||
} else {
|
||||
// Immediately finishes repeating.
|
||||
ordered_footers.push(footer.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Push remaining footers that repeat starting from the top of the grid
|
||||
ordered_footers.extend(repeating_footers.into_iter().rev().cloned());
|
||||
ordered_footers.reverse();
|
||||
|
||||
ordered_footers
|
||||
}
|
||||
|
@ -292,12 +292,35 @@ fn show_cellgrid_html(grid: CellGrid, styles: StyleChain) -> Content {
|
||||
elem(tag::tr, Content::sequence(row))
|
||||
};
|
||||
|
||||
// TODO(subfooters): similarly to headers, take consecutive footers from
|
||||
// the end for 'tfoot'.
|
||||
let footer = grid.footer.map(|ft| {
|
||||
let rows = rows.drain(ft.start..);
|
||||
elem(tag::tfoot, Content::sequence(rows.map(|row| tr(tag::td, row))))
|
||||
});
|
||||
// Store all consecutive headers at the start in 'tfoot'. All remaining
|
||||
// headers are just normal rows across the table body. (There doesn't
|
||||
// appear to be an equivalent of 'th' for footers in HTML.)
|
||||
// TODO: test
|
||||
let footer = {
|
||||
let mut consecutive_footer_start = grid.rows.len();
|
||||
let footers_at_end = grid
|
||||
.footers
|
||||
.iter()
|
||||
.rev()
|
||||
.take_while(|ft| {
|
||||
let is_consecutive = ft.range.end == consecutive_footer_start;
|
||||
consecutive_footer_start = ft.range.start;
|
||||
|
||||
is_consecutive
|
||||
})
|
||||
.count();
|
||||
|
||||
if footers_at_end > 0 {
|
||||
let last_mid_table_footer = grid.footers.len() - footers_at_end;
|
||||
let removed_footer_rows =
|
||||
grid.footers.get(last_mid_table_footer).unwrap().range.start;
|
||||
let rows = rows.drain(removed_footer_rows..);
|
||||
|
||||
Some(elem(tag::tfoot, Content::sequence(rows.map(|row| tr(tag::td, row)))))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
// Store all consecutive headers at the start in 'thead'. All remaining
|
||||
// headers are just 'th' rows across the table body.
|
||||
@ -567,6 +590,16 @@ pub struct TableFooter {
|
||||
#[default(true)]
|
||||
pub repeat: bool,
|
||||
|
||||
/// The level of the footer. Must not be zero.
|
||||
///
|
||||
/// This allows repeating multiple footers at once. Footers with different
|
||||
/// levels can repeat together, as long as they have descending levels.
|
||||
///
|
||||
/// Notably, when a footer with a lower level stops repeating, all higher
|
||||
/// or equal level headers start repeating, replacing the previous footer.
|
||||
#[default(NonZeroU32::ONE)]
|
||||
pub level: NonZeroU32,
|
||||
|
||||
/// The cells and lines within the footer.
|
||||
#[variadic]
|
||||
pub children: Vec<TableItem>,
|
||||
|
BIN
tests/ref/grid-footer-cell-with-x.png
Normal file
After Width: | Height: | Size: 489 B |
BIN
tests/ref/grid-footer-gutter-short-lived.png
Normal file
After Width: | Height: | Size: 321 B |
BIN
tests/ref/grid-footer-multiple.png
Normal file
After Width: | Height: | Size: 209 B |
BIN
tests/ref/grid-footer-not-at-last-row-two-columns.png
Normal file
After Width: | Height: | Size: 176 B |
BIN
tests/ref/grid-footer-not-at-last-row.png
Normal file
After Width: | Height: | Size: 176 B |
BIN
tests/ref/grid-subfooters-basic-non-consecutive-with-header.png
Normal file
After Width: | Height: | Size: 279 B |
BIN
tests/ref/grid-subfooters-basic-non-consecutive.png
Normal file
After Width: | Height: | Size: 258 B |
BIN
tests/ref/grid-subfooters-basic-replace.png
Normal file
After Width: | Height: | Size: 319 B |
BIN
tests/ref/grid-subfooters-basic-with-header.png
Normal file
After Width: | Height: | Size: 256 B |
BIN
tests/ref/grid-subfooters-basic.png
Normal file
After Width: | Height: | Size: 210 B |
BIN
tests/ref/grid-subfooters-demo.png
Normal file
After Width: | Height: | Size: 6.8 KiB |
BIN
tests/ref/grid-subfooters-repeat-gutter.png
Normal file
After Width: | Height: | Size: 626 B |
BIN
tests/ref/grid-subfooters-repeat-non-consecutive.png
Normal file
After Width: | Height: | Size: 622 B |
BIN
tests/ref/grid-subfooters-repeat-replace-double-widow.png
Normal file
After Width: | Height: | Size: 973 B |
BIN
tests/ref/grid-subfooters-repeat-replace-gutter.png
Normal file
After Width: | Height: | Size: 721 B |
BIN
tests/ref/grid-subfooters-repeat-replace-multiple-levels.png
Normal file
After Width: | Height: | Size: 910 B |
BIN
tests/ref/grid-subfooters-repeat-replace-widow.png
Normal file
After Width: | Height: | Size: 970 B |
BIN
tests/ref/grid-subfooters-repeat-replace.png
Normal file
After Width: | Height: | Size: 963 B |
BIN
tests/ref/grid-subfooters-repeat-with-header.png
Normal file
After Width: | Height: | Size: 596 B |
BIN
tests/ref/grid-subfooters-repeat.png
Normal file
After Width: | Height: | Size: 549 B |
@ -41,6 +41,20 @@
|
||||
)
|
||||
)
|
||||
|
||||
--- grid-footer-gutter-short-lived ---
|
||||
// Gutter, no repetition, short-lived
|
||||
#set page(height: 6em)
|
||||
#set text(6pt)
|
||||
#set table(inset: 2pt, stroke: 0.5pt)
|
||||
#table(
|
||||
gutter: 2pt,
|
||||
align: center + horizon,
|
||||
table.header([a]),
|
||||
table.footer([b]),
|
||||
table.footer([c]),
|
||||
[d],
|
||||
)
|
||||
|
||||
--- grid-cell-override-in-header-and-footer ---
|
||||
#table(
|
||||
table.header(table.cell(stroke: red)[Hello]),
|
||||
@ -89,7 +103,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 +154,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]),
|
||||
@ -166,7 +177,6 @@
|
||||
)
|
||||
|
||||
--- grid-footer-multiple ---
|
||||
// Error: 4:3-4:19 cannot have more than one footer
|
||||
#grid(
|
||||
[a],
|
||||
grid.footer([a]),
|
||||
|
182
tests/suite/layout/grid/subfooters.typ
Normal file
@ -0,0 +1,182 @@
|
||||
--- grid-subfooters-demo ---
|
||||
#set page(height: 15.2em)
|
||||
#table(
|
||||
columns: 2,
|
||||
align: center,
|
||||
table.header(
|
||||
table.cell(colspan: 2)[*Regional User Data*],
|
||||
),
|
||||
table.header(
|
||||
level: 2,
|
||||
table.cell(colspan: 2)[*Germany*],
|
||||
[*Username*], [*Joined*]
|
||||
),
|
||||
[john123], [2024],
|
||||
[rob8], [2025],
|
||||
[joe1], [2025],
|
||||
[joe2], [2025],
|
||||
[martha], [2025],
|
||||
[pear], [2025],
|
||||
table.footer(
|
||||
level: 2,
|
||||
[*Mode*], [2025],
|
||||
table.cell(colspan: 2)[*Totals*],
|
||||
),
|
||||
// TODO: Why does it overflow here?
|
||||
table.header(
|
||||
level: 2,
|
||||
table.cell(colspan: 2)[*United States*],
|
||||
[*Username*], [*Joined*]
|
||||
),
|
||||
[cool4], [2023],
|
||||
[roger], [2023],
|
||||
[bigfan55], [2022],
|
||||
table.footer(
|
||||
level: 2,
|
||||
[*Mode*], [2023],
|
||||
table.cell(colspan: 2)[*Totals*],
|
||||
),
|
||||
table.footer(
|
||||
table.cell(colspan: 2)[*Data Inc.*],
|
||||
),
|
||||
)
|
||||
|
||||
--- grid-subfooters-basic ---
|
||||
#grid(
|
||||
[a],
|
||||
grid.footer(level: 2, [b]),
|
||||
grid.footer([c]),
|
||||
)
|
||||
|
||||
--- grid-subfooters-basic-non-consecutive ---
|
||||
#grid(
|
||||
[x],
|
||||
grid.footer(level: 2, [a]),
|
||||
[y],
|
||||
grid.footer([b]),
|
||||
)
|
||||
|
||||
--- grid-subfooters-basic-replace ---
|
||||
#grid(
|
||||
[x],
|
||||
grid.footer(level: 2, [a]),
|
||||
[y],
|
||||
grid.footer(level: 2, [b]),
|
||||
[z],
|
||||
grid.footer([c]),
|
||||
)
|
||||
|
||||
--- grid-subfooters-basic-with-header ---
|
||||
#grid(
|
||||
grid.header([a]),
|
||||
[b],
|
||||
grid.footer(level: 2, [c]),
|
||||
grid.footer([d]),
|
||||
)
|
||||
|
||||
--- grid-subfooters-basic-non-consecutive-with-header ---
|
||||
#grid(
|
||||
grid.header([a]),
|
||||
[x],
|
||||
grid.footer(level: 2, [b]),
|
||||
[y],
|
||||
grid.footer([f])
|
||||
)
|
||||
|
||||
--- grid-subfooters-repeat ---
|
||||
#set page(height: 8em)
|
||||
#grid(
|
||||
..([a],) * 10,
|
||||
grid.footer(level: 2, [b]),
|
||||
grid.footer([c]),
|
||||
)
|
||||
|
||||
--- grid-subfooters-repeat-non-consecutive ---
|
||||
#set page(height: 8em)
|
||||
#grid(
|
||||
..([y],) * 10,
|
||||
grid.footer(level: 2, [b]),
|
||||
[x],
|
||||
grid.footer([a]),
|
||||
)
|
||||
|
||||
--- grid-subfooters-repeat-with-header ---
|
||||
#set page(height: 8em)
|
||||
#grid(
|
||||
grid.header([a]),
|
||||
..([b],) * 10,
|
||||
grid.footer(level: 2, [c]),
|
||||
[m],
|
||||
grid.footer([f])
|
||||
)
|
||||
|
||||
--- grid-subfooters-repeat-gutter ---
|
||||
// Gutter above the footer is also repeated
|
||||
#set page(height: 8em)
|
||||
#grid(
|
||||
inset: (top: 0.5pt),
|
||||
stroke: (top: 1pt),
|
||||
gutter: (1pt,) * 9 + (6pt, 1pt),
|
||||
..([a],) * 10,
|
||||
grid.footer(level: 2, [b]),
|
||||
grid.footer([c]),
|
||||
)
|
||||
|
||||
--- grid-subfooters-repeat-replace ---
|
||||
#set page(height: 8em)
|
||||
#grid(
|
||||
..([x],) * 10,
|
||||
grid.footer(level: 2, [a]),
|
||||
..([y],) * 10,
|
||||
grid.footer(level: 2, [b]),
|
||||
[z],
|
||||
grid.footer([c]),
|
||||
)
|
||||
|
||||
--- grid-subfooters-repeat-replace-multiple-levels ---
|
||||
// TODO: This is overflowing
|
||||
#set page(height: 8em)
|
||||
#grid(
|
||||
..([x],) * 6,
|
||||
grid.footer(level: 2, [a]),
|
||||
..([y],) * 10,
|
||||
grid.footer(level: 3, [b]),
|
||||
grid.footer(level: 2, [c]),
|
||||
[z],
|
||||
grid.footer([d]),
|
||||
)
|
||||
|
||||
--- grid-subfooters-repeat-replace-gutter ---
|
||||
#set page(height: 8em)
|
||||
#grid(
|
||||
gutter: 3pt,
|
||||
..([x],) * 3,
|
||||
grid.footer(level: 2, [a]),
|
||||
..([y],) * 8,
|
||||
grid.footer(level: 2, [b]),
|
||||
[z],
|
||||
grid.footer([c]),
|
||||
)
|
||||
|
||||
--- grid-subfooters-repeat-replace-widow ---
|
||||
#set page(height: 8em)
|
||||
#grid(
|
||||
..([x],) * 14,
|
||||
grid.footer(level: 2, [a]),
|
||||
..([y],) * 8,
|
||||
grid.footer(level: 2, [b]),
|
||||
[z],
|
||||
grid.footer([c]),
|
||||
)
|
||||
|
||||
--- grid-subfooters-repeat-replace-double-widow ---
|
||||
#set page(height: 8em)
|
||||
#grid(
|
||||
..([x],) * 12,
|
||||
grid.footer(level: 3, [a]),
|
||||
grid.footer(level: 2, [b]),
|
||||
..([y],) * 11,
|
||||
grid.footer(level: 2, [c]),
|
||||
[z],
|
||||
grid.footer([d]),
|
||||
)
|