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