summaryrefslogtreecommitdiff
path: root/crates/typst-library/src/layout/grid.rs
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2023-07-02 19:59:52 +0200
committerLaurenz <laurmaedje@gmail.com>2023-07-02 20:07:43 +0200
commitebfdb1dafa430786db10dad2ef7d5467c1bdbed1 (patch)
tree2bbc24ddb4124c4bb14dec0e536129d4de37b056 /crates/typst-library/src/layout/grid.rs
parent3ab19185093d7709f824b95b979060ce125389d8 (diff)
Move everything into `crates/` directory
Diffstat (limited to 'crates/typst-library/src/layout/grid.rs')
-rw-r--r--crates/typst-library/src/layout/grid.rs708
1 files changed, 708 insertions, 0 deletions
diff --git a/crates/typst-library/src/layout/grid.rs b/crates/typst-library/src/layout/grid.rs
new file mode 100644
index 00000000..4f5175e9
--- /dev/null
+++ b/crates/typst-library/src/layout/grid.rs
@@ -0,0 +1,708 @@
+use crate::prelude::*;
+use crate::text::TextElem;
+
+use super::Sizing;
+
+/// Arranges content in a grid.
+///
+/// The grid element allows you to arrange content in a grid. You can define the
+/// number of rows and columns, as well as the size of the gutters between them.
+/// There are multiple sizing modes for columns and rows that can be used to
+/// create complex layouts.
+///
+/// The sizing of the grid is determined by the track sizes specified in the
+/// arguments. Because each of the sizing parameters accepts the same values, we
+/// will explain them just once, here. Each sizing argument accepts an array of
+/// individual track sizes. A track size is either:
+///
+/// - `{auto}`: The track will be sized to fit its contents. It will be at most
+/// as large as the remaining space. If there is more than one `{auto}` track
+/// which, and together they claim more than the available space, the `{auto}`
+/// tracks will fairly distribute the available space among themselves.
+///
+/// - A fixed or relative length (e.g. `{10pt}` or `{20% - 1cm}`): The track
+/// will be exactly of this size.
+///
+/// - A fractional length (e.g. `{1fr}`): Once all other tracks have been sized,
+/// the remaining space will be divided among the fractional tracks according
+/// to their fractions. For example, if there are two fractional tracks, each
+/// with a fraction of `{1fr}`, they will each take up half of the remaining
+/// space.
+///
+/// To specify a single track, the array can be omitted in favor of a single
+/// value. To specify multiple `{auto}` tracks, enter the number of tracks
+/// instead of an array. For example, `columns:` `{3}` is equivalent to
+/// `columns:` `{(auto, auto, auto)}`.
+///
+/// ## Example { #example }
+/// ```example
+/// #set text(10pt, style: "italic")
+/// #let cell = rect.with(
+/// inset: 8pt,
+/// fill: rgb("e4e5ea"),
+/// width: 100%,
+/// radius: 6pt
+/// )
+/// #grid(
+/// columns: (60pt, 1fr, 60pt),
+/// rows: (60pt, auto),
+/// gutter: 3pt,
+/// cell(height: 100%)[Easy to learn],
+/// cell(height: 100%)[Great output],
+/// cell(height: 100%)[Intuitive],
+/// cell[Our best Typst yet],
+/// cell[
+/// Responsive design in print
+/// for everyone
+/// ],
+/// cell[One more thing...],
+/// )
+/// ```
+///
+/// Display: Grid
+/// Category: layout
+#[element(Layout)]
+pub struct GridElem {
+ /// The column sizes.
+ ///
+ /// Either specify a track size array or provide an integer to create a grid
+ /// with that many `{auto}`-sized columns. Note that opposed to rows and
+ /// gutters, providing a single track size will only ever create a single
+ /// column.
+ pub columns: TrackSizings,
+
+ /// The row sizes.
+ ///
+ /// If there are more cells than fit the defined rows, the last row is
+ /// repeated until there are no more cells.
+ pub rows: TrackSizings,
+
+ /// The gaps between rows & columns.
+ ///
+ /// If there are more gutters than defined sizes, the last gutter is repeated.
+ #[external]
+ pub gutter: TrackSizings,
+
+ /// The gaps between columns. Takes precedence over `gutter`.
+ #[parse(
+ let gutter = args.named("gutter")?;
+ args.named("column-gutter")?.or_else(|| gutter.clone())
+ )]
+ pub column_gutter: TrackSizings,
+
+ /// The gaps between rows. Takes precedence over `gutter`.
+ #[parse(args.named("row-gutter")?.or_else(|| gutter.clone()))]
+ pub row_gutter: TrackSizings,
+
+ /// The contents of the grid cells.
+ ///
+ /// The cells are populated in row-major order.
+ #[variadic]
+ pub children: Vec<Content>,
+}
+
+impl Layout for GridElem {
+ #[tracing::instrument(name = "GridElem::layout", skip_all)]
+ fn layout(
+ &self,
+ vt: &mut Vt,
+ styles: StyleChain,
+ regions: Regions,
+ ) -> SourceResult<Fragment> {
+ // Prepare grid layout by unifying content and gutter tracks.
+ let cells = self.children();
+ let layouter = GridLayouter::new(
+ Axes::new(&self.columns(styles).0, &self.rows(styles).0),
+ Axes::new(&self.column_gutter(styles).0, &self.row_gutter(styles).0),
+ &cells,
+ regions,
+ styles,
+ );
+
+ // Measure the columns and layout the grid row-by-row.
+ Ok(layouter.layout(vt)?.fragment)
+ }
+}
+
+/// Track sizing definitions.
+#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
+pub struct TrackSizings(pub Vec<Sizing>);
+
+cast! {
+ TrackSizings,
+ self => self.0.into_value(),
+ sizing: Sizing => Self(vec![sizing]),
+ count: NonZeroUsize => Self(vec![Sizing::Auto; count.get()]),
+ values: Array => Self(values.into_iter().map(Value::cast).collect::<StrResult<_>>()?),
+}
+
+/// Performs grid layout.
+pub struct GridLayouter<'a> {
+ /// The grid cells.
+ cells: &'a [Content],
+ /// Whether this is an RTL grid.
+ is_rtl: bool,
+ /// Whether this grid has gutters.
+ has_gutter: bool,
+ /// The column tracks including gutter tracks.
+ cols: Vec<Sizing>,
+ /// The row tracks including gutter tracks.
+ rows: Vec<Sizing>,
+ /// The regions to layout children into.
+ regions: Regions<'a>,
+ /// The inherited styles.
+ styles: StyleChain<'a>,
+ /// Resolved column sizes.
+ rcols: Vec<Abs>,
+ /// The sum of `rcols`.
+ width: Abs,
+ /// Resolve row sizes, by region.
+ rrows: Vec<Vec<RowPiece>>,
+ /// Rows in the current region.
+ lrows: Vec<Row>,
+ /// The initial size of the current region before we started subtracting.
+ initial: Size,
+ /// Frames for finished regions.
+ finished: Vec<Frame>,
+}
+
+/// The resulting sizes of columns and rows in a grid.
+#[derive(Debug)]
+pub struct GridLayout {
+ /// The fragment.
+ pub fragment: Fragment,
+ /// The column widths.
+ pub cols: Vec<Abs>,
+ /// The heights of the resulting rows segments, by region.
+ pub rows: Vec<Vec<RowPiece>>,
+}
+
+/// 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.
+enum Row {
+ /// Finished row frame of auto or relative row with y index.
+ Frame(Frame, usize),
+ /// 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(
+ tracks: Axes<&[Sizing]>,
+ gutter: Axes<&[Sizing]>,
+ cells: &'a [Content],
+ regions: Regions<'a>,
+ 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 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();
+ }
+
+ // Reverse for RTL.
+ let is_rtl = TextElem::dir_in(styles) == Dir::RTL;
+ if is_rtl {
+ cols.reverse();
+ }
+
+ // 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 {
+ cells,
+ is_rtl,
+ has_gutter,
+ rows,
+ regions,
+ styles,
+ rcols: vec![Abs::zero(); cols.len()],
+ cols,
+ width: Abs::zero(),
+ rrows: vec![],
+ lrows: vec![],
+ initial: regions.size,
+ finished: vec![],
+ }
+ }
+
+ /// Determines the columns sizes and then layouts the grid row-by-row.
+ pub fn layout(mut self, vt: &mut Vt) -> SourceResult<GridLayout> {
+ self.measure_columns(vt)?;
+
+ 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 self.regions.is_full() && (!self.has_gutter || y % 2 == 0) {
+ self.finish_region(vt)?;
+ }
+
+ match self.rows[y] {
+ Sizing::Auto => self.layout_auto_row(vt, y)?,
+ Sizing::Rel(v) => self.layout_relative_row(vt, v, y)?,
+ Sizing::Fr(v) => self.lrows.push(Row::Fr(v, y)),
+ }
+ }
+
+ self.finish_region(vt)?;
+
+ Ok(GridLayout {
+ fragment: Fragment::frames(self.finished),
+ cols: self.rcols,
+ rows: self.rrows,
+ })
+ }
+
+ /// Determine all column sizes.
+ #[tracing::instrument(name = "GridLayouter::measure_columns", skip_all)]
+ fn measure_columns(&mut self, vt: &mut Vt) -> 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 {
+ 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(vt, 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(())
+ }
+
+ /// Measure the size that is available to auto columns.
+ fn measure_auto_columns(
+ &mut self,
+ vt: &mut Vt,
+ 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 != Sizing::Auto {
+ continue;
+ }
+
+ let mut resolved = Abs::zero();
+ for y in 0..self.rows.len() {
+ if let Some(cell) = self.cell(x, y) {
+ // For relative rows, we can already resolve the correct
+ // base and for auto and fr we could only guess anyway.
+ let height = match self.rows[y] {
+ Sizing::Rel(v) => {
+ v.resolve(self.styles).relative_to(self.regions.base().y)
+ }
+ _ => self.regions.base().y,
+ };
+
+ let size = Size::new(available, height);
+ let pod = Regions::one(size, Axes::splat(false));
+ let frame = cell.measure(vt, self.styles, pod)?.into_frame();
+ 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) {
+ if fr.is_zero() {
+ return;
+ }
+
+ for (&col, rcol) in self.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.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.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, vt: &mut Vt, 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(vt, y, true)? {
+ Some(resolved) => resolved,
+ None => {
+ self.finish_region(vt)?;
+ self.measure_auto_row(vt, y, false)?.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(vt, first, y)?;
+ self.push_row(frame, y);
+ 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 (region, target) in self
+ .regions
+ .iter()
+ .zip(&mut resolved[..len - 1])
+ .skip(self.lrows.iter().any(|row| matches!(row, Row::Fr(..))) as usize)
+ {
+ target.set_max(region.y);
+ }
+
+ // Layout into multiple regions.
+ let fragment = self.layout_multi_row(vt, &resolved, y)?;
+ let len = fragment.len();
+ for (i, frame) in fragment.into_iter().enumerate() {
+ self.push_row(frame, y);
+ if i + 1 < len {
+ self.finish_region(vt)?;
+ }
+ }
+
+ Ok(())
+ }
+
+ /// Measure the regions sizes of an auto row. The option is always `Some(_)`
+ /// if `can_skip` is false.
+ fn measure_auto_row(
+ &mut self,
+ vt: &mut Vt,
+ y: usize,
+ can_skip: bool,
+ ) -> SourceResult<Option<Vec<Abs>>> {
+ let mut resolved: Vec<Abs> = vec![];
+
+ for (x, &rcol) in self.rcols.iter().enumerate() {
+ if let Some(cell) = self.cell(x, y) {
+ let mut pod = self.regions;
+ pod.size.x = rcol;
+
+ let frames = cell.measure(vt, self.styles, pod)?.into_frames();
+
+ // Skip the first region if one cell in it is empty. Then,
+ // remeasure.
+ if let [first, rest @ ..] = frames.as_slice() {
+ if can_skip
+ && first.is_empty()
+ && rest.iter().any(|frame| !frame.is_empty())
+ {
+ return Ok(None);
+ }
+ }
+
+ let mut sizes = frames.iter().map(|frame| frame.height());
+ 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);
+ }
+ }
+
+ 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,
+ vt: &mut Vt,
+ v: Rel<Length>,
+ y: usize,
+ ) -> SourceResult<()> {
+ let resolved = v.resolve(self.styles).relative_to(self.regions.base().y);
+ let frame = self.layout_single_row(vt, resolved, y)?;
+
+ // Skip to fitting region.
+ let height = frame.height();
+ while !self.regions.size.y.fits(height) && !self.regions.in_last() {
+ self.finish_region(vt)?;
+
+ // Don't skip multiple regions for gutter and don't push a row.
+ if self.has_gutter && y % 2 == 1 {
+ return Ok(());
+ }
+ }
+
+ self.push_row(frame, y);
+
+ Ok(())
+ }
+
+ /// Layout a row with fixed height and return its frame.
+ fn layout_single_row(
+ &mut self,
+ vt: &mut Vt,
+ height: Abs,
+ y: usize,
+ ) -> SourceResult<Frame> {
+ let mut output = Frame::new(Size::new(self.width, 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);
+ let mut pod = Regions::one(size, Axes::splat(true));
+ if self.rows[y] == Sizing::Auto {
+ pod.full = self.regions.full;
+ }
+ let frame = cell.layout(vt, self.styles, pod)?.into_frame();
+ output.push_frame(pos, frame);
+ }
+
+ pos.x += rcol;
+ }
+
+ Ok(output)
+ }
+
+ /// Layout a row spanning multiple regions.
+ fn layout_multi_row(
+ &mut self,
+ vt: &mut Vt,
+ heights: &[Abs],
+ y: usize,
+ ) -> SourceResult<Fragment> {
+ // Prepare frames.
+ let mut outputs: Vec<_> = heights
+ .iter()
+ .map(|&h| Frame::new(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() {
+ if let Some(cell) = self.cell(x, y) {
+ pod.size.x = rcol;
+
+ // Push the layouted frames into the individual output frames.
+ let fragment = cell.layout(vt, self.styles, pod)?;
+ for (output, frame) in outputs.iter_mut().zip(fragment) {
+ output.push_frame(pos, frame);
+ }
+ }
+
+ pos.x += rcol;
+ }
+
+ Ok(Fragment::frames(outputs))
+ }
+
+ /// Push a row frame into the current region.
+ fn push_row(&mut self, frame: Frame, y: usize) {
+ self.regions.size.y -= frame.height();
+ self.lrows.push(Row::Frame(frame, y));
+ }
+
+ /// Finish rows for one region.
+ fn finish_region(&mut self, vt: &mut Vt) -> SourceResult<()> {
+ // 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::new(size);
+ let mut pos = Point::zero();
+ let mut rrows = vec![];
+
+ // Place finished rows and layout fractional rows.
+ for row in std::mem::take(&mut self.lrows) {
+ let (frame, y) = match row {
+ Row::Frame(frame, y) => (frame, y),
+ Row::Fr(v, y) => {
+ let remaining = self.regions.full - used;
+ let height = v.share(fr, remaining);
+ (self.layout_single_row(vt, height, y)?, y)
+ }
+ };
+
+ let height = frame.height();
+ output.push_frame(pos, frame);
+ rrows.push(RowPiece { height, y });
+ pos.y += height;
+ }
+
+ self.finished.push(output);
+ self.rrows.push(rrows);
+ self.regions.next();
+ self.initial = self.regions.size;
+
+ 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, mut x: usize, y: usize) -> Option<&'a Content> {
+ assert!(x < self.cols.len());
+ assert!(y < self.rows.len());
+
+ // Columns are reorder, but the cell slice is not.
+ if self.is_rtl {
+ x = self.cols.len() - 1 - x;
+ }
+
+ 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.cells.get((y / 2) * c + x / 2)
+ } else {
+ None
+ }
+ } else {
+ let c = self.cols.len();
+ self.cells.get(y * c + x)
+ }
+ }
+}