diff --git a/src/geom/spec.rs b/src/geom/spec.rs index 782cf0f6c..d0da3bca2 100644 --- a/src/geom/spec.rs +++ b/src/geom/spec.rs @@ -89,6 +89,16 @@ pub enum SpecAxis { } impl SpecAxis { + /// The direction with the given positivity for this axis. + pub fn dir(self, positive: bool) -> Dir { + match (self, positive) { + (Self::Vertical, true) => Dir::TTB, + (Self::Vertical, false) => Dir::BTT, + (Self::Horizontal, true) => Dir::LTR, + (Self::Horizontal, false) => Dir::RTL, + } + } + /// The other axis. pub fn other(self) -> Self { match self { diff --git a/src/layout/grid.rs b/src/layout/grid.rs index 64c330977..6ea99476c 100644 --- a/src/layout/grid.rs +++ b/src/layout/grid.rs @@ -3,14 +3,17 @@ use super::*; /// A node that arranges its children in a grid. #[derive(Debug, Clone, PartialEq, Hash)] pub struct GridNode { - /// The column (cross) direction of this stack. - pub column_dir: Dir, + /// The `main` and `cross` directions of this grid. + /// + /// The rows go along the `main` direction and the columns along the `cross` + /// direction. + pub dirs: Gen, + /// Defines sizing for content rows and columns. + pub tracks: Gen>, + /// Defines sizing of gutter rows and columns between content. + pub gutter: Gen>, /// The nodes to be arranged in a grid. pub children: Vec, - /// Defines sizing for rows and columns. - pub tracks: Gen, - /// Defines sizing of the gutter between rows and columns. - pub gutter: Gen, } impl Layout for GridNode { @@ -33,8 +36,8 @@ struct GridLayouter<'a> { rows: Vec, cells: Vec>, regions: Regions, - rrows: Vec<(usize, Option, Option>>)>, rcols: Vec, + rrows: Vec<(usize, Option, Option>>)>, finished: Vec, } @@ -46,35 +49,38 @@ enum Cell<'a> { impl<'a> GridLayouter<'a> { fn new(grid: &'a GridNode, regions: Regions) -> Self { - let mut col_sizes = vec![]; - let mut row_sizes = vec![]; + let cross = grid.dirs.cross.axis(); + let main = grid.dirs.main.axis(); + + let mut cols = vec![]; + let mut rows = vec![]; let mut cells = vec![]; // A grid always needs to have at least one column. - let cols = grid.tracks.cross.0.len().max(1); + let content_cols = grid.tracks.cross.len().max(1); // Create at least as many rows as specified and also at least as many // as necessary to place each item. - let rows = { + let content_rows = { let len = grid.children.len(); - let specified = grid.tracks.main.0.len(); - let necessary = len / cols + (len % cols).clamp(0, 1); + let specified = grid.tracks.main.len(); + let necessary = len / content_cols + (len % content_cols).clamp(0, 1); specified.max(necessary) }; // Collect the track sizing for all columns, including gutter columns. - for i in 0 .. cols { - col_sizes.push(grid.tracks.cross.get(i)); - if i < cols - 1 { - col_sizes.push(grid.gutter.cross.get(i)); + for i in 0 .. content_cols { + cols.push(grid.tracks.cross.get_or_last(i)); + if i < content_cols - 1 { + cols.push(grid.gutter.cross.get_or_last(i)); } } // Collect the track sizing for all rows, including gutter rows. - for i in 0 .. rows { - row_sizes.push(grid.tracks.main.get(i)); - if i < rows - 1 { - row_sizes.push(grid.gutter.main.get(i)); + for i in 0 .. content_rows { + rows.push(grid.tracks.main.get_or_last(i)); + if i < content_rows - 1 { + rows.push(grid.gutter.main.get_or_last(i)); } } @@ -82,152 +88,155 @@ impl<'a> GridLayouter<'a> { for (i, item) in grid.children.iter().enumerate() { cells.push(Cell::Node(item)); - let row = i / cols; - let col = i % cols; + let row = i / content_cols; + let col = i % content_cols; - if col < cols - 1 { + if col < content_cols - 1 { // Push gutter after each child. cells.push(Cell::Gutter); - } else if row < rows - 1 { + } else if row < content_rows - 1 { // Except for the last child of each row. // There we push a gutter row. - for _ in 0 .. col_sizes.len() { + for _ in 0 .. cols.len() { cells.push(Cell::Gutter); } } } // Fill the thing up. - while cells.len() < col_sizes.len() * row_sizes.len() { - cells.push(Cell::Gutter) + while cells.len() < cols.len() * rows.len() { + cells.push(Cell::Gutter); } Self { - cross: grid.column_dir.axis(), - main: grid.column_dir.axis().other(), - cols: col_sizes, - rows: row_sizes, + cross, + main, + cols, + rows, cells, regions, - rrows: vec![], rcols: vec![], + rrows: vec![], finished: vec![], } } fn layout(mut self, ctx: &mut LayoutContext) -> Vec { - // Shrink area by linear sizing. - let mut available = self.regions.current.get(self.cross); - available -= self - .cols - .iter() - .filter_map(|x| match x { - TrackSizing::Linear(l) => { - Some(l.resolve(self.regions.base.get(self.cross))) - } - _ => None, - }) - .sum(); - - let col_frac: f64 = self - .cols - .iter() - .filter_map(|x| match x { - TrackSizing::Fractional(f) => Some(f.get()), - _ => None, - }) - .sum(); - - let auto_columns = self - .cols - .iter() - .enumerate() - .filter_map(|(i, x)| (x == &TrackSizing::Auto).then(|| i)); - - let mut col_width = vec![]; - - // For each of the auto columns, lay out all elements with - // `preliminary_length` rows and build max. - for x in auto_columns { - let mut max = Length::zero(); - - for (y, row) in self.rows.iter().enumerate() { - let mut size = self.regions.current; - if let TrackSizing::Linear(l) = row { - *size.get_mut(self.main) = - l.resolve(self.regions.base.get(self.main)); - } - - let region = Regions::one(size, Spec::splat(false)); - if let Cell::Node(node) = self.get(x, y) { - let frame = node.layout(ctx, ®ion).remove(0); - max = max.max(frame.size.get(self.cross)) + self.rcols = self.resolve_columns(ctx); + self.layout_rows(ctx); + self.finished + } + + /// Determine the size of all columns. + fn resolve_columns(&self, ctx: &mut LayoutContext) -> Vec { + let current = self.regions.current.to_gen(self.main); + let base = self.regions.base.to_gen(self.main); + + // Prepare vector for resolved column lengths. + let mut rcols = vec![Length::zero(); self.cols.len()]; + + // - Sum of sizes of resolved linear tracks, + // - Sum of fractions of all fractional tracks, + // - Sum of sizes of resolved (through layouting) auto tracks, + // - Number of auto tracks. + let mut linear = Length::zero(); + let mut fr = Fractional::zero(); + let mut auto = Length::zero(); + let mut auto_count = 0; + + // Resolve size of linear columns and compute the sum of all fractional + // tracks. + for (&col, rcol) in self.cols.iter().zip(&mut rcols) { + match col { + TrackSizing::Auto => {} + TrackSizing::Linear(v) => { + let resolved = v.resolve(base.cross); + *rcol = resolved; + linear += resolved; } + TrackSizing::Fractional(v) => fr += v, } - - col_width.push((x, max)); } - // If accumulated auto column size exceeds available size, redistribute - // space proportionally amongst elements that exceed their size - // allocation. - let mut total: Length = col_width.iter().map(|(_, x)| *x).sum(); - if total > available { - let alloc = available / col_width.len() as f64; + // Size available to auto columns (not used by fixed-size columns). + let available = current.cross - linear; + if available <= Length::zero() { + return rcols; + } - let mut count: usize = 0; - let mut redistributable = Length::zero(); + // Resolve size of auto columns by laying out all cells in those + // columns, measuring them and finding the largest one. + for (x, (&col, rcol)) in self.cols.iter().zip(&mut rcols).enumerate() { + if col == TrackSizing::Auto { + let mut resolved = Length::zero(); - for &(_, l) in &col_width { - if l > alloc { - redistributable += l; - count += 1; + for (y, &row) in self.rows.iter().enumerate() { + if let Cell::Node(node) = self.get(x, y) { + // Set the correct main size if the row is fixed-size. + let main = match row { + TrackSizing::Linear(v) => v.resolve(base.main), + _ => current.main, + }; + + let size = Gen::new(available, main).to_size(self.main); + let regions = Regions::one(size, Spec::splat(false)); + let frame = node.layout(ctx, ®ions).remove(0); + resolved = resolved.max(frame.size.get(self.cross)) + } + } + + *rcol = resolved; + auto += resolved; + auto_count += 1; + } + } + + // If there is remaining space, distribute it to fractional columns, + // otherwise shrink auto columns. + let remaining = available - auto; + if remaining >= Length::zero() { + for (&col, rcol) in self.cols.iter().zip(&mut rcols) { + if let TrackSizing::Fractional(v) = col { + let ratio = v / fr; + if ratio.is_finite() { + *rcol = ratio * remaining; + } } } + } else { + // The fair share each auto column may have. + let fair = available / auto_count as f64; - let x = (available - total + redistributable) / count as f64; + // The number of overlarge auto columns and the space that will be + // equally redistributed to them. + let mut overlarge: usize = 0; + let mut redistribute = available; - if !redistributable.is_zero() { - for (_, l) in &mut col_width { - if *l > alloc { - *l = x; + for (&col, rcol) in self.cols.iter().zip(&mut rcols) { + if col == TrackSizing::Auto { + if *rcol > fair { + overlarge += 1; + } else { + redistribute -= *rcol; } } } - total = available; - } - - // Build rcols - for (x, len) in col_width - .into_iter() - .map(|(x, s)| (x, Some(s))) - .chain(std::iter::once((self.cols.len(), None))) - { - for i in self.rcols.len() .. x { - let len = match self.cols[i] { - TrackSizing::Linear(l) => { - l.resolve(self.regions.base.get(self.cross)) + // Redistribute the space equally. + let share = redistribute / overlarge as f64; + if overlarge > 0 { + for (&col, rcol) in self.cols.iter().zip(&mut rcols) { + if col == TrackSizing::Auto && *rcol > fair { + *rcol = share; } - TrackSizing::Fractional(f) => { - if col_frac == 0.0 { - Length::zero() - } else { - let res: Length = (available - total) * (f.get() / col_frac); - if res.is_finite() { res } else { Length::zero() } - } - } - TrackSizing::Auto => unreachable!("x is an auto track"), - }; - - self.rcols.push(len); - } - - if let Some(len) = len { - self.rcols.push(len); + } } } + rcols + } + + fn layout_rows(&mut self, ctx: &mut LayoutContext) { // Determine non-`fr` row heights let mut total_frs = 0.0; let mut current = self.regions.current.get(self.main); @@ -276,11 +285,12 @@ impl<'a> GridLayouter<'a> { self.rrows.push((y, Some(local_max), None)); let res = self.finish_region(ctx, total_frs, Some(last_size)); max = if let Some(overflow) = res.as_ref() { - overflow.iter() - .filter_map(|x| x.as_ref()) - .map(|x| x.size.get(self.main)) - .max() - .unwrap_or(Length::zero()) + overflow + .iter() + .filter_map(|x| x.as_ref()) + .map(|x| x.size.get(self.main)) + .max() + .unwrap_or(Length::zero()) } else { local_max }; @@ -319,7 +329,6 @@ impl<'a> GridLayouter<'a> { } self.finish_region(ctx, total_frs, None); - self.finished } fn finish_region( @@ -367,7 +376,6 @@ impl<'a> GridLayouter<'a> { total_main += h; if let Some(layouted) = layouted { - for (col_index, frame) in layouted.into_iter().enumerate() { if let Some(frame) = frame { self.finished @@ -401,7 +409,10 @@ impl<'a> GridLayouter<'a> { let (last_region, last_size) = last_sizes[x]; regions.unique_regions(last_region + 1); - *regions.nth_mut(last_region).unwrap().get_mut(self.main) = last_size; + *regions + .nth_mut(last_region) + .unwrap() + .get_mut(self.main) = last_size; regions } else { Regions::one(region_size, Spec::splat(true)) @@ -431,7 +442,7 @@ impl<'a> GridLayouter<'a> { if overshoot_columns.iter().any(|(_, items)| !items.is_empty()) { for (x, col) in overshoot_columns { let mut cross_offset = Length::zero(); - for col in 0..x { + for col in 0 .. x { cross_offset += self.rcols[col]; } @@ -501,18 +512,15 @@ impl<'a> GridLayouter<'a> { } } -/// A list of track sizing definitions. -#[derive(Default, Debug, Clone, PartialEq, Hash)] -pub struct Tracks(pub Vec); +trait TracksExt { + /// Get the sizing for the track at the given `idx` or fallback to the + /// last defined track or `auto`. + fn get_or_last(&self, idx: usize) -> TrackSizing; +} -impl Tracks { - /// Get the sizing for the track at the given `idx`. - fn get(&self, idx: usize) -> TrackSizing { - self.0 - .get(idx) - .or(self.0.last()) - .copied() - .unwrap_or(TrackSizing::Auto) +impl TracksExt for Vec { + fn get_or_last(&self, idx: usize) -> TrackSizing { + self.get(idx).or(self.last()).copied().unwrap_or(TrackSizing::Auto) } } diff --git a/src/layout/par.rs b/src/layout/par.rs index 241854581..d29920400 100644 --- a/src/layout/par.rs +++ b/src/layout/par.rs @@ -535,7 +535,7 @@ impl<'a> LineLayout<'a> { } } -/// Helper methods for BiDi levels. +/// Additional methods for BiDi levels. trait LevelExt: Sized { fn from_dir(dir: Dir) -> Option; fn dir(self) -> Dir; diff --git a/src/layout/stack.rs b/src/layout/stack.rs index 29b00a985..a6d836952 100644 --- a/src/layout/stack.rs +++ b/src/layout/stack.rs @@ -41,7 +41,7 @@ impl From for AnyNode { #[derive(Debug)] struct StackLayouter<'a> { - /// The directions of the stack. + /// The stack node to layout. stack: &'a StackNode, /// The axis of the main direction. main: SpecAxis, diff --git a/src/library/grid.rs b/src/library/grid.rs index 192dee9dd..81bd655c6 100644 --- a/src/library/grid.rs +++ b/src/library/grid.rs @@ -1,4 +1,4 @@ -use crate::layout::{GridNode, TrackSizing, Tracks}; +use crate::layout::{GridNode, TrackSizing}; use super::*; @@ -10,10 +10,11 @@ use super::*; /// # Named parameters /// - Column sizing: `columns`, of type `tracks`. /// - Row sizing: `rows`, of type `tracks`. -/// - Column direction: `column-dir`, of type `direction`. -/// - Gutter: `gutter`, shorthand for equal column and row gutter, of type `tracks`. +/// - Gutter: `gutter`, shorthand for equal gutter everywhere, of type `length`. /// - Gutter for rows: `gutter-rows`, of type `tracks`. /// - Gutter for columns: `gutter-columns`, of type `tracks`. +/// - Column direction: `column-dir`, of type `direction`. +/// - Row direction: `row-dir`, of type `direction`. /// /// # Return value /// A template that arranges its children along the specified grid cells. @@ -35,12 +36,14 @@ use super::*; pub fn grid(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { let columns = args.eat_named::(ctx, "columns").unwrap_or_default(); let rows = args.eat_named::(ctx, "rows").unwrap_or_default(); - let column_dir = args.eat_named(ctx, "column-dir"); - let gutter = args.eat_named::(ctx, "gutter") - .map(|v| Tracks(vec![TrackSizing::Linear(v)])) + let gutter = args + .eat_named::(ctx, "gutter") + .map(|v| vec![TrackSizing::Linear(v)]) .unwrap_or_default(); let gutter_columns = args.eat_named::(ctx, "gutter-columns"); let gutter_rows = args.eat_named::(ctx, "gutter-rows"); + let column_dir = args.eat_named(ctx, "column-dir"); + let row_dir = args.eat_named(ctx, "row-dir"); let children = args.eat_all::(ctx); Value::template("grid", move |ctx| { @@ -49,26 +52,31 @@ pub fn grid(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { .map(|child| ctx.exec_template_stack(child).into()) .collect(); + let cross_dir = column_dir.unwrap_or(ctx.state.lang.dir); + let main_dir = row_dir.unwrap_or(cross_dir.axis().other().dir(true)); + ctx.push_into_stack(GridNode { - column_dir: column_dir.unwrap_or(ctx.state.lang.dir), - children, + dirs: Gen::new(cross_dir, main_dir), tracks: Gen::new(columns.clone(), rows.clone()), gutter: Gen::new( gutter_columns.as_ref().unwrap_or(&gutter).clone(), gutter_rows.as_ref().unwrap_or(&gutter).clone(), ), + children, }) }) } +/// Defines size of rows and columns in a grid. +type Tracks = Vec; + value! { Tracks: "array of `auto`s, linears, and fractionals", - Value::Int(count) => Self(vec![TrackSizing::Auto; count.max(0) as usize]), - Value::Array(values) => Self(values + Value::Int(count) => vec![TrackSizing::Auto; count.max(0) as usize], + Value::Array(values) => values .into_iter() .filter_map(|v| v.cast().ok()) - .collect() - ), + .collect(), } value! {