diff options
| author | Laurenz <laurmaedje@gmail.com> | 2022-11-03 11:44:53 +0100 |
|---|---|---|
| committer | Laurenz <laurmaedje@gmail.com> | 2022-11-03 13:35:39 +0100 |
| commit | 37a7afddfaffd44cb9bc013c9506599267e08983 (patch) | |
| tree | 20e7d62d3c5418baff01a21d0406b91bf3096214 /library/src/layout/grid.rs | |
| parent | 56342bd972a13ffe21beaf2b87ab7eb1597704b4 (diff) | |
Split crates
Diffstat (limited to 'library/src/layout/grid.rs')
| -rw-r--r-- | library/src/layout/grid.rs | 600 |
1 files changed, 600 insertions, 0 deletions
diff --git a/library/src/layout/grid.rs b/library/src/layout/grid.rs new file mode 100644 index 00000000..f6610d78 --- /dev/null +++ b/library/src/layout/grid.rs @@ -0,0 +1,600 @@ +use crate::prelude::*; + +/// Arrange content in a grid. +#[derive(Debug, Hash)] +pub struct GridNode { + /// Defines sizing for content rows and columns. + pub tracks: Axes<Vec<TrackSizing>>, + /// Defines sizing of gutter rows and columns between content. + pub gutter: Axes<Vec<TrackSizing>>, + /// The content to be arranged in a grid. + pub cells: Vec<Content>, +} + +#[node(LayoutBlock)] +impl GridNode { + fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> { + let TrackSizings(columns) = args.named("columns")?.unwrap_or_default(); + let TrackSizings(rows) = args.named("rows")?.unwrap_or_default(); + let TrackSizings(base_gutter) = args.named("gutter")?.unwrap_or_default(); + let column_gutter = args.named("column-gutter")?.map(|TrackSizings(v)| v); + let row_gutter = args.named("row-gutter")?.map(|TrackSizings(v)| v); + Ok(Self { + tracks: Axes::new(columns, rows), + gutter: Axes::new( + column_gutter.unwrap_or_else(|| base_gutter.clone()), + row_gutter.unwrap_or(base_gutter), + ), + cells: args.all()?, + } + .pack()) + } +} + +impl LayoutBlock for GridNode { + fn layout_block( + &self, + world: Tracked<dyn World>, + regions: &Regions, + styles: StyleChain, + ) -> SourceResult<Vec<Frame>> { + // Prepare grid layout by unifying content and gutter tracks. + let layouter = GridLayouter::new( + world, + self.tracks.as_deref(), + self.gutter.as_deref(), + &self.cells, + regions, + styles, + ); + + // Measure the columns and layout the grid row-by-row. + layouter.layout() + } +} + +/// Defines how to size a grid cell along an axis. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum TrackSizing { + /// A track that fits its cell's contents. + Auto, + /// A track size specified in absolute terms and relative to the parent's + /// size. + Relative(Rel<Length>), + /// A track size specified as a fraction of the remaining free space in the + /// parent. + Fractional(Fr), +} + +/// Track sizing definitions. +#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] +pub struct TrackSizings(pub Vec<TrackSizing>); + +castable! { + TrackSizings, + Expected: "integer, auto, relative length, fraction, or array of the latter three", + Value::Auto => Self(vec![TrackSizing::Auto]), + Value::Length(v) => Self(vec![TrackSizing::Relative(v.into())]), + Value::Ratio(v) => Self(vec![TrackSizing::Relative(v.into())]), + Value::Relative(v) => Self(vec![TrackSizing::Relative(v)]), + Value::Fraction(v) => Self(vec![TrackSizing::Fractional(v)]), + Value::Int(v) => Self(vec![ + TrackSizing::Auto; + Value::Int(v).cast::<NonZeroUsize>()?.get() + ]), + Value::Array(values) => Self(values + .into_iter() + .filter_map(|v| v.cast().ok()) + .collect()), +} + +castable! { + TrackSizing, + Expected: "auto, relative length, or fraction", + Value::Auto => Self::Auto, + Value::Length(v) => Self::Relative(v.into()), + Value::Ratio(v) => Self::Relative(v.into()), + Value::Relative(v) => Self::Relative(v), + Value::Fraction(v) => Self::Fractional(v), +} + +/// Performs grid layout. +pub struct GridLayouter<'a> { + /// The core context. + world: Tracked<'a, dyn World>, + /// The grid cells. + cells: &'a [Content], + /// The column tracks including gutter tracks. + cols: Vec<TrackSizing>, + /// The row tracks including gutter tracks. + rows: Vec<TrackSizing>, + /// The regions to layout children into. + regions: Regions, + /// The inherited styles. + styles: StyleChain<'a>, + /// Resolved column sizes. + rcols: Vec<Abs>, + /// Rows in the current region. + lrows: Vec<Row>, + /// The full height of the current region. + full: Abs, + /// The used-up size of the current region. The horizontal size is + /// determined once after columns are resolved and not touched again. + used: Size, + /// The sum of fractions in the current region. + fr: Fr, + /// Frames for finished regions. + finished: Vec<Frame>, +} + +/// Produced by initial row layout, auto and relative rows are already finished, +/// fractional rows not yet. +enum Row { + /// Finished row frame of auto or relative row. + Frame(Frame), + /// Fractional row with y index. + Fr(Fr, usize), +} + +impl<'a> GridLayouter<'a> { + /// Create a new grid layouter. + /// + /// This prepares grid layout by unifying content and gutter tracks. + pub fn new( + world: Tracked<'a, dyn World>, + tracks: Axes<&[TrackSizing]>, + gutter: Axes<&[TrackSizing]>, + cells: &'a [Content], + regions: &Regions, + styles: StyleChain<'a>, + ) -> 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 = cells.len(); + let given = tracks.y.len(); + let needed = len / c + (len % c).clamp(0, 1); + given.max(needed) + }; + + let auto = TrackSizing::Auto; + let zero = TrackSizing::Relative(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)); + 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)); + rows.push(get_or(gutter.y, y, zero)); + } + + // Remove superfluous gutter tracks. + cols.pop(); + rows.pop(); + + let full = regions.first.y; + let rcols = vec![Abs::zero(); cols.len()]; + let lrows = vec![]; + + // We use the regions for auto row measurement. Since at that moment, + // columns are already sized, we can enable horizontal expansion. + let mut regions = regions.clone(); + regions.expand = Axes::new(true, false); + + Self { + world, + cells, + cols, + rows, + regions, + styles, + rcols, + lrows, + full, + used: Size::zero(), + fr: Fr::zero(), + finished: vec![], + } + } + + /// Determines the columns sizes and then layouts the grid row-by-row. + pub fn layout(mut self) -> SourceResult<Vec<Frame>> { + self.measure_columns()?; + + for y in 0 .. self.rows.len() { + // Skip to next region if current one is full, but only for content + // rows, not for gutter rows. + if y % 2 == 0 && self.regions.is_full() { + self.finish_region()?; + } + + match self.rows[y] { + TrackSizing::Auto => self.layout_auto_row(y)?, + TrackSizing::Relative(v) => self.layout_relative_row(v, y)?, + TrackSizing::Fractional(v) => { + self.lrows.push(Row::Fr(v, y)); + self.fr += v; + } + } + } + + self.finish_region()?; + Ok(self.finished) + } + + /// Determine all column sizes. + fn measure_columns(&mut self) -> 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.cols.iter().zip(&mut self.rcols) { + match col { + TrackSizing::Auto => {} + TrackSizing::Relative(v) => { + let resolved = + v.resolve(self.styles).relative_to(self.regions.base.x); + *rcol = resolved; + rel += resolved; + } + TrackSizing::Fractional(v) => fr += v, + } + } + + // Size that is not used by fixed-size columns. + let available = self.regions.first.x - rel; + if available >= Abs::zero() { + // Determine size of auto columns. + let (auto, count) = self.measure_auto_columns(available)?; + + // If there is remaining space, distribute it to fractional columns, + // otherwise shrink auto columns. + let remaining = available - auto; + if remaining >= Abs::zero() { + if !fr.is_zero() { + self.grow_fractional_columns(remaining, fr); + } + } else { + self.shrink_auto_columns(available, count); + } + } + + // Sum up the resolved column sizes once here. + self.used.x = self.rcols.iter().sum(); + + Ok(()) + } + + /// Measure the size that is available to auto columns. + fn measure_auto_columns(&mut self, available: Abs) -> SourceResult<(Abs, usize)> { + let mut auto = Abs::zero(); + let mut count = 0; + + // 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.cols.iter().enumerate() { + if col != TrackSizing::Auto { + continue; + } + + let mut resolved = Abs::zero(); + for y in 0 .. self.rows.len() { + if let Some(cell) = self.cell(x, y) { + let size = Size::new(available, self.regions.base.y); + let mut pod = + Regions::one(size, self.regions.base, Axes::splat(false)); + + // For relative rows, we can already resolve the correct + // base, for auto it's already correct and for fr we could + // only guess anyway. + if let TrackSizing::Relative(v) = self.rows[y] { + pod.base.y = + v.resolve(self.styles).relative_to(self.regions.base.y); + } + + let frame = + cell.layout_block(self.world, &pod, self.styles)?.remove(0); + resolved.set_max(frame.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) { + for (&col, rcol) in self.cols.iter().zip(&mut self.rcols) { + if let TrackSizing::Fractional(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) { + // The fair share each auto column may have. + let fair = available / 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; + + // Find out the number of and space used by overlarge auto columns. + for (&col, rcol) in self.cols.iter().zip(&mut self.rcols) { + if col == TrackSizing::Auto { + if *rcol > fair { + overlarge += 1; + } else { + redistribute -= *rcol; + } + } + } + + // Redistribute the space equally. + let share = redistribute / overlarge as f64; + for (&col, rcol) in self.cols.iter().zip(&mut self.rcols) { + if col == TrackSizing::Auto && *rcol > fair { + *rcol = share; + } + } + } + + /// Layout a row with automatic height. Such a row may break across multiple + /// regions. + fn layout_auto_row(&mut self, y: usize) -> SourceResult<()> { + let mut resolved: Vec<Abs> = vec![]; + + // Determine the size for each region of the row. + for (x, &rcol) in self.rcols.iter().enumerate() { + if let Some(cell) = self.cell(x, y) { + let mut pod = self.regions.clone(); + pod.first.x = rcol; + pod.base.x = rcol; + + // All widths should be `rcol` except the base for auto columns. + if self.cols[x] == TrackSizing::Auto { + pod.base.x = self.regions.base.x; + } + + let mut sizes = cell + .layout_block(self.world, &pod, self.styles)? + .into_iter() + .map(|frame| frame.height()); + + // For each region, we want to know the maximum height any + // column requires. + 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); + } + } + + // 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(first, y)?; + self.push_row(frame); + return Ok(()); + } + + // Expand all but the last region if the space is not + // eaten up by any fr rows. + if self.fr.is_zero() { + let len = resolved.len(); + for (region, target) in self.regions.iter().zip(&mut resolved[.. len - 1]) { + target.set_max(region.y); + } + } + + // Layout into multiple regions. + let frames = self.layout_multi_row(&resolved, y)?; + let len = frames.len(); + for (i, frame) in frames.into_iter().enumerate() { + self.push_row(frame); + if i + 1 < len { + self.finish_region()?; + } + } + + Ok(()) + } + + /// 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, v: Rel<Length>, y: usize) -> SourceResult<()> { + let resolved = v.resolve(self.styles).relative_to(self.regions.base.y); + let frame = self.layout_single_row(resolved, y)?; + + // Skip to fitting region. + let height = frame.height(); + while !self.regions.first.y.fits(height) && !self.regions.in_last() { + self.finish_region()?; + + // Don't skip multiple regions for gutter and don't push a row. + if y % 2 == 1 { + return Ok(()); + } + } + + self.push_row(frame); + + Ok(()) + } + + /// Layout a row with fixed height and return its frame. + fn layout_single_row(&mut self, height: Abs, y: usize) -> SourceResult<Frame> { + let mut output = Frame::new(Size::new(self.used.x, height)); + + let mut pos = Point::zero(); + + for (x, &rcol) in self.rcols.iter().enumerate() { + if let Some(cell) = self.cell(x, y) { + let size = Size::new(rcol, height); + + // Set the base to the region's base for auto rows and to the + // size for relative and fractional rows. + let base = Axes::new(self.cols[x], self.rows[y]) + .map(|s| s == TrackSizing::Auto) + .select(self.regions.base, size); + + let pod = Regions::one(size, base, Axes::splat(true)); + let frame = cell.layout_block(self.world, &pod, self.styles)?.remove(0); + match frame.role() { + Some(Role::ListLabel | Role::ListItemBody) => { + output.apply_role(Role::ListItem) + } + Some(Role::TableCell) => output.apply_role(Role::TableRow), + _ => {} + } + + output.push_frame(pos, frame); + } + + pos.x += rcol; + } + + Ok(output) + } + + /// Layout a row spanning multiple regions. + fn layout_multi_row( + &mut self, + heights: &[Abs], + y: usize, + ) -> SourceResult<Vec<Frame>> { + // Prepare frames. + let mut outputs: Vec<_> = heights + .iter() + .map(|&h| Frame::new(Size::new(self.used.x, h))) + .collect(); + + // Prepare regions. + let size = Size::new(self.used.x, heights[0]); + let mut pod = Regions::one(size, self.regions.base, Axes::splat(true)); + pod.backlog = heights[1 ..].to_vec(); + + // Layout the row. + let mut pos = Point::zero(); + for (x, &rcol) in self.rcols.iter().enumerate() { + if let Some(cell) = self.cell(x, y) { + pod.first.x = rcol; + pod.base.x = rcol; + + // All widths should be `rcol` except the base for auto columns. + if self.cols[x] == TrackSizing::Auto { + pod.base.x = self.regions.base.x; + } + + // Push the layouted frames into the individual output frames. + let frames = cell.layout_block(self.world, &pod, self.styles)?; + for (output, frame) in outputs.iter_mut().zip(frames) { + match frame.role() { + Some(Role::ListLabel | Role::ListItemBody) => { + output.apply_role(Role::ListItem) + } + Some(Role::TableCell) => output.apply_role(Role::TableRow), + _ => {} + } + output.push_frame(pos, frame); + } + } + + pos.x += rcol; + } + + Ok(outputs) + } + + /// Push a row frame into the current region. + fn push_row(&mut self, frame: Frame) { + self.regions.first.y -= frame.height(); + self.used.y += frame.height(); + self.lrows.push(Row::Frame(frame)); + } + + /// Finish rows for one region. + fn finish_region(&mut self) -> SourceResult<()> { + // Determine the size of the grid in this region, expanding fully if + // there are fr rows. + let mut size = self.used; + if self.fr.get() > 0.0 && self.full.is_finite() { + size.y = self.full; + } + + // The frame for the region. + let mut output = Frame::new(size); + let mut pos = Point::zero(); + + // Place finished rows and layout fractional rows. + for row in std::mem::take(&mut self.lrows) { + let frame = match row { + Row::Frame(frame) => frame, + Row::Fr(v, y) => { + let remaining = self.full - self.used.y; + let height = v.share(self.fr, remaining); + self.layout_single_row(height, y)? + } + }; + + let height = frame.height(); + output.push_frame(pos, frame); + pos.y += height; + } + + self.finished.push(output); + self.regions.next(); + self.full = self.regions.first.y; + self.used.y = Abs::zero(); + self.fr = Fr::zero(); + + Ok(()) + } + + /// Get the content of the cell in column `x` and row `y`. + /// + /// Returns `None` if it's a gutter cell. + #[track_caller] + fn cell(&self, x: usize, y: usize) -> Option<&'a Content> { + assert!(x < self.cols.len()); + assert!(y < self.rows.len()); + + // 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.cells.get((y / 2) * c + x / 2) + } else { + None + } + } +} |
