From 37a7afddfaffd44cb9bc013c9506599267e08983 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Thu, 3 Nov 2022 11:44:53 +0100 Subject: Split crates --- library/src/layout/align.rs | 62 +++ library/src/layout/columns.rs | 117 ++++++ library/src/layout/container.rs | 80 ++++ library/src/layout/flow.rs | 267 +++++++++++++ library/src/layout/grid.rs | 600 +++++++++++++++++++++++++++++ library/src/layout/mod.rs | 835 ++++++++++++++++++++++++++++++++++++++++ library/src/layout/pad.rs | 83 ++++ library/src/layout/page.rs | 421 ++++++++++++++++++++ library/src/layout/place.rs | 56 +++ library/src/layout/spacing.rs | 100 +++++ library/src/layout/stack.rs | 322 ++++++++++++++++ library/src/layout/transform.rs | 117 ++++++ 12 files changed, 3060 insertions(+) create mode 100644 library/src/layout/align.rs create mode 100644 library/src/layout/columns.rs create mode 100644 library/src/layout/container.rs create mode 100644 library/src/layout/flow.rs create mode 100644 library/src/layout/grid.rs create mode 100644 library/src/layout/mod.rs create mode 100644 library/src/layout/pad.rs create mode 100644 library/src/layout/page.rs create mode 100644 library/src/layout/place.rs create mode 100644 library/src/layout/spacing.rs create mode 100644 library/src/layout/stack.rs create mode 100644 library/src/layout/transform.rs (limited to 'library/src/layout') diff --git a/library/src/layout/align.rs b/library/src/layout/align.rs new file mode 100644 index 00000000..5e1b3555 --- /dev/null +++ b/library/src/layout/align.rs @@ -0,0 +1,62 @@ +use crate::prelude::*; +use crate::text::{HorizontalAlign, ParNode}; + +/// Align content along the layouting axes. +#[derive(Debug, Hash)] +pub struct AlignNode { + /// How to align the content horizontally and vertically. + pub aligns: Axes>, + /// The content to be aligned. + pub child: Content, +} + +#[node(LayoutBlock)] +impl AlignNode { + fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { + let aligns: Axes> = args.find()?.unwrap_or_default(); + let body: Content = args.expect("body")?; + + if let Axes { x: Some(x), y: None } = aligns { + if !body.has::() { + return Ok(body.styled(ParNode::ALIGN, HorizontalAlign(x))); + } + } + + Ok(body.aligned(aligns)) + } +} + +impl LayoutBlock for AlignNode { + fn layout_block( + &self, + world: Tracked, + regions: &Regions, + styles: StyleChain, + ) -> SourceResult> { + // The child only needs to expand along an axis if there's no alignment. + let mut pod = regions.clone(); + pod.expand &= self.aligns.as_ref().map(Option::is_none); + + // Align paragraphs inside the child. + let mut passed = StyleMap::new(); + if let Some(align) = self.aligns.x { + passed.set(ParNode::ALIGN, HorizontalAlign(align)); + } + + // Layout the child. + let mut frames = self.child.layout_block(world, &pod, passed.chain(&styles))?; + for (region, frame) in regions.iter().zip(&mut frames) { + // Align in the target size. The target size depends on whether we + // should expand. + let target = regions.expand.select(region, frame.size()); + let aligns = self + .aligns + .map(|align| align.resolve(styles)) + .unwrap_or(Axes::new(Align::Left, Align::Top)); + + frame.resize(target, aligns); + } + + Ok(frames) + } +} diff --git a/library/src/layout/columns.rs b/library/src/layout/columns.rs new file mode 100644 index 00000000..8eae922b --- /dev/null +++ b/library/src/layout/columns.rs @@ -0,0 +1,117 @@ +use crate::prelude::*; +use crate::text::TextNode; + +/// Separate a region into multiple equally sized columns. +#[derive(Debug, Hash)] +pub struct ColumnsNode { + /// How many columns there should be. + pub columns: NonZeroUsize, + /// The child to be layouted into the columns. Most likely, this should be a + /// flow or stack node. + pub child: Content, +} + +#[node(LayoutBlock)] +impl ColumnsNode { + /// The size of the gutter space between each column. + #[property(resolve)] + pub const GUTTER: Rel = Ratio::new(0.04).into(); + + fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { + Ok(Self { + columns: args.expect("column count")?, + child: args.expect("body")?, + } + .pack()) + } +} + +impl LayoutBlock for ColumnsNode { + fn layout_block( + &self, + world: Tracked, + regions: &Regions, + styles: StyleChain, + ) -> SourceResult> { + // Separating the infinite space into infinite columns does not make + // much sense. + if !regions.first.x.is_finite() { + return self.child.layout_block(world, regions, styles); + } + + // Determine the width of the gutter and each column. + let columns = self.columns.get(); + let gutter = styles.get(Self::GUTTER).relative_to(regions.base.x); + let width = (regions.first.x - gutter * (columns - 1) as f64) / columns as f64; + + // Create the pod regions. + let pod = Regions { + first: Size::new(width, regions.first.y), + base: Size::new(width, regions.base.y), + backlog: std::iter::once(®ions.first.y) + .chain(regions.backlog.as_slice()) + .flat_map(|&height| std::iter::repeat(height).take(columns)) + .skip(1) + .collect(), + last: regions.last, + expand: Axes::new(true, regions.expand.y), + }; + + // Layout the children. + let mut frames = self.child.layout_block(world, &pod, styles)?.into_iter(); + let mut finished = vec![]; + + let dir = styles.get(TextNode::DIR); + let total_regions = (frames.len() as f32 / columns as f32).ceil() as usize; + + // Stitch together the columns for each region. + for region in regions.iter().take(total_regions) { + // The height should be the parent height if we should expand. + // Otherwise its the maximum column height for the frame. In that + // case, the frame is first created with zero height and then + // resized. + let height = if regions.expand.y { region.y } else { Abs::zero() }; + let mut output = Frame::new(Size::new(regions.first.x, height)); + let mut cursor = Abs::zero(); + + for _ in 0 .. columns { + let frame = match frames.next() { + Some(frame) => frame, + None => break, + }; + + if !regions.expand.y { + output.size_mut().y.set_max(frame.height()); + } + + let width = frame.width(); + let x = if dir.is_positive() { + cursor + } else { + regions.first.x - cursor - width + }; + + output.push_frame(Point::with_x(x), frame); + cursor += width + gutter; + } + + finished.push(output); + } + + Ok(finished) + } +} + +/// A column break. +#[derive(Debug, Clone, Hash)] +pub struct ColbreakNode { + pub weak: bool, +} + +#[node] +impl ColbreakNode { + fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { + let weak = args.named("weak")?.unwrap_or(false); + Ok(Self { weak }.pack()) + } +} diff --git a/library/src/layout/container.rs b/library/src/layout/container.rs new file mode 100644 index 00000000..d65b78b6 --- /dev/null +++ b/library/src/layout/container.rs @@ -0,0 +1,80 @@ +use crate::prelude::*; + +/// An inline-level container that sizes content. +#[derive(Debug, Clone, Hash)] +pub struct BoxNode { + /// How to size the content horizontally and vertically. + pub sizing: Axes>>, + /// The content to be sized. + pub child: Content, +} + +#[node(LayoutInline)] +impl BoxNode { + fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { + let width = args.named("width")?; + let height = args.named("height")?; + let body = args.eat::()?.unwrap_or_default(); + Ok(body.boxed(Axes::new(width, height))) + } +} + +impl LayoutInline for BoxNode { + fn layout_inline( + &self, + world: Tracked, + regions: &Regions, + styles: StyleChain, + ) -> SourceResult> { + // The "pod" is the region into which the child will be layouted. + let pod = { + // Resolve the sizing to a concrete size. + let size = self + .sizing + .resolve(styles) + .zip(regions.base) + .map(|(s, b)| s.map(|v| v.relative_to(b))) + .unwrap_or(regions.first); + + // Select the appropriate base and expansion for the child depending + // on whether it is automatically or relatively sized. + let is_auto = self.sizing.as_ref().map(Option::is_none); + let base = is_auto.select(regions.base, size); + let expand = regions.expand | !is_auto; + + Regions::one(size, base, expand) + }; + + // Layout the child. + let mut frames = self.child.layout_inline(world, &pod, styles)?; + + // Ensure frame size matches regions size if expansion is on. + let frame = &mut frames[0]; + let target = regions.expand.select(regions.first, frame.size()); + frame.resize(target, Align::LEFT_TOP); + + Ok(frames) + } +} + +/// A block-level container that places content into a separate flow. +#[derive(Debug, Clone, Hash)] +pub struct BlockNode(pub Content); + +#[node(LayoutBlock)] +impl BlockNode { + fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { + Ok(Self(args.eat()?.unwrap_or_default()).pack()) + } +} + +impl LayoutBlock for BlockNode { + fn layout_block( + &self, + world: Tracked, + regions: &Regions, + styles: StyleChain, + ) -> SourceResult> { + self.0.layout_block(world, regions, styles) + } +} diff --git a/library/src/layout/flow.rs b/library/src/layout/flow.rs new file mode 100644 index 00000000..a5992796 --- /dev/null +++ b/library/src/layout/flow.rs @@ -0,0 +1,267 @@ +use std::cmp::Ordering; + +use super::{AlignNode, PlaceNode, Spacing}; +use crate::prelude::*; +use crate::text::ParNode; + +/// Arrange spacing, paragraphs and block-level nodes into a flow. +/// +/// This node is reponsible for layouting both the top-level content flow and +/// the contents of boxes. +#[derive(Hash)] +pub struct FlowNode(pub StyleVec); + +/// A child of a flow node. +#[derive(Hash, PartialEq)] +pub enum FlowChild { + /// Vertical spacing between other children. + Spacing(Spacing), + /// Arbitrary block-level content. + Block(Content), + /// A column / region break. + Colbreak, +} + +#[node(LayoutBlock)] +impl FlowNode {} + +impl LayoutBlock for FlowNode { + fn layout_block( + &self, + world: Tracked, + regions: &Regions, + styles: StyleChain, + ) -> SourceResult> { + let mut layouter = FlowLayouter::new(regions); + + for (child, map) in self.0.iter() { + let styles = map.chain(&styles); + match child { + FlowChild::Spacing(kind) => { + layouter.layout_spacing(*kind, styles); + } + FlowChild::Block(block) => { + layouter.layout_block(world, block, styles)?; + } + FlowChild::Colbreak => { + layouter.finish_region(); + } + } + } + + Ok(layouter.finish()) + } +} + +impl Debug for FlowNode { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_str("Flow ")?; + self.0.fmt(f) + } +} + +impl Debug for FlowChild { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::Spacing(kind) => write!(f, "{:?}", kind), + Self::Block(block) => block.fmt(f), + Self::Colbreak => f.pad("Colbreak"), + } + } +} + +impl PartialOrd for FlowChild { + fn partial_cmp(&self, other: &Self) -> Option { + match (self, other) { + (Self::Spacing(a), Self::Spacing(b)) => a.partial_cmp(b), + _ => None, + } + } +} + +/// Performs flow layout. +pub struct FlowLayouter { + /// The regions to layout children into. + regions: Regions, + /// Whether the flow should expand to fill the region. + expand: Axes, + /// The full size of `regions.size` that was available before we started + /// subtracting. + full: Size, + /// The size used by the frames for the current region. + used: Size, + /// The sum of fractions in the current region. + fr: Fr, + /// Spacing and layouted blocks. + items: Vec, + /// Finished frames for previous regions. + finished: Vec, +} + +/// A prepared item in a flow layout. +enum FlowItem { + /// Absolute spacing between other items. + Absolute(Abs), + /// Fractional spacing between other items. + Fractional(Fr), + /// A frame for a layouted block and how to align it. + Frame(Frame, Axes), + /// An absolutely placed frame. + Placed(Frame), +} + +impl FlowLayouter { + /// Create a new flow layouter. + pub fn new(regions: &Regions) -> Self { + let expand = regions.expand; + let full = regions.first; + + // Disable vertical expansion for children. + let mut regions = regions.clone(); + regions.expand.y = false; + + Self { + regions, + expand, + full, + used: Size::zero(), + fr: Fr::zero(), + items: vec![], + finished: vec![], + } + } + + /// Layout spacing. + pub fn layout_spacing(&mut self, spacing: Spacing, styles: StyleChain) { + match spacing { + Spacing::Relative(v) => { + // Resolve the spacing and limit it to the remaining space. + let resolved = v.resolve(styles).relative_to(self.full.y); + let limited = resolved.min(self.regions.first.y); + self.regions.first.y -= limited; + self.used.y += limited; + self.items.push(FlowItem::Absolute(resolved)); + } + Spacing::Fractional(v) => { + self.items.push(FlowItem::Fractional(v)); + self.fr += v; + } + } + } + + /// Layout a block. + pub fn layout_block( + &mut self, + world: Tracked, + block: &Content, + styles: StyleChain, + ) -> SourceResult<()> { + // Don't even try layouting into a full region. + if self.regions.is_full() { + self.finish_region(); + } + + // Placed nodes that are out of flow produce placed items which aren't + // aligned later. + if let Some(placed) = block.downcast::() { + if placed.out_of_flow() { + let frame = block.layout_block(world, &self.regions, styles)?.remove(0); + self.items.push(FlowItem::Placed(frame)); + return Ok(()); + } + } + + // How to align the block. + let aligns = Axes::new( + // For non-expanding paragraphs it is crucial that we align the + // whole paragraph as it is itself aligned. + styles.get(ParNode::ALIGN), + // Vertical align node alignment is respected by the flow. + block + .downcast::() + .and_then(|aligned| aligned.aligns.y) + .map(|align| align.resolve(styles)) + .unwrap_or(Align::Top), + ); + + let frames = block.layout_block(world, &self.regions, styles)?; + let len = frames.len(); + for (i, mut frame) in frames.into_iter().enumerate() { + // Set the generic block role. + frame.apply_role(Role::GenericBlock); + + // Grow our size, shrink the region and save the frame for later. + let size = frame.size(); + self.used.y += size.y; + self.used.x.set_max(size.x); + self.regions.first.y -= size.y; + self.items.push(FlowItem::Frame(frame, aligns)); + + if i + 1 < len { + self.finish_region(); + } + } + + Ok(()) + } + + /// Finish the frame for one region. + pub fn finish_region(&mut self) { + // Determine the size of the flow in this region dependening on whether + // the region expands. + let mut size = self.expand.select(self.full, self.used); + + // Account for fractional spacing in the size calculation. + let remaining = self.full.y - self.used.y; + if self.fr.get() > 0.0 && self.full.y.is_finite() { + self.used.y = self.full.y; + size.y = self.full.y; + } + + let mut output = Frame::new(size); + let mut offset = Abs::zero(); + let mut ruler = Align::Top; + + // Place all frames. + for item in self.items.drain(..) { + match item { + FlowItem::Absolute(v) => { + offset += v; + } + FlowItem::Fractional(v) => { + offset += v.share(self.fr, remaining); + } + FlowItem::Frame(frame, aligns) => { + ruler = ruler.max(aligns.y); + let x = aligns.x.position(size.x - frame.width()); + let y = offset + ruler.position(size.y - self.used.y); + let pos = Point::new(x, y); + offset += frame.height(); + output.push_frame(pos, frame); + } + FlowItem::Placed(frame) => { + output.push_frame(Point::zero(), frame); + } + } + } + + // Advance to the next region. + self.regions.next(); + self.full = self.regions.first; + self.used = Size::zero(); + self.fr = Fr::zero(); + self.finished.push(output); + } + + /// Finish layouting and return the resulting frames. + pub fn finish(mut self) -> Vec { + if self.expand.y { + while !self.regions.backlog.is_empty() { + self.finish_region(); + } + } + + self.finish_region(); + self.finished + } +} 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>, + /// Defines sizing of gutter rows and columns between content. + pub gutter: Axes>, + /// The content to be arranged in a grid. + pub cells: Vec, +} + +#[node(LayoutBlock)] +impl GridNode { + fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { + 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, + regions: &Regions, + styles: StyleChain, + ) -> SourceResult> { + // 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), + /// 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); + +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::()?.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, + /// The row tracks including gutter tracks. + rows: Vec, + /// The regions to layout children into. + regions: Regions, + /// The inherited styles. + styles: StyleChain<'a>, + /// Resolved column sizes. + rcols: Vec, + /// Rows in the current region. + lrows: Vec, + /// 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, +} + +/// 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> { + 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 = 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, 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 { + 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> { + // 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 + } + } +} diff --git a/library/src/layout/mod.rs b/library/src/layout/mod.rs new file mode 100644 index 00000000..ddfaa351 --- /dev/null +++ b/library/src/layout/mod.rs @@ -0,0 +1,835 @@ +//! Composable layouts. + +mod align; +mod columns; +mod container; +mod flow; +mod grid; +mod pad; +mod page; +mod place; +mod spacing; +mod stack; +mod transform; + +pub use align::*; +pub use columns::*; +pub use container::*; +pub use flow::*; +pub use grid::*; +pub use pad::*; +pub use page::*; +pub use place::*; +pub use spacing::*; +pub use stack::*; +pub use transform::*; + +use std::mem; + +use comemo::Tracked; +use typed_arena::Arena; +use typst::diag::SourceResult; +use typst::frame::Frame; +use typst::geom::*; +use typst::model::{ + capability, Barrier, Content, Node, SequenceNode, Show, StyleChain, StyleEntry, + StyleMap, StyleVec, StyleVecBuilder, StyledNode, Target, +}; +use typst::World; + +use crate::structure::{ + DescNode, DocNode, EnumNode, ListItem, ListNode, DESC, ENUM, LIST, +}; +use crate::text::{ + LinebreakNode, ParChild, ParNode, ParbreakNode, SmartQuoteNode, SpaceNode, TextNode, +}; + +/// The root-level layout. +#[capability] +pub trait Layout: 'static + Sync + Send { + /// Layout into one frame per page. + fn layout(&self, world: Tracked) -> SourceResult>; +} + +impl Layout for Content { + #[comemo::memoize] + fn layout(&self, world: Tracked) -> SourceResult> { + let styles = StyleChain::with_root(&world.config().styles); + let scratch = Scratch::default(); + + let mut builder = Builder::new(world, &scratch, true); + builder.accept(self, styles)?; + + let (doc, shared) = builder.into_doc(styles)?; + doc.layout(world, shared) + } +} + +/// Block-level layout. +#[capability] +pub trait LayoutBlock: 'static + Sync + Send { + /// Layout into one frame per region. + fn layout_block( + &self, + world: Tracked, + regions: &Regions, + styles: StyleChain, + ) -> SourceResult>; +} + +impl LayoutBlock for Content { + #[comemo::memoize] + fn layout_block( + &self, + world: Tracked, + regions: &Regions, + styles: StyleChain, + ) -> SourceResult> { + if let Some(node) = self.to::() { + let barrier = StyleEntry::Barrier(Barrier::new(self.id())); + let styles = barrier.chain(&styles); + return node.layout_block(world, regions, styles); + } + + let scratch = Scratch::default(); + let mut builder = Builder::new(world, &scratch, false); + builder.accept(self, styles)?; + let (flow, shared) = builder.into_flow(styles)?; + flow.layout_block(world, regions, shared) + } +} + +/// Inline-level layout. +#[capability] +pub trait LayoutInline: 'static + Sync + Send { + /// Layout into a single frame. + fn layout_inline( + &self, + world: Tracked, + regions: &Regions, + styles: StyleChain, + ) -> SourceResult>; +} + +impl LayoutInline for Content { + #[comemo::memoize] + fn layout_inline( + &self, + world: Tracked, + regions: &Regions, + styles: StyleChain, + ) -> SourceResult> { + if let Some(node) = self.to::() { + let barrier = StyleEntry::Barrier(Barrier::new(self.id())); + let styles = barrier.chain(&styles); + return node.layout_inline(world, regions, styles); + } + + if let Some(node) = self.to::() { + let barrier = StyleEntry::Barrier(Barrier::new(self.id())); + let styles = barrier.chain(&styles); + return node.layout_block(world, regions, styles); + } + + let scratch = Scratch::default(); + let mut builder = Builder::new(world, &scratch, false); + builder.accept(self, styles)?; + let (flow, shared) = builder.into_flow(styles)?; + flow.layout_block(world, regions, shared) + } +} + +/// A sequence of regions to layout into. +#[derive(Debug, Clone, Hash)] +pub struct Regions { + /// The (remaining) size of the first region. + pub first: Size, + /// The base size for relative sizing. + pub base: Size, + /// The height of followup regions. The width is the same for all regions. + pub backlog: Vec, + /// The height of the final region that is repeated once the backlog is + /// drained. The width is the same for all regions. + pub last: Option, + /// Whether nodes should expand to fill the regions instead of shrinking to + /// fit the content. + pub expand: Axes, +} + +impl Regions { + /// Create a new region sequence with exactly one region. + pub fn one(size: Size, base: Size, expand: Axes) -> Self { + Self { + first: size, + base, + backlog: vec![], + last: None, + expand, + } + } + + /// Create a new sequence of same-size regions that repeats indefinitely. + pub fn repeat(size: Size, base: Size, expand: Axes) -> Self { + Self { + first: size, + base, + backlog: vec![], + last: Some(size.y), + expand, + } + } + + /// Create new regions where all sizes are mapped with `f`. + /// + /// Note that since all regions must have the same width, the width returned + /// by `f` is ignored for the backlog and the final region. + pub fn map(&self, mut f: F) -> Self + where + F: FnMut(Size) -> Size, + { + let x = self.first.x; + Self { + first: f(self.first), + base: f(self.base), + backlog: self.backlog.iter().map(|&y| f(Size::new(x, y)).y).collect(), + last: self.last.map(|y| f(Size::new(x, y)).y), + expand: self.expand, + } + } + + /// Whether the first region is full and a region break is called for. + pub fn is_full(&self) -> bool { + Abs::zero().fits(self.first.y) && !self.in_last() + } + + /// Whether the first region is the last usable region. + /// + /// If this is true, calling `next()` will have no effect. + pub fn in_last(&self) -> bool { + self.backlog.is_empty() && self.last.map_or(true, |height| self.first.y == height) + } + + /// Advance to the next region if there is any. + pub fn next(&mut self) { + if let Some(height) = (!self.backlog.is_empty()) + .then(|| self.backlog.remove(0)) + .or(self.last) + { + self.first.y = height; + self.base.y = height; + } + } + + /// An iterator that returns the sizes of the first and all following + /// regions, equivalently to what would be produced by calling + /// [`next()`](Self::next) repeatedly until all regions are exhausted. + /// This iterater may be infinite. + pub fn iter(&self) -> impl Iterator + '_ { + let first = std::iter::once(self.first); + let backlog = self.backlog.iter(); + let last = self.last.iter().cycle(); + first.chain(backlog.chain(last).map(|&h| Size::new(self.first.x, h))) + } +} + +/// Builds a document or a flow node from content. +struct Builder<'a> { + /// The core context. + world: Tracked<'a, dyn World>, + /// Scratch arenas for building. + scratch: &'a Scratch<'a>, + /// The current document building state. + doc: Option>, + /// The current flow building state. + flow: FlowBuilder<'a>, + /// The current paragraph building state. + par: ParBuilder<'a>, + /// The current list building state. + list: ListBuilder<'a>, +} + +/// Temporary storage arenas for building. +#[derive(Default)] +struct Scratch<'a> { + /// An arena where intermediate style chains are stored. + styles: Arena>, + /// An arena where intermediate content resulting from show rules is stored. + templates: Arena, +} + +/// Determines whether a style could interrupt some composable structure. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] +pub enum Interruption { + /// The style forces a list break. + List, + /// The style forces a paragraph break. + Par, + /// The style forces a page break. + Page, +} + +impl<'a> Builder<'a> { + pub fn new( + world: Tracked<'a, dyn World>, + scratch: &'a Scratch<'a>, + top: bool, + ) -> Self { + Self { + world, + scratch, + doc: top.then(|| DocBuilder::default()), + flow: FlowBuilder::default(), + par: ParBuilder::default(), + list: ListBuilder::default(), + } + } + + pub fn into_doc( + mut self, + styles: StyleChain<'a>, + ) -> SourceResult<(DocNode, StyleChain<'a>)> { + self.interrupt(Interruption::Page, styles, true)?; + let (pages, shared) = self.doc.unwrap().pages.finish(); + Ok((DocNode(pages), shared)) + } + + pub fn into_flow( + mut self, + styles: StyleChain<'a>, + ) -> SourceResult<(FlowNode, StyleChain<'a>)> { + self.interrupt(Interruption::Par, styles, false)?; + let (children, shared) = self.flow.0.finish(); + Ok((FlowNode(children), shared)) + } + + pub fn accept( + &mut self, + content: &'a Content, + styles: StyleChain<'a>, + ) -> SourceResult<()> { + if let Some(text) = content.downcast::() { + if let Some(realized) = styles.apply(self.world, Target::Text(&text.0))? { + let stored = self.scratch.templates.alloc(realized); + return self.accept(stored, styles); + } + } else if let Some(styled) = content.downcast::() { + return self.styled(styled, styles); + } else if let Some(seq) = content.downcast::() { + return self.sequence(seq, styles); + } else if content.has::() && self.show(content, styles)? { + return Ok(()); + } + + if self.list.accept(content, styles) { + return Ok(()); + } + + self.interrupt(Interruption::List, styles, false)?; + + if content.is::() { + self.list.accept(content, styles); + return Ok(()); + } + + if self.par.accept(content, styles) { + return Ok(()); + } + + self.interrupt(Interruption::Par, styles, false)?; + + if self.flow.accept(content, styles) { + return Ok(()); + } + + let keep = content + .downcast::() + .map_or(false, |pagebreak| !pagebreak.weak); + self.interrupt(Interruption::Page, styles, keep)?; + + if let Some(doc) = &mut self.doc { + doc.accept(content, styles); + } + + // We might want to issue a warning or error for content that wasn't + // handled (e.g. a pagebreak in a flow building process). However, we + // don't have the spans here at the moment. + Ok(()) + } + + fn show( + &mut self, + content: &'a Content, + styles: StyleChain<'a>, + ) -> SourceResult { + if let Some(mut realized) = styles.apply(self.world, Target::Node(content))? { + let mut map = StyleMap::new(); + let barrier = Barrier::new(content.id()); + map.push(StyleEntry::Barrier(barrier)); + map.push(StyleEntry::Barrier(barrier)); + realized = realized.styled_with_map(map); + let stored = self.scratch.templates.alloc(realized); + self.accept(stored, styles)?; + Ok(true) + } else { + Ok(false) + } + } + + fn styled( + &mut self, + styled: &'a StyledNode, + styles: StyleChain<'a>, + ) -> SourceResult<()> { + let stored = self.scratch.styles.alloc(styles); + let styles = styled.map.chain(stored); + + let intr = if styled.map.interrupts::() { + Some(Interruption::Page) + } else if styled.map.interrupts::() { + Some(Interruption::Par) + } else if styled.map.interrupts::() + || styled.map.interrupts::() + || styled.map.interrupts::() + { + Some(Interruption::List) + } else { + None + }; + + if let Some(intr) = intr { + self.interrupt(intr, styles, false)?; + } + + self.accept(&styled.sub, styles)?; + + if let Some(intr) = intr { + self.interrupt(intr, styles, true)?; + } + + Ok(()) + } + + fn interrupt( + &mut self, + intr: Interruption, + styles: StyleChain<'a>, + keep: bool, + ) -> SourceResult<()> { + if intr >= Interruption::List && !self.list.is_empty() { + mem::take(&mut self.list).finish(self)?; + } + + if intr >= Interruption::Par && !self.par.is_empty() { + mem::take(&mut self.par).finish(self); + } + + if intr >= Interruption::Page { + if let Some(doc) = &mut self.doc { + if !self.flow.is_empty() || (doc.keep_next && keep) { + mem::take(&mut self.flow).finish(doc, styles); + } + doc.keep_next = !keep; + } + } + + Ok(()) + } + + fn sequence( + &mut self, + seq: &'a SequenceNode, + styles: StyleChain<'a>, + ) -> SourceResult<()> { + for content in &seq.0 { + self.accept(content, styles)?; + } + Ok(()) + } +} + +/// Accepts pagebreaks and pages. +struct DocBuilder<'a> { + /// The page runs built so far. + pages: StyleVecBuilder<'a, PageNode>, + /// Whether to keep a following page even if it is empty. + keep_next: bool, +} + +impl<'a> DocBuilder<'a> { + fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) { + if let Some(pagebreak) = content.downcast::() { + self.keep_next = !pagebreak.weak; + } + + if let Some(page) = content.downcast::() { + self.pages.push(page.clone(), styles); + self.keep_next = false; + } + } +} + +impl Default for DocBuilder<'_> { + fn default() -> Self { + Self { + pages: StyleVecBuilder::new(), + keep_next: true, + } + } +} + +/// Accepts flow content. +#[derive(Default)] +struct FlowBuilder<'a>(CollapsingBuilder<'a, FlowChild>); + +impl<'a> FlowBuilder<'a> { + fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) -> bool { + // Weak flow elements: + // Weakness | Element + // 0 | weak colbreak + // 1 | weak fractional spacing + // 2 | weak spacing + // 3 | generated weak spacing + // 4 | generated weak fractional spacing + // 5 | par spacing + + if content.is::() { + /* Nothing to do */ + } else if let Some(colbreak) = content.downcast::() { + if colbreak.weak { + self.0.weak(FlowChild::Colbreak, styles, 0); + } else { + self.0.destructive(FlowChild::Colbreak, styles); + } + } else if let Some(vertical) = content.downcast::() { + let child = FlowChild::Spacing(vertical.amount); + let frac = vertical.amount.is_fractional(); + if vertical.weak { + let weakness = 1 + u8::from(frac) + 2 * u8::from(vertical.generated); + self.0.weak(child, styles, weakness); + } else if frac { + self.0.destructive(child, styles); + } else { + self.0.ignorant(child, styles); + } + } else if content.has::() { + let child = FlowChild::Block(content.clone()); + if content.is::() { + self.0.ignorant(child, styles); + } else { + self.0.supportive(child, styles); + } + } else { + return false; + } + + true + } + + fn par(&mut self, par: ParNode, styles: StyleChain<'a>, indent: bool) { + let amount = if indent && !styles.get(ParNode::SPACING_AND_INDENT) { + styles.get(ParNode::LEADING).into() + } else { + styles.get(ParNode::SPACING).into() + }; + + self.0.weak(FlowChild::Spacing(amount), styles, 5); + self.0.supportive(FlowChild::Block(par.pack()), styles); + self.0.weak(FlowChild::Spacing(amount), styles, 5); + } + + fn finish(self, doc: &mut DocBuilder<'a>, styles: StyleChain<'a>) { + let (flow, shared) = self.0.finish(); + let styles = if flow.is_empty() { styles } else { shared }; + let node = PageNode(FlowNode(flow).pack()); + doc.pages.push(node, styles); + } + + fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +/// Accepts paragraph content. +#[derive(Default)] +struct ParBuilder<'a>(CollapsingBuilder<'a, ParChild>); + +impl<'a> ParBuilder<'a> { + fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) -> bool { + // Weak par elements: + // Weakness | Element + // 0 | weak fractional spacing + // 1 | weak spacing + // 2 | space + + if content.is::() { + self.0.weak(ParChild::Text(' '.into()), styles, 2); + } else if let Some(linebreak) = content.downcast::() { + let c = if linebreak.justify { '\u{2028}' } else { '\n' }; + self.0.destructive(ParChild::Text(c.into()), styles); + } else if let Some(horizontal) = content.downcast::() { + let child = ParChild::Spacing(horizontal.amount); + let frac = horizontal.amount.is_fractional(); + if horizontal.weak { + let weakness = u8::from(!frac); + self.0.weak(child, styles, weakness); + } else if frac { + self.0.destructive(child, styles); + } else { + self.0.ignorant(child, styles); + } + } else if let Some(quote) = content.downcast::() { + self.0.supportive(ParChild::Quote { double: quote.double }, styles); + } else if let Some(text) = content.downcast::() { + self.0.supportive(ParChild::Text(text.0.clone()), styles); + } else if content.has::() { + self.0.supportive(ParChild::Inline(content.clone()), styles); + } else { + return false; + } + + true + } + + fn finish(self, parent: &mut Builder<'a>) { + let (mut children, shared) = self.0.finish(); + if children.is_empty() { + return; + } + + // Paragraph indent should only apply if the paragraph starts with + // text and follows directly after another paragraph. + let indent = shared.get(ParNode::INDENT); + if !indent.is_zero() + && children + .items() + .find_map(|child| match child { + ParChild::Spacing(_) => None, + ParChild::Text(_) | ParChild::Quote { .. } => Some(true), + ParChild::Inline(_) => Some(false), + }) + .unwrap_or_default() + && parent + .flow + .0 + .items() + .rev() + .find_map(|child| match child { + FlowChild::Spacing(_) => None, + FlowChild::Block(content) => Some(content.is::()), + FlowChild::Colbreak => Some(false), + }) + .unwrap_or_default() + { + children.push_front(ParChild::Spacing(indent.into())); + } + + parent.flow.par(ParNode(children), shared, !indent.is_zero()); + } + + fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +/// Accepts list / enum items, spaces, paragraph breaks. +struct ListBuilder<'a> { + /// The list items collected so far. + items: StyleVecBuilder<'a, ListItem>, + /// Whether the list contains no paragraph breaks. + tight: bool, + /// Whether the list can be attached. + attachable: bool, + /// Trailing content for which it is unclear whether it is part of the list. + staged: Vec<(&'a Content, StyleChain<'a>)>, +} + +impl<'a> ListBuilder<'a> { + fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) -> bool { + if self.items.is_empty() { + if content.is::() { + self.attachable = false; + } else if !content.is::() && !content.is::() { + self.attachable = true; + } + } + + if let Some(item) = content.downcast::() { + if self + .items + .items() + .next() + .map_or(true, |first| item.kind() == first.kind()) + { + self.items.push(item.clone(), styles); + self.tight &= self.staged.drain(..).all(|(t, _)| !t.is::()); + } else { + return false; + } + } else if !self.items.is_empty() + && (content.is::() || content.is::()) + { + self.staged.push((content, styles)); + } else { + return false; + } + + true + } + + fn finish(self, parent: &mut Builder<'a>) -> SourceResult<()> { + let (items, shared) = self.items.finish(); + let kind = match items.items().next() { + Some(item) => item.kind(), + None => return Ok(()), + }; + + let tight = self.tight; + let attached = tight && self.attachable; + let content = match kind { + LIST => ListNode:: { tight, attached, items }.pack(), + ENUM => ListNode:: { tight, attached, items }.pack(), + DESC | _ => ListNode:: { tight, attached, items }.pack(), + }; + + let stored = parent.scratch.templates.alloc(content); + parent.accept(stored, shared)?; + + for (content, styles) in self.staged { + parent.accept(content, styles)?; + } + + parent.list.attachable = true; + + Ok(()) + } + + fn is_empty(&self) -> bool { + self.items.is_empty() + } +} + +impl Default for ListBuilder<'_> { + fn default() -> Self { + Self { + items: StyleVecBuilder::default(), + tight: true, + attachable: true, + staged: vec![], + } + } +} + +/// A wrapper around a [`StyleVecBuilder`] that allows to collapse items. +struct CollapsingBuilder<'a, T> { + /// The internal builder. + builder: StyleVecBuilder<'a, T>, + /// Staged weak and ignorant items that we can't yet commit to the builder. + /// The option is `Some(_)` for weak items and `None` for ignorant items. + staged: Vec<(T, StyleChain<'a>, Option)>, + /// What the last non-ignorant item was. + last: Last, +} + +/// What the last non-ignorant item was. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +enum Last { + Weak, + Destructive, + Supportive, +} + +impl<'a, T> CollapsingBuilder<'a, T> { + /// Create a new style-vec builder. + pub fn new() -> Self { + Self { + builder: StyleVecBuilder::new(), + staged: vec![], + last: Last::Destructive, + } + } + + /// Whether the builder is empty. + pub fn is_empty(&self) -> bool { + self.builder.is_empty() && self.staged.is_empty() + } + + /// Can only exist when there is at least one supportive item to its left + /// and to its right, with no destructive items in between. There may be + /// ignorant items in between in both directions. + /// + /// Between weak items, there may be at least one per layer and among the + /// candidates the strongest one (smallest `weakness`) wins. When tied, + /// the one that compares larger through `PartialOrd` wins. + pub fn weak(&mut self, item: T, styles: StyleChain<'a>, weakness: u8) + where + T: PartialOrd, + { + if self.last == Last::Destructive { + return; + } + + if self.last == Last::Weak { + if let Some(i) = + self.staged.iter().position(|(prev_item, _, prev_weakness)| { + prev_weakness.map_or(false, |prev_weakness| { + weakness < prev_weakness + || (weakness == prev_weakness && item > *prev_item) + }) + }) + { + self.staged.remove(i); + } else { + return; + } + } + + self.staged.push((item, styles, Some(weakness))); + self.last = Last::Weak; + } + + /// Forces nearby weak items to collapse. + pub fn destructive(&mut self, item: T, styles: StyleChain<'a>) { + self.flush(false); + self.builder.push(item, styles); + self.last = Last::Destructive; + } + + /// Allows nearby weak items to exist. + pub fn supportive(&mut self, item: T, styles: StyleChain<'a>) { + self.flush(true); + self.builder.push(item, styles); + self.last = Last::Supportive; + } + + /// Has no influence on other items. + pub fn ignorant(&mut self, item: T, styles: StyleChain<'a>) { + self.staged.push((item, styles, None)); + } + + /// Iterate over the contained items. + pub fn items(&self) -> impl DoubleEndedIterator { + self.builder.items().chain(self.staged.iter().map(|(item, ..)| item)) + } + + /// Return the finish style vec and the common prefix chain. + pub fn finish(mut self) -> (StyleVec, StyleChain<'a>) { + self.flush(false); + self.builder.finish() + } + + /// Push the staged items, filtering out weak items if `supportive` is + /// false. + fn flush(&mut self, supportive: bool) { + for (item, styles, meta) in self.staged.drain(..) { + if supportive || meta.is_none() { + self.builder.push(item, styles); + } + } + } +} + +impl<'a, T> Default for CollapsingBuilder<'a, T> { + fn default() -> Self { + Self::new() + } +} diff --git a/library/src/layout/pad.rs b/library/src/layout/pad.rs new file mode 100644 index 00000000..318d9f27 --- /dev/null +++ b/library/src/layout/pad.rs @@ -0,0 +1,83 @@ +use crate::prelude::*; + +/// Pad content at the sides. +#[derive(Debug, Hash)] +pub struct PadNode { + /// The amount of padding. + pub padding: Sides>, + /// The content whose sides to pad. + pub child: Content, +} + +#[node(LayoutBlock)] +impl PadNode { + fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { + let all = args.named("rest")?.or(args.find()?); + let x = args.named("x")?; + let y = args.named("y")?; + let left = args.named("left")?.or(x).or(all).unwrap_or_default(); + let top = args.named("top")?.or(y).or(all).unwrap_or_default(); + let right = args.named("right")?.or(x).or(all).unwrap_or_default(); + let bottom = args.named("bottom")?.or(y).or(all).unwrap_or_default(); + let body = args.expect::("body")?; + let padding = Sides::new(left, top, right, bottom); + Ok(body.padded(padding)) + } +} + +impl LayoutBlock for PadNode { + fn layout_block( + &self, + world: Tracked, + regions: &Regions, + styles: StyleChain, + ) -> SourceResult> { + // Layout child into padded regions. + let padding = self.padding.resolve(styles); + let pod = regions.map(|size| shrink(size, padding)); + let mut frames = self.child.layout_block(world, &pod, styles)?; + + for frame in &mut frames { + // Apply the padding inversely such that the grown size padded + // yields the frame's size. + let padded = grow(frame.size(), padding); + let padding = padding.relative_to(padded); + let offset = Point::new(padding.left, padding.top); + + // Grow the frame and translate everything in the frame inwards. + frame.set_size(padded); + frame.translate(offset); + } + + Ok(frames) + } +} + +/// Shrink a size by padding relative to the size itself. +fn shrink(size: Size, padding: Sides>) -> Size { + size - padding.relative_to(size).sum_by_axis() +} + +/// Grow a size by padding relative to the grown size. +/// This is the inverse operation to `shrink()`. +/// +/// For the horizontal axis the derivation looks as follows. +/// (Vertical axis is analogous.) +/// +/// Let w be the grown target width, +/// s be the given width, +/// l be the left padding, +/// r be the right padding, +/// p = l + r. +/// +/// We want that: w - l.resolve(w) - r.resolve(w) = s +/// +/// Thus: w - l.resolve(w) - r.resolve(w) = s +/// <=> w - p.resolve(w) = s +/// <=> w - p.rel * w - p.abs = s +/// <=> (1 - p.rel) * w = s + p.abs +/// <=> w = (s + p.abs) / (1 - p.rel) +fn grow(size: Size, padding: Sides>) -> Size { + size.zip(padding.sum_by_axis()) + .map(|(s, p)| (s + p.abs).safe_div(1.0 - p.rel.get())) +} diff --git a/library/src/layout/page.rs b/library/src/layout/page.rs new file mode 100644 index 00000000..53a8cbc7 --- /dev/null +++ b/library/src/layout/page.rs @@ -0,0 +1,421 @@ +use std::str::FromStr; + +use super::ColumnsNode; +use crate::prelude::*; + +/// Layouts its child onto one or multiple pages. +#[derive(PartialEq, Clone, Hash)] +pub struct PageNode(pub Content); + +#[node] +impl PageNode { + /// The unflipped width of the page. + #[property(resolve)] + pub const WIDTH: Smart = Smart::Custom(Paper::A4.width().into()); + /// The unflipped height of the page. + #[property(resolve)] + pub const HEIGHT: Smart = Smart::Custom(Paper::A4.height().into()); + /// Whether the page is flipped into landscape orientation. + pub const FLIPPED: bool = false; + + /// The page's margins. + #[property(fold)] + pub const MARGINS: Sides>>> = Sides::splat(Smart::Auto); + + /// How many columns the page has. + pub const COLUMNS: NonZeroUsize = NonZeroUsize::new(1).unwrap(); + /// The page's background color. + pub const FILL: Option = None; + + /// The page's header. + #[property(referenced)] + pub const HEADER: Marginal = Marginal::None; + /// The page's footer. + #[property(referenced)] + pub const FOOTER: Marginal = Marginal::None; + /// Content in the page's background. + #[property(referenced)] + pub const BACKGROUND: Marginal = Marginal::None; + /// Content in the page's foreground. + #[property(referenced)] + pub const FOREGROUND: Marginal = Marginal::None; + + fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { + Ok(Self(args.expect("body")?).pack()) + } + + fn set(...) { + if let Some(paper) = args.named_or_find::("paper")? { + styles.set(Self::WIDTH, Smart::Custom(paper.width().into())); + styles.set(Self::HEIGHT, Smart::Custom(paper.height().into())); + } + } +} + +impl PageNode { + /// Layout the page run into a sequence of frames, one per page. + pub fn layout( + &self, + world: Tracked, + mut page: usize, + styles: StyleChain, + ) -> SourceResult> { + // When one of the lengths is infinite the page fits its content along + // that axis. + let width = styles.get(Self::WIDTH).unwrap_or(Abs::inf()); + let height = styles.get(Self::HEIGHT).unwrap_or(Abs::inf()); + let mut size = Size::new(width, height); + if styles.get(Self::FLIPPED) { + std::mem::swap(&mut size.x, &mut size.y); + } + + let mut min = width.min(height); + if !min.is_finite() { + min = Paper::A4.width(); + } + + // Determine the margins. + let default = Rel::from(0.1190 * min); + let padding = styles.get(Self::MARGINS).map(|side| side.unwrap_or(default)); + + let mut child = self.0.clone(); + + // Realize columns. + let columns = styles.get(Self::COLUMNS); + if columns.get() > 1 { + child = ColumnsNode { columns, child: self.0.clone() }.pack(); + } + + // Realize margins. + child = child.padded(padding); + + // Realize background fill. + if let Some(fill) = styles.get(Self::FILL) { + child = child.filled(fill); + } + + // Layout the child. + let regions = Regions::repeat(size, size, size.map(Abs::is_finite)); + let mut frames = child.layout_block(world, ®ions, styles)?; + + let header = styles.get(Self::HEADER); + let footer = styles.get(Self::FOOTER); + let foreground = styles.get(Self::FOREGROUND); + let background = styles.get(Self::BACKGROUND); + + // Realize overlays. + for frame in &mut frames { + let size = frame.size(); + let pad = padding.resolve(styles).relative_to(size); + let pw = size.x - pad.left - pad.right; + let py = size.y - pad.bottom; + for (role, marginal, pos, area) in [ + ( + Role::Header, + header, + Point::with_x(pad.left), + Size::new(pw, pad.top), + ), + ( + Role::Footer, + footer, + Point::new(pad.left, py), + Size::new(pw, pad.bottom), + ), + (Role::Foreground, foreground, Point::zero(), size), + (Role::Background, background, Point::zero(), size), + ] { + if let Some(content) = marginal.resolve(world, page)? { + let pod = Regions::one(area, area, Axes::splat(true)); + let mut sub = content.layout_block(world, &pod, styles)?.remove(0); + sub.apply_role(role); + + if role == Role::Background { + frame.prepend_frame(pos, sub); + } else { + frame.push_frame(pos, sub); + } + } + } + + page += 1; + } + + Ok(frames) + } +} + +impl Debug for PageNode { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_str("Page(")?; + self.0.fmt(f)?; + f.write_str(")") + } +} + +/// A page break. +#[derive(Debug, Copy, Clone, Hash)] +pub struct PagebreakNode { + pub weak: bool, +} + +#[node] +impl PagebreakNode { + fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { + let weak = args.named("weak")?.unwrap_or(false); + Ok(Self { weak }.pack()) + } +} + +/// A header, footer, foreground or background definition. +#[derive(Debug, Clone, PartialEq, Hash)] +pub enum Marginal { + /// Nothing, + None, + /// Bare content. + Content(Content), + /// A closure mapping from a page number to content. + Func(Func, Span), +} + +impl Marginal { + /// Resolve the marginal based on the page number. + pub fn resolve( + &self, + world: Tracked, + page: usize, + ) -> SourceResult> { + Ok(match self { + Self::None => None, + Self::Content(content) => Some(content.clone()), + Self::Func(func, span) => { + let args = Args::new(*span, [Value::Int(page as i64)]); + Some(func.call_detached(world, args)?.display(world)) + } + }) + } +} + +impl Cast> for Marginal { + fn is(value: &Spanned) -> bool { + matches!(&value.v, Value::Content(_) | Value::Func(_)) + } + + fn cast(value: Spanned) -> StrResult { + match value.v { + Value::None => Ok(Self::None), + Value::Str(v) => Ok(Self::Content(TextNode(v.into()).pack())), + Value::Content(v) => Ok(Self::Content(v)), + Value::Func(v) => Ok(Self::Func(v, value.span)), + v => Err(format!( + "expected none, content or function, found {}", + v.type_name(), + )), + } + } +} + +/// Specification of a paper. +#[derive(Debug, Copy, Clone)] +pub struct Paper { + /// The width of the paper in millimeters. + width: f64, + /// The height of the paper in millimeters. + height: f64, +} + +impl Paper { + /// The width of the paper. + pub fn width(self) -> Abs { + Abs::mm(self.width) + } + + /// The height of the paper. + pub fn height(self) -> Abs { + Abs::mm(self.height) + } +} + +/// Defines paper constants and a paper parsing implementation. +macro_rules! papers { + ($(($var:ident: $width:expr, $height: expr, $($pats:tt)*))*) => { + /// Predefined papers. + /// + /// Each paper is parsable from its name in kebab-case. + impl Paper { + $(pub const $var: Self = Self { width: $width, height: $height };)* + } + + impl FromStr for Paper { + type Err = &'static str; + + fn from_str(name: &str) -> Result { + match name.to_lowercase().as_str() { + $($($pats)* => Ok(Self::$var),)* + _ => Err("invalid paper name"), + } + } + } + }; +} + +castable! { + Paper, + Expected: "string", + Value::Str(string) => Self::from_str(&string)?, +} + +// All paper sizes in mm. +// +// Resources: +// - https://papersizes.io/ +// - https://en.wikipedia.org/wiki/Paper_size +// - https://www.theedkins.co.uk/jo/units/oldunits/print.htm +// - https://vintagepaper.co/blogs/news/traditional-paper-sizes +papers! { + // ---------------------------------------------------------------------- // + // ISO 216 A Series + (A0: 841.0, 1189.0, "a0") + (A1: 594.0, 841.0, "a1") + (A2: 420.0, 594.0, "a2") + (A3: 297.0, 420.0, "a3") + (A4: 210.0, 297.0, "a4") + (A5: 148.0, 210.0, "a5") + (A6: 105.0, 148.0, "a6") + (A7: 74.0, 105.0, "a7") + (A8: 52.0, 74.0, "a8") + (A9: 37.0, 52.0, "a9") + (A10: 26.0, 37.0, "a10") + (A11: 18.0, 26.0, "a11") + + // ISO 216 B Series + (ISO_B1: 707.0, 1000.0, "iso-b1") + (ISO_B2: 500.0, 707.0, "iso-b2") + (ISO_B3: 353.0, 500.0, "iso-b3") + (ISO_B4: 250.0, 353.0, "iso-b4") + (ISO_B5: 176.0, 250.0, "iso-b5") + (ISO_B6: 125.0, 176.0, "iso-b6") + (ISO_B7: 88.0, 125.0, "iso-b7") + (ISO_B8: 62.0, 88.0, "iso-b8") + + // ISO 216 C Series + (ISO_C3: 324.0, 458.0, "iso-c3") + (ISO_C4: 229.0, 324.0, "iso-c4") + (ISO_C5: 162.0, 229.0, "iso-c5") + (ISO_C6: 114.0, 162.0, "iso-c6") + (ISO_C7: 81.0, 114.0, "iso-c7") + (ISO_C8: 57.0, 81.0, "iso-c8") + + // DIN D Series (extension to ISO) + (DIN_D3: 272.0, 385.0, "din-d3") + (DIN_D4: 192.0, 272.0, "din-d4") + (DIN_D5: 136.0, 192.0, "din-d5") + (DIN_D6: 96.0, 136.0, "din-d6") + (DIN_D7: 68.0, 96.0, "din-d7") + (DIN_D8: 48.0, 68.0, "din-d8") + + // SIS (used in academia) + (SIS_G5: 169.0, 239.0, "sis-g5") + (SIS_E5: 115.0, 220.0, "sis-e5") + + // ANSI Extensions + (ANSI_A: 216.0, 279.0, "ansi-a") + (ANSI_B: 279.0, 432.0, "ansi-b") + (ANSI_C: 432.0, 559.0, "ansi-c") + (ANSI_D: 559.0, 864.0, "ansi-d") + (ANSI_E: 864.0, 1118.0, "ansi-e") + + // ANSI Architectural Paper + (ARCH_A: 229.0, 305.0, "arch-a") + (ARCH_B: 305.0, 457.0, "arch-b") + (ARCH_C: 457.0, 610.0, "arch-c") + (ARCH_D: 610.0, 914.0, "arch-d") + (ARCH_E1: 762.0, 1067.0, "arch-e1") + (ARCH_E: 914.0, 1219.0, "arch-e") + + // JIS B Series + (JIS_B0: 1030.0, 1456.0, "jis-b0") + (JIS_B1: 728.0, 1030.0, "jis-b1") + (JIS_B2: 515.0, 728.0, "jis-b2") + (JIS_B3: 364.0, 515.0, "jis-b3") + (JIS_B4: 257.0, 364.0, "jis-b4") + (JIS_B5: 182.0, 257.0, "jis-b5") + (JIS_B6: 128.0, 182.0, "jis-b6") + (JIS_B7: 91.0, 128.0, "jis-b7") + (JIS_B8: 64.0, 91.0, "jis-b8") + (JIS_B9: 45.0, 64.0, "jis-b9") + (JIS_B10: 32.0, 45.0, "jis-b10") + (JIS_B11: 22.0, 32.0, "jis-b11") + + // SAC D Series + (SAC_D0: 764.0, 1064.0, "sac-d0") + (SAC_D1: 532.0, 760.0, "sac-d1") + (SAC_D2: 380.0, 528.0, "sac-d2") + (SAC_D3: 264.0, 376.0, "sac-d3") + (SAC_D4: 188.0, 260.0, "sac-d4") + (SAC_D5: 130.0, 184.0, "sac-d5") + (SAC_D6: 92.0, 126.0, "sac-d6") + + // ISO 7810 ID + (ISO_ID_1: 85.6, 53.98, "iso-id-1") + (ISO_ID_2: 74.0, 105.0, "iso-id-2") + (ISO_ID_3: 88.0, 125.0, "iso-id-3") + + // ---------------------------------------------------------------------- // + // Asia + (ASIA_F4: 210.0, 330.0, "asia-f4") + + // Japan + (JP_SHIROKU_BAN_4: 264.0, 379.0, "jp-shiroku-ban-4") + (JP_SHIROKU_BAN_5: 189.0, 262.0, "jp-shiroku-ban-5") + (JP_SHIROKU_BAN_6: 127.0, 188.0, "jp-shiroku-ban-6") + (JP_KIKU_4: 227.0, 306.0, "jp-kiku-4") + (JP_KIKU_5: 151.0, 227.0, "jp-kiku-5") + (JP_BUSINESS_CARD: 91.0, 55.0, "jp-business-card") + + // China + (CN_BUSINESS_CARD: 90.0, 54.0, "cn-business-card") + + // Europe + (EU_BUSINESS_CARD: 85.0, 55.0, "eu-business-card") + + // French Traditional (AFNOR) + (FR_TELLIERE: 340.0, 440.0, "fr-tellière") + (FR_COURONNE_ECRITURE: 360.0, 460.0, "fr-couronne-écriture") + (FR_COURONNE_EDITION: 370.0, 470.0, "fr-couronne-édition") + (FR_RAISIN: 500.0, 650.0, "fr-raisin") + (FR_CARRE: 450.0, 560.0, "fr-carré") + (FR_JESUS: 560.0, 760.0, "fr-jésus") + + // United Kingdom Imperial + (UK_BRIEF: 406.4, 342.9, "uk-brief") + (UK_DRAFT: 254.0, 406.4, "uk-draft") + (UK_FOOLSCAP: 203.2, 330.2, "uk-foolscap") + (UK_QUARTO: 203.2, 254.0, "uk-quarto") + (UK_CROWN: 508.0, 381.0, "uk-crown") + (UK_BOOK_A: 111.0, 178.0, "uk-book-a") + (UK_BOOK_B: 129.0, 198.0, "uk-book-b") + + // Unites States + (US_LETTER: 215.9, 279.4, "us-letter") + (US_LEGAL: 215.9, 355.6, "us-legal") + (US_TABLOID: 279.4, 431.8, "us-tabloid") + (US_EXECUTIVE: 84.15, 266.7, "us-executive") + (US_FOOLSCAP_FOLIO: 215.9, 342.9, "us-foolscap-folio") + (US_STATEMENT: 139.7, 215.9, "us-statement") + (US_LEDGER: 431.8, 279.4, "us-ledger") + (US_OFICIO: 215.9, 340.36, "us-oficio") + (US_GOV_LETTER: 203.2, 266.7, "us-gov-letter") + (US_GOV_LEGAL: 215.9, 330.2, "us-gov-legal") + (US_BUSINESS_CARD: 88.9, 50.8, "us-business-card") + (US_DIGEST: 139.7, 215.9, "us-digest") + (US_TRADE: 152.4, 228.6, "us-trade") + + // ---------------------------------------------------------------------- // + // Other + (NEWSPAPER_COMPACT: 280.0, 430.0, "newspaper-compact") + (NEWSPAPER_BERLINER: 315.0, 470.0, "newspaper-berliner") + (NEWSPAPER_BROADSHEET: 381.0, 578.0, "newspaper-broadsheet") + (PRESENTATION_16_9: 297.0, 167.0625, "presentation-16-9") + (PRESENTATION_4_3: 280.0, 210.0, "presentation-4-3") +} diff --git a/library/src/layout/place.rs b/library/src/layout/place.rs new file mode 100644 index 00000000..7d760ab6 --- /dev/null +++ b/library/src/layout/place.rs @@ -0,0 +1,56 @@ +use super::AlignNode; +use crate::prelude::*; + +/// Place content at an absolute position. +#[derive(Debug, Hash)] +pub struct PlaceNode(pub Content); + +#[node(LayoutBlock)] +impl PlaceNode { + fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { + let aligns = args.find()?.unwrap_or(Axes::with_x(Some(GenAlign::Start))); + let dx = args.named("dx")?.unwrap_or_default(); + let dy = args.named("dy")?.unwrap_or_default(); + let body = args.expect::("body")?; + Ok(Self(body.moved(Axes::new(dx, dy)).aligned(aligns)).pack()) + } +} + +impl LayoutBlock for PlaceNode { + fn layout_block( + &self, + world: Tracked, + regions: &Regions, + styles: StyleChain, + ) -> SourceResult> { + let out_of_flow = self.out_of_flow(); + + // The pod is the base area of the region because for absolute + // placement we don't really care about the already used area. + let pod = { + let finite = regions.base.map(Abs::is_finite); + let expand = finite & (regions.expand | out_of_flow); + Regions::one(regions.base, regions.base, expand) + }; + + let mut frames = self.0.layout_block(world, &pod, styles)?; + + // If expansion is off, zero all sizes so that we don't take up any + // space in our parent. Otherwise, respect the expand settings. + let target = regions.expand.select(regions.first, Size::zero()); + frames[0].resize(target, Align::LEFT_TOP); + + Ok(frames) + } +} + +impl PlaceNode { + /// Whether this node wants to be placed relative to its its parent's base + /// origin. Instead of relative to the parent's current flow/cursor + /// position. + pub fn out_of_flow(&self) -> bool { + self.0 + .downcast::() + .map_or(false, |node| node.aligns.y.is_some()) + } +} diff --git a/library/src/layout/spacing.rs b/library/src/layout/spacing.rs new file mode 100644 index 00000000..67fff5db --- /dev/null +++ b/library/src/layout/spacing.rs @@ -0,0 +1,100 @@ +use std::cmp::Ordering; + +use crate::prelude::*; +use crate::text::ParNode; + +/// Horizontal spacing. +#[derive(Debug, Clone, Hash)] +pub struct HNode { + pub amount: Spacing, + pub weak: bool, +} + +#[node] +impl HNode { + fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { + let amount = args.expect("spacing")?; + let weak = args.named("weak")?.unwrap_or(false); + Ok(Self { amount, weak }.pack()) + } +} + +/// Vertical spacing. +#[derive(Debug, Clone, Hash)] +pub struct VNode { + pub amount: Spacing, + pub weak: bool, + pub generated: bool, +} + +#[node] +impl VNode { + fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { + let amount = args.expect("spacing")?; + let weak = args.named("weak")?.unwrap_or(false); + Ok(Self { amount, weak, generated: false }.pack()) + } +} + +/// Kinds of spacing. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum Spacing { + /// Spacing specified in absolute terms and relative to the parent's size. + Relative(Rel), + /// Spacing specified as a fraction of the remaining free space in the + /// parent. + Fractional(Fr), +} + +impl Spacing { + /// Whether this is fractional spacing. + pub fn is_fractional(self) -> bool { + matches!(self, Self::Fractional(_)) + } +} + +impl From for Spacing { + fn from(abs: Abs) -> Self { + Self::Relative(abs.into()) + } +} + +impl PartialOrd for Spacing { + fn partial_cmp(&self, other: &Self) -> Option { + match (self, other) { + (Self::Relative(a), Self::Relative(b)) => a.partial_cmp(b), + (Self::Fractional(a), Self::Fractional(b)) => a.partial_cmp(b), + _ => None, + } + } +} + +castable! { + Spacing, + Expected: "relative length or fraction", + 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), +} + +/// Spacing around and between blocks, relative to paragraph spacing. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct BlockSpacing(Rel); + +castable!(BlockSpacing: Rel); + +impl Resolve for BlockSpacing { + type Output = Abs; + + fn resolve(self, styles: StyleChain) -> Self::Output { + let whole = styles.get(ParNode::SPACING); + self.0.resolve(styles).relative_to(whole) + } +} + +impl From for BlockSpacing { + fn from(ratio: Ratio) -> Self { + Self(ratio.into()) + } +} diff --git a/library/src/layout/stack.rs b/library/src/layout/stack.rs new file mode 100644 index 00000000..44bcbf67 --- /dev/null +++ b/library/src/layout/stack.rs @@ -0,0 +1,322 @@ +use typst::model::StyledNode; + +use super::{AlignNode, Spacing}; +use crate::prelude::*; +use crate::text::ParNode; + +/// Arrange content and spacing along an axis. +#[derive(Debug, Hash)] +pub struct StackNode { + /// The stacking direction. + pub dir: Dir, + /// The spacing between non-spacing children. + pub spacing: Option, + /// The children to be stacked. + pub children: Vec, +} + +#[node(LayoutBlock)] +impl StackNode { + fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { + Ok(Self { + dir: args.named("dir")?.unwrap_or(Dir::TTB), + spacing: args.named("spacing")?, + children: args.all()?, + } + .pack()) + } +} + +impl LayoutBlock for StackNode { + fn layout_block( + &self, + world: Tracked, + regions: &Regions, + styles: StyleChain, + ) -> SourceResult> { + let mut layouter = StackLayouter::new(self.dir, regions, styles); + + // Spacing to insert before the next block. + let mut deferred = None; + + for child in &self.children { + match child { + StackChild::Spacing(kind) => { + layouter.layout_spacing(*kind); + deferred = None; + } + StackChild::Block(block) => { + if let Some(kind) = deferred { + layouter.layout_spacing(kind); + } + + layouter.layout_block(world, block, styles)?; + deferred = self.spacing; + } + } + } + + Ok(layouter.finish()) + } +} + +/// A child of a stack node. +#[derive(Hash)] +pub enum StackChild { + /// Spacing between other children. + Spacing(Spacing), + /// Arbitrary block-level content. + Block(Content), +} + +impl Debug for StackChild { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::Spacing(kind) => kind.fmt(f), + Self::Block(block) => block.fmt(f), + } + } +} + +castable! { + StackChild, + Expected: "relative length, fraction, or content", + Value::Length(v) => Self::Spacing(Spacing::Relative(v.into())), + Value::Ratio(v) => Self::Spacing(Spacing::Relative(v.into())), + Value::Relative(v) => Self::Spacing(Spacing::Relative(v)), + Value::Fraction(v) => Self::Spacing(Spacing::Fractional(v)), + Value::Content(v) => Self::Block(v), +} + +/// Performs stack layout. +pub struct StackLayouter<'a> { + /// The stacking direction. + dir: Dir, + /// The axis of the stacking direction. + axis: Axis, + /// The regions to layout children into. + regions: Regions, + /// The inherited styles. + styles: StyleChain<'a>, + /// Whether the stack itself should expand to fill the region. + expand: Axes, + /// The full size of the current region that was available at the start. + full: Size, + /// The generic size used by the frames for the current region. + used: Gen, + /// The sum of fractions in the current region. + fr: Fr, + /// Already layouted items whose exact positions are not yet known due to + /// fractional spacing. + items: Vec, + /// Finished frames for previous regions. + finished: Vec, +} + +/// A prepared item in a stack layout. +enum StackItem { + /// Absolute spacing between other items. + Absolute(Abs), + /// Fractional spacing between other items. + Fractional(Fr), + /// A frame for a layouted block. + Frame(Frame, Align), +} + +impl<'a> StackLayouter<'a> { + /// Create a new stack layouter. + pub fn new(dir: Dir, regions: &Regions, styles: StyleChain<'a>) -> Self { + let axis = dir.axis(); + let expand = regions.expand; + let full = regions.first; + + // Disable expansion along the block axis for children. + let mut regions = regions.clone(); + regions.expand.set(axis, false); + + Self { + dir, + axis, + regions, + styles, + expand, + full, + used: Gen::zero(), + fr: Fr::zero(), + items: vec![], + finished: vec![], + } + } + + /// Add spacing along the spacing direction. + pub fn layout_spacing(&mut self, spacing: Spacing) { + match spacing { + Spacing::Relative(v) => { + // Resolve the spacing and limit it to the remaining space. + let resolved = + v.resolve(self.styles).relative_to(self.regions.base.get(self.axis)); + let remaining = self.regions.first.get_mut(self.axis); + let limited = resolved.min(*remaining); + *remaining -= limited; + self.used.main += limited; + self.items.push(StackItem::Absolute(resolved)); + } + Spacing::Fractional(v) => { + self.fr += v; + self.items.push(StackItem::Fractional(v)); + } + } + } + + /// Layout an arbitrary block. + pub fn layout_block( + &mut self, + world: Tracked, + block: &Content, + styles: StyleChain, + ) -> SourceResult<()> { + if self.regions.is_full() { + self.finish_region(); + } + + // Block-axis alignment of the `AlignNode` is respected + // by the stack node. + let align = block + .downcast::() + .and_then(|node| node.aligns.get(self.axis)) + .map(|align| align.resolve(styles)) + .unwrap_or_else(|| { + if let Some(styled) = block.downcast::() { + let map = &styled.map; + if map.contains(ParNode::ALIGN) { + return StyleChain::with_root(map).get(ParNode::ALIGN); + } + } + + self.dir.start().into() + }); + + let frames = block.layout_block(world, &self.regions, styles)?; + let len = frames.len(); + for (i, mut frame) in frames.into_iter().enumerate() { + // Set the generic block role. + frame.apply_role(Role::GenericBlock); + + // Grow our size, shrink the region and save the frame for later. + let size = frame.size(); + let size = match self.axis { + Axis::X => Gen::new(size.y, size.x), + Axis::Y => Gen::new(size.x, size.y), + }; + + self.used.main += size.main; + self.used.cross.set_max(size.cross); + *self.regions.first.get_mut(self.axis) -= size.main; + self.items.push(StackItem::Frame(frame, align)); + + if i + 1 < len { + self.finish_region(); + } + } + + Ok(()) + } + + /// Advance to the next region. + pub fn finish_region(&mut self) { + // Determine the size of the stack in this region dependening on whether + // the region expands. + let used = self.used.to_axes(self.axis); + let mut size = self.expand.select(self.full, used); + + // Expand fully if there are fr spacings. + let full = self.full.get(self.axis); + let remaining = full - self.used.main; + if self.fr.get() > 0.0 && full.is_finite() { + self.used.main = full; + size.set(self.axis, full); + } + + let mut output = Frame::new(size); + let mut cursor = Abs::zero(); + let mut ruler: Align = self.dir.start().into(); + + // Place all frames. + for item in self.items.drain(..) { + match item { + StackItem::Absolute(v) => cursor += v, + StackItem::Fractional(v) => cursor += v.share(self.fr, remaining), + StackItem::Frame(frame, align) => { + if self.dir.is_positive() { + ruler = ruler.max(align); + } else { + ruler = ruler.min(align); + } + + // Align along the block axis. + let parent = size.get(self.axis); + let child = frame.size().get(self.axis); + let block = ruler.position(parent - self.used.main) + + if self.dir.is_positive() { + cursor + } else { + self.used.main - child - cursor + }; + + let pos = Gen::new(Abs::zero(), block).to_point(self.axis); + cursor += child; + output.push_frame(pos, frame); + } + } + } + + // Advance to the next region. + self.regions.next(); + self.full = self.regions.first; + self.used = Gen::zero(); + self.fr = Fr::zero(); + self.finished.push(output); + } + + /// Finish layouting and return the resulting frames. + pub fn finish(mut self) -> Vec { + self.finish_region(); + self.finished + } +} + +/// A container with a main and cross component. +#[derive(Default, Copy, Clone, Eq, PartialEq, Hash)] +pub struct Gen { + /// The main component. + pub cross: T, + /// The cross component. + pub main: T, +} + +impl Gen { + /// Create a new instance from the two components. + pub const fn new(cross: T, main: T) -> Self { + Self { cross, main } + } + + /// Convert to the specific representation, given the current main axis. + pub fn to_axes(self, main: Axis) -> Axes { + match main { + Axis::X => Axes::new(self.main, self.cross), + Axis::Y => Axes::new(self.cross, self.main), + } + } +} + +impl Gen { + /// The zero value. + pub fn zero() -> Self { + Self { cross: Abs::zero(), main: Abs::zero() } + } + + /// Convert to a point. + pub fn to_point(self, main: Axis) -> Point { + self.to_axes(main).to_point() + } +} diff --git a/library/src/layout/transform.rs b/library/src/layout/transform.rs new file mode 100644 index 00000000..4e0b8ac2 --- /dev/null +++ b/library/src/layout/transform.rs @@ -0,0 +1,117 @@ +use typst::geom::Transform; + +use crate::prelude::*; + +/// Move content without affecting layout. +#[derive(Debug, Hash)] +pub struct MoveNode { + /// The offset by which to move the content. + pub delta: Axes>, + /// The content that should be moved. + pub child: Content, +} + +#[node(LayoutInline)] +impl MoveNode { + fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { + let dx = args.named("dx")?.unwrap_or_default(); + let dy = args.named("dy")?.unwrap_or_default(); + Ok(Self { + delta: Axes::new(dx, dy), + child: args.expect("body")?, + } + .pack()) + } +} + +impl LayoutInline for MoveNode { + fn layout_inline( + &self, + world: Tracked, + regions: &Regions, + styles: StyleChain, + ) -> SourceResult> { + let mut frames = self.child.layout_inline(world, regions, styles)?; + + let delta = self.delta.resolve(styles); + for frame in &mut frames { + let delta = delta.zip(frame.size()).map(|(d, s)| d.relative_to(s)); + frame.translate(delta.to_point()); + } + + Ok(frames) + } +} + +/// Transform content without affecting layout. +#[derive(Debug, Hash)] +pub struct TransformNode { + /// Transformation to apply to the content. + pub transform: Transform, + /// The content that should be transformed. + pub child: Content, +} + +/// Rotate content without affecting layout. +pub type RotateNode = TransformNode; + +/// Scale content without affecting layout. +pub type ScaleNode = TransformNode; + +#[node(LayoutInline)] +impl TransformNode { + /// The origin of the transformation. + #[property(resolve)] + pub const ORIGIN: Axes> = Axes::default(); + + fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { + let transform = match T { + ROTATE => { + let angle = args.named_or_find("angle")?.unwrap_or_default(); + Transform::rotate(angle) + } + SCALE | _ => { + let all = args.find()?; + let sx = args.named("x")?.or(all).unwrap_or(Ratio::one()); + let sy = args.named("y")?.or(all).unwrap_or(Ratio::one()); + Transform::scale(sx, sy) + } + }; + + Ok(Self { transform, child: args.expect("body")? }.pack()) + } +} + +impl LayoutInline for TransformNode { + fn layout_inline( + &self, + world: Tracked, + regions: &Regions, + styles: StyleChain, + ) -> SourceResult> { + let origin = styles.get(Self::ORIGIN).unwrap_or(Align::CENTER_HORIZON); + let mut frames = self.child.layout_inline(world, regions, styles)?; + + for frame in &mut frames { + let Axes { x, y } = origin.zip(frame.size()).map(|(o, s)| o.position(s)); + let transform = Transform::translate(x, y) + .pre_concat(self.transform) + .pre_concat(Transform::translate(-x, -y)); + + frame.transform(transform); + } + + Ok(frames) + } +} + +/// Kinds of transformations. +/// +/// The move transformation is handled separately. +pub type TransformKind = usize; + +/// A rotational transformation. +const ROTATE: TransformKind = 1; + +/// A scale transformation. +const SCALE: TransformKind = 2; -- cgit v1.2.3