diff options
| author | Laurenz <laurmaedje@gmail.com> | 2022-02-28 15:50:48 +0100 |
|---|---|---|
| committer | Laurenz <laurmaedje@gmail.com> | 2022-02-28 23:54:34 +0100 |
| commit | 3ca5b238238e1128aa7bbfbd5db9e632045d8600 (patch) | |
| tree | 2471f4b340a15695b7f4d518c0b39fabaea676c4 /src/library/layout/grid.rs | |
| parent | b63c21c91d99a1554a019dc275f955d3e6a34271 (diff) | |
Reorganize library
Diffstat (limited to 'src/library/layout/grid.rs')
| -rw-r--r-- | src/library/layout/grid.rs | 579 |
1 files changed, 579 insertions, 0 deletions
diff --git a/src/library/layout/grid.rs b/src/library/layout/grid.rs new file mode 100644 index 00000000..63cd83b1 --- /dev/null +++ b/src/library/layout/grid.rs @@ -0,0 +1,579 @@ +use crate::library::prelude::*; + +/// Arrange nodes in a grid. +#[derive(Debug, Hash)] +pub struct GridNode { + /// Defines sizing for content rows and columns. + pub tracks: Spec<Vec<TrackSizing>>, + /// Defines sizing of gutter rows and columns between content. + pub gutter: Spec<Vec<TrackSizing>>, + /// The nodes to be arranged in a grid. + pub children: Vec<LayoutNode>, +} + +#[class] +impl GridNode { + fn construct(_: &mut Context, args: &mut Args) -> TypResult<Template> { + let columns = args.named("columns")?.unwrap_or_default(); + let rows = args.named("rows")?.unwrap_or_default(); + let base_gutter: Vec<TrackSizing> = args.named("gutter")?.unwrap_or_default(); + let column_gutter = args.named("column-gutter")?; + let row_gutter = args.named("row-gutter")?; + Ok(Template::block(Self { + tracks: Spec::new(columns, rows), + gutter: Spec::new( + column_gutter.unwrap_or_else(|| base_gutter.clone()), + row_gutter.unwrap_or(base_gutter), + ), + children: args.all()?, + })) + } +} + +impl Layout for GridNode { + fn layout( + &self, + ctx: &mut Context, + regions: &Regions, + styles: StyleChain, + ) -> TypResult<Vec<Arc<Frame>>> { + // Prepare grid layout by unifying content and gutter tracks. + let layouter = GridLayouter::new( + self.tracks.as_deref(), + self.gutter.as_deref(), + &self.children, + regions, + styles, + ); + + // Measure the columns and layout the grid row-by-row. + layouter.layout(ctx) + } +} + +/// Defines how to size a grid cell along an axis. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum TrackSizing { + /// Fit the cell to its contents. + Auto, + /// A length stated in absolute values and/or relative to the parent's size. + Linear(Linear), + /// A length that is the fraction of the remaining free space in the parent. + Fractional(Fractional), +} + +castable! { + Vec<TrackSizing>, + Expected: "integer or (auto, linear, fractional, or array thereof)", + Value::Auto => vec![TrackSizing::Auto], + Value::Length(v) => vec![TrackSizing::Linear(v.into())], + Value::Relative(v) => vec![TrackSizing::Linear(v.into())], + Value::Linear(v) => vec![TrackSizing::Linear(v)], + Value::Fractional(v) => vec![TrackSizing::Fractional(v)], + Value::Int(v) => vec![TrackSizing::Auto; Value::Int(v).cast::<NonZeroUsize>()?.get()], + Value::Array(values) => values + .into_iter() + .filter_map(|v| v.cast().ok()) + .collect(), +} + +castable! { + TrackSizing, + Expected: "auto, linear, or fractional", + Value::Auto => Self::Auto, + Value::Length(v) => Self::Linear(v.into()), + Value::Relative(v) => Self::Linear(v.into()), + Value::Linear(v) => Self::Linear(v), + Value::Fractional(v) => Self::Fractional(v), +} + +/// Performs grid layout. +pub struct GridLayouter<'a> { + /// The grid cells. + cells: &'a [LayoutNode], + /// 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<Length>, + /// Rows in the current region. + lrows: Vec<Row>, + /// The full height of the current region. + full: Length, + /// 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 fractional ratios in the current region. + fr: Fractional, + /// Frames for finished regions. + finished: Vec<Arc<Frame>>, +} + +/// Produced by initial row layout, auto and linear rows are already finished, +/// fractional rows not yet. +enum Row { + /// Finished row frame of auto or linear row. + Frame(Frame), + /// Ratio of a fractional row and y index of the track. + Fr(Fractional, usize), +} + +impl<'a> GridLayouter<'a> { + /// Create a new grid layouter. + /// + /// This prepares grid layout by unifying content and gutter tracks. + pub fn new( + tracks: Spec<&[TrackSizing]>, + gutter: Spec<&[TrackSizing]>, + cells: &'a [LayoutNode], + 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::Linear(Linear::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![Length::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 = Spec::new(true, false); + + Self { + cells, + cols, + rows, + regions, + styles, + rcols, + lrows, + full, + used: Size::zero(), + fr: Fractional::zero(), + finished: vec![], + } + } + + /// Determines the columns sizes and then layouts the grid row-by-row. + pub fn layout(mut self, ctx: &mut Context) -> TypResult<Vec<Arc<Frame>>> { + self.measure_columns(ctx)?; + + 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(ctx)?; + } + + match self.rows[y] { + TrackSizing::Auto => self.layout_auto_row(ctx, y)?, + TrackSizing::Linear(v) => self.layout_linear_row(ctx, v, y)?, + TrackSizing::Fractional(v) => { + self.lrows.push(Row::Fr(v, y)); + self.fr += v; + } + } + } + + self.finish_region(ctx)?; + Ok(self.finished) + } + + /// Determine all column sizes. + fn measure_columns(&mut self, ctx: &mut Context) -> TypResult<()> { + // Sum of sizes of resolved linear tracks. + let mut linear = Length::zero(); + + // Sum of fractions of all fractional tracks. + let mut fr = Fractional::zero(); + + // Resolve the size of all linear 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::Linear(v) => { + let resolved = v.resolve(self.regions.base.x); + *rcol = resolved; + linear += resolved; + } + TrackSizing::Fractional(v) => fr += v, + } + } + + // Size that is not used by fixed-size columns. + let available = self.regions.first.x - linear; + if available >= Length::zero() { + // Determine size of auto columns. + let (auto, count) = self.measure_auto_columns(ctx, available)?; + + // If there is remaining space, distribute it to fractional columns, + // otherwise shrink auto columns. + let remaining = available - auto; + if remaining >= Length::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, + ctx: &mut Context, + available: Length, + ) -> TypResult<(Length, usize)> { + let mut auto = Length::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 = Length::zero(); + for y in 0 .. self.rows.len() { + if let Some(node) = self.cell(x, y) { + let size = Size::new(available, self.regions.base.y); + let mut pod = + Regions::one(size, self.regions.base, Spec::splat(false)); + + // For linear 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::Linear(v) = self.rows[y] { + pod.base.y = v.resolve(self.regions.base.y); + } + + let frame = node.layout(ctx, &pod, self.styles)?.remove(0); + resolved.set_max(frame.size.x); + } + } + + self.rcols[x] = resolved; + auto += resolved; + count += 1; + } + + Ok((auto, count)) + } + + /// Distribute remaining space to fractional columns. + fn grow_fractional_columns(&mut self, remaining: Length, fr: Fractional) { + for (&col, rcol) in self.cols.iter().zip(&mut self.rcols) { + if let TrackSizing::Fractional(v) = col { + *rcol = v.resolve(fr, remaining); + } + } + } + + /// Redistribute space to auto columns so that each gets a fair share. + fn shrink_auto_columns(&mut self, available: Length, 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, ctx: &mut Context, y: usize) -> TypResult<()> { + let mut resolved: Vec<Length> = vec![]; + + // Determine the size for each region of the row. + for (x, &rcol) in self.rcols.iter().enumerate() { + if let Some(node) = self.cell(x, y) { + let mut pod = self.regions.clone(); + pod.first.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 = node + .layout(ctx, &pod, self.styles)? + .into_iter() + .map(|frame| frame.size.y); + + // 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(ctx, 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(ctx, &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(ctx)?; + } + } + + Ok(()) + } + + /// Layout a row with linear height. Such a row cannot break across multiple + /// regions, but it may force a region break. + fn layout_linear_row( + &mut self, + ctx: &mut Context, + v: Linear, + y: usize, + ) -> TypResult<()> { + let resolved = v.resolve(self.regions.base.y); + let frame = self.layout_single_row(ctx, resolved, y)?; + + // Skip to fitting region. + let height = frame.size.y; + while !self.regions.first.y.fits(height) && !self.regions.in_last() { + self.finish_region(ctx)?; + + // 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( + &self, + ctx: &mut Context, + height: Length, + y: usize, + ) -> TypResult<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(node) = 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 linear and fractional rows. + let base = Spec::new(self.cols[x], self.rows[y]) + .map(|s| s == TrackSizing::Auto) + .select(self.regions.base, size); + + let pod = Regions::one(size, base, Spec::splat(true)); + let frame = node.layout(ctx, &pod, self.styles)?.remove(0); + output.push_frame(pos, frame); + } + + pos.x += rcol; + } + + Ok(output) + } + + /// Layout a row spanning multiple regions. + fn layout_multi_row( + &self, + ctx: &mut Context, + heights: &[Length], + y: usize, + ) -> TypResult<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, Spec::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(node) = self.cell(x, y) { + pod.first.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 = node.layout(ctx, &pod, self.styles)?; + for (output, frame) in outputs.iter_mut().zip(frames) { + 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.size.y; + self.used.y += frame.size.y; + self.lrows.push(Row::Frame(frame)); + } + + /// Finish rows for one region. + fn finish_region(&mut self, ctx: &mut Context) -> TypResult<()> { + // 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.resolve(self.fr, remaining); + self.layout_single_row(ctx, height, y)? + } + }; + + let height = frame.size.y; + output.merge_frame(pos, frame); + pos.y += height; + } + + self.finished.push(Arc::new(output)); + self.regions.next(); + self.full = self.regions.first.y; + self.used.y = Length::zero(); + self.fr = Fractional::zero(); + + Ok(()) + } + + /// Get the node in 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 LayoutNode> { + 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 + } + } +} |
