summaryrefslogtreecommitdiff
path: root/src/library/layout
diff options
context:
space:
mode:
Diffstat (limited to 'src/library/layout')
-rw-r--r--src/library/layout/align.rs71
-rw-r--r--src/library/layout/columns.rs111
-rw-r--r--src/library/layout/container.rs24
-rw-r--r--src/library/layout/flow.rs272
-rw-r--r--src/library/layout/grid.rs579
-rw-r--r--src/library/layout/hide.rs30
-rw-r--r--src/library/layout/mod.rs27
-rw-r--r--src/library/layout/pad.rs83
-rw-r--r--src/library/layout/page.rs419
-rw-r--r--src/library/layout/place.rs59
-rw-r--r--src/library/layout/spacing.rs52
-rw-r--r--src/library/layout/stack.rs259
-rw-r--r--src/library/layout/transform.rs86
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(&regions.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, &regions, 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;