use typst_library::diag::SourceResult; use typst_library::engine::Engine; use typst_library::foundations::Resolve; use typst_library::layout::grid::resolve::Repeatable; use typst_library::layout::{Abs, Axes, Frame, Point, Region, Regions, Size, Sizing}; use super::layouter::{in_last_with_offset, points, Row, RowPiece}; use super::{layout_cell, 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. /// /// This is the offset from the text direction start, meaning that, on RTL /// grids, this is the offset from the right of the grid, whereas, on LTR /// grids, it is the offset from the left. 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, /// 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, } /// 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, /// 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, /// 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 GridLayouter<'_> { /// 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 cell = self.grid.cell(x, y).unwrap(); let width = self.cell_spanned_width(cell, x); // In RTL cells expand to the left, thus the position // must additionally be offset by the cell's width. let dx = if self.is_rtl { self.width - (dx + width) } 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 = layout_cell(cell, 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) { let offsets = points(self.rcols.iter().copied()); for (x, dx) in (0..self.rcols.len()).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, regions: &Regions<'_>, engine: &mut Engine, disambiguator: usize, ) -> SourceResult { 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, 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 = 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::>() } 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::>() }; 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, cell: &Cell, parent_y: usize, rowspan: usize, unbreakable_rows_left: usize, measurement_data: &CellMeasurementData<'_>, ) -> bool { if sizes.len() <= 1 && sizes.first().is_none_or(|&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, pending_rowspans: &[(usize, usize, Vec)], 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 = 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, engine: &mut Engine, last_resolved_size: Option, unbreakable_rows_left: usize, mut disambiguator: usize, ) -> SourceResult { // 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::() - 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 { 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, 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; } } }