diff options
| author | Laurenz <laurmaedje@gmail.com> | 2022-02-28 15:50:48 +0100 |
|---|---|---|
| committer | Laurenz <laurmaedje@gmail.com> | 2022-02-28 23:54:34 +0100 |
| commit | 3ca5b238238e1128aa7bbfbd5db9e632045d8600 (patch) | |
| tree | 2471f4b340a15695b7f4d518c0b39fabaea676c4 /src/library/layout | |
| parent | b63c21c91d99a1554a019dc275f955d3e6a34271 (diff) | |
Reorganize library
Diffstat (limited to 'src/library/layout')
| -rw-r--r-- | src/library/layout/align.rs | 71 | ||||
| -rw-r--r-- | src/library/layout/columns.rs | 111 | ||||
| -rw-r--r-- | src/library/layout/container.rs | 24 | ||||
| -rw-r--r-- | src/library/layout/flow.rs | 272 | ||||
| -rw-r--r-- | src/library/layout/grid.rs | 579 | ||||
| -rw-r--r-- | src/library/layout/hide.rs | 30 | ||||
| -rw-r--r-- | src/library/layout/mod.rs | 27 | ||||
| -rw-r--r-- | src/library/layout/pad.rs | 83 | ||||
| -rw-r--r-- | src/library/layout/page.rs | 419 | ||||
| -rw-r--r-- | src/library/layout/place.rs | 59 | ||||
| -rw-r--r-- | src/library/layout/spacing.rs | 52 | ||||
| -rw-r--r-- | src/library/layout/stack.rs | 259 | ||||
| -rw-r--r-- | src/library/layout/transform.rs | 86 |
13 files changed, 2072 insertions, 0 deletions
diff --git a/src/library/layout/align.rs b/src/library/layout/align.rs new file mode 100644 index 00000000..7fbe0d01 --- /dev/null +++ b/src/library/layout/align.rs @@ -0,0 +1,71 @@ +use crate::library::prelude::*; +use crate::library::text::ParNode; + +/// Align a node along the layouting axes. +#[derive(Debug, Hash)] +pub struct AlignNode { + /// How to align the node horizontally and vertically. + pub aligns: Spec<Option<Align>>, + /// The node to be aligned. + pub child: LayoutNode, +} + +#[class] +impl AlignNode { + fn construct(_: &mut Context, args: &mut Args) -> TypResult<Template> { + let aligns: Spec<_> = args.find()?.unwrap_or_default(); + let body: LayoutNode = args.expect("body")?; + Ok(Template::block(body.aligned(aligns))) + } +} + +impl Layout for AlignNode { + fn layout( + &self, + ctx: &mut Context, + regions: &Regions, + styles: StyleChain, + ) -> TypResult<Vec<Arc<Frame>>> { + // The child only needs to expand along an axis if there's no alignment. + let mut pod = regions.clone(); + pod.expand &= self.aligns.map_is_none(); + + // Align paragraphs inside the child. + let mut passed = StyleMap::new(); + if let Some(align) = self.aligns.x { + passed.set(ParNode::ALIGN, align); + } + + // Layout the child. + let mut frames = self.child.layout(ctx, &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 default = Spec::new(Align::Left, Align::Top); + let aligns = self.aligns.unwrap_or(default); + Arc::make_mut(frame).resize(target, aligns); + } + + Ok(frames) + } +} + +dynamic! { + Align: "alignment", +} + +dynamic! { + Spec<Align>: "2d alignment", +} + +castable! { + Spec<Option<Align>>, + Expected: "1d or 2d alignment", + @align: Align => { + let mut aligns = Spec::default(); + aligns.set(align.axis(), Some(*align)); + aligns + }, + @aligns: Spec<Align> => aligns.map(Some), +} diff --git a/src/library/layout/columns.rs b/src/library/layout/columns.rs new file mode 100644 index 00000000..167e7068 --- /dev/null +++ b/src/library/layout/columns.rs @@ -0,0 +1,111 @@ +use crate::library::prelude::*; +use crate::library::text::ParNode; + +/// 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: LayoutNode, +} + +#[class] +impl ColumnsNode { + /// The size of the gutter space between each column. + pub const GUTTER: Linear = Relative::new(0.04).into(); + + fn construct(_: &mut Context, args: &mut Args) -> TypResult<Template> { + Ok(Template::block(Self { + columns: args.expect("column count")?, + child: args.expect("body")?, + })) + } +} + +impl Layout for ColumnsNode { + fn layout( + &self, + ctx: &mut Context, + regions: &Regions, + styles: StyleChain, + ) -> TypResult<Vec<Arc<Frame>>> { + // Separating the infinite space into infinite columns does not make + // much sense. + if regions.first.x.is_infinite() { + return self.child.layout(ctx, regions, styles); + } + + // Determine the width of the gutter and each column. + let columns = self.columns.get(); + let gutter = styles.get(Self::GUTTER).resolve(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: Spec::new(true, regions.expand.y), + }; + + // Layout the children. + let mut frames = self.child.layout(ctx, &pod, styles)?.into_iter(); + + let dir = styles.get(ParNode::DIR); + let total_regions = (frames.len() as f32 / columns as f32).ceil() as usize; + let mut finished = vec![]; + + // Stitch together the columns for each region. + for region in regions.iter().take(total_regions) { + // The height should be the parent height if the node shall 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 { Length::zero() }; + let mut output = Frame::new(Size::new(regions.first.x, height)); + let mut cursor = Length::zero(); + + for _ in 0 .. columns { + let frame = match frames.next() { + Some(frame) => frame, + None => break, + }; + + if !regions.expand.y { + output.size.y.set_max(frame.size.y); + } + + let width = frame.size.x; + 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(Arc::new(output)); + } + + Ok(finished) + } +} + +/// A column break. +pub struct ColbreakNode; + +#[class] +impl ColbreakNode { + fn construct(_: &mut Context, _: &mut Args) -> TypResult<Template> { + Ok(Template::Colbreak) + } +} diff --git a/src/library/layout/container.rs b/src/library/layout/container.rs new file mode 100644 index 00000000..55579878 --- /dev/null +++ b/src/library/layout/container.rs @@ -0,0 +1,24 @@ +use crate::library::prelude::*; + +/// An inline-level container that sizes content and places it into a paragraph. +pub struct BoxNode; + +#[class] +impl BoxNode { + fn construct(_: &mut Context, args: &mut Args) -> TypResult<Template> { + let width = args.named("width")?; + let height = args.named("height")?; + let body: LayoutNode = args.find()?.unwrap_or_default(); + Ok(Template::inline(body.sized(Spec::new(width, height)))) + } +} + +/// A block-level container that places content into a separate flow. +pub struct BlockNode; + +#[class] +impl BlockNode { + fn construct(_: &mut Context, args: &mut Args) -> TypResult<Template> { + Ok(Template::Block(args.find()?.unwrap_or_default())) + } +} diff --git a/src/library/layout/flow.rs b/src/library/layout/flow.rs new file mode 100644 index 00000000..f4b885b1 --- /dev/null +++ b/src/library/layout/flow.rs @@ -0,0 +1,272 @@ +use super::{AlignNode, PlaceNode, SpacingKind}; +use crate::library::prelude::*; +use crate::library::text::{ParNode, TextNode}; + +/// Arrange spacing, paragraphs and other 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<FlowChild>); + +/// A child of a flow node. +#[derive(Hash)] +pub enum FlowChild { + /// Leading between other children. + Leading, + /// A paragraph / block break. + Parbreak, + /// A column / region break. + Colbreak, + /// Vertical spacing between other children. + Spacing(SpacingKind), + /// An arbitrary block-level node. + Node(LayoutNode), +} + +impl Layout for FlowNode { + fn layout( + &self, + ctx: &mut Context, + regions: &Regions, + styles: StyleChain, + ) -> TypResult<Vec<Arc<Frame>>> { + let mut layouter = FlowLayouter::new(regions); + + for (child, map) in self.0.iter() { + let styles = map.chain(&styles); + match child { + FlowChild::Leading => { + let em = styles.get(TextNode::SIZE).abs; + let amount = styles.get(ParNode::LEADING).resolve(em); + layouter.layout_spacing(amount.into()); + } + FlowChild::Parbreak => { + let em = styles.get(TextNode::SIZE).abs; + let leading = styles.get(ParNode::LEADING); + let spacing = styles.get(ParNode::SPACING); + let amount = (leading + spacing).resolve(em); + layouter.layout_spacing(amount.into()); + } + FlowChild::Colbreak => { + layouter.finish_region(); + } + FlowChild::Spacing(kind) => { + layouter.layout_spacing(*kind); + } + FlowChild::Node(ref node) => { + layouter.layout_node(ctx, node, styles)?; + } + } + } + + Ok(layouter.finish()) + } +} + +impl Merge for FlowChild { + fn merge(&mut self, _: &Self) -> bool { + false + } +} + +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::Leading => f.pad("Leading"), + Self::Parbreak => f.pad("Parbreak"), + Self::Colbreak => f.pad("Colbreak"), + Self::Spacing(kind) => write!(f, "{:?}", kind), + Self::Node(node) => node.fmt(f), + } + } +} + +/// Performs flow layout. +pub struct FlowLayouter { + /// The regions to layout children into. + regions: Regions, + /// Whether the flow should expand to fill the region. + expand: Spec<bool>, + /// 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 fractional ratios in the current region. + fr: Fractional, + /// Spacing and layouted nodes. + items: Vec<FlowItem>, + /// Finished frames for previous regions. + finished: Vec<Arc<Frame>>, +} + +/// A prepared item in a flow layout. +enum FlowItem { + /// Absolute spacing between other items. + Absolute(Length), + /// Fractional spacing between other items. + Fractional(Fractional), + /// A frame for a layouted child node and how to align it. + Frame(Arc<Frame>, Spec<Align>), + /// An absolutely placed frame. + Placed(Arc<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: Fractional::zero(), + items: vec![], + finished: vec![], + } + } + + /// Layout spacing. + pub fn layout_spacing(&mut self, spacing: SpacingKind) { + match spacing { + SpacingKind::Linear(v) => { + // Resolve the linear and limit it to the remaining space. + let resolved = v.resolve(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)); + } + SpacingKind::Fractional(v) => { + self.items.push(FlowItem::Fractional(v)); + self.fr += v; + } + } + } + + /// Layout a node. + pub fn layout_node( + &mut self, + ctx: &mut Context, + node: &LayoutNode, + styles: StyleChain, + ) -> TypResult<()> { + // 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) = node.downcast::<PlaceNode>() { + if placed.out_of_flow() { + let frame = node.layout(ctx, &self.regions, styles)?.remove(0); + self.items.push(FlowItem::Placed(frame)); + return Ok(()); + } + } + + // How to align the node. + let aligns = Spec::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 node. + node.downcast::<AlignNode>() + .and_then(|aligned| aligned.aligns.y) + .unwrap_or(Align::Top), + ); + + let frames = node.layout(ctx, &self.regions, styles)?; + let len = frames.len(); + for (i, frame) in frames.into_iter().enumerate() { + // 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 = Length::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.resolve(self.fr, remaining); + } + FlowItem::Frame(frame, aligns) => { + ruler = ruler.max(aligns.y); + let x = aligns.x.resolve(size.x - frame.size.x); + let y = offset + ruler.resolve(size.y - self.used.y); + let pos = Point::new(x, y); + offset += frame.size.y; + 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 = Fractional::zero(); + self.finished.push(Arc::new(output)); + } + + /// Finish layouting and return the resulting frames. + pub fn finish(mut self) -> Vec<Arc<Frame>> { + if self.expand.y { + while self.regions.backlog.len() > 0 { + self.finish_region(); + } + } + + self.finish_region(); + self.finished + } +} diff --git a/src/library/layout/grid.rs b/src/library/layout/grid.rs new file mode 100644 index 00000000..63cd83b1 --- /dev/null +++ b/src/library/layout/grid.rs @@ -0,0 +1,579 @@ +use crate::library::prelude::*; + +/// Arrange nodes in a grid. +#[derive(Debug, Hash)] +pub struct GridNode { + /// Defines sizing for content rows and columns. + pub tracks: Spec<Vec<TrackSizing>>, + /// Defines sizing of gutter rows and columns between content. + pub gutter: Spec<Vec<TrackSizing>>, + /// The nodes to be arranged in a grid. + pub children: Vec<LayoutNode>, +} + +#[class] +impl GridNode { + fn construct(_: &mut Context, args: &mut Args) -> TypResult<Template> { + let columns = args.named("columns")?.unwrap_or_default(); + let rows = args.named("rows")?.unwrap_or_default(); + let base_gutter: Vec<TrackSizing> = args.named("gutter")?.unwrap_or_default(); + let column_gutter = args.named("column-gutter")?; + let row_gutter = args.named("row-gutter")?; + Ok(Template::block(Self { + tracks: Spec::new(columns, rows), + gutter: Spec::new( + column_gutter.unwrap_or_else(|| base_gutter.clone()), + row_gutter.unwrap_or(base_gutter), + ), + children: args.all()?, + })) + } +} + +impl Layout for GridNode { + fn layout( + &self, + ctx: &mut Context, + regions: &Regions, + styles: StyleChain, + ) -> TypResult<Vec<Arc<Frame>>> { + // Prepare grid layout by unifying content and gutter tracks. + let layouter = GridLayouter::new( + self.tracks.as_deref(), + self.gutter.as_deref(), + &self.children, + regions, + styles, + ); + + // Measure the columns and layout the grid row-by-row. + layouter.layout(ctx) + } +} + +/// Defines how to size a grid cell along an axis. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum TrackSizing { + /// Fit the cell to its contents. + Auto, + /// A length stated in absolute values and/or relative to the parent's size. + Linear(Linear), + /// A length that is the fraction of the remaining free space in the parent. + Fractional(Fractional), +} + +castable! { + Vec<TrackSizing>, + Expected: "integer or (auto, linear, fractional, or array thereof)", + Value::Auto => vec![TrackSizing::Auto], + Value::Length(v) => vec![TrackSizing::Linear(v.into())], + Value::Relative(v) => vec![TrackSizing::Linear(v.into())], + Value::Linear(v) => vec![TrackSizing::Linear(v)], + Value::Fractional(v) => vec![TrackSizing::Fractional(v)], + Value::Int(v) => vec![TrackSizing::Auto; Value::Int(v).cast::<NonZeroUsize>()?.get()], + Value::Array(values) => values + .into_iter() + .filter_map(|v| v.cast().ok()) + .collect(), +} + +castable! { + TrackSizing, + Expected: "auto, linear, or fractional", + Value::Auto => Self::Auto, + Value::Length(v) => Self::Linear(v.into()), + Value::Relative(v) => Self::Linear(v.into()), + Value::Linear(v) => Self::Linear(v), + Value::Fractional(v) => Self::Fractional(v), +} + +/// Performs grid layout. +pub struct GridLayouter<'a> { + /// The grid cells. + cells: &'a [LayoutNode], + /// The column tracks including gutter tracks. + cols: Vec<TrackSizing>, + /// The row tracks including gutter tracks. + rows: Vec<TrackSizing>, + /// The regions to layout children into. + regions: Regions, + /// The inherited styles. + styles: StyleChain<'a>, + /// Resolved column sizes. + rcols: Vec<Length>, + /// Rows in the current region. + lrows: Vec<Row>, + /// The full height of the current region. + full: Length, + /// The used-up size of the current region. The horizontal size is + /// determined once after columns are resolved and not touched again. + used: Size, + /// The sum of fractional ratios in the current region. + fr: Fractional, + /// Frames for finished regions. + finished: Vec<Arc<Frame>>, +} + +/// Produced by initial row layout, auto and linear rows are already finished, +/// fractional rows not yet. +enum Row { + /// Finished row frame of auto or linear row. + Frame(Frame), + /// Ratio of a fractional row and y index of the track. + Fr(Fractional, usize), +} + +impl<'a> GridLayouter<'a> { + /// Create a new grid layouter. + /// + /// This prepares grid layout by unifying content and gutter tracks. + pub fn new( + tracks: Spec<&[TrackSizing]>, + gutter: Spec<&[TrackSizing]>, + cells: &'a [LayoutNode], + regions: &Regions, + styles: StyleChain<'a>, + ) -> Self { + let mut cols = vec![]; + let mut rows = vec![]; + + // Number of content columns: Always at least one. + let c = tracks.x.len().max(1); + + // Number of content rows: At least as many as given, but also at least + // as many as needed to place each item. + let r = { + let len = cells.len(); + let given = tracks.y.len(); + let needed = len / c + (len % c).clamp(0, 1); + given.max(needed) + }; + + let auto = TrackSizing::Auto; + let zero = TrackSizing::Linear(Linear::zero()); + let get_or = |tracks: &[_], idx, default| { + tracks.get(idx).or(tracks.last()).copied().unwrap_or(default) + }; + + // Collect content and gutter columns. + for x in 0 .. c { + cols.push(get_or(tracks.x, x, auto)); + cols.push(get_or(gutter.x, x, zero)); + } + + // Collect content and gutter rows. + for y in 0 .. r { + rows.push(get_or(tracks.y, y, auto)); + rows.push(get_or(gutter.y, y, zero)); + } + + // Remove superfluous gutter tracks. + cols.pop(); + rows.pop(); + + let full = regions.first.y; + let rcols = vec![Length::zero(); cols.len()]; + let lrows = vec![]; + + // We use the regions for auto row measurement. Since at that moment, + // columns are already sized, we can enable horizontal expansion. + let mut regions = regions.clone(); + regions.expand = Spec::new(true, false); + + Self { + cells, + cols, + rows, + regions, + styles, + rcols, + lrows, + full, + used: Size::zero(), + fr: Fractional::zero(), + finished: vec![], + } + } + + /// Determines the columns sizes and then layouts the grid row-by-row. + pub fn layout(mut self, ctx: &mut Context) -> TypResult<Vec<Arc<Frame>>> { + self.measure_columns(ctx)?; + + for y in 0 .. self.rows.len() { + // Skip to next region if current one is full, but only for content + // rows, not for gutter rows. + if y % 2 == 0 && self.regions.is_full() { + self.finish_region(ctx)?; + } + + match self.rows[y] { + TrackSizing::Auto => self.layout_auto_row(ctx, y)?, + TrackSizing::Linear(v) => self.layout_linear_row(ctx, v, y)?, + TrackSizing::Fractional(v) => { + self.lrows.push(Row::Fr(v, y)); + self.fr += v; + } + } + } + + self.finish_region(ctx)?; + Ok(self.finished) + } + + /// Determine all column sizes. + fn measure_columns(&mut self, ctx: &mut Context) -> TypResult<()> { + // Sum of sizes of resolved linear tracks. + let mut linear = Length::zero(); + + // Sum of fractions of all fractional tracks. + let mut fr = Fractional::zero(); + + // Resolve the size of all linear columns and compute the sum of all + // fractional tracks. + for (&col, rcol) in self.cols.iter().zip(&mut self.rcols) { + match col { + TrackSizing::Auto => {} + TrackSizing::Linear(v) => { + let resolved = v.resolve(self.regions.base.x); + *rcol = resolved; + linear += resolved; + } + TrackSizing::Fractional(v) => fr += v, + } + } + + // Size that is not used by fixed-size columns. + let available = self.regions.first.x - linear; + if available >= Length::zero() { + // Determine size of auto columns. + let (auto, count) = self.measure_auto_columns(ctx, available)?; + + // If there is remaining space, distribute it to fractional columns, + // otherwise shrink auto columns. + let remaining = available - auto; + if remaining >= Length::zero() { + if !fr.is_zero() { + self.grow_fractional_columns(remaining, fr); + } + } else { + self.shrink_auto_columns(available, count); + } + } + + // Sum up the resolved column sizes once here. + self.used.x = self.rcols.iter().sum(); + + Ok(()) + } + + /// Measure the size that is available to auto columns. + fn measure_auto_columns( + &mut self, + ctx: &mut Context, + available: Length, + ) -> TypResult<(Length, usize)> { + let mut auto = Length::zero(); + let mut count = 0; + + // Determine size of auto columns by laying out all cells in those + // columns, measuring them and finding the largest one. + for (x, &col) in self.cols.iter().enumerate() { + if col != TrackSizing::Auto { + continue; + } + + let mut resolved = Length::zero(); + for y in 0 .. self.rows.len() { + if let Some(node) = self.cell(x, y) { + let size = Size::new(available, self.regions.base.y); + let mut pod = + Regions::one(size, self.regions.base, Spec::splat(false)); + + // For linear rows, we can already resolve the correct + // base, for auto it's already correct and for fr we could + // only guess anyway. + if let TrackSizing::Linear(v) = self.rows[y] { + pod.base.y = v.resolve(self.regions.base.y); + } + + let frame = node.layout(ctx, &pod, self.styles)?.remove(0); + resolved.set_max(frame.size.x); + } + } + + self.rcols[x] = resolved; + auto += resolved; + count += 1; + } + + Ok((auto, count)) + } + + /// Distribute remaining space to fractional columns. + fn grow_fractional_columns(&mut self, remaining: Length, fr: Fractional) { + for (&col, rcol) in self.cols.iter().zip(&mut self.rcols) { + if let TrackSizing::Fractional(v) = col { + *rcol = v.resolve(fr, remaining); + } + } + } + + /// Redistribute space to auto columns so that each gets a fair share. + fn shrink_auto_columns(&mut self, available: Length, count: usize) { + // The fair share each auto column may have. + let fair = available / count as f64; + + // The number of overlarge auto columns and the space that will be + // equally redistributed to them. + let mut overlarge: usize = 0; + let mut redistribute = available; + + // Find out the number of and space used by overlarge auto columns. + for (&col, rcol) in self.cols.iter().zip(&mut self.rcols) { + if col == TrackSizing::Auto { + if *rcol > fair { + overlarge += 1; + } else { + redistribute -= *rcol; + } + } + } + + // Redistribute the space equally. + let share = redistribute / overlarge as f64; + for (&col, rcol) in self.cols.iter().zip(&mut self.rcols) { + if col == TrackSizing::Auto && *rcol > fair { + *rcol = share; + } + } + } + + /// Layout a row with automatic height. Such a row may break across multiple + /// regions. + fn layout_auto_row(&mut self, ctx: &mut Context, y: usize) -> TypResult<()> { + let mut resolved: Vec<Length> = vec![]; + + // Determine the size for each region of the row. + for (x, &rcol) in self.rcols.iter().enumerate() { + if let Some(node) = self.cell(x, y) { + let mut pod = self.regions.clone(); + pod.first.x = rcol; + + // All widths should be `rcol` except the base for auto columns. + if self.cols[x] == TrackSizing::Auto { + pod.base.x = self.regions.base.x; + } + + let mut sizes = node + .layout(ctx, &pod, self.styles)? + .into_iter() + .map(|frame| frame.size.y); + + // For each region, we want to know the maximum height any + // column requires. + for (target, size) in resolved.iter_mut().zip(&mut sizes) { + target.set_max(size); + } + + // New heights are maximal by virtue of being new. Note that + // this extend only uses the rest of the sizes iterator. + resolved.extend(sizes); + } + } + + // Nothing to layout. + if resolved.is_empty() { + return Ok(()); + } + + // Layout into a single region. + if let &[first] = resolved.as_slice() { + let frame = self.layout_single_row(ctx, first, y)?; + self.push_row(frame); + return Ok(()); + } + + // Expand all but the last region if the space is not + // eaten up by any fr rows. + if self.fr.is_zero() { + let len = resolved.len(); + for (region, target) in self.regions.iter().zip(&mut resolved[.. len - 1]) { + target.set_max(region.y); + } + } + + // Layout into multiple regions. + let frames = self.layout_multi_row(ctx, &resolved, y)?; + let len = frames.len(); + for (i, frame) in frames.into_iter().enumerate() { + self.push_row(frame); + if i + 1 < len { + self.finish_region(ctx)?; + } + } + + Ok(()) + } + + /// Layout a row with linear height. Such a row cannot break across multiple + /// regions, but it may force a region break. + fn layout_linear_row( + &mut self, + ctx: &mut Context, + v: Linear, + y: usize, + ) -> TypResult<()> { + let resolved = v.resolve(self.regions.base.y); + let frame = self.layout_single_row(ctx, resolved, y)?; + + // Skip to fitting region. + let height = frame.size.y; + while !self.regions.first.y.fits(height) && !self.regions.in_last() { + self.finish_region(ctx)?; + + // Don't skip multiple regions for gutter and don't push a row. + if y % 2 == 1 { + return Ok(()); + } + } + + self.push_row(frame); + + Ok(()) + } + + /// Layout a row with fixed height and return its frame. + fn layout_single_row( + &self, + ctx: &mut Context, + height: Length, + y: usize, + ) -> TypResult<Frame> { + let mut output = Frame::new(Size::new(self.used.x, height)); + let mut pos = Point::zero(); + + for (x, &rcol) in self.rcols.iter().enumerate() { + if let Some(node) = self.cell(x, y) { + let size = Size::new(rcol, height); + + // Set the base to the region's base for auto rows and to the + // size for linear and fractional rows. + let base = Spec::new(self.cols[x], self.rows[y]) + .map(|s| s == TrackSizing::Auto) + .select(self.regions.base, size); + + let pod = Regions::one(size, base, Spec::splat(true)); + let frame = node.layout(ctx, &pod, self.styles)?.remove(0); + output.push_frame(pos, frame); + } + + pos.x += rcol; + } + + Ok(output) + } + + /// Layout a row spanning multiple regions. + fn layout_multi_row( + &self, + ctx: &mut Context, + heights: &[Length], + y: usize, + ) -> TypResult<Vec<Frame>> { + // Prepare frames. + let mut outputs: Vec<_> = heights + .iter() + .map(|&h| Frame::new(Size::new(self.used.x, h))) + .collect(); + + // Prepare regions. + let size = Size::new(self.used.x, heights[0]); + let mut pod = Regions::one(size, self.regions.base, Spec::splat(true)); + pod.backlog = heights[1 ..].to_vec(); + + // Layout the row. + let mut pos = Point::zero(); + for (x, &rcol) in self.rcols.iter().enumerate() { + if let Some(node) = self.cell(x, y) { + pod.first.x = rcol; + + // All widths should be `rcol` except the base for auto columns. + if self.cols[x] == TrackSizing::Auto { + pod.base.x = self.regions.base.x; + } + + // Push the layouted frames into the individual output frames. + let frames = node.layout(ctx, &pod, self.styles)?; + for (output, frame) in outputs.iter_mut().zip(frames) { + output.push_frame(pos, frame); + } + } + + pos.x += rcol; + } + + Ok(outputs) + } + + /// Push a row frame into the current region. + fn push_row(&mut self, frame: Frame) { + self.regions.first.y -= frame.size.y; + self.used.y += frame.size.y; + self.lrows.push(Row::Frame(frame)); + } + + /// Finish rows for one region. + fn finish_region(&mut self, ctx: &mut Context) -> TypResult<()> { + // Determine the size of the grid in this region, expanding fully if + // there are fr rows. + let mut size = self.used; + if self.fr.get() > 0.0 && self.full.is_finite() { + size.y = self.full; + } + + // The frame for the region. + let mut output = Frame::new(size); + let mut pos = Point::zero(); + + // Place finished rows and layout fractional rows. + for row in std::mem::take(&mut self.lrows) { + let frame = match row { + Row::Frame(frame) => frame, + Row::Fr(v, y) => { + let remaining = self.full - self.used.y; + let height = v.resolve(self.fr, remaining); + self.layout_single_row(ctx, height, y)? + } + }; + + let height = frame.size.y; + output.merge_frame(pos, frame); + pos.y += height; + } + + self.finished.push(Arc::new(output)); + self.regions.next(); + self.full = self.regions.first.y; + self.used.y = Length::zero(); + self.fr = Fractional::zero(); + + Ok(()) + } + + /// Get the node in the cell in column `x` and row `y`. + /// + /// Returns `None` if it's a gutter cell. + #[track_caller] + fn cell(&self, x: usize, y: usize) -> Option<&'a LayoutNode> { + assert!(x < self.cols.len()); + assert!(y < self.rows.len()); + + // Even columns and rows are children, odd ones are gutter. + if x % 2 == 0 && y % 2 == 0 { + let c = 1 + self.cols.len() / 2; + self.cells.get((y / 2) * c + x / 2) + } else { + None + } + } +} diff --git a/src/library/layout/hide.rs b/src/library/layout/hide.rs new file mode 100644 index 00000000..861a1208 --- /dev/null +++ b/src/library/layout/hide.rs @@ -0,0 +1,30 @@ +use crate::library::prelude::*; + +/// Hide a node without affecting layout. +#[derive(Debug, Hash)] +pub struct HideNode(pub LayoutNode); + +#[class] +impl HideNode { + fn construct(_: &mut Context, args: &mut Args) -> TypResult<Template> { + Ok(Template::inline(Self(args.expect("body")?))) + } +} + +impl Layout for HideNode { + fn layout( + &self, + ctx: &mut Context, + regions: &Regions, + styles: StyleChain, + ) -> TypResult<Vec<Arc<Frame>>> { + let mut frames = self.0.layout(ctx, regions, styles)?; + + // Clear the frames. + for frame in &mut frames { + *frame = Arc::new(Frame { elements: vec![], ..**frame }); + } + + Ok(frames) + } +} diff --git a/src/library/layout/mod.rs b/src/library/layout/mod.rs new file mode 100644 index 00000000..944548ab --- /dev/null +++ b/src/library/layout/mod.rs @@ -0,0 +1,27 @@ +//! Composable layouts. + +mod align; +mod columns; +mod container; +mod flow; +mod grid; +mod hide; +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 hide::*; +pub use pad::*; +pub use page::*; +pub use place::*; +pub use spacing::*; +pub use stack::*; +pub use transform::*; diff --git a/src/library/layout/pad.rs b/src/library/layout/pad.rs new file mode 100644 index 00000000..175a54f0 --- /dev/null +++ b/src/library/layout/pad.rs @@ -0,0 +1,83 @@ +use crate::library::prelude::*; + +/// Pad a node at the sides. +#[derive(Debug, Hash)] +pub struct PadNode { + /// The amount of padding. + pub padding: Sides<Linear>, + /// The child node whose sides to pad. + pub child: LayoutNode, +} + +#[class] +impl PadNode { + fn construct(_: &mut Context, args: &mut Args) -> TypResult<Template> { + let all = args.find()?; + let hor = args.named("horizontal")?; + let ver = args.named("vertical")?; + let left = args.named("left")?.or(hor).or(all).unwrap_or_default(); + let top = args.named("top")?.or(ver).or(all).unwrap_or_default(); + let right = args.named("right")?.or(hor).or(all).unwrap_or_default(); + let bottom = args.named("bottom")?.or(ver).or(all).unwrap_or_default(); + let body: LayoutNode = args.expect("body")?; + let padding = Sides::new(left, top, right, bottom); + Ok(Template::block(body.padded(padding))) + } +} + +impl Layout for PadNode { + fn layout( + &self, + ctx: &mut Context, + regions: &Regions, + styles: StyleChain, + ) -> TypResult<Vec<Arc<Frame>>> { + // Layout child into padded regions. + let pod = regions.map(|size| shrink(size, self.padding)); + let mut frames = self.child.layout(ctx, &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, self.padding); + let padding = self.padding.resolve(padded); + let offset = Point::new(padding.left, padding.top); + + // Grow the frame and translate everything in the frame inwards. + let frame = Arc::make_mut(frame); + frame.size = padded; + frame.translate(offset); + } + + Ok(frames) + } +} + +/// Shrink a size by padding relative to the size itself. +fn shrink(size: Size, padding: Sides<Linear>) -> Size { + size - padding.resolve(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 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<Linear>) -> Size { + size.zip(padding.sum_by_axis()) + .map(|(s, p)| (s + p.abs).safe_div(1.0 - p.rel.get())) +} diff --git a/src/library/layout/page.rs b/src/library/layout/page.rs new file mode 100644 index 00000000..f5d766a5 --- /dev/null +++ b/src/library/layout/page.rs @@ -0,0 +1,419 @@ +use std::fmt::{self, Display, Formatter}; +use std::str::FromStr; + +use super::ColumnsNode; +use crate::library::prelude::*; + +/// Layouts its child onto one or multiple pages. +#[derive(Clone, PartialEq, Hash)] +pub struct PageNode(pub LayoutNode); + +#[class] +impl PageNode { + /// The unflipped width of the page. + pub const WIDTH: Smart<Length> = Smart::Custom(Paper::A4.width()); + /// The unflipped height of the page. + pub const HEIGHT: Smart<Length> = Smart::Custom(Paper::A4.height()); + /// Whether the page is flipped into landscape orientation. + pub const FLIPPED: bool = false; + /// The left margin. + pub const LEFT: Smart<Linear> = Smart::Auto; + /// The right margin. + pub const RIGHT: Smart<Linear> = Smart::Auto; + /// The top margin. + pub const TOP: Smart<Linear> = Smart::Auto; + /// The bottom margin. + pub const BOTTOM: Smart<Linear> = Smart::Auto; + /// The page's background color. + pub const FILL: Option<Paint> = None; + /// How many columns the page has. + pub const COLUMNS: NonZeroUsize = NonZeroUsize::new(1).unwrap(); + /// The page's header. + pub const HEADER: Marginal = Marginal::None; + /// The page's footer. + pub const FOOTER: Marginal = Marginal::None; + + fn construct(_: &mut Context, args: &mut Args) -> TypResult<Template> { + Ok(Template::Page(Self(args.expect("body")?))) + } + + fn set(args: &mut Args, styles: &mut StyleMap) -> TypResult<()> { + if let Some(paper) = args.named_or_find::<Paper>("paper")? { + styles.set(Self::WIDTH, Smart::Custom(paper.width())); + styles.set(Self::HEIGHT, Smart::Custom(paper.height())); + } + + styles.set_opt(Self::WIDTH, args.named("width")?); + styles.set_opt(Self::HEIGHT, args.named("height")?); + + let all = args.named("margins")?; + let hor = args.named("horizontal")?; + let ver = args.named("vertical")?; + styles.set_opt(Self::LEFT, args.named("left")?.or(hor).or(all)); + styles.set_opt(Self::TOP, args.named("top")?.or(ver).or(all)); + styles.set_opt(Self::RIGHT, args.named("right")?.or(hor).or(all)); + styles.set_opt(Self::BOTTOM, args.named("bottom")?.or(ver).or(all)); + + styles.set_opt(Self::FLIPPED, args.named("flipped")?); + styles.set_opt(Self::FILL, args.named("fill")?); + styles.set_opt(Self::COLUMNS, args.named("columns")?); + styles.set_opt(Self::HEADER, args.named("header")?); + styles.set_opt(Self::FOOTER, args.named("footer")?); + + Ok(()) + } +} + +impl PageNode { + /// Layout the page run into a sequence of frames, one per page. + pub fn layout( + &self, + ctx: &mut Context, + mut page: usize, + styles: StyleChain, + ) -> TypResult<Vec<Arc<Frame>>> { + // When one of the lengths is infinite the page fits its content along + // that axis. + let width = styles.get(Self::WIDTH).unwrap_or(Length::inf()); + let height = styles.get(Self::HEIGHT).unwrap_or(Length::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_infinite() { + min = Paper::A4.width(); + } + + // Determine the margins. + let default = Linear::from(0.1190 * min); + let padding = Sides { + left: styles.get(Self::LEFT).unwrap_or(default), + right: styles.get(Self::RIGHT).unwrap_or(default), + top: styles.get(Self::TOP).unwrap_or(default), + bottom: styles.get(Self::BOTTOM).unwrap_or(default), + }; + + let mut child = self.0.clone(); + + // Realize columns with columns node. + 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(Length::is_finite)); + let mut frames = child.layout(ctx, ®ions, styles)?; + + let header = styles.get_ref(Self::HEADER); + let footer = styles.get_ref(Self::FOOTER); + + // Realize header and footer. + for frame in &mut frames { + let size = frame.size; + let padding = padding.resolve(size); + for (y, h, marginal) in [ + (Length::zero(), padding.top, header), + (size.y - padding.bottom, padding.bottom, footer), + ] { + if let Some(template) = marginal.resolve(ctx, page)? { + let pos = Point::new(padding.left, y); + let w = size.x - padding.left - padding.right; + let area = Size::new(w, h); + let pod = Regions::one(area, area, area.map(Length::is_finite)); + let sub = Layout::layout(&template, ctx, &pod, styles)?.remove(0); + Arc::make_mut(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. +pub struct PagebreakNode; + +#[class] +impl PagebreakNode { + fn construct(_: &mut Context, _: &mut Args) -> TypResult<Template> { + Ok(Template::Pagebreak) + } +} + +/// A header or footer definition. +#[derive(Debug, Clone, PartialEq, Hash)] +pub enum Marginal { + /// Nothing, + None, + /// A bare template. + Template(Template), + /// A closure mapping from a page number to a template. + Func(Func, Span), +} + +impl Marginal { + /// Resolve the marginal based on the page number. + pub fn resolve(&self, ctx: &mut Context, page: usize) -> TypResult<Option<Template>> { + Ok(match self { + Self::None => None, + Self::Template(template) => Some(template.clone()), + Self::Func(func, span) => { + let args = Args::from_values(*span, [Value::Int(page as i64)]); + func.call(ctx, args)?.cast().at(*span)? + } + }) + } +} + +impl Cast<Spanned<Value>> for Marginal { + fn is(value: &Spanned<Value>) -> bool { + matches!(&value.v, Value::Template(_) | Value::Func(_)) + } + + fn cast(value: Spanned<Value>) -> StrResult<Self> { + match value.v { + Value::None => Ok(Self::None), + Value::Template(v) => Ok(Self::Template(v)), + Value::Func(v) => Ok(Self::Func(v, value.span)), + _ => Err("expected none, template or function")?, + } + } +} + +/// 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) -> Length { + Length::mm(self.width) + } + + /// The height of the paper. + pub fn height(self) -> Length { + Length::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 = PaperError; + + fn from_str(name: &str) -> Result<Self, Self::Err> { + match name.to_lowercase().as_str() { + $($($pats)* => Ok(Self::$var),)* + _ => Err(PaperError), + } + } + } + }; +} + +// 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") +} + +castable! { + Paper, + Expected: "string", + Value::Str(string) => Paper::from_str(&string).map_err(|e| e.to_string())?, +} + +/// The error when parsing a [`Paper`] from a string fails. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct PaperError; + +impl Display for PaperError { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad("invalid paper name") + } +} + +impl std::error::Error for PaperError {} diff --git a/src/library/layout/place.rs b/src/library/layout/place.rs new file mode 100644 index 00000000..d65b3836 --- /dev/null +++ b/src/library/layout/place.rs @@ -0,0 +1,59 @@ +use super::AlignNode; +use crate::library::prelude::*; + +/// Place a node at an absolute position. +#[derive(Debug, Hash)] +pub struct PlaceNode(pub LayoutNode); + +#[class] +impl PlaceNode { + fn construct(_: &mut Context, args: &mut Args) -> TypResult<Template> { + let aligns = args.find()?.unwrap_or(Spec::with_x(Some(Align::Left))); + let tx = args.named("dx")?.unwrap_or_default(); + let ty = args.named("dy")?.unwrap_or_default(); + let body: LayoutNode = args.expect("body")?; + Ok(Template::block(Self( + body.moved(Point::new(tx, ty)).aligned(aligns), + ))) + } +} + +impl Layout for PlaceNode { + fn layout( + &self, + ctx: &mut Context, + regions: &Regions, + styles: StyleChain, + ) -> TypResult<Vec<Arc<Frame>>> { + 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(Length::is_finite); + let expand = finite & (regions.expand | out_of_flow); + Regions::one(regions.base, regions.base, expand) + }; + + let mut frames = self.0.layout(ctx, &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 frame = &mut frames[0]; + let target = regions.expand.select(regions.first, Size::zero()); + Arc::make_mut(frame).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::<AlignNode>() + .map_or(false, |node| node.aligns.y.is_some()) + } +} diff --git a/src/library/layout/spacing.rs b/src/library/layout/spacing.rs new file mode 100644 index 00000000..3bebfb14 --- /dev/null +++ b/src/library/layout/spacing.rs @@ -0,0 +1,52 @@ +use crate::library::prelude::*; + +/// Horizontal spacing. +pub struct HNode; + +#[class] +impl HNode { + fn construct(_: &mut Context, args: &mut Args) -> TypResult<Template> { + Ok(Template::Horizontal(args.expect("spacing")?)) + } +} + +/// Vertical spacing. +pub struct VNode; + +#[class] +impl VNode { + fn construct(_: &mut Context, args: &mut Args) -> TypResult<Template> { + Ok(Template::Vertical(args.expect("spacing")?)) + } +} + +/// Kinds of spacing. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum SpacingKind { + /// A length stated in absolute values and/or relative to the parent's size. + Linear(Linear), + /// A length that is the fraction of the remaining free space in the parent. + Fractional(Fractional), +} + +impl SpacingKind { + /// Whether this is fractional spacing. + pub fn is_fractional(self) -> bool { + matches!(self, Self::Fractional(_)) + } +} + +impl From<Length> for SpacingKind { + fn from(length: Length) -> Self { + Self::Linear(length.into()) + } +} + +castable! { + SpacingKind, + Expected: "linear or fractional", + Value::Length(v) => Self::Linear(v.into()), + Value::Relative(v) => Self::Linear(v.into()), + Value::Linear(v) => Self::Linear(v), + Value::Fractional(v) => Self::Fractional(v), +} diff --git a/src/library/layout/stack.rs b/src/library/layout/stack.rs new file mode 100644 index 00000000..414490ef --- /dev/null +++ b/src/library/layout/stack.rs @@ -0,0 +1,259 @@ +use super::{AlignNode, SpacingKind}; +use crate::library::prelude::*; + +/// Arrange nodes 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<SpacingKind>, + /// The children to be stacked. + pub children: Vec<StackChild>, +} + +#[class] +impl StackNode { + fn construct(_: &mut Context, args: &mut Args) -> TypResult<Template> { + Ok(Template::block(Self { + dir: args.named("dir")?.unwrap_or(Dir::TTB), + spacing: args.named("spacing")?, + children: args.all()?, + })) + } +} + +impl Layout for StackNode { + fn layout( + &self, + ctx: &mut Context, + regions: &Regions, + styles: StyleChain, + ) -> TypResult<Vec<Arc<Frame>>> { + let mut layouter = StackLayouter::new(self.dir, regions); + + // Spacing to insert before the next node. + let mut deferred = None; + + for child in &self.children { + match child { + StackChild::Spacing(kind) => { + layouter.layout_spacing(*kind); + deferred = None; + } + StackChild::Node(node) => { + if let Some(kind) = deferred { + layouter.layout_spacing(kind); + } + + layouter.layout_node(ctx, node, styles)?; + deferred = self.spacing; + } + } + } + + Ok(layouter.finish()) + } +} + +/// A child of a stack node. +#[derive(Hash)] +pub enum StackChild { + /// Spacing between other nodes. + Spacing(SpacingKind), + /// An arbitrary node. + Node(LayoutNode), +} + +impl Debug for StackChild { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::Spacing(kind) => kind.fmt(f), + Self::Node(node) => node.fmt(f), + } + } +} + +castable! { + StackChild, + Expected: "linear, fractional or template", + Value::Length(v) => Self::Spacing(SpacingKind::Linear(v.into())), + Value::Relative(v) => Self::Spacing(SpacingKind::Linear(v.into())), + Value::Linear(v) => Self::Spacing(SpacingKind::Linear(v)), + Value::Fractional(v) => Self::Spacing(SpacingKind::Fractional(v)), + Value::Template(v) => Self::Node(v.pack()), +} + +/// Performs stack layout. +pub struct StackLayouter { + /// The stacking direction. + dir: Dir, + /// The axis of the stacking direction. + axis: SpecAxis, + /// The regions to layout children into. + regions: Regions, + /// Whether the stack itself should expand to fill the region. + expand: Spec<bool>, + /// 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<Length>, + /// The sum of fractional ratios in the current region. + fr: Fractional, + /// Already layouted items whose exact positions are not yet known due to + /// fractional spacing. + items: Vec<StackItem>, + /// Finished frames for previous regions. + finished: Vec<Arc<Frame>>, +} + +/// A prepared item in a stack layout. +enum StackItem { + /// Absolute spacing between other items. + Absolute(Length), + /// Fractional spacing between other items. + Fractional(Fractional), + /// A frame for a layouted child node. + Frame(Arc<Frame>, Align), +} + +impl StackLayouter { + /// Create a new stack layouter. + pub fn new(dir: Dir, regions: &Regions) -> 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, + expand, + full, + used: Gen::zero(), + fr: Fractional::zero(), + items: vec![], + finished: vec![], + } + } + + /// Add spacing along the spacing direction. + pub fn layout_spacing(&mut self, spacing: SpacingKind) { + match spacing { + SpacingKind::Linear(v) => { + // Resolve the linear and limit it to the remaining space. + let resolved = v.resolve(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)); + } + SpacingKind::Fractional(v) => { + self.fr += v; + self.items.push(StackItem::Fractional(v)); + } + } + } + + /// Layout an arbitrary node. + pub fn layout_node( + &mut self, + ctx: &mut Context, + node: &LayoutNode, + styles: StyleChain, + ) -> TypResult<()> { + if self.regions.is_full() { + self.finish_region(); + } + + // Align nodes' block-axis alignment is respected by the stack node. + let align = node + .downcast::<AlignNode>() + .and_then(|node| node.aligns.get(self.axis)) + .unwrap_or(self.dir.start().into()); + + let frames = node.layout(ctx, &self.regions, styles)?; + let len = frames.len(); + for (i, frame) in frames.into_iter().enumerate() { + // Grow our size, shrink the region and save the frame for later. + let size = frame.size.to_gen(self.axis); + 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_spec(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 = Length::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.resolve(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.resolve(parent - self.used.main) + + if self.dir.is_positive() { + cursor + } else { + self.used.main - child - cursor + }; + + let pos = Gen::new(Length::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 = Fractional::zero(); + self.finished.push(Arc::new(output)); + } + + /// Finish layouting and return the resulting frames. + pub fn finish(mut self) -> Vec<Arc<Frame>> { + self.finish_region(); + self.finished + } +} diff --git a/src/library/layout/transform.rs b/src/library/layout/transform.rs new file mode 100644 index 00000000..fafb37a4 --- /dev/null +++ b/src/library/layout/transform.rs @@ -0,0 +1,86 @@ +use crate::geom::Transform; +use crate::library::prelude::*; + +/// Transform a node without affecting layout. +#[derive(Debug, Hash)] +pub struct TransformNode<const T: TransformKind> { + /// Transformation to apply to the contents. + pub transform: Transform, + /// The node whose contents should be transformed. + pub child: LayoutNode, +} + +/// Transform a node by translating it without affecting layout. +pub type MoveNode = TransformNode<MOVE>; + +/// Transform a node by rotating it without affecting layout. +pub type RotateNode = TransformNode<ROTATE>; + +/// Transform a node by scaling it without affecting layout. +pub type ScaleNode = TransformNode<SCALE>; + +#[class] +impl<const T: TransformKind> TransformNode<T> { + /// The origin of the transformation. + pub const ORIGIN: Spec<Option<Align>> = Spec::default(); + + fn construct(_: &mut Context, args: &mut Args) -> TypResult<Template> { + let transform = match T { + MOVE => { + let tx = args.named("x")?.unwrap_or_default(); + let ty = args.named("y")?.unwrap_or_default(); + Transform::translation(tx, ty) + } + ROTATE => { + let angle = args.named_or_find("angle")?.unwrap_or_default(); + Transform::rotation(angle) + } + SCALE | _ => { + let all = args.find()?; + let sx = args.named("x")?.or(all).unwrap_or(Relative::one()); + let sy = args.named("y")?.or(all).unwrap_or(Relative::one()); + Transform::scale(sx, sy) + } + }; + + Ok(Template::inline(Self { + transform, + child: args.expect("body")?, + })) + } +} + +impl<const T: TransformKind> Layout for TransformNode<T> { + fn layout( + &self, + ctx: &mut Context, + regions: &Regions, + styles: StyleChain, + ) -> TypResult<Vec<Arc<Frame>>> { + let origin = styles.get(Self::ORIGIN).unwrap_or(Align::CENTER_HORIZON); + let mut frames = self.child.layout(ctx, regions, styles)?; + + for frame in &mut frames { + let Spec { x, y } = origin.zip(frame.size).map(|(o, s)| o.resolve(s)); + let transform = Transform::translation(x, y) + .pre_concat(self.transform) + .pre_concat(Transform::translation(-x, -y)); + + Arc::make_mut(frame).transform(transform); + } + + Ok(frames) + } +} + +/// Kinds of transformations. +pub type TransformKind = usize; + +/// A translation on the X and Y axes. +const MOVE: TransformKind = 0; + +/// A rotational transformation. +const ROTATE: TransformKind = 1; + +/// A scale transformation. +const SCALE: TransformKind = 2; |
