2990 lines
122 KiB
Rust
2990 lines
122 KiB
Rust
use std::fmt::Debug;
|
|
use std::hash::Hash;
|
|
use std::num::NonZeroUsize;
|
|
use std::sync::Arc;
|
|
|
|
use ecow::eco_format;
|
|
|
|
use super::lines::{
|
|
generate_line_segments, hline_stroke_at_column, vline_stroke_at_row, Line,
|
|
LinePosition, LineSegment,
|
|
};
|
|
use super::rowspans::{Rowspan, UnbreakableRowGroup};
|
|
use crate::diag::{
|
|
bail, At, Hint, HintedStrResult, HintedString, SourceResult, StrResult,
|
|
};
|
|
use crate::engine::Engine;
|
|
use crate::foundations::{
|
|
Array, CastInfo, Content, Context, Fold, FromValue, Func, IntoValue, Reflect,
|
|
Resolve, Smart, StyleChain, Value,
|
|
};
|
|
use crate::layout::{
|
|
Abs, Alignment, Axes, Dir, Fr, Fragment, Frame, FrameItem, LayoutMultiple, Length,
|
|
Point, Regions, Rel, Sides, Size, Sizing,
|
|
};
|
|
use crate::syntax::Span;
|
|
use crate::text::TextElem;
|
|
use crate::util::{MaybeReverseIter, NonZeroExt, Numeric};
|
|
use crate::visualize::{Geometry, Paint, Stroke};
|
|
|
|
/// A value that can be configured per cell.
|
|
#[derive(Debug, Clone, PartialEq, Hash)]
|
|
pub enum Celled<T> {
|
|
/// A bare value, the same for all cells.
|
|
Value(T),
|
|
/// A closure mapping from cell coordinates to a value.
|
|
Func(Func),
|
|
/// An array of alignment values corresponding to each column.
|
|
Array(Vec<T>),
|
|
}
|
|
|
|
impl<T: Default + Clone + FromValue> Celled<T> {
|
|
/// Resolve the value based on the cell position.
|
|
pub fn resolve(
|
|
&self,
|
|
engine: &mut Engine,
|
|
styles: StyleChain,
|
|
x: usize,
|
|
y: usize,
|
|
) -> SourceResult<T> {
|
|
Ok(match self {
|
|
Self::Value(value) => value.clone(),
|
|
Self::Func(func) => func
|
|
.call(engine, &Context::new(None, Some(styles)), [x, y])?
|
|
.cast()
|
|
.at(func.span())?,
|
|
Self::Array(array) => x
|
|
.checked_rem(array.len())
|
|
.and_then(|i| array.get(i))
|
|
.cloned()
|
|
.unwrap_or_default(),
|
|
})
|
|
}
|
|
}
|
|
|
|
impl<T: Default> Default for Celled<T> {
|
|
fn default() -> Self {
|
|
Self::Value(T::default())
|
|
}
|
|
}
|
|
|
|
impl<T: Reflect> Reflect for Celled<T> {
|
|
fn input() -> CastInfo {
|
|
T::input() + Array::input() + Func::input()
|
|
}
|
|
|
|
fn output() -> CastInfo {
|
|
T::output() + Array::output() + Func::output()
|
|
}
|
|
|
|
fn castable(value: &Value) -> bool {
|
|
Array::castable(value) || Func::castable(value) || T::castable(value)
|
|
}
|
|
}
|
|
|
|
impl<T: IntoValue> IntoValue for Celled<T> {
|
|
fn into_value(self) -> Value {
|
|
match self {
|
|
Self::Value(value) => value.into_value(),
|
|
Self::Func(func) => func.into_value(),
|
|
Self::Array(arr) => arr.into_value(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<T: FromValue> FromValue for Celled<T> {
|
|
fn from_value(value: Value) -> StrResult<Self> {
|
|
match value {
|
|
Value::Func(v) => Ok(Self::Func(v)),
|
|
Value::Array(array) => Ok(Self::Array(
|
|
array.into_iter().map(T::from_value).collect::<StrResult<_>>()?,
|
|
)),
|
|
v if T::castable(&v) => Ok(Self::Value(T::from_value(v)?)),
|
|
v => Err(Self::error(&v)),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<T: Fold> Fold for Celled<T> {
|
|
fn fold(self, outer: Self) -> Self {
|
|
match (self, outer) {
|
|
(Self::Value(inner), Self::Value(outer)) => Self::Value(inner.fold(outer)),
|
|
(self_, _) => self_,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<T: Resolve> Resolve for Celled<T> {
|
|
type Output = ResolvedCelled<T>;
|
|
|
|
fn resolve(self, styles: StyleChain) -> Self::Output {
|
|
match self {
|
|
Self::Value(value) => ResolvedCelled(Celled::Value(value.resolve(styles))),
|
|
Self::Func(func) => ResolvedCelled(Celled::Func(func)),
|
|
Self::Array(values) => ResolvedCelled(Celled::Array(
|
|
values.into_iter().map(|value| value.resolve(styles)).collect(),
|
|
)),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The result of resolving a Celled's value according to styles.
|
|
/// Holds resolved values which depend on each grid cell's position.
|
|
/// When it is a closure, however, it is only resolved when the closure is
|
|
/// called.
|
|
#[derive(Default, Clone)]
|
|
pub struct ResolvedCelled<T: Resolve>(Celled<T::Output>);
|
|
|
|
impl<T> ResolvedCelled<T>
|
|
where
|
|
T: FromValue + Resolve,
|
|
<T as Resolve>::Output: Default + Clone,
|
|
{
|
|
/// Resolve the value based on the cell position.
|
|
pub fn resolve(
|
|
&self,
|
|
engine: &mut Engine,
|
|
styles: StyleChain,
|
|
x: usize,
|
|
y: usize,
|
|
) -> SourceResult<T::Output> {
|
|
Ok(match &self.0 {
|
|
Celled::Value(value) => value.clone(),
|
|
Celled::Func(func) => func
|
|
.call(engine, &Context::new(None, Some(styles)), [x, y])?
|
|
.cast::<T>()
|
|
.at(func.span())?
|
|
.resolve(styles),
|
|
Celled::Array(array) => x
|
|
.checked_rem(array.len())
|
|
.and_then(|i| array.get(i))
|
|
.cloned()
|
|
.unwrap_or_default(),
|
|
})
|
|
}
|
|
}
|
|
|
|
/// Represents a cell in CellGrid, to be laid out by GridLayouter.
|
|
#[derive(Clone)]
|
|
pub struct Cell {
|
|
/// The cell's body.
|
|
pub body: Content,
|
|
/// The cell's fill.
|
|
pub fill: Option<Paint>,
|
|
/// The amount of columns spanned by the cell.
|
|
pub colspan: NonZeroUsize,
|
|
/// The amount of rows spanned by the cell.
|
|
pub rowspan: NonZeroUsize,
|
|
/// The cell's stroke.
|
|
///
|
|
/// We use an Arc to avoid unnecessary space usage when all sides are the
|
|
/// same, or when the strokes come from a common source.
|
|
pub stroke: Sides<Option<Arc<Stroke<Abs>>>>,
|
|
/// Which stroke sides were explicitly overridden by the cell, over the
|
|
/// grid's global stroke setting.
|
|
///
|
|
/// This is used to define whether or not this cell's stroke sides should
|
|
/// have priority over adjacent cells' stroke sides, if those don't
|
|
/// override their own stroke properties (and thus have less priority when
|
|
/// defining with which stroke to draw grid lines around this cell).
|
|
pub stroke_overridden: Sides<bool>,
|
|
/// Whether rows spanned by this cell can be placed in different pages.
|
|
/// By default, a cell spanning only fixed-size rows is unbreakable, while
|
|
/// a cell spanning at least one `auto`-sized row is breakable.
|
|
pub breakable: bool,
|
|
}
|
|
|
|
impl From<Content> for Cell {
|
|
/// Create a simple cell given its body.
|
|
fn from(body: Content) -> Self {
|
|
Self {
|
|
body,
|
|
fill: None,
|
|
colspan: NonZeroUsize::ONE,
|
|
rowspan: NonZeroUsize::ONE,
|
|
stroke: Sides::splat(None),
|
|
stroke_overridden: Sides::splat(false),
|
|
breakable: true,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl LayoutMultiple for Cell {
|
|
fn layout(
|
|
&self,
|
|
engine: &mut Engine,
|
|
styles: StyleChain,
|
|
regions: Regions,
|
|
) -> SourceResult<Fragment> {
|
|
self.body.layout(engine, styles, regions)
|
|
}
|
|
}
|
|
|
|
/// A grid entry.
|
|
#[derive(Clone)]
|
|
pub(super) enum Entry {
|
|
/// An entry which holds a cell.
|
|
Cell(Cell),
|
|
/// An entry which is merged with another cell.
|
|
Merged {
|
|
/// The index of the cell this entry is merged with.
|
|
parent: usize,
|
|
},
|
|
}
|
|
|
|
impl Entry {
|
|
/// Obtains the cell inside this entry, if this is not a merged cell.
|
|
fn as_cell(&self) -> Option<&Cell> {
|
|
match self {
|
|
Self::Cell(cell) => Some(cell),
|
|
Self::Merged { .. } => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A repeatable grid header. Starts at the first row.
|
|
pub(super) struct Header {
|
|
/// The index after the last row included in this header.
|
|
pub(super) end: usize,
|
|
}
|
|
|
|
/// A repeatable grid footer. Stops at the last row.
|
|
pub(super) struct Footer {
|
|
/// The first row included in this footer.
|
|
pub(super) start: usize,
|
|
}
|
|
|
|
/// A possibly repeatable grid object.
|
|
/// It still exists even when not repeatable, but must not have additional
|
|
/// considerations by grid layout, other than for consistency (such as making
|
|
/// a certain group of rows unbreakable).
|
|
pub(super) enum Repeatable<T> {
|
|
Repeated(T),
|
|
NotRepeated(T),
|
|
}
|
|
|
|
impl<T> Repeatable<T> {
|
|
/// Gets the value inside this repeatable, regardless of whether
|
|
/// it repeats.
|
|
pub(super) fn unwrap(&self) -> &T {
|
|
match self {
|
|
Self::Repeated(repeated) => repeated,
|
|
Self::NotRepeated(not_repeated) => not_repeated,
|
|
}
|
|
}
|
|
|
|
/// Returns `Some` if the value is repeated, `None` otherwise.
|
|
pub(super) fn as_repeated(&self) -> Option<&T> {
|
|
match self {
|
|
Self::Repeated(repeated) => Some(repeated),
|
|
Self::NotRepeated(_) => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A grid item, possibly affected by automatic cell positioning. Can be either
|
|
/// a line or a cell.
|
|
pub enum ResolvableGridItem<T: ResolvableCell> {
|
|
/// A horizontal line in the grid.
|
|
HLine {
|
|
/// The row above which the horizontal line is drawn.
|
|
y: Smart<usize>,
|
|
start: usize,
|
|
end: Option<NonZeroUsize>,
|
|
stroke: Option<Arc<Stroke<Abs>>>,
|
|
/// The span of the corresponding line element.
|
|
span: Span,
|
|
/// The line's position. "before" here means on top of row `y`, while
|
|
/// "after" means below it.
|
|
position: LinePosition,
|
|
},
|
|
/// A vertical line in the grid.
|
|
VLine {
|
|
/// The column before which the vertical line is drawn.
|
|
x: Smart<usize>,
|
|
start: usize,
|
|
end: Option<NonZeroUsize>,
|
|
stroke: Option<Arc<Stroke<Abs>>>,
|
|
/// The span of the corresponding line element.
|
|
span: Span,
|
|
/// The line's position. "before" here means to the left of column `x`,
|
|
/// while "after" means to its right (both considering LTR).
|
|
position: LinePosition,
|
|
},
|
|
/// A cell in the grid.
|
|
Cell(T),
|
|
}
|
|
|
|
/// Any grid child, which can be either a header or an item.
|
|
pub enum ResolvableGridChild<T: ResolvableCell, I> {
|
|
Header { repeat: bool, span: Span, items: I },
|
|
Footer { repeat: bool, span: Span, items: I },
|
|
Item(ResolvableGridItem<T>),
|
|
}
|
|
|
|
/// Used for cell-like elements which are aware of their final properties in
|
|
/// the table, and may have property overrides.
|
|
pub trait ResolvableCell {
|
|
/// Resolves the cell's fields, given its coordinates and default grid-wide
|
|
/// fill, align, inset and stroke properties, plus the expected value of
|
|
/// the `breakable` field.
|
|
/// Returns a final Cell.
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn resolve_cell(
|
|
self,
|
|
x: usize,
|
|
y: usize,
|
|
fill: &Option<Paint>,
|
|
align: Smart<Alignment>,
|
|
inset: Sides<Option<Rel<Length>>>,
|
|
stroke: Sides<Option<Option<Arc<Stroke<Abs>>>>>,
|
|
breakable: bool,
|
|
styles: StyleChain,
|
|
) -> Cell;
|
|
|
|
/// Returns this cell's column override.
|
|
fn x(&self, styles: StyleChain) -> Smart<usize>;
|
|
|
|
/// Returns this cell's row override.
|
|
fn y(&self, styles: StyleChain) -> Smart<usize>;
|
|
|
|
/// The amount of columns spanned by this cell.
|
|
fn colspan(&self, styles: StyleChain) -> NonZeroUsize;
|
|
|
|
/// The amount of rows spanned by this cell.
|
|
fn rowspan(&self, styles: StyleChain) -> NonZeroUsize;
|
|
|
|
/// The cell's span, for errors.
|
|
fn span(&self) -> Span;
|
|
}
|
|
|
|
/// A grid of cells, including the columns, rows, and cell data.
|
|
pub struct CellGrid {
|
|
/// The grid cells.
|
|
pub(super) entries: Vec<Entry>,
|
|
/// The column tracks including gutter tracks.
|
|
pub(super) cols: Vec<Sizing>,
|
|
/// The row tracks including gutter tracks.
|
|
pub(super) rows: Vec<Sizing>,
|
|
/// The vertical lines before each column, or on the end border.
|
|
/// Gutter columns are not included.
|
|
/// Contains up to 'cols_without_gutter.len() + 1' vectors of lines.
|
|
pub(super) vlines: Vec<Vec<Line>>,
|
|
/// The horizontal lines on top of each row, or on the bottom border.
|
|
/// Gutter rows are not included.
|
|
/// Contains up to 'rows_without_gutter.len() + 1' vectors of lines.
|
|
pub(super) hlines: Vec<Vec<Line>>,
|
|
/// The repeatable header of this grid.
|
|
pub(super) header: Option<Repeatable<Header>>,
|
|
/// The repeatable footer of this grid.
|
|
pub(super) footer: Option<Repeatable<Footer>>,
|
|
/// Whether this grid has gutters.
|
|
pub(super) has_gutter: bool,
|
|
}
|
|
|
|
impl CellGrid {
|
|
/// Generates the cell grid, given the tracks and cells.
|
|
pub fn new(
|
|
tracks: Axes<&[Sizing]>,
|
|
gutter: Axes<&[Sizing]>,
|
|
cells: impl IntoIterator<Item = Cell>,
|
|
) -> Self {
|
|
let entries = cells.into_iter().map(Entry::Cell).collect();
|
|
Self::new_internal(tracks, gutter, vec![], vec![], None, None, entries)
|
|
}
|
|
|
|
/// Resolves and positions all cells in the grid before creating it.
|
|
/// Allows them to keep track of their final properties and positions
|
|
/// and adjust their fields accordingly.
|
|
/// Cells must implement Clone as they will be owned. Additionally, they
|
|
/// must implement Default in order to fill positions in the grid which
|
|
/// weren't explicitly specified by the user with empty cells.
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub fn resolve<T, C, I>(
|
|
tracks: Axes<&[Sizing]>,
|
|
gutter: Axes<&[Sizing]>,
|
|
children: C,
|
|
fill: &Celled<Option<Paint>>,
|
|
align: &Celled<Smart<Alignment>>,
|
|
inset: &Celled<Sides<Option<Rel<Length>>>>,
|
|
stroke: &ResolvedCelled<Sides<Option<Option<Arc<Stroke>>>>>,
|
|
engine: &mut Engine,
|
|
styles: StyleChain,
|
|
span: Span,
|
|
) -> SourceResult<Self>
|
|
where
|
|
T: ResolvableCell + Default,
|
|
I: Iterator<Item = ResolvableGridItem<T>>,
|
|
C: IntoIterator<Item = ResolvableGridChild<T, I>>,
|
|
C::IntoIter: ExactSizeIterator,
|
|
{
|
|
// Number of content columns: Always at least one.
|
|
let c = tracks.x.len().max(1);
|
|
|
|
// Lists of lines.
|
|
// Horizontal lines are only pushed later to be able to check for row
|
|
// validity, since the amount of rows isn't known until all items were
|
|
// analyzed in the for loop below.
|
|
// We keep their spans so we can report errors later.
|
|
let mut pending_hlines: Vec<(Span, Line)> = vec![];
|
|
|
|
// For consistency, only push vertical lines later as well.
|
|
let mut pending_vlines: Vec<(Span, Line)> = vec![];
|
|
let has_gutter = gutter.any(|tracks| !tracks.is_empty());
|
|
|
|
let mut header: Option<Header> = None;
|
|
let mut repeat_header = false;
|
|
|
|
// Stores where the footer is supposed to end, its span, and the
|
|
// actual footer structure.
|
|
let mut footer: Option<(usize, Span, Footer)> = None;
|
|
let mut repeat_footer = false;
|
|
|
|
// Resolve the breakability of a cell, based on whether or not it spans
|
|
// an auto row.
|
|
let resolve_breakable = |y, rowspan| {
|
|
let auto = Sizing::Auto;
|
|
let zero = Sizing::Rel(Rel::zero());
|
|
tracks
|
|
.y
|
|
.iter()
|
|
.chain(std::iter::repeat(tracks.y.last().unwrap_or(&auto)))
|
|
.skip(y)
|
|
.take(rowspan)
|
|
.any(|row| row == &Sizing::Auto)
|
|
|| gutter
|
|
.y
|
|
.iter()
|
|
.chain(std::iter::repeat(gutter.y.last().unwrap_or(&zero)))
|
|
.skip(y)
|
|
.take(rowspan - 1)
|
|
.any(|row_gutter| row_gutter == &Sizing::Auto)
|
|
};
|
|
|
|
// We can't just use the cell's index in the 'cells' vector to
|
|
// determine its automatic position, since cells could have arbitrary
|
|
// positions, so the position of a cell in 'cells' can differ from its
|
|
// final position in 'resolved_cells' (see below).
|
|
// Therefore, we use a counter, 'auto_index', to determine the position
|
|
// of the next cell with (x: auto, y: auto). It is only stepped when
|
|
// a cell with (x: auto, y: auto), usually the vast majority, is found.
|
|
let mut auto_index: usize = 0;
|
|
|
|
// We have to rebuild the grid to account for arbitrary positions.
|
|
// Create at least 'children.len()' positions, since there could be at
|
|
// least 'children.len()' cells (if no explicit lines were specified),
|
|
// even though some of them might be placed in arbitrary positions and
|
|
// thus cause the grid to expand.
|
|
// Additionally, make sure we allocate up to the next multiple of 'c',
|
|
// since each row will have 'c' cells, even if the last few cells
|
|
// weren't explicitly specified by the user.
|
|
// We apply '% c' twice so that the amount of cells potentially missing
|
|
// is zero when 'children.len()' is already a multiple of 'c' (thus
|
|
// 'children.len() % c' would be zero).
|
|
let children = children.into_iter();
|
|
let Some(child_count) = children.len().checked_add((c - children.len() % c) % c)
|
|
else {
|
|
bail!(span, "too many cells or lines were given")
|
|
};
|
|
let mut resolved_cells: Vec<Option<Entry>> = Vec::with_capacity(child_count);
|
|
for child in children {
|
|
let mut is_header = false;
|
|
let mut is_footer = false;
|
|
let mut child_start = usize::MAX;
|
|
let mut child_end = 0;
|
|
let mut child_span = Span::detached();
|
|
let mut min_auto_index = 0;
|
|
|
|
let (header_footer_items, simple_item) = match child {
|
|
ResolvableGridChild::Header { repeat, span, items, .. } => {
|
|
if header.is_some() {
|
|
bail!(span, "cannot have more than one header");
|
|
}
|
|
|
|
is_header = true;
|
|
child_span = span;
|
|
repeat_header = repeat;
|
|
|
|
// If any cell in the header is automatically positioned,
|
|
// have it skip to the next row. This is to avoid having a
|
|
// header after a partially filled row just add cells to
|
|
// that row instead of starting a new one.
|
|
// FIXME: Revise this approach when headers can start from
|
|
// arbitrary rows.
|
|
min_auto_index = auto_index.next_multiple_of(c);
|
|
|
|
(Some(items), None)
|
|
}
|
|
ResolvableGridChild::Footer { repeat, span, items, .. } => {
|
|
if footer.is_some() {
|
|
bail!(span, "cannot have more than one footer");
|
|
}
|
|
|
|
is_footer = true;
|
|
child_span = span;
|
|
repeat_footer = repeat;
|
|
|
|
// If any cell in the footer is automatically positioned,
|
|
// have it skip to the next row. This is to avoid having a
|
|
// footer after a partially filled row just add cells to
|
|
// that row instead of starting a new one.
|
|
min_auto_index = auto_index.next_multiple_of(c);
|
|
|
|
(Some(items), None)
|
|
}
|
|
ResolvableGridChild::Item(item) => (None, Some(item)),
|
|
};
|
|
|
|
let items = header_footer_items
|
|
.into_iter()
|
|
.flatten()
|
|
.chain(simple_item.into_iter());
|
|
for item in items {
|
|
let cell = match item {
|
|
ResolvableGridItem::HLine {
|
|
y,
|
|
start,
|
|
end,
|
|
stroke,
|
|
span,
|
|
position,
|
|
} => {
|
|
let y = y.unwrap_or_else(|| {
|
|
// When no 'y' is specified for the hline, we place
|
|
// it under the latest automatically positioned
|
|
// cell.
|
|
// The current value of the auto index is always
|
|
// the index of the latest automatically positioned
|
|
// cell placed plus one (that's what we do in
|
|
// 'resolve_cell_position'), so we subtract 1 to
|
|
// get that cell's index, and place the hline below
|
|
// its row. The exception is when the auto_index is
|
|
// 0, meaning no automatically positioned cell was
|
|
// placed yet. In that case, we place the hline at
|
|
// the top of the table.
|
|
//
|
|
// Exceptionally, the hline will be placed before
|
|
// the minimum auto index if the current auto index
|
|
// from previous iterations is smaller than the
|
|
// minimum it should have for the current grid
|
|
// child. Effectively, this means that a hline at
|
|
// the start of a header will always appear above
|
|
// that header's first row. Similarly for footers.
|
|
auto_index
|
|
.max(min_auto_index)
|
|
.checked_sub(1)
|
|
.map_or(0, |last_auto_index| last_auto_index / c + 1)
|
|
});
|
|
if end.is_some_and(|end| end.get() < start) {
|
|
bail!(span, "line cannot end before it starts");
|
|
}
|
|
let line = Line { index: y, start, end, stroke, position };
|
|
|
|
// Since the amount of rows is dynamic, delay placing
|
|
// hlines until after all cells were placed so we can
|
|
// properly verify if they are valid. Note that we
|
|
// can't place hlines even if we already know they
|
|
// would be in a valid row, since it's possible that we
|
|
// pushed pending hlines in the same row as this one in
|
|
// previous iterations, and we need to ensure that
|
|
// hlines from previous iterations are pushed to the
|
|
// final vector of hlines first - the order of hlines
|
|
// must be kept, as this matters when determining which
|
|
// one "wins" in case of conflict. Pushing the current
|
|
// hline before we push pending hlines later would
|
|
// change their order!
|
|
pending_hlines.push((span, line));
|
|
continue;
|
|
}
|
|
ResolvableGridItem::VLine {
|
|
x,
|
|
start,
|
|
end,
|
|
stroke,
|
|
span,
|
|
position,
|
|
} => {
|
|
let x = x.unwrap_or_else(|| {
|
|
// When no 'x' is specified for the vline, we place
|
|
// it after the latest automatically positioned
|
|
// cell.
|
|
// The current value of the auto index is always
|
|
// the index of the latest automatically positioned
|
|
// cell placed plus one (that's what we do in
|
|
// 'resolve_cell_position'), so we subtract 1 to
|
|
// get that cell's index, and place the vline after
|
|
// its column. The exception is when the auto_index
|
|
// is 0, meaning no automatically positioned cell
|
|
// was placed yet. In that case, we place the vline
|
|
// to the left of the table.
|
|
//
|
|
// Exceptionally, a vline is also placed to the
|
|
// left of the table if the current auto index from
|
|
// past iterations is smaller than the minimum auto
|
|
// index. For example, this means that a vline at
|
|
// the beginning of a header will be placed to its
|
|
// left rather than after the previous
|
|
// automatically positioned cell. Same for footers.
|
|
auto_index
|
|
.checked_sub(1)
|
|
.filter(|last_auto_index| {
|
|
last_auto_index >= &min_auto_index
|
|
})
|
|
.map_or(0, |last_auto_index| last_auto_index % c + 1)
|
|
});
|
|
if end.is_some_and(|end| end.get() < start) {
|
|
bail!(span, "line cannot end before it starts");
|
|
}
|
|
let line = Line { index: x, start, end, stroke, position };
|
|
|
|
// For consistency with hlines, we only push vlines to
|
|
// the final vector of vlines after processing every
|
|
// cell.
|
|
pending_vlines.push((span, line));
|
|
continue;
|
|
}
|
|
ResolvableGridItem::Cell(cell) => cell,
|
|
};
|
|
let cell_span = cell.span();
|
|
// Let's calculate the cell's final position based on its
|
|
// requested position.
|
|
let resolved_index = {
|
|
let cell_x = cell.x(styles);
|
|
let cell_y = cell.y(styles);
|
|
resolve_cell_position(
|
|
cell_x,
|
|
cell_y,
|
|
&resolved_cells,
|
|
&mut auto_index,
|
|
min_auto_index,
|
|
c,
|
|
)
|
|
.at(cell_span)?
|
|
};
|
|
let x = resolved_index % c;
|
|
let y = resolved_index / c;
|
|
let colspan = cell.colspan(styles).get();
|
|
let rowspan = cell.rowspan(styles).get();
|
|
|
|
if colspan > c - x {
|
|
bail!(
|
|
cell_span,
|
|
"cell's colspan would cause it to exceed the available column(s)";
|
|
hint: "try placing the cell in another position or reducing its colspan"
|
|
)
|
|
}
|
|
|
|
let Some(largest_index) = c
|
|
.checked_mul(rowspan - 1)
|
|
.and_then(|full_rowspan_offset| {
|
|
resolved_index.checked_add(full_rowspan_offset)
|
|
})
|
|
.and_then(|last_row_pos| last_row_pos.checked_add(colspan - 1))
|
|
else {
|
|
bail!(
|
|
cell_span,
|
|
"cell would span an exceedingly large position";
|
|
hint: "try reducing the cell's rowspan or colspan"
|
|
)
|
|
};
|
|
|
|
// Let's resolve the cell so it can determine its own fields
|
|
// based on its final position.
|
|
let cell = cell.resolve_cell(
|
|
x,
|
|
y,
|
|
&fill.resolve(engine, styles, x, y)?,
|
|
align.resolve(engine, styles, x, y)?,
|
|
inset.resolve(engine, styles, x, y)?,
|
|
stroke.resolve(engine, styles, x, y)?,
|
|
resolve_breakable(y, rowspan),
|
|
styles,
|
|
);
|
|
|
|
if largest_index >= resolved_cells.len() {
|
|
// Ensure the length of the vector of resolved cells is
|
|
// always a multiple of 'c' by pushing full rows every
|
|
// time. Here, we add enough absent positions (later
|
|
// converted to empty cells) to ensure the last row in the
|
|
// new vector length is completely filled. This is
|
|
// necessary so that those positions, even if not
|
|
// explicitly used at the end, are eventually susceptible
|
|
// to show rules and receive grid styling, as they will be
|
|
// resolved as empty cells in a second loop below.
|
|
let Some(new_len) = largest_index
|
|
.checked_add(1)
|
|
.and_then(|new_len| new_len.checked_add((c - new_len % c) % c))
|
|
else {
|
|
bail!(cell_span, "cell position too large")
|
|
};
|
|
|
|
// Here, the cell needs to be placed in a position which
|
|
// doesn't exist yet in the grid (out of bounds). We will
|
|
// add enough absent positions for this to be possible.
|
|
// They must be absent as no cells actually occupy them
|
|
// (they can be overridden later); however, if no cells
|
|
// occupy them as we finish building the grid, then such
|
|
// positions will be replaced by empty cells.
|
|
resolved_cells.resize(new_len, None);
|
|
}
|
|
|
|
// The vector is large enough to contain the cell, so we can
|
|
// just index it directly to access the position it will be
|
|
// placed in. However, we still need to ensure we won't try to
|
|
// place a cell where there already is one.
|
|
let slot = &mut resolved_cells[resolved_index];
|
|
if slot.is_some() {
|
|
bail!(
|
|
cell_span,
|
|
"attempted to place a second cell at column {x}, row {y}";
|
|
hint: "try specifying your cells in a different order"
|
|
);
|
|
}
|
|
|
|
*slot = Some(Entry::Cell(cell));
|
|
|
|
// Now, if the cell spans more than one row or column, we fill
|
|
// the spanned positions in the grid with Entry::Merged
|
|
// pointing to the original cell as its parent.
|
|
for rowspan_offset in 0..rowspan {
|
|
let spanned_y = y + rowspan_offset;
|
|
let first_row_index = resolved_index + c * rowspan_offset;
|
|
for (colspan_offset, slot) in resolved_cells[first_row_index..]
|
|
[..colspan]
|
|
.iter_mut()
|
|
.enumerate()
|
|
{
|
|
let spanned_x = x + colspan_offset;
|
|
if spanned_x == x && spanned_y == y {
|
|
// This is the parent cell.
|
|
continue;
|
|
}
|
|
if slot.is_some() {
|
|
bail!(
|
|
cell_span,
|
|
"cell would span a previously placed cell at column {spanned_x}, row {spanned_y}";
|
|
hint: "try specifying your cells in a different order or reducing the cell's rowspan or colspan"
|
|
)
|
|
}
|
|
*slot = Some(Entry::Merged { parent: resolved_index });
|
|
}
|
|
}
|
|
|
|
if is_header || is_footer {
|
|
// Ensure each cell in a header or footer is fully
|
|
// contained within it.
|
|
child_start = child_start.min(y);
|
|
child_end = child_end.max(y + rowspan);
|
|
}
|
|
}
|
|
|
|
if (is_header || is_footer) && child_start == usize::MAX {
|
|
// Empty header/footer: consider the header/footer to be
|
|
// one row after the latest auto index.
|
|
child_start = auto_index.div_ceil(c);
|
|
child_end = child_start + 1;
|
|
|
|
if resolved_cells.len() <= c * child_start {
|
|
// Ensure the automatically chosen row actually exists.
|
|
resolved_cells.resize_with(c * (child_start + 1), || None);
|
|
}
|
|
}
|
|
|
|
if is_header {
|
|
if child_start != 0 {
|
|
bail!(
|
|
child_span,
|
|
"header must start at the first row";
|
|
hint: "remove any rows before the header"
|
|
);
|
|
}
|
|
|
|
header = Some(Header {
|
|
// Later on, we have to correct this number in case there
|
|
// is gutter. But only once all cells have been analyzed
|
|
// and the header has fully expanded in the fixup loop
|
|
// below.
|
|
end: child_end,
|
|
});
|
|
}
|
|
|
|
if is_footer {
|
|
// Only check if the footer is at the end later, once we know
|
|
// the final amount of rows.
|
|
footer = Some((
|
|
child_end,
|
|
child_span,
|
|
Footer {
|
|
// Later on, we have to correct this number in case there
|
|
// is gutter, but only once all cells have been analyzed
|
|
// and the header's and footer's exact boundaries are
|
|
// known. That is because the gutter row immediately
|
|
// before the footer might not be included as part of
|
|
// the footer if it is contained within the header.
|
|
start: child_start,
|
|
},
|
|
));
|
|
}
|
|
|
|
if is_header || is_footer {
|
|
// Next automatically positioned cell goes under this header.
|
|
// FIXME: Consider only doing this if the header has any fully
|
|
// automatically positioned cells. Otherwise,
|
|
// `resolve_cell_position` should be smart enough to skip
|
|
// upcoming headers.
|
|
// Additionally, consider that cells with just an 'x' override
|
|
// could end up going too far back and making previous
|
|
// non-header rows into header rows (maybe they should be
|
|
// placed at the first row that is fully empty or something).
|
|
// Nothing we can do when both 'x' and 'y' were overridden, of
|
|
// course.
|
|
// None of the above are concerns for now, as headers must
|
|
// start at the first row.
|
|
auto_index = auto_index.max(c * child_end);
|
|
}
|
|
}
|
|
|
|
// Fixup phase (final step in cell grid generation):
|
|
// 1. Replace absent entries by resolved empty cells, and produce a
|
|
// vector of 'Entry' from 'Option<Entry>'.
|
|
// 2. If any cells were added to the header's rows after the header's
|
|
// creation, ensure the header expands enough to accommodate them
|
|
// across all of their spanned rows. Same for the footer.
|
|
// 3. If any cells before the footer try to span it, error.
|
|
let resolved_cells = resolved_cells
|
|
.into_iter()
|
|
.enumerate()
|
|
.map(|(i, cell)| {
|
|
if let Some(cell) = cell {
|
|
if let Some(parent_cell) = cell.as_cell() {
|
|
if let Some(header) = &mut header
|
|
{
|
|
let y = i / c;
|
|
if y < header.end {
|
|
// Ensure the header expands enough such that
|
|
// all cells inside it, even those added later,
|
|
// are fully contained within the header.
|
|
// FIXME: check if start < y < end when start can
|
|
// be != 0.
|
|
// FIXME: when start can be != 0, decide what
|
|
// happens when a cell after the header placed
|
|
// above it tries to span the header (either
|
|
// error or expand upwards).
|
|
header.end = header.end.max(y + parent_cell.rowspan.get());
|
|
}
|
|
}
|
|
|
|
if let Some((end, footer_span, footer)) = &mut footer {
|
|
let x = i % c;
|
|
let y = i / c;
|
|
let cell_end = y + parent_cell.rowspan.get();
|
|
if y < footer.start && cell_end > footer.start {
|
|
// Don't allow a cell before the footer to span
|
|
// it. Surely, we could move the footer to
|
|
// start at where this cell starts, so this is
|
|
// more of a design choice, as it's unlikely
|
|
// for the user to intentionally include a cell
|
|
// before the footer spanning it but not
|
|
// being repeated with it.
|
|
bail!(
|
|
*footer_span,
|
|
"footer would conflict with a cell placed before it at column {x} row {y}";
|
|
hint: "try reducing that cell's rowspan or moving the footer"
|
|
);
|
|
}
|
|
if y >= footer.start && y < *end {
|
|
// Expand the footer to include all rows
|
|
// spanned by this cell, as it is inside the
|
|
// footer.
|
|
*end = (*end).max(cell_end);
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(cell)
|
|
} else {
|
|
let x = i % c;
|
|
let y = i / c;
|
|
|
|
// Ensure all absent entries are affected by show rules and
|
|
// grid styling by turning them into resolved empty cells.
|
|
let new_cell = T::default().resolve_cell(
|
|
x,
|
|
y,
|
|
&fill.resolve(engine, styles, x, y)?,
|
|
align.resolve(engine, styles, x, y)?,
|
|
inset.resolve(engine, styles, x, y)?,
|
|
stroke.resolve(engine, styles, x, y)?,
|
|
resolve_breakable(y, 1),
|
|
styles,
|
|
);
|
|
Ok(Entry::Cell(new_cell))
|
|
}
|
|
})
|
|
.collect::<SourceResult<Vec<Entry>>>()?;
|
|
|
|
// Populate the final lists of lines.
|
|
// For each line type (horizontal or vertical), we keep a vector for
|
|
// every group of lines with the same index.
|
|
let mut vlines: Vec<Vec<Line>> = vec![];
|
|
let mut hlines: Vec<Vec<Line>> = vec![];
|
|
let row_amount = resolved_cells.len().div_ceil(c);
|
|
|
|
for (line_span, line) in pending_hlines {
|
|
let y = line.index;
|
|
if y > row_amount {
|
|
bail!(line_span, "cannot place horizontal line at invalid row {y}");
|
|
}
|
|
if y == row_amount && line.position == LinePosition::After {
|
|
bail!(
|
|
line_span,
|
|
"cannot place horizontal line at the 'bottom' position of the bottom border (y = {y})";
|
|
hint: "set the line's position to 'top' or place it at a smaller 'y' index"
|
|
);
|
|
}
|
|
let line = if line.position == LinePosition::After
|
|
&& (!has_gutter || y + 1 == row_amount)
|
|
{
|
|
// Just place the line on top of the next row if
|
|
// there's no gutter and the line should be placed
|
|
// after the one with given index.
|
|
//
|
|
// Note that placing after the last row is also the same as
|
|
// just placing on the grid's bottom border, even with
|
|
// gutter.
|
|
Line {
|
|
index: y + 1,
|
|
position: LinePosition::Before,
|
|
..line
|
|
}
|
|
} else {
|
|
line
|
|
};
|
|
let y = line.index;
|
|
|
|
if hlines.len() <= y {
|
|
hlines.resize_with(y + 1, Vec::new);
|
|
}
|
|
hlines[y].push(line);
|
|
}
|
|
|
|
for (line_span, line) in pending_vlines {
|
|
let x = line.index;
|
|
if x > c {
|
|
bail!(line_span, "cannot place vertical line at invalid column {x}");
|
|
}
|
|
if x == c && line.position == LinePosition::After {
|
|
bail!(
|
|
line_span,
|
|
"cannot place vertical line at the 'end' position of the end border (x = {c})";
|
|
hint: "set the line's position to 'start' or place it at a smaller 'x' index"
|
|
);
|
|
}
|
|
let line =
|
|
if line.position == LinePosition::After && (!has_gutter || x + 1 == c) {
|
|
// Just place the line before the next column if
|
|
// there's no gutter and the line should be placed
|
|
// after the one with given index.
|
|
//
|
|
// Note that placing after the last column is also the
|
|
// same as just placing on the grid's end border, even
|
|
// with gutter.
|
|
Line {
|
|
index: x + 1,
|
|
position: LinePosition::Before,
|
|
..line
|
|
}
|
|
} else {
|
|
line
|
|
};
|
|
let x = line.index;
|
|
|
|
if vlines.len() <= x {
|
|
vlines.resize_with(x + 1, Vec::new);
|
|
}
|
|
vlines[x].push(line);
|
|
}
|
|
|
|
let header = header
|
|
.map(|mut header| {
|
|
// Repeat the gutter below a header (hence why we don't
|
|
// subtract 1 from the gutter case).
|
|
// Don't do this if there are no rows under the header.
|
|
if has_gutter {
|
|
// - 'header.end' is always 'last y + 1'. The header stops
|
|
// before that row.
|
|
// - Therefore, '2 * header.end' will be 2 * (last y + 1),
|
|
// which is the adjusted index of the row before which the
|
|
// header stops, meaning it will still stop right before it
|
|
// even with gutter thanks to the multiplication below.
|
|
// - This means that it will span all rows up to
|
|
// '2 * (last y + 1) - 1 = 2 * last y + 1', which equates
|
|
// to the index of the gutter row right below the header,
|
|
// which is what we want (that gutter spacing should be
|
|
// repeated across pages to maintain uniformity).
|
|
header.end *= 2;
|
|
|
|
// If the header occupies the entire grid, ensure we don't
|
|
// include an extra gutter row when it doesn't exist, since
|
|
// the last row of the header is at the very bottom,
|
|
// therefore '2 * last y + 1' is not a valid index.
|
|
let row_amount = (2 * row_amount).saturating_sub(1);
|
|
header.end = header.end.min(row_amount);
|
|
}
|
|
header
|
|
})
|
|
.map(|header| {
|
|
if repeat_header {
|
|
Repeatable::Repeated(header)
|
|
} else {
|
|
Repeatable::NotRepeated(header)
|
|
}
|
|
});
|
|
|
|
let footer = footer
|
|
.map(|(footer_end, footer_span, mut footer)| {
|
|
if footer_end != row_amount {
|
|
bail!(footer_span, "footer must end at the last row");
|
|
}
|
|
|
|
let header_end =
|
|
header.as_ref().map(Repeatable::unwrap).map(|header| header.end);
|
|
|
|
if has_gutter {
|
|
// Convert the footer's start index to post-gutter coordinates.
|
|
footer.start *= 2;
|
|
|
|
// Include the gutter right before the footer, unless there is
|
|
// none, or the gutter is already included in the header (no
|
|
// rows between the header and the footer).
|
|
if header_end.map_or(true, |header_end| header_end != footer.start) {
|
|
footer.start = footer.start.saturating_sub(1);
|
|
}
|
|
}
|
|
|
|
if header_end.is_some_and(|header_end| header_end > footer.start) {
|
|
bail!(footer_span, "header and footer must not have common rows");
|
|
}
|
|
|
|
Ok(footer)
|
|
})
|
|
.transpose()?
|
|
.map(|footer| {
|
|
if repeat_footer {
|
|
Repeatable::Repeated(footer)
|
|
} else {
|
|
Repeatable::NotRepeated(footer)
|
|
}
|
|
});
|
|
|
|
Ok(Self::new_internal(
|
|
tracks,
|
|
gutter,
|
|
vlines,
|
|
hlines,
|
|
header,
|
|
footer,
|
|
resolved_cells,
|
|
))
|
|
}
|
|
|
|
/// Generates the cell grid, given the tracks and resolved entries.
|
|
pub(super) fn new_internal(
|
|
tracks: Axes<&[Sizing]>,
|
|
gutter: Axes<&[Sizing]>,
|
|
vlines: Vec<Vec<Line>>,
|
|
hlines: Vec<Vec<Line>>,
|
|
header: Option<Repeatable<Header>>,
|
|
footer: Option<Repeatable<Footer>>,
|
|
entries: Vec<Entry>,
|
|
) -> Self {
|
|
let mut cols = vec![];
|
|
let mut rows = vec![];
|
|
|
|
// Number of content columns: Always at least one.
|
|
let c = tracks.x.len().max(1);
|
|
|
|
// Number of content rows: At least as many as given, but also at least
|
|
// as many as needed to place each item.
|
|
let r = {
|
|
let len = entries.len();
|
|
let given = tracks.y.len();
|
|
let needed = len / c + (len % c).clamp(0, 1);
|
|
given.max(needed)
|
|
};
|
|
|
|
let has_gutter = gutter.any(|tracks| !tracks.is_empty());
|
|
let auto = Sizing::Auto;
|
|
let zero = Sizing::Rel(Rel::zero());
|
|
let get_or = |tracks: &[_], idx, default| {
|
|
tracks.get(idx).or(tracks.last()).copied().unwrap_or(default)
|
|
};
|
|
|
|
// Collect content and gutter columns.
|
|
for x in 0..c {
|
|
cols.push(get_or(tracks.x, x, auto));
|
|
if has_gutter {
|
|
cols.push(get_or(gutter.x, x, zero));
|
|
}
|
|
}
|
|
|
|
// Collect content and gutter rows.
|
|
for y in 0..r {
|
|
rows.push(get_or(tracks.y, y, auto));
|
|
if has_gutter {
|
|
rows.push(get_or(gutter.y, y, zero));
|
|
}
|
|
}
|
|
|
|
// Remove superfluous gutter tracks.
|
|
if has_gutter {
|
|
cols.pop();
|
|
rows.pop();
|
|
}
|
|
|
|
Self {
|
|
cols,
|
|
rows,
|
|
entries,
|
|
vlines,
|
|
hlines,
|
|
header,
|
|
footer,
|
|
has_gutter,
|
|
}
|
|
}
|
|
|
|
/// Get the grid entry in column `x` and row `y`.
|
|
///
|
|
/// Returns `None` if it's a gutter cell.
|
|
#[track_caller]
|
|
pub(super) fn entry(&self, x: usize, y: usize) -> Option<&Entry> {
|
|
assert!(x < self.cols.len());
|
|
assert!(y < self.rows.len());
|
|
|
|
if self.has_gutter {
|
|
// Even columns and rows are children, odd ones are gutter.
|
|
if x % 2 == 0 && y % 2 == 0 {
|
|
let c = 1 + self.cols.len() / 2;
|
|
self.entries.get((y / 2) * c + x / 2)
|
|
} else {
|
|
None
|
|
}
|
|
} else {
|
|
let c = self.cols.len();
|
|
self.entries.get(y * c + x)
|
|
}
|
|
}
|
|
|
|
/// Get the content of the cell in column `x` and row `y`.
|
|
///
|
|
/// Returns `None` if it's a gutter cell or merged position.
|
|
#[track_caller]
|
|
pub(super) fn cell(&self, x: usize, y: usize) -> Option<&Cell> {
|
|
self.entry(x, y).and_then(Entry::as_cell)
|
|
}
|
|
|
|
/// Returns the position of the parent cell of the grid entry at the given
|
|
/// position. It is guaranteed to have a non-gutter, non-merged cell at
|
|
/// the returned position, due to how the grid is built.
|
|
/// - If the entry at the given position is a cell, returns the given
|
|
/// position.
|
|
/// - If it is a merged cell, returns the parent cell's position.
|
|
/// - If it is a gutter cell, returns None.
|
|
#[track_caller]
|
|
pub(super) fn parent_cell_position(&self, x: usize, y: usize) -> Option<Axes<usize>> {
|
|
self.entry(x, y).map(|entry| match entry {
|
|
Entry::Cell(_) => Axes::new(x, y),
|
|
Entry::Merged { parent } => {
|
|
let c = if self.has_gutter {
|
|
1 + self.cols.len() / 2
|
|
} else {
|
|
self.cols.len()
|
|
};
|
|
let factor = if self.has_gutter { 2 } else { 1 };
|
|
Axes::new(factor * (*parent % c), factor * (*parent / c))
|
|
}
|
|
})
|
|
}
|
|
|
|
/// Returns the position of the actual parent cell of a merged position,
|
|
/// even if the given position is gutter, in which case we return the
|
|
/// parent of the nearest adjacent content cell which could possibly span
|
|
/// the given gutter position. If the given position is not a gutter cell,
|
|
/// then this function will return the same as `parent_cell_position` would.
|
|
/// If the given position is a gutter cell, but no cell spans it, returns
|
|
/// `None`.
|
|
///
|
|
/// This is useful for lines. A line needs to check if a cell next to it
|
|
/// has a stroke override - even at a gutter position there could be a
|
|
/// stroke override, since a cell could be merged with two cells at both
|
|
/// ends of the gutter cell (e.g. to its left and to its right), and thus
|
|
/// that cell would impose a stroke under the gutter. This function allows
|
|
/// getting the position of that cell (which spans the given gutter
|
|
/// position, if it is gutter), if it exists; otherwise returns None (it's
|
|
/// gutter and no cell spans it).
|
|
#[track_caller]
|
|
pub(super) fn effective_parent_cell_position(
|
|
&self,
|
|
x: usize,
|
|
y: usize,
|
|
) -> Option<Axes<usize>> {
|
|
if self.has_gutter {
|
|
// If (x, y) is a gutter cell, we skip it (skip a gutter column and
|
|
// row) to the nearest adjacent content cell, in the direction
|
|
// which merged cells grow toward (increasing x and increasing y),
|
|
// such that we can verify if that adjacent cell is merged with the
|
|
// gutter cell by checking if its parent would come before (x, y).
|
|
// Otherwise, no cell is merged with this gutter cell, and we
|
|
// return None.
|
|
self.parent_cell_position(x + x % 2, y + y % 2)
|
|
.filter(|&parent| parent.x <= x && parent.y <= y)
|
|
} else {
|
|
self.parent_cell_position(x, y)
|
|
}
|
|
}
|
|
|
|
/// Checks if the track with the given index is gutter.
|
|
/// Does not check if the index is a valid track.
|
|
#[inline]
|
|
pub(super) fn is_gutter_track(&self, index: usize) -> bool {
|
|
self.has_gutter && index % 2 == 1
|
|
}
|
|
|
|
/// Returns the effective colspan of a cell, considering the gutters it
|
|
/// might span if the grid has gutters.
|
|
#[inline]
|
|
pub(super) fn effective_colspan_of_cell(&self, cell: &Cell) -> usize {
|
|
if self.has_gutter {
|
|
2 * cell.colspan.get() - 1
|
|
} else {
|
|
cell.colspan.get()
|
|
}
|
|
}
|
|
|
|
/// Returns the effective rowspan of a cell, considering the gutters it
|
|
/// might span if the grid has gutters.
|
|
#[inline]
|
|
pub(super) fn effective_rowspan_of_cell(&self, cell: &Cell) -> usize {
|
|
if self.has_gutter {
|
|
2 * cell.rowspan.get() - 1
|
|
} else {
|
|
cell.rowspan.get()
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Given a cell's requested x and y, the vector with the resolved cell
|
|
/// positions, the `auto_index` counter (determines the position of the next
|
|
/// `(auto, auto)` cell) and the amount of columns in the grid, returns the
|
|
/// final index of this cell in the vector of resolved cells.
|
|
///
|
|
/// The `min_auto_index` parameter is used to bump the auto index to that value
|
|
/// if it is currently smaller than it and a cell requests fully automatic
|
|
/// positioning. Useful with headers: if a cell in a header has automatic
|
|
/// positioning, it should start at the header's first row, and not at the end
|
|
/// of the previous row.
|
|
fn resolve_cell_position(
|
|
cell_x: Smart<usize>,
|
|
cell_y: Smart<usize>,
|
|
resolved_cells: &[Option<Entry>],
|
|
auto_index: &mut usize,
|
|
min_auto_index: usize,
|
|
columns: usize,
|
|
) -> HintedStrResult<usize> {
|
|
// Translates a (x, y) position to the equivalent index in the final cell vector.
|
|
// Errors if the position would be too large.
|
|
let cell_index = |x, y: usize| {
|
|
y.checked_mul(columns)
|
|
.and_then(|row_index| row_index.checked_add(x))
|
|
.ok_or_else(|| HintedString::from(eco_format!("cell position too large")))
|
|
};
|
|
match (cell_x, cell_y) {
|
|
// Fully automatic cell positioning. The cell did not
|
|
// request a coordinate.
|
|
(Smart::Auto, Smart::Auto) => {
|
|
// Let's find the first available position starting from the
|
|
// automatic position counter, searching in row-major order.
|
|
let mut resolved_index = min_auto_index.max(*auto_index);
|
|
while let Some(Some(_)) = resolved_cells.get(resolved_index) {
|
|
// Skip any non-absent cell positions (`Some(None)`) to
|
|
// determine where this cell will be placed. An out of bounds
|
|
// position (thus `None`) is also a valid new position (only
|
|
// requires expanding the vector).
|
|
resolved_index += 1;
|
|
}
|
|
|
|
// Ensure the next cell with automatic position will be
|
|
// placed after this one (maybe not immediately after).
|
|
*auto_index = resolved_index + 1;
|
|
|
|
Ok(resolved_index)
|
|
}
|
|
// Cell has chosen at least its column.
|
|
(Smart::Custom(cell_x), cell_y) => {
|
|
if cell_x >= columns {
|
|
return Err(HintedString::from(eco_format!(
|
|
"cell could not be placed at invalid column {cell_x}"
|
|
)));
|
|
}
|
|
if let Smart::Custom(cell_y) = cell_y {
|
|
// Cell has chosen its exact position.
|
|
cell_index(cell_x, cell_y)
|
|
} else {
|
|
// Cell has only chosen its column.
|
|
// Let's find the first row which has that column available.
|
|
let mut resolved_y = 0;
|
|
while let Some(Some(_)) =
|
|
resolved_cells.get(cell_index(cell_x, resolved_y)?)
|
|
{
|
|
// Try each row until either we reach an absent position
|
|
// (`Some(None)`) or an out of bounds position (`None`),
|
|
// in which case we'd create a new row to place this cell in.
|
|
resolved_y += 1;
|
|
}
|
|
cell_index(cell_x, resolved_y)
|
|
}
|
|
}
|
|
// Cell has only chosen its row, not its column.
|
|
(Smart::Auto, Smart::Custom(cell_y)) => {
|
|
// Let's find the first column which has that row available.
|
|
let first_row_pos = cell_index(0, cell_y)?;
|
|
let last_row_pos = first_row_pos
|
|
.checked_add(columns)
|
|
.ok_or_else(|| eco_format!("cell position too large"))?;
|
|
|
|
(first_row_pos..last_row_pos)
|
|
.find(|possible_index| {
|
|
// Much like in the previous cases, we skip any occupied
|
|
// positions until we either reach an absent position
|
|
// (`Some(None)`) or an out of bounds position (`None`),
|
|
// in which case we can just expand the vector enough to
|
|
// place this cell. In either case, we found an available
|
|
// position.
|
|
!matches!(resolved_cells.get(*possible_index), Some(Some(_)))
|
|
})
|
|
.ok_or_else(|| {
|
|
eco_format!(
|
|
"cell could not be placed in row {cell_y} because it was full"
|
|
)
|
|
})
|
|
.hint("try specifying your cells in a different order")
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Performs grid layout.
|
|
pub struct GridLayouter<'a> {
|
|
/// The grid of cells.
|
|
pub(super) grid: &'a CellGrid,
|
|
/// The regions to layout children into.
|
|
pub(super) regions: Regions<'a>,
|
|
/// The inherited styles.
|
|
pub(super) styles: StyleChain<'a>,
|
|
/// Resolved column sizes.
|
|
pub(super) rcols: Vec<Abs>,
|
|
/// The sum of `rcols`.
|
|
pub(super) width: Abs,
|
|
/// Resolve row sizes, by region.
|
|
pub(super) rrows: Vec<Vec<RowPiece>>,
|
|
/// Rows in the current region.
|
|
pub(super) lrows: Vec<Row>,
|
|
/// The amount of unbreakable rows remaining to be laid out in the
|
|
/// current unbreakable row group. While this is positive, no region breaks
|
|
/// should occur.
|
|
pub(super) unbreakable_rows_left: usize,
|
|
/// Rowspans not yet laid out because not all of their spanned rows were
|
|
/// laid out yet.
|
|
pub(super) rowspans: Vec<Rowspan>,
|
|
/// The initial size of the current region before we started subtracting.
|
|
pub(super) initial: Size,
|
|
/// Frames for finished regions.
|
|
pub(super) finished: Vec<Frame>,
|
|
/// Whether this is an RTL grid.
|
|
pub(super) is_rtl: bool,
|
|
/// The simulated header height.
|
|
/// This field is reset in `layout_header` and properly updated by
|
|
/// `layout_auto_row` and `layout_relative_row`, and should not be read
|
|
/// before all header rows are fully laid out. It is usually fine because
|
|
/// header rows themselves are unbreakable, and unbreakable rows do not
|
|
/// need to read this field at all.
|
|
pub(super) header_height: Abs,
|
|
/// The simulated footer height for this region.
|
|
/// The simulation occurs before any rows are laid out for a region.
|
|
pub(super) footer_height: Abs,
|
|
/// The span of the grid element.
|
|
pub(super) span: Span,
|
|
}
|
|
|
|
/// Details about a resulting row piece.
|
|
#[derive(Debug)]
|
|
pub struct RowPiece {
|
|
/// The height of the segment.
|
|
pub height: Abs,
|
|
/// The index of the row.
|
|
pub y: usize,
|
|
}
|
|
|
|
/// Produced by initial row layout, auto and relative rows are already finished,
|
|
/// fractional rows not yet.
|
|
pub(super) enum Row {
|
|
/// Finished row frame of auto or relative row with y index.
|
|
/// The last parameter indicates whether or not this is the last region
|
|
/// where this row is laid out, and it can only be false when a row uses
|
|
/// `layout_multi_row`, which in turn is only used by breakable auto rows.
|
|
Frame(Frame, usize, bool),
|
|
/// Fractional row with y index.
|
|
Fr(Fr, usize),
|
|
}
|
|
|
|
impl Row {
|
|
/// Returns the `y` index of this row.
|
|
fn index(&self) -> usize {
|
|
match self {
|
|
Self::Frame(_, y, _) => *y,
|
|
Self::Fr(_, y) => *y,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<'a> GridLayouter<'a> {
|
|
/// Create a new grid layouter.
|
|
///
|
|
/// This prepares grid layout by unifying content and gutter tracks.
|
|
pub fn new(
|
|
grid: &'a CellGrid,
|
|
regions: Regions<'a>,
|
|
styles: StyleChain<'a>,
|
|
span: Span,
|
|
) -> Self {
|
|
// We use these regions for auto row measurement. Since at that moment,
|
|
// columns are already sized, we can enable horizontal expansion.
|
|
let mut regions = regions;
|
|
regions.expand = Axes::new(true, false);
|
|
|
|
Self {
|
|
grid,
|
|
regions,
|
|
styles,
|
|
rcols: vec![Abs::zero(); grid.cols.len()],
|
|
width: Abs::zero(),
|
|
rrows: vec![],
|
|
lrows: vec![],
|
|
unbreakable_rows_left: 0,
|
|
rowspans: vec![],
|
|
initial: regions.size,
|
|
finished: vec![],
|
|
is_rtl: TextElem::dir_in(styles) == Dir::RTL,
|
|
header_height: Abs::zero(),
|
|
footer_height: Abs::zero(),
|
|
span,
|
|
}
|
|
}
|
|
|
|
/// Determines the columns sizes and then layouts the grid row-by-row.
|
|
pub fn layout(mut self, engine: &mut Engine) -> SourceResult<Fragment> {
|
|
self.measure_columns(engine)?;
|
|
|
|
if let Some(Repeatable::Repeated(footer)) = &self.grid.footer {
|
|
// Ensure rows in the first region will be aware of the possible
|
|
// presence of the footer.
|
|
self.prepare_footer(footer, engine)?;
|
|
if matches!(self.grid.header, None | Some(Repeatable::NotRepeated(_))) {
|
|
// No repeatable header, so we won't subtract it later.
|
|
self.regions.size.y -= self.footer_height;
|
|
}
|
|
}
|
|
|
|
for y in 0..self.grid.rows.len() {
|
|
if let Some(Repeatable::Repeated(header)) = &self.grid.header {
|
|
if y < header.end {
|
|
if y == 0 {
|
|
self.layout_header(header, engine)?;
|
|
self.regions.size.y -= self.footer_height;
|
|
}
|
|
// Skip header rows during normal layout.
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if let Some(Repeatable::Repeated(footer)) = &self.grid.footer {
|
|
if y >= footer.start {
|
|
if y == footer.start {
|
|
self.layout_footer(footer, engine)?;
|
|
}
|
|
continue;
|
|
}
|
|
}
|
|
|
|
self.layout_row(y, engine)?;
|
|
}
|
|
|
|
self.finish_region(engine)?;
|
|
|
|
// Layout any missing rowspans.
|
|
// There are only two possibilities for rowspans not yet laid out
|
|
// (usually, a rowspan is laid out as soon as its last row, or any row
|
|
// after it, is laid out):
|
|
// 1. The rowspan was fully empty and only spanned fully empty auto
|
|
// rows, which were all prevented from being laid out. Those rowspans
|
|
// are ignored by 'layout_rowspan', and are not of any concern.
|
|
//
|
|
// 2. The rowspan's last row was an auto row at the last region which
|
|
// was not laid out, and no other rows were laid out after it. Those
|
|
// might still need to be laid out, so we check for them.
|
|
for rowspan in std::mem::take(&mut self.rowspans) {
|
|
self.layout_rowspan(rowspan, None, engine)?;
|
|
}
|
|
|
|
self.render_fills_strokes()
|
|
}
|
|
|
|
/// Layout the given row.
|
|
fn layout_row(&mut self, y: usize, engine: &mut Engine) -> SourceResult<()> {
|
|
// Skip to next region if current one is full, but only for content
|
|
// rows, not for gutter rows, and only if we aren't laying out an
|
|
// unbreakable group of rows.
|
|
let is_content_row = !self.grid.is_gutter_track(y);
|
|
if self.unbreakable_rows_left == 0 && self.regions.is_full() && is_content_row {
|
|
self.finish_region(engine)?;
|
|
}
|
|
|
|
if is_content_row {
|
|
// Gutter rows have no rowspans or possibly unbreakable cells.
|
|
self.check_for_rowspans(y);
|
|
self.check_for_unbreakable_rows(y, engine)?;
|
|
}
|
|
|
|
// Don't layout gutter rows at the top of a region.
|
|
if is_content_row || !self.lrows.is_empty() {
|
|
match self.grid.rows[y] {
|
|
Sizing::Auto => self.layout_auto_row(engine, y)?,
|
|
Sizing::Rel(v) => self.layout_relative_row(engine, v, y)?,
|
|
Sizing::Fr(v) => self.lrows.push(Row::Fr(v, y)),
|
|
}
|
|
}
|
|
|
|
self.unbreakable_rows_left = self.unbreakable_rows_left.saturating_sub(1);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Add lines and backgrounds.
|
|
fn render_fills_strokes(mut self) -> SourceResult<Fragment> {
|
|
let mut finished = std::mem::take(&mut self.finished);
|
|
let frame_amount = finished.len();
|
|
for ((frame_index, frame), rows) in
|
|
finished.iter_mut().enumerate().zip(&self.rrows)
|
|
{
|
|
if self.rcols.is_empty() || rows.is_empty() {
|
|
continue;
|
|
}
|
|
|
|
// Render grid lines.
|
|
// We collect lines into a vector before rendering so we can sort
|
|
// them based on thickness, such that the lines with largest
|
|
// thickness are drawn on top; and also so we can prepend all of
|
|
// them at once in the frame, as calling prepend() for each line,
|
|
// and thus pushing all frame items forward each time, would result
|
|
// in quadratic complexity.
|
|
let mut lines = vec![];
|
|
|
|
// Which line position to look for in the list of lines for a
|
|
// track, such that placing lines with those positions will
|
|
// correspond to placing them before the given track index.
|
|
//
|
|
// If the index represents a gutter track, this means the list of
|
|
// lines will actually correspond to the list of lines in the
|
|
// previous index, so we must look for lines positioned after the
|
|
// previous index, and not before, to determine which lines should
|
|
// be placed before gutter.
|
|
//
|
|
// Note that the maximum index is always an odd number when
|
|
// there's gutter, so we must check for it to ensure we don't give
|
|
// it the same treatment as a line before a gutter track.
|
|
let expected_line_position = |index, is_max_index: bool| {
|
|
if self.grid.is_gutter_track(index) && !is_max_index {
|
|
LinePosition::After
|
|
} else {
|
|
LinePosition::Before
|
|
}
|
|
};
|
|
|
|
// Render vertical lines.
|
|
// Render them first so horizontal lines have priority later.
|
|
for (x, dx) in points(self.rcols.iter().copied()).enumerate() {
|
|
let dx = if self.is_rtl { self.width - dx } else { dx };
|
|
let is_end_border = x == self.grid.cols.len();
|
|
let expected_vline_position = expected_line_position(x, is_end_border);
|
|
|
|
let vlines_at_column = self
|
|
.grid
|
|
.vlines
|
|
.get(if !self.grid.has_gutter {
|
|
x
|
|
} else if is_end_border {
|
|
// The end border has its own vector of lines, but
|
|
// dividing it by 2 and flooring would give us the
|
|
// vector of lines with the index of the last column.
|
|
// Add 1 so we get the border's lines.
|
|
x / 2 + 1
|
|
} else {
|
|
// If x is a gutter column, this will round down to the
|
|
// index of the previous content column, which is
|
|
// intentional - the only lines which can appear before
|
|
// a gutter column are lines for the previous column
|
|
// marked with "LinePosition::After". Therefore, we get
|
|
// the previous column's lines. Worry not, as
|
|
// 'generate_line_segments' will correctly filter lines
|
|
// based on their LinePosition for us.
|
|
//
|
|
// If x is a content column, this will correctly return
|
|
// its index before applying gutters, so nothing
|
|
// special here (lines with "LinePosition::After" would
|
|
// then be ignored for this column, as we are drawing
|
|
// lines before it, not after).
|
|
x / 2
|
|
})
|
|
.into_iter()
|
|
.flatten()
|
|
.filter(|line| line.position == expected_vline_position);
|
|
|
|
let tracks = rows.iter().map(|row| (row.y, row.height));
|
|
|
|
// Determine all different line segments we have to draw in
|
|
// this column, and convert them to points and shapes.
|
|
//
|
|
// Even a single, uniform line might generate more than one
|
|
// segment, if it happens to cross a colspan (over which it
|
|
// must not be drawn).
|
|
let segments = generate_line_segments(
|
|
self.grid,
|
|
tracks,
|
|
x,
|
|
vlines_at_column,
|
|
vline_stroke_at_row,
|
|
)
|
|
.map(|segment| {
|
|
let LineSegment { stroke, offset: dy, length, priority } = segment;
|
|
let stroke = (*stroke).clone().unwrap_or_default();
|
|
let thickness = stroke.thickness;
|
|
let half = thickness / 2.0;
|
|
let target = Point::with_y(length + thickness);
|
|
let vline = Geometry::Line(target).stroked(stroke);
|
|
(
|
|
thickness,
|
|
priority,
|
|
Point::new(dx, dy - half),
|
|
FrameItem::Shape(vline, self.span),
|
|
)
|
|
});
|
|
|
|
lines.extend(segments);
|
|
}
|
|
|
|
// Render horizontal lines.
|
|
// They are rendered second as they default to appearing on top.
|
|
// First, calculate their offsets from the top of the frame.
|
|
let hline_offsets = points(rows.iter().map(|piece| piece.height));
|
|
|
|
// Additionally, determine their indices (the indices of the
|
|
// rows they are drawn on top of). In principle, this will
|
|
// correspond to the rows' indices directly, except for the
|
|
// last hline index, which must be (amount of rows) in order to
|
|
// draw the table's bottom border.
|
|
let hline_indices = rows
|
|
.iter()
|
|
.map(|piece| piece.y)
|
|
.chain(std::iter::once(self.grid.rows.len()));
|
|
|
|
// Converts a row to the corresponding index in the vector of
|
|
// hlines.
|
|
let hline_index_of_row = |y: usize| {
|
|
if !self.grid.has_gutter {
|
|
y
|
|
} else if y == self.grid.rows.len() {
|
|
y / 2 + 1
|
|
} else {
|
|
// Check the vlines loop for an explanation regarding
|
|
// these index operations.
|
|
y / 2
|
|
}
|
|
};
|
|
|
|
let get_hlines_at = |y| {
|
|
self.grid
|
|
.hlines
|
|
.get(hline_index_of_row(y))
|
|
.map(Vec::as_slice)
|
|
.unwrap_or(&[])
|
|
};
|
|
|
|
let mut prev_y = None;
|
|
for (y, dy) in hline_indices.zip(hline_offsets) {
|
|
// Position of lines below the row index in the previous iteration.
|
|
let expected_prev_line_position = prev_y
|
|
.map(|prev_y| {
|
|
expected_line_position(
|
|
prev_y + 1,
|
|
prev_y + 1 == self.grid.rows.len(),
|
|
)
|
|
})
|
|
.unwrap_or(LinePosition::Before);
|
|
|
|
// If some grid rows were omitted between the previous resolved
|
|
// row and the current one, we ensure lines below the previous
|
|
// row don't "disappear" and are considered, albeit with less
|
|
// priority. However, don't do this when we're below a header,
|
|
// as it must have more priority instead of less, so it is
|
|
// chained later instead of before.
|
|
let prev_lines = prev_y
|
|
.filter(|prev_y| {
|
|
prev_y + 1 != y
|
|
&& !self
|
|
.grid
|
|
.header
|
|
.as_ref()
|
|
.and_then(Repeatable::as_repeated)
|
|
.is_some_and(|header| prev_y + 1 == header.end)
|
|
})
|
|
.map(|prev_y| get_hlines_at(prev_y + 1))
|
|
.unwrap_or(&[]);
|
|
|
|
let expected_hline_position =
|
|
expected_line_position(y, y == self.grid.rows.len());
|
|
|
|
let hlines_at_y = get_hlines_at(y)
|
|
.iter()
|
|
.filter(|line| line.position == expected_hline_position);
|
|
|
|
let top_border_hlines = if prev_y.is_none() && y != 0 {
|
|
// For lines at the top of the region, give priority to
|
|
// the lines at the top border.
|
|
get_hlines_at(0)
|
|
} else {
|
|
&[]
|
|
};
|
|
|
|
// The header lines, if any, will correspond to the lines under
|
|
// the previous row, so they function similarly to 'prev_lines'.
|
|
let expected_header_line_position = expected_prev_line_position;
|
|
let header_hlines = if let Some((Repeatable::Repeated(header), prev_y)) =
|
|
self.grid.header.as_ref().zip(prev_y)
|
|
{
|
|
if prev_y + 1 != y
|
|
&& prev_y + 1 == header.end
|
|
&& !self.grid.has_gutter
|
|
{
|
|
// For lines below a header, give priority to the
|
|
// lines originally below the header rather than
|
|
// the lines of what's below the repeated header.
|
|
// However, no need to do that when we're laying
|
|
// out the header for the first time, since the
|
|
// lines being normally laid out then will be
|
|
// precisely the lines below the header.
|
|
//
|
|
// Additionally, we don't append header lines when
|
|
// gutter is enabled, since, in that case, there will
|
|
// be a gutter row between header and content, so no
|
|
// lines should overlap.
|
|
get_hlines_at(header.end)
|
|
} else {
|
|
&[]
|
|
}
|
|
} else {
|
|
&[]
|
|
};
|
|
|
|
// The effective hlines to be considered at this row index are
|
|
// chained in order of increasing priority:
|
|
// 1. Lines from the row right above us, if needed;
|
|
// 2. Lines from the current row (usually, only those are
|
|
// present);
|
|
// 3. Lines from the top border (above the top cells, hence
|
|
// 'before' position only);
|
|
// 4. Lines from the header above us, if present.
|
|
let hlines_at_row =
|
|
prev_lines
|
|
.iter()
|
|
.filter(|line| line.position == expected_prev_line_position)
|
|
.chain(hlines_at_y)
|
|
.chain(
|
|
top_border_hlines
|
|
.iter()
|
|
.filter(|line| line.position == LinePosition::Before),
|
|
)
|
|
.chain(header_hlines.iter().filter(|line| {
|
|
line.position == expected_header_line_position
|
|
}));
|
|
|
|
let tracks = self.rcols.iter().copied().enumerate();
|
|
|
|
// Normally, given an hline above row y, the row above it is
|
|
// 'y - 1' (if y > 0). However, sometimes that's not true, for
|
|
// example if 'y - 1' is in a previous region, or if 'y - 1'
|
|
// was an empty auto row which was removed. Therefore, we tell
|
|
// the hlines at this index which row is actually above them in
|
|
// the laid out region so they can include that row's bottom
|
|
// strokes in the folding process.
|
|
let local_top_y = prev_y;
|
|
|
|
// When we're in the last region, the bottom border stroke
|
|
// doesn't necessarily gain priority like it does in previous
|
|
// regions.
|
|
let in_last_region = frame_index + 1 == frame_amount;
|
|
|
|
// Determine all different line segments we have to draw in
|
|
// this row, and convert them to points and shapes.
|
|
let segments = generate_line_segments(
|
|
self.grid,
|
|
tracks,
|
|
y,
|
|
hlines_at_row,
|
|
|grid, y, x, stroke| {
|
|
hline_stroke_at_column(
|
|
grid,
|
|
rows,
|
|
local_top_y,
|
|
in_last_region,
|
|
y,
|
|
x,
|
|
stroke,
|
|
)
|
|
},
|
|
)
|
|
.map(|segment| {
|
|
let LineSegment { stroke, offset: dx, length, priority } = segment;
|
|
let stroke = (*stroke).clone().unwrap_or_default();
|
|
let thickness = stroke.thickness;
|
|
let half = thickness / 2.0;
|
|
let dx = if self.is_rtl { self.width - dx - length } else { dx };
|
|
let target = Point::with_x(length + thickness);
|
|
let hline = Geometry::Line(target).stroked(stroke);
|
|
(
|
|
thickness,
|
|
priority,
|
|
Point::new(dx - half, dy),
|
|
FrameItem::Shape(hline, self.span),
|
|
)
|
|
});
|
|
|
|
// Draw later (after we sort all lines below.)
|
|
lines.extend(segments);
|
|
|
|
prev_y = Some(y);
|
|
}
|
|
|
|
// Sort by increasing thickness, so that we draw larger strokes
|
|
// on top. When the thickness is the same, sort by priority.
|
|
//
|
|
// Sorting by thickness avoids layering problems where a smaller
|
|
// hline appears "inside" a larger vline. When both have the same
|
|
// size, hlines are drawn on top (since the sort is stable, and
|
|
// they are pushed later).
|
|
lines.sort_by_key(|(thickness, priority, ..)| (*thickness, *priority));
|
|
|
|
// Render cell backgrounds.
|
|
// We collect them into a vector so they can all be prepended at
|
|
// once to the frame, together with lines.
|
|
let mut fills = vec![];
|
|
|
|
// Reverse with RTL so that later columns start first.
|
|
let mut dx = Abs::zero();
|
|
for (x, &col) in self.rcols.iter().enumerate().rev_if(self.is_rtl) {
|
|
let mut dy = Abs::zero();
|
|
for row in rows {
|
|
// We want to only draw the fill starting at the parent
|
|
// positions of cells. However, sometimes the parent
|
|
// position is absent from the current region, either
|
|
// because the first few rows of a rowspan were empty auto
|
|
// rows and thus removed from layout, or because the parent
|
|
// cell was in a previous region (in which case we'd want
|
|
// to draw its fill again, in the current region).
|
|
// Therefore, we first analyze the parent position to see
|
|
// if the current row would be the first row spanned by the
|
|
// parent cell in this region. If so, this means we have to
|
|
// start drawing the cell's fill here. If not, we ignore
|
|
// the position `(x, row.y)`, as its fill will already have
|
|
// been rendered before.
|
|
//
|
|
// Note: In the case of gutter rows, we have to check the
|
|
// row below before discarding them fully, because a
|
|
// gutter row might be the first row spanned by a rowspan
|
|
// in this region (e.g. if the first row was empty and
|
|
// therefore removed), so its fill could start in that
|
|
// gutter row. That's why we use
|
|
// 'effective_parent_cell_position'.
|
|
let parent = self
|
|
.grid
|
|
.effective_parent_cell_position(x, row.y)
|
|
.filter(|parent| {
|
|
// Ensure this is the first column spanned by the
|
|
// cell before drawing its fill, otherwise we
|
|
// already rendered its fill in a previous
|
|
// iteration of the outer loop (and/or this is a
|
|
// gutter column, which we ignore).
|
|
//
|
|
// Additionally, we should only draw the fill when
|
|
// this row is the local parent Y for this cell,
|
|
// that is, the first row spanned by the cell's
|
|
// parent in this region, because if the parent
|
|
// cell's fill was already drawn in a previous
|
|
// region, we must render it again in later regions
|
|
// spanned by that cell. Note that said condition
|
|
// always holds when the current cell has a rowspan
|
|
// of 1 and we're not currently at a gutter row.
|
|
parent.x == x
|
|
&& (parent.y == row.y
|
|
|| rows
|
|
.iter()
|
|
.find(|row| row.y >= parent.y)
|
|
.is_some_and(|first_spanned_row| {
|
|
first_spanned_row.y == row.y
|
|
}))
|
|
});
|
|
|
|
if let Some(parent) = parent {
|
|
let cell = self.grid.cell(parent.x, parent.y).unwrap();
|
|
let fill = cell.fill.clone();
|
|
if let Some(fill) = fill {
|
|
let rowspan = self.grid.effective_rowspan_of_cell(cell);
|
|
let height = if rowspan == 1 {
|
|
row.height
|
|
} else {
|
|
rows.iter()
|
|
.filter(|row| {
|
|
(parent.y..parent.y + rowspan).contains(&row.y)
|
|
})
|
|
.map(|row| row.height)
|
|
.sum()
|
|
};
|
|
let width = self.cell_spanned_width(cell, x);
|
|
// In the grid, cell colspans expand to the right,
|
|
// so we're at the leftmost (lowest 'x') column
|
|
// spanned by the cell. However, in RTL, cells
|
|
// expand to the left. Therefore, without the
|
|
// offset below, cell fills would start at the
|
|
// rightmost visual position of a cell and extend
|
|
// over to unrelated columns to the right in RTL.
|
|
// We avoid this by ensuring the fill starts at the
|
|
// very left of the cell, even with colspan > 1.
|
|
let offset =
|
|
if self.is_rtl { -width + col } else { Abs::zero() };
|
|
let pos = Point::new(dx + offset, dy);
|
|
let size = Size::new(width, height);
|
|
let rect = Geometry::Rect(size).filled(fill);
|
|
fills.push((pos, FrameItem::Shape(rect, self.span)));
|
|
}
|
|
}
|
|
dy += row.height;
|
|
}
|
|
dx += col;
|
|
}
|
|
|
|
// Now we render each fill and stroke by prepending to the frame,
|
|
// such that both appear below cell contents. Fills come first so
|
|
// that they appear below lines.
|
|
frame.prepend_multiple(
|
|
fills
|
|
.into_iter()
|
|
.chain(lines.into_iter().map(|(_, _, point, shape)| (point, shape))),
|
|
);
|
|
}
|
|
|
|
Ok(Fragment::frames(finished))
|
|
}
|
|
|
|
/// Determine all column sizes.
|
|
fn measure_columns(&mut self, engine: &mut Engine) -> SourceResult<()> {
|
|
// Sum of sizes of resolved relative tracks.
|
|
let mut rel = Abs::zero();
|
|
|
|
// Sum of fractions of all fractional tracks.
|
|
let mut fr = Fr::zero();
|
|
|
|
// Resolve the size of all relative columns and compute the sum of all
|
|
// fractional tracks.
|
|
for (&col, rcol) in self.grid.cols.iter().zip(&mut self.rcols) {
|
|
match col {
|
|
Sizing::Auto => {}
|
|
Sizing::Rel(v) => {
|
|
let resolved =
|
|
v.resolve(self.styles).relative_to(self.regions.base().x);
|
|
*rcol = resolved;
|
|
rel += resolved;
|
|
}
|
|
Sizing::Fr(v) => fr += v,
|
|
}
|
|
}
|
|
|
|
// Size that is not used by fixed-size columns.
|
|
let available = self.regions.size.x - rel;
|
|
if available >= Abs::zero() {
|
|
// Determine size of auto columns.
|
|
let (auto, count) = self.measure_auto_columns(engine, available)?;
|
|
|
|
// If there is remaining space, distribute it to fractional columns,
|
|
// otherwise shrink auto columns.
|
|
let remaining = available - auto;
|
|
if remaining >= Abs::zero() {
|
|
self.grow_fractional_columns(remaining, fr);
|
|
} else {
|
|
self.shrink_auto_columns(available, count);
|
|
}
|
|
}
|
|
|
|
// Sum up the resolved column sizes once here.
|
|
self.width = self.rcols.iter().sum();
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Total width spanned by the cell (among resolved columns).
|
|
/// Includes spanned gutter columns.
|
|
pub(super) fn cell_spanned_width(&self, cell: &Cell, x: usize) -> Abs {
|
|
let colspan = self.grid.effective_colspan_of_cell(cell);
|
|
self.rcols.iter().skip(x).take(colspan).sum()
|
|
}
|
|
|
|
/// Measure the size that is available to auto columns.
|
|
fn measure_auto_columns(
|
|
&mut self,
|
|
engine: &mut Engine,
|
|
available: Abs,
|
|
) -> SourceResult<(Abs, usize)> {
|
|
let mut auto = Abs::zero();
|
|
let mut count = 0;
|
|
let all_frac_cols = self
|
|
.grid
|
|
.cols
|
|
.iter()
|
|
.enumerate()
|
|
.filter(|(_, col)| col.is_fractional())
|
|
.map(|(x, _)| x)
|
|
.collect::<Vec<_>>();
|
|
|
|
// Determine size of auto columns by laying out all cells in those
|
|
// columns, measuring them and finding the largest one.
|
|
for (x, &col) in self.grid.cols.iter().enumerate() {
|
|
if col != Sizing::Auto {
|
|
continue;
|
|
}
|
|
|
|
let mut resolved = Abs::zero();
|
|
for y in 0..self.grid.rows.len() {
|
|
// We get the parent cell in case this is a merged position.
|
|
let Some(parent) = self.grid.parent_cell_position(x, y) else {
|
|
continue;
|
|
};
|
|
if parent.y != y {
|
|
// Don't check the width of rowspans more than once.
|
|
continue;
|
|
}
|
|
let cell = self.grid.cell(parent.x, parent.y).unwrap();
|
|
let colspan = self.grid.effective_colspan_of_cell(cell);
|
|
if colspan > 1 {
|
|
let last_spanned_auto_col = self
|
|
.grid
|
|
.cols
|
|
.iter()
|
|
.enumerate()
|
|
.skip(parent.x)
|
|
.take(colspan)
|
|
.rev()
|
|
.find(|(_, col)| **col == Sizing::Auto)
|
|
.map(|(x, _)| x);
|
|
|
|
if last_spanned_auto_col != Some(x) {
|
|
// A colspan only affects the size of the last spanned
|
|
// auto column.
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if colspan > 1
|
|
&& self.regions.size.x.is_finite()
|
|
&& !all_frac_cols.is_empty()
|
|
&& all_frac_cols
|
|
.iter()
|
|
.all(|x| (parent.x..parent.x + colspan).contains(x))
|
|
{
|
|
// Additionally, as a heuristic, a colspan won't affect the
|
|
// size of auto columns if it already spans all fractional
|
|
// columns, since those would already expand to provide all
|
|
// remaining available after auto column sizing to that
|
|
// cell. However, this heuristic is only valid in finite
|
|
// regions (pages without 'auto' width), since otherwise
|
|
// the fractional columns don't expand at all.
|
|
continue;
|
|
}
|
|
|
|
// Sum the heights of spanned rows to find the expected
|
|
// available height for the cell, unless it spans a fractional
|
|
// or auto column.
|
|
let rowspan = self.grid.effective_rowspan_of_cell(cell);
|
|
let height = self
|
|
.grid
|
|
.rows
|
|
.iter()
|
|
.skip(y)
|
|
.take(rowspan)
|
|
.try_fold(Abs::zero(), |acc, col| {
|
|
// For relative rows, we can already resolve the correct
|
|
// base and for auto and fr we could only guess anyway.
|
|
match col {
|
|
Sizing::Rel(v) => Some(
|
|
acc + v
|
|
.resolve(self.styles)
|
|
.relative_to(self.regions.base().y),
|
|
),
|
|
_ => None,
|
|
}
|
|
})
|
|
.unwrap_or_else(|| self.regions.base().y);
|
|
|
|
// Don't expand this auto column more than the cell actually
|
|
// needs. To do this, we check how much the other, previously
|
|
// resolved columns provide to the cell in terms of width
|
|
// (if it is a colspan), and subtract this from its expected
|
|
// width when comparing with other cells in this column. Note
|
|
// that, since this is the last auto column spanned by this
|
|
// cell, all other auto columns will already have been resolved
|
|
// and will be considered.
|
|
// Only fractional columns will be excluded from this
|
|
// calculation, which can lead to auto columns being expanded
|
|
// unnecessarily when cells span both a fractional column and
|
|
// an auto column. One mitigation for this is the heuristic
|
|
// used above to not expand the last auto column spanned by a
|
|
// cell if it spans all fractional columns in a finite region.
|
|
let already_covered_width = self.cell_spanned_width(cell, parent.x);
|
|
|
|
let size = Size::new(available, height);
|
|
let pod = Regions::one(size, Axes::splat(false));
|
|
let frame = cell.measure(engine, self.styles, pod)?.into_frame();
|
|
resolved.set_max(frame.width() - already_covered_width);
|
|
}
|
|
|
|
self.rcols[x] = resolved;
|
|
auto += resolved;
|
|
count += 1;
|
|
}
|
|
|
|
Ok((auto, count))
|
|
}
|
|
|
|
/// Distribute remaining space to fractional columns.
|
|
fn grow_fractional_columns(&mut self, remaining: Abs, fr: Fr) {
|
|
if fr.is_zero() {
|
|
return;
|
|
}
|
|
|
|
for (&col, rcol) in self.grid.cols.iter().zip(&mut self.rcols) {
|
|
if let Sizing::Fr(v) = col {
|
|
*rcol = v.share(fr, remaining);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Redistribute space to auto columns so that each gets a fair share.
|
|
fn shrink_auto_columns(&mut self, available: Abs, count: usize) {
|
|
let mut last;
|
|
let mut fair = -Abs::inf();
|
|
let mut redistribute = available;
|
|
let mut overlarge = count;
|
|
let mut changed = true;
|
|
|
|
// Iteratively remove columns that don't need to be shrunk.
|
|
while changed && overlarge > 0 {
|
|
changed = false;
|
|
last = fair;
|
|
fair = redistribute / (overlarge as f64);
|
|
|
|
for (&col, &rcol) in self.grid.cols.iter().zip(&self.rcols) {
|
|
// Remove an auto column if it is not overlarge (rcol <= fair),
|
|
// but also hasn't already been removed (rcol > last).
|
|
if col == Sizing::Auto && rcol <= fair && rcol > last {
|
|
redistribute -= rcol;
|
|
overlarge -= 1;
|
|
changed = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Redistribute space fairly among overlarge columns.
|
|
for (&col, rcol) in self.grid.cols.iter().zip(&mut self.rcols) {
|
|
if col == Sizing::Auto && *rcol > fair {
|
|
*rcol = fair;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Layout a row with automatic height. Such a row may break across multiple
|
|
/// regions.
|
|
fn layout_auto_row(&mut self, engine: &mut Engine, y: usize) -> SourceResult<()> {
|
|
// Determine the size for each region of the row. If the first region
|
|
// ends up empty for some column, skip the region and remeasure.
|
|
let mut resolved = match self.measure_auto_row(
|
|
engine,
|
|
y,
|
|
true,
|
|
self.unbreakable_rows_left,
|
|
None,
|
|
)? {
|
|
Some(resolved) => resolved,
|
|
None => {
|
|
self.finish_region(engine)?;
|
|
self.measure_auto_row(engine, y, false, self.unbreakable_rows_left, None)?
|
|
.unwrap()
|
|
}
|
|
};
|
|
|
|
// Nothing to layout.
|
|
if resolved.is_empty() {
|
|
return Ok(());
|
|
}
|
|
|
|
// Layout into a single region.
|
|
if let &[first] = resolved.as_slice() {
|
|
let frame = self.layout_single_row(engine, first, y)?;
|
|
self.push_row(frame, y, true);
|
|
|
|
if self
|
|
.grid
|
|
.header
|
|
.as_ref()
|
|
.and_then(Repeatable::as_repeated)
|
|
.is_some_and(|header| y < header.end)
|
|
{
|
|
// Add to header height.
|
|
self.header_height += first;
|
|
}
|
|
|
|
return Ok(());
|
|
}
|
|
|
|
// Expand all but the last region.
|
|
// Skip the first region if the space is eaten up by an fr row.
|
|
let len = resolved.len();
|
|
for ((i, region), target) in self
|
|
.regions
|
|
.iter()
|
|
.enumerate()
|
|
.zip(&mut resolved[..len - 1])
|
|
.skip(self.lrows.iter().any(|row| matches!(row, Row::Fr(..))) as usize)
|
|
{
|
|
// Subtract header and footer heights from the region height when
|
|
// it's not the first.
|
|
target.set_max(
|
|
region.y
|
|
- if i > 0 {
|
|
self.header_height + self.footer_height
|
|
} else {
|
|
Abs::zero()
|
|
},
|
|
);
|
|
}
|
|
|
|
// Layout into multiple regions.
|
|
let fragment = self.layout_multi_row(engine, &resolved, y)?;
|
|
let len = fragment.len();
|
|
for (i, frame) in fragment.into_iter().enumerate() {
|
|
self.push_row(frame, y, i + 1 == len);
|
|
if i + 1 < len {
|
|
self.finish_region(engine)?;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Measure the regions sizes of an auto row. The option is always `Some(_)`
|
|
/// if `can_skip` is false.
|
|
/// If `unbreakable_rows_left` is positive, this function shall only return
|
|
/// a single frame. Useful when an unbreakable rowspan crosses this auto
|
|
/// row.
|
|
/// The `row_group_data` option is used within the unbreakable row group
|
|
/// simulator to predict the height of the auto row if previous rows in the
|
|
/// group were placed in the same region.
|
|
pub(super) fn measure_auto_row(
|
|
&self,
|
|
engine: &mut Engine,
|
|
y: usize,
|
|
can_skip: bool,
|
|
unbreakable_rows_left: usize,
|
|
row_group_data: Option<&UnbreakableRowGroup>,
|
|
) -> SourceResult<Option<Vec<Abs>>> {
|
|
let breakable = unbreakable_rows_left == 0;
|
|
let mut resolved: Vec<Abs> = vec![];
|
|
let mut pending_rowspans: Vec<(usize, usize, Vec<Abs>)> = vec![];
|
|
|
|
for x in 0..self.rcols.len() {
|
|
// Get the parent cell in case this is a merged position.
|
|
let Some(parent) = self.grid.parent_cell_position(x, y) else {
|
|
// Skip gutter columns.
|
|
continue;
|
|
};
|
|
if parent.x != x {
|
|
// Only check the height of a colspan once.
|
|
continue;
|
|
}
|
|
// The parent cell is never a gutter or merged position.
|
|
let cell = self.grid.cell(parent.x, parent.y).unwrap();
|
|
let rowspan = self.grid.effective_rowspan_of_cell(cell);
|
|
|
|
if rowspan > 1 {
|
|
let last_spanned_auto_row = self
|
|
.grid
|
|
.rows
|
|
.iter()
|
|
.enumerate()
|
|
.skip(parent.y)
|
|
.take(rowspan)
|
|
.rev()
|
|
.find(|(_, &row)| row == Sizing::Auto)
|
|
.map(|(y, _)| y);
|
|
|
|
if last_spanned_auto_row != Some(y) {
|
|
// A rowspan should only affect the height of its last
|
|
// spanned auto row.
|
|
continue;
|
|
}
|
|
}
|
|
|
|
let measurement_data = self.prepare_auto_row_cell_measurement(
|
|
parent,
|
|
cell,
|
|
breakable,
|
|
row_group_data,
|
|
);
|
|
let size = Axes::new(measurement_data.width, measurement_data.height);
|
|
let backlog =
|
|
measurement_data.backlog.unwrap_or(&measurement_data.custom_backlog);
|
|
|
|
let pod = if !breakable {
|
|
// Force cell to fit into a single region when the row is
|
|
// unbreakable, even when it is a breakable rowspan, as a best
|
|
// effort.
|
|
let mut pod = Regions::one(size, self.regions.expand);
|
|
pod.full = measurement_data.full;
|
|
|
|
if measurement_data.frames_in_previous_regions > 0 {
|
|
// Best effort to conciliate a breakable rowspan which
|
|
// started at a previous region going through an
|
|
// unbreakable auto row. Ensure it goes through previously
|
|
// laid out regions, but stops at this one when measuring.
|
|
pod.backlog = backlog;
|
|
}
|
|
|
|
pod
|
|
} else {
|
|
// This row is breakable, so measure the cell normally, with
|
|
// the initial height and backlog determined previously.
|
|
let mut pod = self.regions;
|
|
pod.size = size;
|
|
pod.backlog = backlog;
|
|
pod.full = measurement_data.full;
|
|
pod.last = measurement_data.last;
|
|
|
|
pod
|
|
};
|
|
|
|
let frames = cell.measure(engine, self.styles, pod)?.into_frames();
|
|
|
|
// Skip the first region if one cell in it is empty. Then,
|
|
// remeasure.
|
|
if let Some([first, rest @ ..]) =
|
|
frames.get(measurement_data.frames_in_previous_regions..)
|
|
{
|
|
if can_skip
|
|
&& breakable
|
|
&& first.is_empty()
|
|
&& rest.iter().any(|frame| !frame.is_empty())
|
|
{
|
|
return Ok(None);
|
|
}
|
|
}
|
|
|
|
// Skip frames from previous regions if applicable.
|
|
let mut sizes = frames
|
|
.iter()
|
|
.skip(measurement_data.frames_in_previous_regions)
|
|
.map(|frame| frame.height())
|
|
.collect::<Vec<_>>();
|
|
|
|
// Don't expand this row more than the cell needs.
|
|
// To figure out how much height the cell needs, we must first
|
|
// subtract, from the cell's expected height, the already resolved
|
|
// heights of its spanned rows. Note that this is the last spanned
|
|
// auto row, so all previous auto rows were already resolved, as
|
|
// well as fractional rows in previous regions.
|
|
// Additionally, we subtract the heights of fixed-size rows which
|
|
// weren't laid out yet, since those heights won't change in
|
|
// principle.
|
|
// Upcoming fractional rows are ignored.
|
|
// Upcoming gutter rows might be removed, so we need to simulate
|
|
// them.
|
|
if rowspan > 1 {
|
|
let should_simulate = self.prepare_rowspan_sizes(
|
|
y,
|
|
&mut sizes,
|
|
cell,
|
|
parent.y,
|
|
rowspan,
|
|
unbreakable_rows_left,
|
|
&measurement_data,
|
|
);
|
|
|
|
if should_simulate {
|
|
// Rowspan spans gutter and is breakable. We'll need to
|
|
// run a simulation to predict how much this auto row needs
|
|
// to expand so that the rowspan's contents fit into the
|
|
// table.
|
|
pending_rowspans.push((parent.y, rowspan, sizes));
|
|
continue;
|
|
}
|
|
}
|
|
|
|
let mut sizes = sizes.into_iter();
|
|
|
|
for (target, size) in resolved.iter_mut().zip(&mut sizes) {
|
|
target.set_max(size);
|
|
}
|
|
|
|
// New heights are maximal by virtue of being new. Note that
|
|
// this extend only uses the rest of the sizes iterator.
|
|
resolved.extend(sizes);
|
|
}
|
|
|
|
// Simulate the upcoming regions in order to predict how much we need
|
|
// to expand this auto row for rowspans which span gutter.
|
|
if !pending_rowspans.is_empty() {
|
|
self.simulate_and_measure_rowspans_in_auto_row(
|
|
y,
|
|
&mut resolved,
|
|
&pending_rowspans,
|
|
unbreakable_rows_left,
|
|
row_group_data,
|
|
engine,
|
|
)?;
|
|
}
|
|
|
|
debug_assert!(breakable || resolved.len() <= 1);
|
|
|
|
Ok(Some(resolved))
|
|
}
|
|
|
|
/// Layout a row with relative height. Such a row cannot break across
|
|
/// multiple regions, but it may force a region break.
|
|
fn layout_relative_row(
|
|
&mut self,
|
|
engine: &mut Engine,
|
|
v: Rel<Length>,
|
|
y: usize,
|
|
) -> SourceResult<()> {
|
|
let resolved = v.resolve(self.styles).relative_to(self.regions.base().y);
|
|
let frame = self.layout_single_row(engine, resolved, y)?;
|
|
|
|
if self
|
|
.grid
|
|
.header
|
|
.as_ref()
|
|
.and_then(Repeatable::as_repeated)
|
|
.is_some_and(|header| y < header.end)
|
|
{
|
|
// Add to header height.
|
|
self.header_height += resolved;
|
|
}
|
|
|
|
// Skip to fitting region, but only if we aren't part of an unbreakable
|
|
// row group. We use 'in_last_with_offset' so our 'in_last' call
|
|
// properly considers that a header and a footer would be added on each
|
|
// region break.
|
|
let height = frame.height();
|
|
while self.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(engine)?;
|
|
|
|
// Don't skip multiple regions for gutter and don't push a row.
|
|
if self.grid.is_gutter_track(y) {
|
|
return Ok(());
|
|
}
|
|
}
|
|
|
|
self.push_row(frame, y, true);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Layout a row with fixed height and return its frame.
|
|
fn layout_single_row(
|
|
&mut self,
|
|
engine: &mut Engine,
|
|
height: Abs,
|
|
y: usize,
|
|
) -> SourceResult<Frame> {
|
|
if !height.is_finite() {
|
|
bail!(self.span, "cannot create grid with infinite height");
|
|
}
|
|
|
|
let mut output = Frame::soft(Size::new(self.width, height));
|
|
let mut pos = Point::zero();
|
|
|
|
// Reverse the column order when using RTL.
|
|
for (x, &rcol) in self.rcols.iter().enumerate().rev_if(self.is_rtl) {
|
|
if let Some(cell) = self.grid.cell(x, y) {
|
|
// Rowspans have a separate layout step
|
|
if cell.rowspan.get() == 1 {
|
|
let width = self.cell_spanned_width(cell, x);
|
|
let size = Size::new(width, height);
|
|
let mut pod = Regions::one(size, Axes::splat(true));
|
|
if self.grid.rows[y] == Sizing::Auto {
|
|
pod.full = self.regions.full;
|
|
}
|
|
let frame = cell.layout(engine, self.styles, pod)?.into_frame();
|
|
let mut pos = pos;
|
|
if self.is_rtl {
|
|
// In the grid, cell colspans expand to the right,
|
|
// so we're at the leftmost (lowest 'x') column
|
|
// spanned by the cell. However, in RTL, cells
|
|
// expand to the left. Therefore, without the
|
|
// offset below, the cell's contents would be laid out
|
|
// starting at its rightmost visual position and extend
|
|
// over to unrelated cells to its right in RTL.
|
|
// We avoid this by ensuring the rendered cell starts at
|
|
// the very left of the cell, even with colspan > 1.
|
|
let offset = -width + rcol;
|
|
pos.x += offset;
|
|
}
|
|
output.push_frame(pos, frame);
|
|
}
|
|
}
|
|
|
|
pos.x += rcol;
|
|
}
|
|
|
|
Ok(output)
|
|
}
|
|
|
|
/// Layout a row spanning multiple regions.
|
|
fn layout_multi_row(
|
|
&mut self,
|
|
engine: &mut Engine,
|
|
heights: &[Abs],
|
|
y: usize,
|
|
) -> SourceResult<Fragment> {
|
|
// Prepare frames.
|
|
let mut outputs: Vec<_> = heights
|
|
.iter()
|
|
.map(|&h| Frame::soft(Size::new(self.width, h)))
|
|
.collect();
|
|
|
|
// Prepare regions.
|
|
let size = Size::new(self.width, heights[0]);
|
|
let mut pod = Regions::one(size, Axes::splat(true));
|
|
pod.full = self.regions.full;
|
|
pod.backlog = &heights[1..];
|
|
|
|
// Layout the row.
|
|
let mut pos = Point::zero();
|
|
for (x, &rcol) in self.rcols.iter().enumerate().rev_if(self.is_rtl) {
|
|
if let Some(cell) = self.grid.cell(x, y) {
|
|
// Rowspans have a separate layout step
|
|
if cell.rowspan.get() == 1 {
|
|
let width = self.cell_spanned_width(cell, x);
|
|
pod.size.x = width;
|
|
|
|
// Push the layouted frames into the individual output frames.
|
|
let fragment = cell.layout(engine, self.styles, pod)?;
|
|
for (output, frame) in outputs.iter_mut().zip(fragment) {
|
|
let mut pos = pos;
|
|
if self.is_rtl {
|
|
let offset = -width + rcol;
|
|
pos.x += offset;
|
|
}
|
|
output.push_frame(pos, frame);
|
|
}
|
|
}
|
|
}
|
|
|
|
pos.x += rcol;
|
|
}
|
|
|
|
Ok(Fragment::frames(outputs))
|
|
}
|
|
|
|
/// Push a row frame into the current region.
|
|
/// The `is_last` parameter must be `true` if this is the last frame which
|
|
/// will be pushed for this particular row. It can be `false` for rows
|
|
/// spanning multiple regions.
|
|
fn push_row(&mut self, frame: Frame, y: usize, is_last: bool) {
|
|
self.regions.size.y -= frame.height();
|
|
self.lrows.push(Row::Frame(frame, y, is_last));
|
|
}
|
|
|
|
/// Finish rows for one region.
|
|
pub(super) fn finish_region(&mut self, engine: &mut Engine) -> SourceResult<()> {
|
|
if self
|
|
.lrows
|
|
.last()
|
|
.is_some_and(|row| self.grid.is_gutter_track(row.index()))
|
|
{
|
|
// Remove the last row in the region if it is a gutter row.
|
|
self.lrows.pop().unwrap();
|
|
}
|
|
|
|
// If no rows other than the footer have been laid out so far, and
|
|
// there are rows beside the footer, then don't lay it out at all.
|
|
// This check doesn't apply, and is thus overridden, when there is a
|
|
// header.
|
|
let mut footer_would_be_orphan = self.lrows.is_empty()
|
|
&& !in_last_with_offset(
|
|
self.regions,
|
|
self.header_height + self.footer_height,
|
|
)
|
|
&& self
|
|
.grid
|
|
.footer
|
|
.as_ref()
|
|
.and_then(Repeatable::as_repeated)
|
|
.is_some_and(|footer| footer.start != 0);
|
|
|
|
if let Some(Repeatable::Repeated(header)) = &self.grid.header {
|
|
if self.grid.rows.len() > header.end
|
|
&& self
|
|
.grid
|
|
.footer
|
|
.as_ref()
|
|
.and_then(Repeatable::as_repeated)
|
|
.map_or(true, |footer| footer.start != header.end)
|
|
&& self.lrows.last().is_some_and(|row| row.index() < header.end)
|
|
&& !in_last_with_offset(
|
|
self.regions,
|
|
self.header_height + self.footer_height,
|
|
)
|
|
{
|
|
// Header and footer would be alone in this region, but there are more
|
|
// rows beyond the header and the footer. Push an empty region.
|
|
self.lrows.clear();
|
|
footer_would_be_orphan = true;
|
|
}
|
|
}
|
|
|
|
let mut laid_out_footer_start = None;
|
|
if let Some(Repeatable::Repeated(footer)) = &self.grid.footer {
|
|
// Don't layout the footer if it would be alone with the header in
|
|
// the page, and don't layout it twice.
|
|
if !footer_would_be_orphan
|
|
&& self.lrows.iter().all(|row| row.index() < footer.start)
|
|
{
|
|
laid_out_footer_start = Some(footer.start);
|
|
self.layout_footer(footer, engine)?;
|
|
}
|
|
}
|
|
|
|
// Determine the height of existing rows in the region.
|
|
let mut used = Abs::zero();
|
|
let mut fr = Fr::zero();
|
|
for row in &self.lrows {
|
|
match row {
|
|
Row::Frame(frame, _, _) => used += frame.height(),
|
|
Row::Fr(v, _) => fr += *v,
|
|
}
|
|
}
|
|
|
|
// Determine the size of the grid in this region, expanding fully if
|
|
// there are fr rows.
|
|
let mut size = Size::new(self.width, used).min(self.initial);
|
|
if fr.get() > 0.0 && self.initial.y.is_finite() {
|
|
size.y = self.initial.y;
|
|
}
|
|
|
|
// The frame for the region.
|
|
let mut output = Frame::soft(size);
|
|
let mut pos = Point::zero();
|
|
let mut rrows = vec![];
|
|
let current_region = self.finished.len();
|
|
|
|
// Place finished rows and layout fractional rows.
|
|
for row in std::mem::take(&mut self.lrows) {
|
|
let (frame, y, is_last) = match row {
|
|
Row::Frame(frame, y, is_last) => (frame, y, is_last),
|
|
Row::Fr(v, y) => {
|
|
let remaining = self.regions.full - used;
|
|
let height = v.share(fr, remaining);
|
|
(self.layout_single_row(engine, height, y)?, y, true)
|
|
}
|
|
};
|
|
|
|
let height = frame.height();
|
|
|
|
// Ensure rowspans which span this row will have enough space to
|
|
// be laid out over it later.
|
|
for rowspan in self
|
|
.rowspans
|
|
.iter_mut()
|
|
.filter(|rowspan| (rowspan.y..rowspan.y + rowspan.rowspan).contains(&y))
|
|
.filter(|rowspan| {
|
|
rowspan.max_resolved_row.map_or(true, |max_row| y > max_row)
|
|
})
|
|
{
|
|
// If the first region wasn't defined yet, it will have the the
|
|
// initial value of usize::MAX, so we can set it to the current
|
|
// region's index.
|
|
if rowspan.first_region > current_region {
|
|
rowspan.first_region = current_region;
|
|
// The rowspan starts at this region, precisely at this
|
|
// row. In other regions, it will start at dy = 0.
|
|
rowspan.dy = pos.y;
|
|
// When we layout the rowspan later, the full size of the
|
|
// pod must be equal to the full size of the first region
|
|
// it appears in.
|
|
rowspan.region_full = self.regions.full;
|
|
}
|
|
let amount_missing_heights = (current_region + 1)
|
|
.saturating_sub(rowspan.heights.len() + rowspan.first_region);
|
|
|
|
// Ensure the vector of heights is long enough such that the
|
|
// last height is the one for the current region.
|
|
rowspan
|
|
.heights
|
|
.extend(std::iter::repeat(Abs::zero()).take(amount_missing_heights));
|
|
|
|
// Ensure that, in this region, the rowspan will span at least
|
|
// this row.
|
|
*rowspan.heights.last_mut().unwrap() += height;
|
|
|
|
if is_last {
|
|
// Do not extend the rowspan through this row again, even
|
|
// if it is repeated in a future region.
|
|
rowspan.max_resolved_row = Some(y);
|
|
}
|
|
}
|
|
|
|
// We use a for loop over indices to avoid borrow checking
|
|
// problems (we need to mutate the rowspans vector, so we can't
|
|
// have an iterator actively borrowing it). We keep a separate
|
|
// 'i' variable so we can step the counter back after removing
|
|
// a rowspan (see explanation below).
|
|
let mut i = 0;
|
|
while let Some(rowspan) = self.rowspans.get(i) {
|
|
// Layout any rowspans which end at this row, but only if this is
|
|
// this row's last frame (to avoid having the rowspan stop being
|
|
// laid out at the first frame of the row).
|
|
// Any rowspans ending before this row are laid out even
|
|
// on this row's first frame.
|
|
if laid_out_footer_start.map_or(true, |footer_start| {
|
|
// If this is a footer row, then only lay out this rowspan
|
|
// if the rowspan is contained within the footer.
|
|
y < footer_start || rowspan.y >= footer_start
|
|
}) && (rowspan.y + rowspan.rowspan < y + 1
|
|
|| rowspan.y + rowspan.rowspan == y + 1 && is_last)
|
|
{
|
|
// Rowspan ends at this or an earlier row, so we take
|
|
// it from the rowspans vector and lay it out.
|
|
// It's safe to pass the current region as a possible
|
|
// region for the rowspan to be laid out in, even if
|
|
// the rowspan's last row was at an earlier region,
|
|
// because the rowspan won't have an entry for this
|
|
// region in its 'heights' vector if it doesn't span
|
|
// any rows in this region.
|
|
//
|
|
// Here we don't advance the index counter ('i') because
|
|
// a new element we haven't checked yet in this loop
|
|
// will take the index of the now removed element, so
|
|
// we have to check the same index again in the next
|
|
// iteration.
|
|
let rowspan = self.rowspans.remove(i);
|
|
self.layout_rowspan(rowspan, Some((&mut output, &rrows)), engine)?;
|
|
} else {
|
|
i += 1;
|
|
}
|
|
}
|
|
|
|
output.push_frame(pos, frame);
|
|
rrows.push(RowPiece { height, y });
|
|
pos.y += height;
|
|
}
|
|
|
|
self.finish_region_internal(output, rrows);
|
|
|
|
if let Some(Repeatable::Repeated(footer)) = &self.grid.footer {
|
|
self.prepare_footer(footer, engine)?;
|
|
}
|
|
|
|
if let Some(Repeatable::Repeated(header)) = &self.grid.header {
|
|
// Add a header to the new region.
|
|
self.layout_header(header, engine)?;
|
|
}
|
|
|
|
// Ensure rows don't try to overrun the footer.
|
|
self.regions.size.y -= self.footer_height;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Advances to the next region, registering the finished output and
|
|
/// resolved rows for the current region in the appropriate vectors.
|
|
fn finish_region_internal(&mut self, output: Frame, resolved_rows: Vec<RowPiece>) {
|
|
self.finished.push(output);
|
|
self.rrows.push(resolved_rows);
|
|
self.regions.next();
|
|
self.initial = self.regions.size;
|
|
}
|
|
|
|
/// Layouts the header's rows.
|
|
/// Skips regions as necessary.
|
|
fn layout_header(
|
|
&mut self,
|
|
header: &Header,
|
|
engine: &mut Engine,
|
|
) -> SourceResult<()> {
|
|
let header_rows = self.simulate_header(header, &self.regions, engine)?;
|
|
let mut skipped_region = false;
|
|
while self.unbreakable_rows_left == 0
|
|
&& !self.regions.size.y.fits(header_rows.height + self.footer_height)
|
|
&& !self.regions.in_last()
|
|
{
|
|
// Advance regions without any output until we can place the
|
|
// header and the footer.
|
|
self.finish_region_internal(Frame::soft(Axes::splat(Abs::zero())), vec![]);
|
|
skipped_region = true;
|
|
}
|
|
|
|
// Reset the header height for this region.
|
|
// It will be re-calculated when laying out each header row.
|
|
self.header_height = Abs::zero();
|
|
|
|
if let Some(Repeatable::Repeated(footer)) = &self.grid.footer {
|
|
if skipped_region {
|
|
// Simulate the footer again; the region's 'full' might have
|
|
// changed.
|
|
self.footer_height =
|
|
self.simulate_footer(footer, &self.regions, engine)?.height;
|
|
}
|
|
}
|
|
|
|
// Header is unbreakable.
|
|
// Thus, no risk of 'finish_region' being recursively called from
|
|
// within 'layout_row'.
|
|
self.unbreakable_rows_left += header.end;
|
|
for y in 0..header.end {
|
|
self.layout_row(y, engine)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Simulate the header's group of rows.
|
|
pub(super) fn simulate_header(
|
|
&self,
|
|
header: &Header,
|
|
regions: &Regions<'_>,
|
|
engine: &mut Engine,
|
|
) -> SourceResult<UnbreakableRowGroup> {
|
|
// Note that we assume the invariant that any rowspan in a header is
|
|
// fully contained within that header. Therefore, there won't be any
|
|
// unbreakable rowspans exceeding the header's rows, and we can safely
|
|
// assume that the amount of unbreakable rows following the first row
|
|
// in the header will be precisely the rows in the header.
|
|
let header_row_group =
|
|
self.simulate_unbreakable_row_group(0, Some(header.end), regions, engine)?;
|
|
|
|
Ok(header_row_group)
|
|
}
|
|
|
|
/// Updates `self.footer_height` by simulating the footer, and skips to fitting region.
|
|
pub(super) fn prepare_footer(
|
|
&mut self,
|
|
footer: &Footer,
|
|
engine: &mut Engine,
|
|
) -> SourceResult<()> {
|
|
let footer_height = self.simulate_footer(footer, &self.regions, engine)?.height;
|
|
let mut skipped_region = false;
|
|
while self.unbreakable_rows_left == 0
|
|
&& !self.regions.size.y.fits(footer_height)
|
|
&& !self.regions.in_last()
|
|
{
|
|
// Advance regions without any output until we can place the
|
|
// footer.
|
|
self.finish_region_internal(Frame::soft(Axes::splat(Abs::zero())), vec![]);
|
|
skipped_region = true;
|
|
}
|
|
|
|
self.footer_height = if skipped_region {
|
|
// Simulate the footer again; the region's 'full' might have
|
|
// changed.
|
|
self.simulate_footer(footer, &self.regions, engine)?.height
|
|
} else {
|
|
footer_height
|
|
};
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Lays out all rows in the footer.
|
|
/// They are unbreakable.
|
|
pub(super) fn layout_footer(
|
|
&mut self,
|
|
footer: &Footer,
|
|
engine: &mut Engine,
|
|
) -> SourceResult<()> {
|
|
// Ensure footer rows have their own height available.
|
|
// Won't change much as we're creating an unbreakable row group
|
|
// anyway, so this is mostly for correctness.
|
|
self.regions.size.y += self.footer_height;
|
|
|
|
let footer_len = self.grid.rows.len() - footer.start;
|
|
self.unbreakable_rows_left += footer_len;
|
|
for y in footer.start..self.grid.rows.len() {
|
|
self.layout_row(y, engine)?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// Simulate the footer's group of rows.
|
|
pub(super) fn simulate_footer(
|
|
&self,
|
|
footer: &Footer,
|
|
regions: &Regions<'_>,
|
|
engine: &mut Engine,
|
|
) -> SourceResult<UnbreakableRowGroup> {
|
|
// Note that we assume the invariant that any rowspan in a footer is
|
|
// fully contained within that footer. Therefore, there won't be any
|
|
// unbreakable rowspans exceeding the footer's rows, and we can safely
|
|
// assume that the amount of unbreakable rows following the first row
|
|
// in the footer will be precisely the rows in the footer.
|
|
let footer_row_group = self.simulate_unbreakable_row_group(
|
|
footer.start,
|
|
Some(self.grid.rows.len() - footer.start),
|
|
regions,
|
|
engine,
|
|
)?;
|
|
|
|
Ok(footer_row_group)
|
|
}
|
|
}
|
|
|
|
/// Turn an iterator of extents into an iterator of offsets before, in between,
|
|
/// and after the extents, e.g. [10mm, 5mm] -> [0mm, 10mm, 15mm].
|
|
pub(super) fn points(
|
|
extents: impl IntoIterator<Item = Abs>,
|
|
) -> impl Iterator<Item = Abs> {
|
|
let mut offset = Abs::zero();
|
|
std::iter::once(Abs::zero()).chain(extents).map(move |extent| {
|
|
offset += extent;
|
|
offset
|
|
})
|
|
}
|
|
|
|
/// Checks if the first region of a sequence of regions is the last usable
|
|
/// region, assuming that the last region will always be occupied by some
|
|
/// specific offset height, even after calling `.next()`, due to some
|
|
/// additional logic which adds content automatically on each region turn (in
|
|
/// our case, headers).
|
|
pub(super) fn in_last_with_offset(regions: Regions<'_>, offset: Abs) -> bool {
|
|
regions.backlog.is_empty()
|
|
&& regions.last.map_or(true, |height| regions.size.y + offset == height)
|
|
}
|