diff options
| author | Laurenz <laurmaedje@gmail.com> | 2024-10-27 19:04:55 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-10-27 18:04:55 +0000 |
| commit | be7cfc85d08c545abfac08098b7b33b4bd71f37e (patch) | |
| tree | f4137fa2aaa57babae1f7603a9b2ed7e688f43d8 /crates/typst-layout/src/grid/rowspans.rs | |
| parent | b8034a343831e8609aec2ec81eb7eeda57aa5d81 (diff) | |
Split out four new crates (#5302)
Diffstat (limited to 'crates/typst-layout/src/grid/rowspans.rs')
| -rw-r--r-- | crates/typst-layout/src/grid/rowspans.rs | 1217 |
1 files changed, 1217 insertions, 0 deletions
diff --git a/crates/typst-layout/src/grid/rowspans.rs b/crates/typst-layout/src/grid/rowspans.rs new file mode 100644 index 00000000..03b4103f --- /dev/null +++ b/crates/typst-layout/src/grid/rowspans.rs @@ -0,0 +1,1217 @@ +use typst_library::diag::SourceResult; +use typst_library::engine::Engine; +use typst_library::foundations::Resolve; +use typst_library::layout::{Abs, Axes, Frame, Point, Region, Regions, Size, Sizing}; +use typst_utils::MaybeReverseIter; + +use super::layouter::{in_last_with_offset, points, Row, RowPiece}; +use super::repeated::Repeatable; +use super::{Cell, GridLayouter}; + +/// All information needed to layout a single rowspan. +pub struct Rowspan { + /// First column of this rowspan. + pub x: usize, + /// First row of this rowspan. + pub y: usize, + /// The disambiguator for laying out the cells. + pub disambiguator: usize, + /// Amount of rows spanned by the cell at (x, y). + pub rowspan: usize, + /// Whether all rows of the rowspan are part of an unbreakable row group. + /// This is true e.g. in headers and footers, regardless of what the user + /// specified for the parent cell's `breakable` field. + pub is_effectively_unbreakable: bool, + /// The horizontal offset of this rowspan in all regions. + pub dx: Abs, + /// The vertical offset of this rowspan in the first region. + pub dy: Abs, + /// The index of the first region this rowspan appears in. + pub first_region: usize, + /// The full height in the first region this rowspan appears in, for + /// relative sizing. + pub region_full: Abs, + /// The vertical space available for this rowspan in each region. + pub heights: Vec<Abs>, + /// The index of the largest resolved spanned row so far. + /// Once a spanned row is resolved and its height added to `heights`, this + /// number is increased. Older rows, even if repeated through e.g. a + /// header, will no longer contribute height to this rowspan. + /// + /// This is `None` if no spanned rows were resolved in `finish_region` yet. + pub max_resolved_row: Option<usize>, +} + +/// The output of the simulation of an unbreakable row group. +#[derive(Default)] +pub struct UnbreakableRowGroup { + /// The rows in this group of unbreakable rows. + /// Includes their indices and their predicted heights. + pub rows: Vec<(usize, Abs)>, + /// The total height of this row group. + pub height: Abs, +} + +/// Data used to measure a cell in an auto row. +pub struct CellMeasurementData<'layouter> { + /// The available width for the cell across all regions. + pub width: Abs, + /// The available height for the cell in its first region. + /// Infinite when the auto row is unbreakable. + pub height: Abs, + /// The backlog of heights available for the cell in later regions. + /// + /// When this is `None`, the `custom_backlog` field should be used instead. + /// That's because, otherwise, this field would have to contain a reference + /// to the `custom_backlog` field, which isn't possible in Rust without + /// resorting to unsafe hacks. + pub backlog: Option<&'layouter [Abs]>, + /// If the backlog needs to be built from scratch instead of reusing the + /// one at the current region, which is the case of a multi-region rowspan + /// (needs to join its backlog of already laid out heights with the current + /// backlog), then this vector will store the new backlog. + pub custom_backlog: Vec<Abs>, + /// The full height of the first region of the cell. + /// Infinite when the auto row is unbreakable. + pub full: Abs, + /// The height of the last repeated region to use in the measurement pod, + /// if any. + pub last: Option<Abs>, + /// The total height of previous rows spanned by the cell in the current + /// region (so far). + pub height_in_this_region: Abs, + /// The amount of previous regions spanned by the cell. + /// They are skipped for measurement purposes. + pub frames_in_previous_regions: usize, +} + +impl<'a> GridLayouter<'a> { + /// Layout a rowspan over the already finished regions, plus the current + /// region's frame and resolved rows, if it wasn't finished yet (because + /// we're being called from `finish_region`, but note that this function is + /// also called once after all regions are finished, in which case + /// `current_region_data` is `None`). + /// + /// We need to do this only once we already know the heights of all + /// spanned rows, which is only possible after laying out the last row + /// spanned by the rowspan (or some row immediately after the last one). + pub fn layout_rowspan( + &mut self, + rowspan_data: Rowspan, + current_region_data: Option<(&mut Frame, &[RowPiece])>, + engine: &mut Engine, + ) -> SourceResult<()> { + let Rowspan { + x, + y, + disambiguator, + rowspan, + is_effectively_unbreakable, + dx, + dy, + first_region, + region_full, + heights, + .. + } = rowspan_data; + let [first_height, backlog @ ..] = heights.as_slice() else { + // Nothing to layout. + return Ok(()); + }; + let first_column = self.rcols[x]; + let cell = self.grid.cell(x, y).unwrap(); + let width = self.cell_spanned_width(cell, x); + let dx = if self.is_rtl { dx - width + first_column } else { dx }; + + // Prepare regions. + let size = Size::new(width, *first_height); + let mut pod: Regions = Region::new(size, Axes::splat(true)).into(); + pod.backlog = backlog; + + if !is_effectively_unbreakable + && self.grid.rows[y..][..rowspan] + .iter() + .any(|spanned_row| spanned_row == &Sizing::Auto) + { + // If the rowspan spans an auto row and is breakable, it will see + // '100%' as the full page height, at least at its first region. + // This is consistent with how it is measured, and with how + // non-rowspan cells behave in auto rows. + pod.full = region_full; + } + + // Push the layouted frames directly into the finished frames. + let fragment = cell.layout(engine, disambiguator, self.styles, pod)?; + let (current_region, current_rrows) = current_region_data.unzip(); + for ((i, finished), frame) in self + .finished + .iter_mut() + .chain(current_region.into_iter()) + .skip(first_region) + .enumerate() + .zip(fragment) + { + let dy = if i == 0 { + // At first, we draw the rowspan starting at its expected + // vertical offset in the first region. + dy + } else { + // The rowspan continuation starts after the header (thus, + // at a position after the sum of the laid out header + // rows). + if let Some(Repeatable::Repeated(header)) = &self.grid.header { + let header_rows = self + .rrows + .get(i) + .map(Vec::as_slice) + .or(current_rrows) + .unwrap_or(&[]) + .iter() + .take_while(|row| row.y < header.end); + + header_rows.map(|row| row.height).sum() + } else { + // Without a header, start at the very top of the region. + Abs::zero() + } + }; + + finished.push_frame(Point::new(dx, dy), frame); + } + + Ok(()) + } + + /// Checks if a row contains the beginning of one or more rowspan cells. + /// If so, adds them to the rowspans vector. + pub fn check_for_rowspans(&mut self, disambiguator: usize, y: usize) { + // We will compute the horizontal offset of each rowspan in advance. + // For that reason, we must reverse the column order when using RTL. + let offsets = points(self.rcols.iter().copied().rev_if(self.is_rtl)); + for (x, dx) in (0..self.rcols.len()).rev_if(self.is_rtl).zip(offsets) { + let Some(cell) = self.grid.cell(x, y) else { + continue; + }; + let rowspan = self.grid.effective_rowspan_of_cell(cell); + if rowspan > 1 { + // Rowspan detected. We will lay it out later. + self.rowspans.push(Rowspan { + x, + y, + disambiguator, + rowspan, + // The field below will be updated in + // 'check_for_unbreakable_rows'. + is_effectively_unbreakable: !cell.breakable, + dx, + // The four fields below will be updated in 'finish_region'. + dy: Abs::zero(), + first_region: usize::MAX, + region_full: Abs::zero(), + heights: vec![], + max_resolved_row: None, + }); + } + } + } + + /// Checks if the upcoming rows will be grouped together under an + /// unbreakable row group, and, if so, advances regions until there is + /// enough space for them. This can be needed, for example, if there's an + /// unbreakable rowspan crossing those rows. + pub fn check_for_unbreakable_rows( + &mut self, + current_row: usize, + 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(Repeatable::NotRepeated(header)) = &self.grid.header { + if current_row < header.end { + // Non-repeated header, so keep it unbreakable. + amount_unbreakable_rows = Some(header.end); + } + } + if let Some(Repeatable::NotRepeated(footer)) = &self.grid.footer { + if current_row >= footer.start { + // Non-repeated footer, so keep it unbreakable. + amount_unbreakable_rows = Some(self.grid.rows.len() - footer.start); + } + } + + let row_group = self.simulate_unbreakable_row_group( + current_row, + amount_unbreakable_rows, + &self.regions, + engine, + 0, + )?; + + // Skip to fitting region. + while !self.regions.size.y.fits(row_group.height) + && !in_last_with_offset( + self.regions, + self.header_height + self.footer_height, + ) + { + self.finish_region(engine, false)?; + } + + // Update unbreakable rows left. + self.unbreakable_rows_left = row_group.rows.len(); + } + + if self.unbreakable_rows_left > 1 { + // Mark rowspans as effectively unbreakable where applicable + // (if all of their spanned rows would be in the same unbreakable + // row group). + // Not needed if only one unbreakable row is left, since, then, + // no rowspan will be effectively unbreakable, at least necessarily. + // Note that this function is called after 'check_for_rowspans' and + // potentially updates the amount of remaining unbreakable rows, so + // it wouldn't be accurate to only check for this condition in that + // function. We need to check here instead. + for rowspan_data in + self.rowspans.iter_mut().filter(|rowspan| rowspan.y == current_row) + { + rowspan_data.is_effectively_unbreakable |= + self.unbreakable_rows_left >= rowspan_data.rowspan; + } + } + + Ok(()) + } + + /// Simulates a group of unbreakable rows, starting with the index of the + /// first row in the group. If `amount_unbreakable_rows` is `None`, keeps + /// adding rows to the group until none have unbreakable cells in common. + /// Otherwise, adds specifically the given amount of rows to the group. + /// + /// This is used to figure out how much height the next unbreakable row + /// group (if any) needs. + pub fn simulate_unbreakable_row_group( + &self, + first_row: usize, + amount_unbreakable_rows: Option<usize>, + regions: &Regions<'_>, + engine: &mut Engine, + disambiguator: usize, + ) -> SourceResult<UnbreakableRowGroup> { + let mut row_group = UnbreakableRowGroup::default(); + let mut unbreakable_rows_left = amount_unbreakable_rows.unwrap_or(0); + for (y, row) in self.grid.rows.iter().enumerate().skip(first_row) { + if amount_unbreakable_rows.is_none() { + // When we don't set a fixed amount of unbreakable rows, + // determine the amount based on the rowspan of unbreakable + // cells in rows. + let additional_unbreakable_rows = self.check_for_unbreakable_cells(y); + unbreakable_rows_left = + unbreakable_rows_left.max(additional_unbreakable_rows); + } + if unbreakable_rows_left == 0 { + // This check is in case the first row does not have any + // unbreakable cells. Therefore, no unbreakable row group + // is formed. + break; + } + let height = match row { + Sizing::Rel(v) => v.resolve(self.styles).relative_to(regions.base().y), + + // No need to pass the regions to the auto row, since + // unbreakable auto rows are always measured with infinite + // height, ignore backlog, and do not invoke the rowspan + // simulation procedure at all. + Sizing::Auto => self + .measure_auto_row( + engine, + disambiguator, + y, + false, + unbreakable_rows_left, + Some(&row_group), + )? + .unwrap() + .first() + .copied() + .unwrap_or_else(Abs::zero), + // Fractional rows don't matter when calculating the space + // needed for unbreakable rows + Sizing::Fr(_) => Abs::zero(), + }; + row_group.height += height; + row_group.rows.push((y, height)); + unbreakable_rows_left -= 1; + if unbreakable_rows_left == 0 { + // This second check is necessary so we can tell distinct + // but consecutive unbreakable row groups apart. If the + // unbreakable row group ended at this row, we stop before + // checking the next one. + break; + } + } + + Ok(row_group) + } + + /// Checks if one or more of the cells at the given row are unbreakable. + /// If so, returns the largest rowspan among the unbreakable cells; + /// the spanned rows must, as a result, be laid out in the same region. + pub fn check_for_unbreakable_cells(&self, y: usize) -> usize { + (0..self.grid.cols.len()) + .filter_map(|x| self.grid.cell(x, y)) + .filter(|cell| !cell.breakable) + .map(|cell| self.grid.effective_rowspan_of_cell(cell)) + .max() + .unwrap_or(0) + } + + /// Used by `measure_auto_row` to gather data needed to measure the cell. + pub fn prepare_auto_row_cell_measurement( + &self, + parent: Axes<usize>, + cell: &Cell, + breakable: bool, + row_group_data: Option<&UnbreakableRowGroup>, + ) -> CellMeasurementData<'_> { + let rowspan = self.grid.effective_rowspan_of_cell(cell); + + // This variable is used to construct a custom backlog if the cell + // is a rowspan, or if headers or footers are used. When measuring, we + // join the heights from previous regions to the current backlog to + // form a rowspan's expected backlog. We also subtract the header's + // and footer's heights from all regions. + let mut custom_backlog: Vec<Abs> = vec![]; + + // This function is used to subtract the expected header and footer + // height from each upcoming region size in the current backlog and + // last region. + let mut subtract_header_footer_height_from_regions = || { + // Only breakable auto rows need to update their backlogs based + // on the presence of a header or footer, given that unbreakable + // auto rows don't depend on the backlog, as they only span one + // region. + if breakable + && (matches!(self.grid.header, Some(Repeatable::Repeated(_))) + || matches!(self.grid.footer, Some(Repeatable::Repeated(_)))) + { + // Subtract header and footer height from all upcoming regions + // when measuring the cell, including the last repeated region. + // + // This will update the 'custom_backlog' vector with the + // updated heights of the upcoming regions. + let mapped_regions = self.regions.map(&mut custom_backlog, |size| { + Size::new(size.x, size.y - self.header_height - self.footer_height) + }); + + // Callees must use the custom backlog instead of the current + // backlog, so we return 'None'. + return (None, mapped_regions.last); + } + + // No need to change the backlog or last region. + (Some(self.regions.backlog), self.regions.last) + }; + + // Each declaration, from top to bottom: + // 1. The height available to the cell in the first region. + // Usually, this will just be the size remaining in the current + // region. + // 2. The backlog of upcoming region heights to specify as + // available to the cell. + // 3. The full height of the first region of the cell. + // 4. Height of the last repeated region to use in the measurement pod. + // 5. The total height of the cell covered by previously spanned + // rows in this region. This is used by rowspans to be able to tell + // how much the auto row needs to expand. + // 6. The amount of frames laid out by this cell in previous + // regions. When the cell isn't a rowspan, this is always zero. + // These frames are skipped after measuring. + let height; + let backlog; + let full; + let last; + let height_in_this_region; + let frames_in_previous_regions; + + if rowspan == 1 { + // Not a rowspan, so the cell only occupies this row. Therefore: + // 1. When we measure the cell below, use the available height + // remaining in the region as the height it has available. + // However, if the auto row is unbreakable, measure with infinite + // height instead to see how much content expands. + // 2. Use the region's backlog and last region when measuring, + // however subtract the expected header and footer heights from + // each upcoming size, if there is a header or footer. + // 3. Use the same full region height. + // 4. No height occupied by this cell in this region so far. + // 5. Yes, this cell started in this region. + height = if breakable { self.regions.size.y } else { Abs::inf() }; + (backlog, last) = subtract_header_footer_height_from_regions(); + full = if breakable { self.regions.full } else { Abs::inf() }; + height_in_this_region = Abs::zero(); + frames_in_previous_regions = 0; + } else { + // Height of the rowspan covered by spanned rows in the current + // region. + let laid_out_height: Abs = self + .lrows + .iter() + .filter_map(|row| match row { + Row::Frame(frame, y, _) + if (parent.y..parent.y + rowspan).contains(y) => + { + Some(frame.height()) + } + // Either we have a row outside of the rowspan, or a + // fractional row, whose size we can't really guess. + _ => None, + }) + .sum(); + + // If we're currently simulating an unbreakable row group, also + // consider the height of previously spanned rows which are in + // the row group but not yet laid out. + let unbreakable_height: Abs = row_group_data + .into_iter() + .flat_map(|row_group| &row_group.rows) + .filter(|(y, _)| (parent.y..parent.y + rowspan).contains(y)) + .map(|(_, height)| height) + .sum(); + + height_in_this_region = laid_out_height + unbreakable_height; + + // Ensure we will measure the rowspan with the correct heights. + // For that, we will gather the total height spanned by this + // rowspan in previous regions. + if let Some((rowspan_full, [rowspan_height, rowspan_other_heights @ ..])) = + self.rowspans + .iter() + .find(|data| data.x == parent.x && data.y == parent.y) + .map(|data| (data.region_full, &*data.heights)) + { + // The rowspan started in a previous region (as it already + // has at least one region height). + // Therefore, its initial height will be the height in its + // first spanned region, and the backlog will be the + // remaining heights, plus the current region's size, plus + // the current backlog. + frames_in_previous_regions = rowspan_other_heights.len() + 1; + + let heights_up_to_current_region = rowspan_other_heights + .iter() + .copied() + .chain(std::iter::once(if breakable { + self.initial.y - self.header_height - self.footer_height + } else { + // When measuring unbreakable auto rows, infinite + // height is available for content to expand. + Abs::inf() + })); + + custom_backlog = if breakable { + // This auto row is breakable. Therefore, join the + // rowspan's already laid out heights with the current + // region's height and current backlog to ensure a good + // level of accuracy in the measurements. + let backlog = self + .regions + .backlog + .iter() + .map(|&size| size - self.header_height - self.footer_height); + + heights_up_to_current_region.chain(backlog).collect::<Vec<_>>() + } else { + // No extra backlog if this is an unbreakable auto row. + // Ensure, when measuring, that the rowspan can be laid + // out through all spanned rows which were already laid + // out so far, but don't go further than this region. + heights_up_to_current_region.collect::<Vec<_>>() + }; + + height = *rowspan_height; + backlog = None; + full = rowspan_full; + last = self + .regions + .last + .map(|size| size - self.header_height - self.footer_height); + } else { + // The rowspan started in the current region, as its vector + // of heights in regions is currently empty. + // Therefore, the initial height it has available will be + // the current available size, plus the size spanned in + // previous rows in this region (and/or unbreakable row + // group, if it's being simulated). + // The backlog and full will be that of the current region. + // However, use infinite height instead if we're measuring an + // unbreakable auto row. + height = if breakable { + height_in_this_region + self.regions.size.y + } else { + Abs::inf() + }; + (backlog, last) = subtract_header_footer_height_from_regions(); + full = if breakable { self.regions.full } else { Abs::inf() }; + frames_in_previous_regions = 0; + } + } + + let width = self.cell_spanned_width(cell, parent.x); + CellMeasurementData { + width, + height, + backlog, + custom_backlog, + full, + last, + height_in_this_region, + frames_in_previous_regions, + } + } + + /// Used in `measure_auto_row` to prepare a rowspan's `sizes` vector. + /// Returns `true` if we'll need to run a simulation to more accurately + /// expand the auto row based on the rowspan's demanded size, or `false` + /// otherwise. + #[allow(clippy::too_many_arguments)] + pub fn prepare_rowspan_sizes( + &self, + auto_row_y: usize, + sizes: &mut Vec<Abs>, + cell: &Cell, + parent_y: usize, + rowspan: usize, + unbreakable_rows_left: usize, + measurement_data: &CellMeasurementData<'_>, + ) -> bool { + if sizes.len() <= 1 + && sizes.first().map_or(true, |&first_frame_size| { + first_frame_size <= measurement_data.height_in_this_region + }) + { + // Ignore a rowspan fully covered by rows in previous + // regions and/or in the current region. + sizes.clear(); + return false; + } + if let Some(first_frame_size) = sizes.first_mut() { + // Subtract already covered height from the size requested + // by this rowspan to the auto row in the first region. + *first_frame_size = (*first_frame_size + - measurement_data.height_in_this_region) + .max(Abs::zero()); + } + + let last_spanned_row = parent_y + rowspan - 1; + + // When the rowspan is unbreakable, or all of its upcoming + // spanned rows are in the same unbreakable row group, its + // spanned gutter will certainly be in the same region as all + // of its other spanned rows, thus gutters won't be removed, + // and we can safely reduce how much the auto row expands by + // without using simulation. + let is_effectively_unbreakable_rowspan = + !cell.breakable || auto_row_y + unbreakable_rows_left > last_spanned_row; + + // If the rowspan doesn't end at this row and the grid has + // gutter, we will need to run a simulation to find out how + // much to expand this row by later. This is because gutters + // spanned by this rowspan might be removed if they appear + // around a pagebreak, so the auto row might have to expand a + // bit more to compensate for the missing gutter height. + // However, unbreakable rowspans aren't affected by that + // problem. + if auto_row_y != last_spanned_row + && !sizes.is_empty() + && self.grid.has_gutter + && !is_effectively_unbreakable_rowspan + { + return true; + } + + // We can only predict the resolved size of upcoming fixed-size + // rows, but not fractional rows. In the future, we might be + // able to simulate and circumvent the problem with fractional + // rows. Relative rows are currently always measured relative + // to the first region as well. + // We can ignore auto rows since this is the last spanned auto + // row. + let will_be_covered_height: Abs = self + .grid + .rows + .iter() + .skip(auto_row_y + 1) + .take(last_spanned_row - auto_row_y) + .map(|row| match row { + Sizing::Rel(v) => { + v.resolve(self.styles).relative_to(self.regions.base().y) + } + _ => Abs::zero(), + }) + .sum(); + + // Remove or reduce the sizes of the rowspan at the current or future + // regions where it will already be covered by further rows spanned by + // it. + subtract_end_sizes(sizes, will_be_covered_height); + + // No need to run a simulation for this rowspan. + false + } + + /// Performs a simulation to predict by how much height the last spanned + /// auto row will have to expand, given the current sizes of the auto row + /// in each region and the pending rowspans' data (parent Y, rowspan amount + /// and vector of requested sizes). + #[allow(clippy::too_many_arguments)] + pub fn simulate_and_measure_rowspans_in_auto_row( + &self, + y: usize, + resolved: &mut Vec<Abs>, + pending_rowspans: &[(usize, usize, Vec<Abs>)], + unbreakable_rows_left: usize, + row_group_data: Option<&UnbreakableRowGroup>, + mut disambiguator: usize, + engine: &mut Engine, + ) -> SourceResult<()> { + // To begin our simulation, we have to unify the sizes demanded by + // each rowspan into one simple vector of sizes, as if they were + // all a single rowspan. These sizes will be appended to + // 'resolved' once we finish our simulation. + let mut simulated_sizes: Vec<Abs> = vec![]; + let last_resolved_size = resolved.last().copied(); + let mut max_spanned_row = y; + for (parent_y, rowspan, sizes) in pending_rowspans { + let mut sizes = sizes.iter(); + for (target, size) in resolved.iter_mut().zip(&mut sizes) { + // First, we update the already resolved sizes as required + // by this rowspan. No need to simulate this since the auto row + // will already expand throughout already resolved regions. + // Our simulation, therefore, won't otherwise change already + // resolved sizes, other than, perhaps, the last one (at the + // last currently resolved region, at which we can expand). + target.set_max(*size); + } + for (simulated_target, rowspan_size) in + simulated_sizes.iter_mut().zip(&mut sizes) + { + // The remaining sizes are exclusive to rowspans, since + // other cells in this row didn't require as many regions. + // We will perform a simulation to see how much of these sizes + // does the auto row actually need to expand by, and how much + // is already covered by upcoming rows spanned by the rowspans. + simulated_target.set_max(*rowspan_size); + } + simulated_sizes.extend(sizes); + max_spanned_row = max_spanned_row.max(parent_y + rowspan - 1); + } + if simulated_sizes.is_empty() && resolved.last() == last_resolved_size.as_ref() { + // The rowspans already fit in the already resolved sizes. + // No need for simulation. + return Ok(()); + } + + // We will be updating the last resolved size (expanding the auto + // row) as needed. Therefore, consider it as part of the simulation. + // At the end, we push it back. + if let Some(modified_last_resolved_size) = resolved.pop() { + simulated_sizes.insert(0, modified_last_resolved_size); + } + + // Prepare regions for simulation. + // If we're currently inside an unbreakable row group simulation, + // subtract the current row group height from the available space + // when simulating rowspans in said group. + let mut simulated_regions = self.regions; + simulated_regions.size.y -= + row_group_data.map_or(Abs::zero(), |row_group| row_group.height); + + for _ in 0..resolved.len() { + // Ensure we start at the region where we will expand the auto + // row. + // Note that we won't accidentally call '.next()' once more than + // desired (we won't skip the last resolved frame, where we will + // expand) because we popped the last resolved size from the + // resolved vector, above. + simulated_regions.next(); + disambiguator += 1; + + // Subtract the initial header and footer height, since that's the + // height we used when subtracting from the region backlog's + // heights while measuring cells. + simulated_regions.size.y -= self.header_height + self.footer_height; + } + + if let Some(original_last_resolved_size) = last_resolved_size { + // We're now at the (current) last region of this auto row. + // Consider resolved height as already taken space. + simulated_regions.size.y -= original_last_resolved_size; + } + + // Now we run the simulation to check how much the auto row needs to + // grow to ensure that rowspans have the height they need. + let simulations_stabilized = self.run_rowspan_simulation( + y, + max_spanned_row, + simulated_regions, + &mut simulated_sizes, + engine, + last_resolved_size, + unbreakable_rows_left, + disambiguator, + )?; + + if !simulations_stabilized { + // If the simulation didn't stabilize above, we will just pretend + // all gutters were removed, as a best effort. That means the auto + // row will expand more than it normally should, but there isn't + // much we can do. + let will_be_covered_height = self + .grid + .rows + .iter() + .enumerate() + .skip(y + 1) + .take(max_spanned_row - y) + .filter(|(y, _)| !self.grid.is_gutter_track(*y)) + .map(|(_, row)| match row { + Sizing::Rel(v) => { + v.resolve(self.styles).relative_to(self.regions.base().y) + } + _ => Abs::zero(), + }) + .sum(); + + subtract_end_sizes(&mut simulated_sizes, will_be_covered_height); + } + + resolved.extend(simulated_sizes); + + Ok(()) + } + + /// Performs a simulation of laying out multiple rowspans (consolidated + /// into a single vector of simulated sizes) ending in a certain auto row + /// in order to find out how much the auto row will need to expand to cover + /// the rowspans' requested sizes, considering how much size has been + /// covered by other rows and by gutter between rows. + /// + /// For example, for a rowspan cell containing a block of 8pt of height + /// spanning rows (1pt, auto, 0.5pt, 0.5pt), with a gutter of 1pt between + /// each row, we have that the rows it spans provide 1pt + 0.5pt + 0.5pt + /// = 2pt of height, plus 1pt + 1pt + 1pt = 3pt of gutter, with a total of + /// 2pt + 3pt = 5pt of height already covered by fixed-size rows and + /// gutters. This means that the auto row must (under normal conditions) + /// expand by 3pt (8pt - 5pt) so that the rowspan has enough height across + /// rows to fully draw its contents. + /// + /// However, it's possible that the last row is sent to the next page to + /// respect a pagebreak, and then the 1pt gutter before it disappears. This + /// would lead to our rowspan having a height of 7pt available if we fail + /// to predict this situation when measuring the auto row. + /// + /// The algorithm below will, thus, attempt to simulate the layout of each + /// spanned row, considering the space available in the current page and in + /// upcoming pages (through the region backlog), in order to predict which + /// rows will be sent to a new page and thus have their preceding gutter + /// spacing removed (meaning the auto row has to grow a bit more). After + /// simulating, we subtract the total height spanned by upcoming rows and + /// gutter from the total rowspan height - this will be how much our auto + /// row has to expand. We then simulate again to check if, if the auto row + /// expanded by that amount, that would prompt the auto row to need to + /// expand even more, because expanding the auto row might cause some other + /// larger gutter spacing to disappear (leading to the rowspan having less + /// space available instead of more); if so, we update the amount to expand + /// and run the simulation again. Otherwise (if it should expand by the + /// same amount, meaning we predicted correctly, or by less, meaning the + /// auto row will be a bit larger than it should be, but that's a + /// compromise we're willing to accept), we conclude the simulation + /// (consider it stabilized) and return the result. + /// + /// Tries up to 5 times. If two consecutive simulations stabilize, then + /// we subtract the predicted expansion height ('amount_to_grow') from the + /// total height requested by rowspans (the 'requested_rowspan_height') to + /// obtain how much height is covered by upcoming rows, according to our + /// simulation, and the result of that operation is used to reduce or + /// remove heights from the end of the vector of simulated sizes, such that + /// the remaining heights are exactly how much the auto row should expand + /// by. Then, we return `true`. + /// + /// If the simulations don't stabilize (they return 5 different and + /// successively larger values), aborts and returns `false`. + #[allow(clippy::too_many_arguments)] + fn run_rowspan_simulation( + &self, + y: usize, + max_spanned_row: usize, + mut simulated_regions: Regions<'_>, + simulated_sizes: &mut Vec<Abs>, + engine: &mut Engine, + last_resolved_size: Option<Abs>, + unbreakable_rows_left: usize, + mut disambiguator: usize, + ) -> SourceResult<bool> { + // The max amount this row can expand will be the total size requested + // by rowspans which was not yet resolved. It is worth noting that, + // earlier, we pushed the last resolved size to 'simulated_sizes' as + // row expansion starts with it, so it's possible a rowspan requested + // to extend that size (we will see, through the simulation, if that's + // needed); however, we must subtract that resolved size from the total + // sum of sizes, as it was already resolved and thus the auto row will + // already grow by at least that much in the last resolved region (we + // would grow by the same size twice otherwise). + let requested_rowspan_height = + simulated_sizes.iter().sum::<Abs>() - last_resolved_size.unwrap_or_default(); + + // The amount the row will effectively grow by, according to the latest + // simulation. + let mut amount_to_grow = Abs::zero(); + + // Try to simulate up to 5 times. If it doesn't stabilize at a value + // which, when used and combined with upcoming spanned rows, covers all + // of the requested rowspan height, we give up. + for _attempt in 0..5 { + let rowspan_simulator = RowspanSimulator::new( + disambiguator, + simulated_regions, + self.header_height, + self.footer_height, + ); + + let total_spanned_height = rowspan_simulator.simulate_rowspan_layout( + y, + max_spanned_row, + amount_to_grow, + requested_rowspan_height, + unbreakable_rows_left, + self, + engine, + )?; + + // If the total height spanned by upcoming spanned rows plus the + // current amount we predict the auto row will have to grow (from + // the previous iteration) are larger than the size requested by + // rowspans, this means the auto row will grow enough in order to + // cover the requested rowspan height, so we stop the simulation. + // + // If that's not yet the case, we will simulate again and make the + // auto row grow even more, and do so until either the auto row has + // grown enough, or we tried to do so over 5 times. + // + // A flaw of this approach is that we consider rowspans' content to + // be contiguous. That is, we treat rowspans' requested heights as + // a simple number, instead of properly using the vector of + // requested heights in each region. This can lead to some + // weirdness when using multi-page rowspans with content that + // reacts to the amount of space available, including paragraphs. + // However, this is probably the best we can do for now. + if (total_spanned_height + amount_to_grow).fits(requested_rowspan_height) { + // Reduce sizes by the amount to be covered by upcoming spanned + // rows, which is equivalent to the amount that we don't grow. + // We reduce from the end as that's where the spanned rows will + // cover. The remaining sizes will all be covered by the auto + // row instead (which will grow by those sizes). + subtract_end_sizes( + simulated_sizes, + requested_rowspan_height - amount_to_grow, + ); + + if let Some(last_resolved_size) = last_resolved_size { + // Ensure the first simulated size is at least as large as + // the last resolved size (its initial value). As it was + // already resolved before, we must not reduce below the + // resolved size to avoid problems with non-rowspan cells. + if let Some(first_simulated_size) = simulated_sizes.first_mut() { + first_simulated_size.set_max(last_resolved_size); + } else { + simulated_sizes.push(last_resolved_size); + } + } + + return Ok(true); + } + + // For the next simulation, we will test if the auto row can grow + // by precisely how much rowspan height is not covered by upcoming + // spanned rows, according to the current simulation. + // We know that the new amount to grow is larger (and thus the + // auto row only expands between each simulation), because we + // checked above if + // 'total_spanned_height + (now old_)amount_to_grow >= requested_rowspan_height', + // which was false, so it holds that + // 'total_spanned_height + old_amount_to_grow < requested_rowspan_height' + // Thus, + // 'old_amount_to_grow < requested_rowspan_height - total_spanned_height' + // Therefore, by definition, 'old_amount_to_grow < amount_to_grow'. + let old_amount_to_grow = std::mem::replace( + &mut amount_to_grow, + requested_rowspan_height - total_spanned_height, + ); + + // We advance the 'regions' variable accordingly, so that, in the + // next simulation, we consider already grown space as final. + // That is, we effectively simulate how rows would be placed if the + // auto row grew by precisely the new value of 'amount_to_grow'. + let mut extra_amount_to_grow = amount_to_grow - old_amount_to_grow; + while extra_amount_to_grow > Abs::zero() + && simulated_regions.size.y < extra_amount_to_grow + { + extra_amount_to_grow -= simulated_regions.size.y.max(Abs::zero()); + simulated_regions.next(); + simulated_regions.size.y -= self.header_height + self.footer_height; + disambiguator += 1; + } + simulated_regions.size.y -= extra_amount_to_grow; + } + + // Simulation didn't succeed in 5 attempts. + Ok(false) + } +} + +/// Auxiliary structure holding state during rowspan simulation. +struct RowspanSimulator<'a> { + /// The number of finished regions. + finished: usize, + /// The state of regions during the simulation. + regions: Regions<'a>, + /// The height of the header in the currently simulated region. + header_height: Abs, + /// The height of the footer in the currently simulated region. + footer_height: Abs, + /// The total spanned height so far in the simulation. + total_spanned_height: Abs, + /// Height of the latest spanned gutter row in the simulation. + /// Zero if it was removed. + latest_spanned_gutter_height: Abs, +} + +impl<'a> RowspanSimulator<'a> { + /// Creates new rowspan simulation state with the given regions and initial + /// header and footer heights. Other fields should always start as zero. + fn new( + finished: usize, + regions: Regions<'a>, + header_height: Abs, + footer_height: Abs, + ) -> Self { + Self { + finished, + regions, + header_height, + footer_height, + total_spanned_height: Abs::zero(), + latest_spanned_gutter_height: Abs::zero(), + } + } + + /// Calculates the total spanned height of the rowspan. + /// Stops calculating if, at any point in the simulation, the value of + /// `total_spanned_height + amount_to_grow` becomes larger than + /// `requested_rowspan_height`, as the results are not going to become any + /// more useful after that point. + #[allow(clippy::too_many_arguments)] + fn simulate_rowspan_layout( + mut self, + y: usize, + max_spanned_row: usize, + amount_to_grow: Abs, + requested_rowspan_height: Abs, + mut unbreakable_rows_left: usize, + layouter: &GridLayouter<'_>, + engine: &mut Engine, + ) -> SourceResult<Abs> { + let spanned_rows = &layouter.grid.rows[y + 1..=max_spanned_row]; + for (offset, row) in spanned_rows.iter().enumerate() { + if (self.total_spanned_height + amount_to_grow).fits(requested_rowspan_height) + { + // Stop the simulation, as the combination of upcoming + // spanned rows (so far) and the current amount the auto + // row expands by has already fully covered the height the + // rowspans need. + return Ok(self.total_spanned_height); + } + let spanned_y = y + 1 + offset; + let is_gutter = layouter.grid.is_gutter_track(spanned_y); + + if unbreakable_rows_left == 0 { + // Simulate unbreakable row groups, and skip regions until + // they fit. There is no risk of infinite recursion, as + // no auto rows participate in the simulation, so the + // unbreakable row group simulator won't recursively call + // 'measure_auto_row' or (consequently) this function. + let row_group = layouter.simulate_unbreakable_row_group( + spanned_y, + None, + &self.regions, + engine, + 0, + )?; + while !self.regions.size.y.fits(row_group.height) + && !in_last_with_offset( + self.regions, + self.header_height + self.footer_height, + ) + { + self.finish_region(layouter, engine)?; + } + + unbreakable_rows_left = row_group.rows.len(); + } + + match row { + // Fixed-size spanned rows are what we are interested in. + // They contribute a fixed amount of height to our rowspan. + Sizing::Rel(v) => { + let height = + v.resolve(layouter.styles).relative_to(self.regions.base().y); + self.total_spanned_height += height; + if is_gutter { + self.latest_spanned_gutter_height = height; + } + + let mut skipped_region = false; + while unbreakable_rows_left == 0 + && !self.regions.size.y.fits(height) + && !in_last_with_offset( + self.regions, + self.header_height + self.footer_height, + ) + { + self.finish_region(layouter, engine)?; + + skipped_region = true; + } + + if !skipped_region || !is_gutter { + // No gutter at the top of a new region, so don't + // account for it if we just skipped a region. + self.regions.size.y -= height; + } + } + Sizing::Auto => { + // We only simulate for rowspans which end at the + // current auto row. Therefore, there won't be any + // further auto rows. + unreachable!(); + } + // For now, we ignore fractional rows on simulation. + Sizing::Fr(_) if is_gutter => { + self.latest_spanned_gutter_height = Abs::zero(); + } + Sizing::Fr(_) => {} + } + + unbreakable_rows_left = unbreakable_rows_left.saturating_sub(1); + } + + Ok(self.total_spanned_height) + } + + fn simulate_header_footer_layout( + &mut self, + layouter: &GridLayouter<'_>, + engine: &mut Engine, + ) -> SourceResult<()> { + // We can't just use the initial header/footer height on each region, + // because header/footer height might vary depending on region size if + // it contains rows with relative lengths. Therefore, we re-simulate + // headers and footers on each new region. + // It's true that, when measuring cells, we reduce each height in the + // backlog to consider the initial header and footer heights; however, + // our simulation checks what happens AFTER the auto row, so we can + // just use the original backlog from `self.regions`. + let disambiguator = self.finished; + let header_height = + if let Some(Repeatable::Repeated(header)) = &layouter.grid.header { + layouter + .simulate_header(header, &self.regions, engine, disambiguator)? + .height + } else { + Abs::zero() + }; + + let footer_height = + if let Some(Repeatable::Repeated(footer)) = &layouter.grid.footer { + layouter + .simulate_footer(footer, &self.regions, engine, disambiguator)? + .height + } else { + Abs::zero() + }; + + let mut skipped_region = false; + + // Skip until we reach a fitting region for both header and footer. + while !self.regions.size.y.fits(header_height + footer_height) + && self.regions.may_progress() + { + self.regions.next(); + self.finished += 1; + skipped_region = true; + } + + if let Some(Repeatable::Repeated(header)) = &layouter.grid.header { + self.header_height = if skipped_region { + // Simulate headers again, at the new region, as + // the full region height may change. + layouter + .simulate_header(header, &self.regions, engine, disambiguator)? + .height + } else { + header_height + }; + } + + if let Some(Repeatable::Repeated(footer)) = &layouter.grid.footer { + 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 + } else { + footer_height + }; + } + + // Consume the header's and footer's heights from the new region, + // but don't consider them spanned. The rowspan does not go over the + // header or footer (as an invariant, any rowspans spanning any header + // or footer rows are fully contained within that header's or footer's rows). + self.regions.size.y -= self.header_height + self.footer_height; + + Ok(()) + } + + fn finish_region( + &mut self, + layouter: &GridLayouter<'_>, + engine: &mut Engine, + ) -> SourceResult<()> { + // If a row was pushed to the next region, the immediately + // preceding gutter row is removed. + self.total_spanned_height -= self.latest_spanned_gutter_height; + self.latest_spanned_gutter_height = Abs::zero(); + self.regions.next(); + self.finished += 1; + + self.simulate_header_footer_layout(layouter, engine) + } +} + +/// Subtracts some size from the end of a vector of sizes. +/// For example, subtracting 5pt from \[2pt, 1pt, 3pt\] will result in \[1pt\]. +fn subtract_end_sizes(sizes: &mut Vec<Abs>, mut subtract: Abs) { + while subtract > Abs::zero() && sizes.last().is_some_and(|&size| size <= subtract) { + subtract -= sizes.pop().unwrap(); + } + if subtract > Abs::zero() { + if let Some(last_size) = sizes.last_mut() { + *last_size -= subtract; + } + } +} |
