mirror of
https://github.com/typst/typst
synced 2025-07-16 00:52:54 +08:00
initial footer simulation and placement
This commit is contained in:
parent
db2ac385a9
commit
b63f6c99df
@ -166,6 +166,12 @@ pub(super) struct Current {
|
|||||||
/// when finding a new header and causing existing repeating headers to
|
/// when finding a new header and causing existing repeating headers to
|
||||||
/// stop.
|
/// stop.
|
||||||
pub(super) repeating_header_heights: Vec<Abs>,
|
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 simulated footer height for this region.
|
||||||
///
|
///
|
||||||
/// The simulation occurs before any rows are laid out for a region.
|
/// The simulation occurs before any rows are laid out for a region.
|
||||||
@ -264,7 +270,7 @@ impl<'a> GridLayouter<'a> {
|
|||||||
repeating_headers: vec![],
|
repeating_headers: vec![],
|
||||||
upcoming_headers: &grid.headers,
|
upcoming_headers: &grid.headers,
|
||||||
pending_headers: Default::default(),
|
pending_headers: Default::default(),
|
||||||
// This is calculated on layout
|
// This is updated on layout
|
||||||
repeating_footers: vec![],
|
repeating_footers: vec![],
|
||||||
upcoming_footers: &grid.footers,
|
upcoming_footers: &grid.footers,
|
||||||
upcoming_sorted_footers: &grid.sorted_footers,
|
upcoming_sorted_footers: &grid.sorted_footers,
|
||||||
@ -279,6 +285,7 @@ impl<'a> GridLayouter<'a> {
|
|||||||
lrows_orphan_snapshot: None,
|
lrows_orphan_snapshot: None,
|
||||||
repeating_header_height: Abs::zero(),
|
repeating_header_height: Abs::zero(),
|
||||||
repeating_header_heights: vec![],
|
repeating_header_heights: vec![],
|
||||||
|
repeating_footer_heights: vec![],
|
||||||
footer_height: Abs::zero(),
|
footer_height: Abs::zero(),
|
||||||
},
|
},
|
||||||
span,
|
span,
|
||||||
@ -289,15 +296,12 @@ impl<'a> GridLayouter<'a> {
|
|||||||
pub fn layout(mut self, engine: &mut Engine) -> SourceResult<Fragment> {
|
pub fn layout(mut self, engine: &mut Engine) -> SourceResult<Fragment> {
|
||||||
self.measure_columns(engine)?;
|
self.measure_columns(engine)?;
|
||||||
|
|
||||||
if let Some(footer) = &self.grid.footer {
|
self.prepare_next_repeating_footers(true, engine)?;
|
||||||
if footer.repeated {
|
|
||||||
// Ensure rows in the first region will be aware of the
|
// Ensure rows in the first region will be aware of the possible
|
||||||
// possible presence of the footer.
|
// presence of the footer.
|
||||||
self.prepare_footer(footer, engine, 0)?;
|
self.regions.size.y -= self.current.footer_height;
|
||||||
self.regions.size.y -= self.current.footer_height;
|
self.current.initial_after_repeats = self.regions.size.y;
|
||||||
self.current.initial_after_repeats = self.regions.size.y;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut y = 0;
|
let mut y = 0;
|
||||||
let mut consecutive_header_count = 0;
|
let mut consecutive_header_count = 0;
|
||||||
@ -313,13 +317,14 @@ impl<'a> GridLayouter<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(footer) = &self.grid.footer {
|
if let Some(next_footer) = self.upcoming_footers.first() {
|
||||||
if footer.repeated && y >= footer.start {
|
// TODO(subfooters): effective range (consider gutter before
|
||||||
if y == footer.start {
|
// if it was removed)
|
||||||
self.layout_footer(footer, engine, self.finished.len())?;
|
if next_footer.range().contains(&y) {
|
||||||
self.flush_orphans();
|
self.place_new_footer(engine, next_footer)?;
|
||||||
}
|
self.flush_orphans();
|
||||||
y = footer.end;
|
y = next_footer.end;
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1581,26 +1586,25 @@ impl<'a> GridLayouter<'a> {
|
|||||||
// TODO(subfooters): explicitly check for short-lived footers.
|
// TODO(subfooters): explicitly check for short-lived footers.
|
||||||
// TODO(subfooters): widow prevention for non-repeated footers with a
|
// TODO(subfooters): widow prevention for non-repeated footers with a
|
||||||
// similar mechanism / when implementing multiple footers.
|
// similar mechanism / when implementing multiple footers.
|
||||||
let footer_would_be_widow = matches!(&self.grid.footer, Some(footer) if footer.repeated)
|
let may_place_footers = !self.repeating_footers.is_empty()
|
||||||
&& self.current.lrows.is_empty()
|
&& self.current.lrows.is_empty()
|
||||||
&& self.current.could_progress_at_top;
|
&& self.current.could_progress_at_top;
|
||||||
|
|
||||||
let mut laid_out_footer_start = None;
|
if may_place_footers {
|
||||||
if !footer_would_be_widow {
|
// Don't layout the footer if it would be alone with the header
|
||||||
if let Some(footer) = &self.grid.footer {
|
// in the page (hence the widow check), and don't layout it
|
||||||
// Don't layout the footer if it would be alone with the header
|
// twice (it is removed from repeating_footers once it is
|
||||||
// in the page (hence the widow check), and don't layout it
|
// reached).
|
||||||
// twice (check below).
|
//
|
||||||
//
|
// Use index for iteration to avoid borrow conflict.
|
||||||
// TODO(subfooters): this check can be replaced by a vector of
|
//
|
||||||
// repeating footers in the future, and/or some "pending
|
// TODO(subfooters): "pending footers" vector for footers we're
|
||||||
// footers" vector for footers we're about to place.
|
// about to place. Needed for widow prevention of non-repeated
|
||||||
if footer.repeated
|
// footers.
|
||||||
&& self.current.lrows.iter().all(|row| row.index() < footer.start)
|
let mut i = 0;
|
||||||
{
|
while let Some(footer) = self.repeating_footers.get(i) {
|
||||||
laid_out_footer_start = Some(footer.start);
|
self.layout_footer(footer, false, engine, self.finished.len())?;
|
||||||
self.layout_footer(footer, engine, self.finished.len())?;
|
i += 1;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1699,12 +1703,24 @@ impl<'a> GridLayouter<'a> {
|
|||||||
// laid out at the first frame of the row).
|
// laid out at the first frame of the row).
|
||||||
// Any rowspans ending before this row are laid out even
|
// Any rowspans ending before this row are laid out even
|
||||||
// on this row's first frame.
|
// on this row's first frame.
|
||||||
if laid_out_footer_start.is_none_or(|footer_start| {
|
if !may_place_footers
|
||||||
// If this is a footer row, then only lay out this rowspan
|
|| self.repeating_footers.iter().all(|footer| {
|
||||||
// if the rowspan is contained within the footer.
|
// If this is a footer row, then only lay out this rowspan
|
||||||
y < footer_start || rowspan.y >= footer_start
|
// if the rowspan is contained within the footer.
|
||||||
}) && (rowspan.y + rowspan.rowspan < y + 1
|
// Since the footer is a row from "the future", it
|
||||||
|| rowspan.y + rowspan.rowspan == y + 1 && is_last)
|
// 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
|
// Rowspan ends at this or an earlier row, so we take
|
||||||
// it from the rowspans vector and lay it out.
|
// it from the rowspans vector and lay it out.
|
||||||
@ -1748,10 +1764,15 @@ impl<'a> GridLayouter<'a> {
|
|||||||
|
|
||||||
if !last {
|
if !last {
|
||||||
let disambiguator = self.finished.len();
|
let disambiguator = self.finished.len();
|
||||||
if let Some(footer) =
|
if !self.repeating_footers.is_empty() {
|
||||||
self.grid.footer.as_ref().and_then(Repeatable::as_repeated)
|
// TODO(subfooters): let's not...
|
||||||
{
|
let footers = self.repeating_footers.clone();
|
||||||
self.prepare_footer(footer, engine, disambiguator)?;
|
self.prepare_repeating_footers(
|
||||||
|
footers.iter().map(|f| *f),
|
||||||
|
true,
|
||||||
|
engine,
|
||||||
|
disambiguator,
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure rows don't try to overrun the footer.
|
// Ensure rows don't try to overrun the footer.
|
||||||
@ -1794,6 +1815,8 @@ impl<'a> GridLayouter<'a> {
|
|||||||
self.current.last_repeated_header_end = 0;
|
self.current.last_repeated_header_end = 0;
|
||||||
self.current.repeating_header_height = Abs::zero();
|
self.current.repeating_header_height = Abs::zero();
|
||||||
self.current.repeating_header_heights.clear();
|
self.current.repeating_header_heights.clear();
|
||||||
|
self.current.footer_height = Abs::zero();
|
||||||
|
self.current.repeating_footer_heights.clear();
|
||||||
|
|
||||||
if !self.grid.headers.is_empty() {
|
if !self.grid.headers.is_empty() {
|
||||||
self.finished_header_rows.push(header_row_info);
|
self.finished_header_rows.push(header_row_info);
|
||||||
|
@ -240,16 +240,17 @@ impl<'a> GridLayouter<'a> {
|
|||||||
self.current.initial_after_repeats = self.regions.size.y;
|
self.current.initial_after_repeats = self.regions.size.y;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(footer) = &self.grid.footer {
|
if !self.repeating_footers.is_empty() && skipped_region {
|
||||||
if footer.repeated && skipped_region {
|
// Simulate the footer again; the region's 'full' might have
|
||||||
// Simulate the footer again; the region's 'full' might have
|
// changed.
|
||||||
// changed.
|
let (footer_height, footer_heights) = self.simulate_footer_heights(
|
||||||
self.regions.size.y += self.current.footer_height;
|
self.repeating_footers.iter().map(|x| *x),
|
||||||
self.current.footer_height = self
|
engine,
|
||||||
.simulate_footer(footer, &self.regions, engine, disambiguator)?
|
disambiguator,
|
||||||
.height;
|
)?;
|
||||||
self.regions.size.y -= self.current.footer_height;
|
|
||||||
}
|
self.current.footer_height = footer_height;
|
||||||
|
self.current.repeating_footer_heights.extend(footer_heights);
|
||||||
}
|
}
|
||||||
|
|
||||||
let repeating_header_rows =
|
let repeating_header_rows =
|
||||||
@ -463,19 +464,89 @@ impl<'a> GridLayouter<'a> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn bump_repeating_footers(&mut self) -> &'a [Repeatable<Footer>] {
|
/// Place a footer we have reached through normal row layout.
|
||||||
let [next_footer, other_footers @ ..] = self.upcoming_sorted_footers else {
|
pub fn place_new_footer(
|
||||||
return &[];
|
&mut self,
|
||||||
};
|
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;
|
||||||
|
|
||||||
if !next_footer.repeated {
|
// Skip to fitting region where only this footer fits.
|
||||||
// TODO(subfooters): grouping and laying them out together?
|
while self.unbreakable_rows_left == 0
|
||||||
self.upcoming_sorted_footers = other_footers;
|
&& !self.regions.size.y.fits(footer_height)
|
||||||
return &[];
|
&& 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.start, footer.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 other_footers.is_empty() {
|
// If the next group of footers would conflict with other repeating
|
||||||
return std::mem::replace(&mut self.upcoming_sorted_footers, &[]);
|
// footers, wait for them to finish repeating before adding more to
|
||||||
|
// repeat.
|
||||||
|
if self.repeating_footers.is_empty()
|
||||||
|
|| self
|
||||||
|
.upcoming_sorted_footers
|
||||||
|
.first()
|
||||||
|
.is_none_or(|f| f.level >= footer.level)
|
||||||
|
{
|
||||||
|
self.prepare_next_repeating_footers(false, engine);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
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(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let first_conflicting_index = other_footers
|
let first_conflicting_index = other_footers
|
||||||
@ -488,77 +559,110 @@ impl<'a> GridLayouter<'a> {
|
|||||||
self.upcoming_sorted_footers.split_at(first_conflicting_index);
|
self.upcoming_sorted_footers.split_at(first_conflicting_index);
|
||||||
|
|
||||||
self.upcoming_sorted_footers = new_upcoming_footers;
|
self.upcoming_sorted_footers = new_upcoming_footers;
|
||||||
return next_repeating_footers;
|
self.prepare_repeating_footers(
|
||||||
|
next_repeating_footers.iter().map(Repeatable::deref),
|
||||||
|
first_footers,
|
||||||
|
engine,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Updates `self.footer_height` by simulating the footer, and skips to fitting region.
|
/// Updates `self.current.repeating_footer_height` by simulating repeating
|
||||||
pub fn prepare_footer(
|
/// footers, and skips to fitting region.
|
||||||
|
pub fn prepare_repeating_footers(
|
||||||
&mut self,
|
&mut self,
|
||||||
footer: &Footer,
|
footers: impl Iterator<Item = &'a Footer> + ExactSizeIterator + Clone,
|
||||||
|
at_region_top: bool,
|
||||||
engine: &mut Engine,
|
engine: &mut Engine,
|
||||||
disambiguator: usize,
|
disambiguator: usize,
|
||||||
) -> SourceResult<()> {
|
) -> SourceResult<()> {
|
||||||
let footer_height = self
|
let (mut expected_footer_height, mut expected_footer_heights) =
|
||||||
.simulate_footer(footer, &self.regions, engine, disambiguator)?
|
self.simulate_footer_heights(footers.clone(), engine, disambiguator)?;
|
||||||
.height;
|
|
||||||
|
// 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;
|
let mut skipped_region = false;
|
||||||
while self.unbreakable_rows_left == 0
|
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()
|
&& self.regions.may_progress()
|
||||||
{
|
{
|
||||||
// Advance regions without any output until we can place the
|
// Advance regions without any output until we can place the
|
||||||
// footer.
|
// footer.
|
||||||
self.finish_region_internal(
|
if at_region_top {
|
||||||
Frame::soft(Axes::splat(Abs::zero())),
|
self.finish_region_internal(
|
||||||
vec![],
|
Frame::soft(Axes::splat(Abs::zero())),
|
||||||
Default::default(),
|
vec![],
|
||||||
);
|
Default::default(),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
self.finish_region(engine, false)?;
|
||||||
|
}
|
||||||
skipped_region = true;
|
skipped_region = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(subfooters): Consider resetting header height etc. if we skip
|
if skipped_region {
|
||||||
// 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 {
|
|
||||||
// Simulate the footer again; the region's 'full' might have
|
// Simulate the footer again; the region's 'full' might have
|
||||||
// changed.
|
// changed, and the vector of heights was cleared.
|
||||||
self.simulate_footer(footer, &self.regions, engine, disambiguator)?
|
(expected_footer_height, expected_footer_heights) =
|
||||||
.height
|
self.simulate_footer_heights(footers, engine, disambiguator)?;
|
||||||
} else {
|
}
|
||||||
footer_height
|
|
||||||
};
|
self.current.footer_height += expected_footer_height;
|
||||||
|
self.current.repeating_footer_heights.extend(expected_footer_heights);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn simulate_footer_heights(
|
||||||
|
&self,
|
||||||
|
footers: impl Iterator<Item = &'a Footer> + ExactSizeIterator,
|
||||||
|
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, &self.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.
|
/// Lays out all rows in the footer.
|
||||||
/// They are unbreakable.
|
/// They are unbreakable.
|
||||||
pub fn layout_footer(
|
pub fn layout_footer(
|
||||||
&mut self,
|
&mut self,
|
||||||
footer: &Footer,
|
footer: &Footer,
|
||||||
|
as_short_lived: bool,
|
||||||
engine: &mut Engine,
|
engine: &mut Engine,
|
||||||
disambiguator: usize,
|
disambiguator: usize,
|
||||||
) -> SourceResult<()> {
|
) -> SourceResult<()> {
|
||||||
// Ensure footer rows have their own height available.
|
let footer_len = footer.end - footer.start;
|
||||||
// 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;
|
|
||||||
self.unbreakable_rows_left += footer_len;
|
self.unbreakable_rows_left += footer_len;
|
||||||
|
|
||||||
|
// TODO(subfooters): also consider omitted gutter before the footer
|
||||||
|
// when there is a header right before it taking it.
|
||||||
for y in footer.start..self.grid.rows.len() {
|
for y in footer.start..self.grid.rows.len() {
|
||||||
self.layout_row_with_state(
|
self.layout_row_with_state(
|
||||||
y,
|
y,
|
||||||
engine,
|
engine,
|
||||||
disambiguator,
|
disambiguator,
|
||||||
RowState {
|
RowState {
|
||||||
in_active_repeatable: repeats,
|
in_active_repeatable: !as_short_lived,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
@ -597,3 +701,11 @@ pub fn total_header_row_count<'h>(
|
|||||||
) -> usize {
|
) -> usize {
|
||||||
headers.into_iter().map(|h| h.range.end - h.range.start).sum()
|
headers.into_iter().map(|h| h.range.end - h.range.start).sum()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The total amount of rows in the given list of headers.
|
||||||
|
#[inline]
|
||||||
|
pub fn total_footer_row_count<'f>(
|
||||||
|
footers: impl IntoIterator<Item = &'f Footer>,
|
||||||
|
) -> usize {
|
||||||
|
footers.into_iter().map(|f| f.end - f.start).sum()
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user