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! {
|