summaryrefslogtreecommitdiff
path: root/crates/typst-library/src/layout
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2024-10-27 19:04:55 +0100
committerGitHub <noreply@github.com>2024-10-27 18:04:55 +0000
commitbe7cfc85d08c545abfac08098b7b33b4bd71f37e (patch)
treef4137fa2aaa57babae1f7603a9b2ed7e688f43d8 /crates/typst-library/src/layout
parentb8034a343831e8609aec2ec81eb7eeda57aa5d81 (diff)
Split out four new crates (#5302)
Diffstat (limited to 'crates/typst-library/src/layout')
-rw-r--r--crates/typst-library/src/layout/abs.rs279
-rw-r--r--crates/typst-library/src/layout/align.rs748
-rw-r--r--crates/typst-library/src/layout/angle.rs244
-rw-r--r--crates/typst-library/src/layout/axes.rs320
-rw-r--r--crates/typst-library/src/layout/columns.rs98
-rw-r--r--crates/typst-library/src/layout/container.rs563
-rw-r--r--crates/typst-library/src/layout/corners.rs315
-rw-r--r--crates/typst-library/src/layout/dir.rs132
-rw-r--r--crates/typst-library/src/layout/em.rs168
-rw-r--r--crates/typst-library/src/layout/fr.rs145
-rw-r--r--crates/typst-library/src/layout/fragment.rs94
-rw-r--r--crates/typst-library/src/layout/frame.rs606
-rw-r--r--crates/typst-library/src/layout/grid.rs924
-rw-r--r--crates/typst-library/src/layout/hide.rs34
-rw-r--r--crates/typst-library/src/layout/layout.rs107
-rw-r--r--crates/typst-library/src/layout/length.rs276
-rw-r--r--crates/typst-library/src/layout/measure.rs115
-rw-r--r--crates/typst-library/src/layout/mod.rs106
-rw-r--r--crates/typst-library/src/layout/pad.rs65
-rw-r--r--crates/typst-library/src/layout/page.rs924
-rw-r--r--crates/typst-library/src/layout/place.rs208
-rw-r--r--crates/typst-library/src/layout/point.rs168
-rw-r--r--crates/typst-library/src/layout/ratio.rs153
-rw-r--r--crates/typst-library/src/layout/regions.rs159
-rw-r--r--crates/typst-library/src/layout/rel.rs276
-rw-r--r--crates/typst-library/src/layout/repeat.rs49
-rw-r--r--crates/typst-library/src/layout/sides.rs343
-rw-r--r--crates/typst-library/src/layout/size.rs87
-rw-r--r--crates/typst-library/src/layout/spacing.rs193
-rw-r--r--crates/typst-library/src/layout/stack.rs84
-rw-r--r--crates/typst-library/src/layout/transform.rs408
31 files changed, 8391 insertions, 0 deletions
diff --git a/crates/typst-library/src/layout/abs.rs b/crates/typst-library/src/layout/abs.rs
new file mode 100644
index 00000000..e19b037a
--- /dev/null
+++ b/crates/typst-library/src/layout/abs.rs
@@ -0,0 +1,279 @@
+use std::fmt::{self, Debug, Formatter};
+use std::iter::Sum;
+use std::ops::{Add, Div, Mul, Neg, Rem};
+
+use ecow::EcoString;
+use typst_utils::{Numeric, Scalar};
+
+use crate::foundations::{cast, repr, Fold, Repr, Value};
+
+/// An absolute length.
+#[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
+pub struct Abs(Scalar);
+
+impl Abs {
+ /// The zero length.
+ pub const fn zero() -> Self {
+ Self(Scalar::ZERO)
+ }
+
+ /// The infinite length.
+ pub const fn inf() -> Self {
+ Self(Scalar::INFINITY)
+ }
+
+ /// Create an absolute length from a number of raw units.
+ pub const fn raw(raw: f64) -> Self {
+ Self(Scalar::new(raw))
+ }
+
+ /// Create an absolute length from a value in a unit.
+ pub fn with_unit(val: f64, unit: AbsUnit) -> Self {
+ Self(Scalar::new(val * unit.raw_scale()))
+ }
+
+ /// Create an absolute length from a number of points.
+ pub fn pt(pt: f64) -> Self {
+ Self::with_unit(pt, AbsUnit::Pt)
+ }
+
+ /// Create an absolute length from a number of millimeters.
+ pub fn mm(mm: f64) -> Self {
+ Self::with_unit(mm, AbsUnit::Mm)
+ }
+
+ /// Create an absolute length from a number of centimeters.
+ pub fn cm(cm: f64) -> Self {
+ Self::with_unit(cm, AbsUnit::Cm)
+ }
+
+ /// Create an absolute length from a number of inches.
+ pub fn inches(inches: f64) -> Self {
+ Self::with_unit(inches, AbsUnit::In)
+ }
+
+ /// Get the value of this absolute length in raw units.
+ pub const fn to_raw(self) -> f64 {
+ self.0.get()
+ }
+
+ /// Get the value of this absolute length in a unit.
+ pub fn to_unit(self, unit: AbsUnit) -> f64 {
+ self.to_raw() / unit.raw_scale()
+ }
+
+ /// Convert this to a number of points.
+ pub fn to_pt(self) -> f64 {
+ self.to_unit(AbsUnit::Pt)
+ }
+
+ /// Convert this to a number of millimeters.
+ pub fn to_mm(self) -> f64 {
+ self.to_unit(AbsUnit::Mm)
+ }
+
+ /// Convert this to a number of centimeters.
+ pub fn to_cm(self) -> f64 {
+ self.to_unit(AbsUnit::Cm)
+ }
+
+ /// Convert this to a number of inches.
+ pub fn to_inches(self) -> f64 {
+ self.to_unit(AbsUnit::In)
+ }
+
+ /// The absolute value of this length.
+ pub fn abs(self) -> Self {
+ Self::raw(self.to_raw().abs())
+ }
+
+ /// The minimum of this and another absolute length.
+ pub fn min(self, other: Self) -> Self {
+ Self(self.0.min(other.0))
+ }
+
+ /// Set to the minimum of this and another absolute length.
+ pub fn set_min(&mut self, other: Self) {
+ *self = (*self).min(other);
+ }
+
+ /// The maximum of this and another absolute length.
+ pub fn max(self, other: Self) -> Self {
+ Self(self.0.max(other.0))
+ }
+
+ /// Set to the maximum of this and another absolute length.
+ pub fn set_max(&mut self, other: Self) {
+ *self = (*self).max(other);
+ }
+
+ /// Whether the other absolute length fits into this one (i.e. is smaller).
+ /// Allows for a bit of slack.
+ pub fn fits(self, other: Self) -> bool {
+ self.0 + AbsUnit::EPS >= other.0
+ }
+
+ /// Compares two absolute lengths for whether they are approximately equal.
+ pub fn approx_eq(self, other: Self) -> bool {
+ self == other || (self - other).to_raw().abs() < AbsUnit::EPS
+ }
+
+ /// Whether the size is close to zero or negative.
+ pub fn approx_empty(self) -> bool {
+ self.to_raw() <= AbsUnit::EPS
+ }
+
+ /// Returns a number that represent the sign of this length
+ pub fn signum(self) -> f64 {
+ self.0.get().signum()
+ }
+}
+
+impl Numeric for Abs {
+ fn zero() -> Self {
+ Self::zero()
+ }
+
+ fn is_finite(self) -> bool {
+ self.0.is_finite()
+ }
+}
+
+impl Debug for Abs {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ write!(f, "{:?}pt", self.to_pt())
+ }
+}
+
+impl Repr for Abs {
+ fn repr(&self) -> EcoString {
+ repr::format_float_with_unit(self.to_pt(), "pt")
+ }
+}
+
+impl Neg for Abs {
+ type Output = Self;
+
+ fn neg(self) -> Self {
+ Self(-self.0)
+ }
+}
+
+impl Add for Abs {
+ type Output = Self;
+
+ fn add(self, other: Self) -> Self {
+ Self(self.0 + other.0)
+ }
+}
+
+typst_utils::sub_impl!(Abs - Abs -> Abs);
+
+impl Mul<f64> for Abs {
+ type Output = Self;
+
+ fn mul(self, other: f64) -> Self {
+ Self(self.0 * other)
+ }
+}
+
+impl Mul<Abs> for f64 {
+ type Output = Abs;
+
+ fn mul(self, other: Abs) -> Abs {
+ other * self
+ }
+}
+
+impl Div<f64> for Abs {
+ type Output = Self;
+
+ fn div(self, other: f64) -> Self {
+ Self(self.0 / other)
+ }
+}
+
+impl Div for Abs {
+ type Output = f64;
+
+ fn div(self, other: Self) -> f64 {
+ self.to_raw() / other.to_raw()
+ }
+}
+
+typst_utils::assign_impl!(Abs += Abs);
+typst_utils::assign_impl!(Abs -= Abs);
+typst_utils::assign_impl!(Abs *= f64);
+typst_utils::assign_impl!(Abs /= f64);
+
+impl Rem for Abs {
+ type Output = Self;
+
+ fn rem(self, other: Self) -> Self::Output {
+ Self(self.0 % other.0)
+ }
+}
+
+impl Sum for Abs {
+ fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
+ Self(iter.map(|s| s.0).sum())
+ }
+}
+
+impl<'a> Sum<&'a Self> for Abs {
+ fn sum<I: Iterator<Item = &'a Self>>(iter: I) -> Self {
+ Self(iter.map(|s| s.0).sum())
+ }
+}
+
+impl Fold for Abs {
+ fn fold(self, _: Self) -> Self {
+ self
+ }
+}
+
+cast! {
+ Abs,
+ self => Value::Length(self.into()),
+}
+
+/// Different units of absolute measurement.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub enum AbsUnit {
+ /// Points.
+ Pt,
+ /// Millimeters.
+ Mm,
+ /// Centimeters.
+ Cm,
+ /// Inches.
+ In,
+}
+
+impl AbsUnit {
+ /// The epsilon for approximate length comparisons.
+ const EPS: f64 = 1e-4;
+
+ /// How many raw units correspond to a value of `1.0` in this unit.
+ const fn raw_scale(self) -> f64 {
+ // We choose a raw scale which has an integer conversion value to all
+ // four units of interest, so that whole numbers in all units can be
+ // represented accurately.
+ match self {
+ AbsUnit::Pt => 127.0,
+ AbsUnit::Mm => 360.0,
+ AbsUnit::Cm => 3600.0,
+ AbsUnit::In => 9144.0,
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_length_unit_conversion() {
+ assert!((Abs::mm(150.0).to_cm() - 15.0) < 1e-4);
+ }
+}
diff --git a/crates/typst-library/src/layout/align.rs b/crates/typst-library/src/layout/align.rs
new file mode 100644
index 00000000..e8ba4d7c
--- /dev/null
+++ b/crates/typst-library/src/layout/align.rs
@@ -0,0 +1,748 @@
+use std::ops::Add;
+
+use ecow::{eco_format, EcoString};
+
+use crate::diag::{bail, HintedStrResult, SourceResult, StrResult};
+use crate::engine::Engine;
+use crate::foundations::{
+ cast, elem, func, scope, ty, CastInfo, Content, Fold, FromValue, IntoValue, Packed,
+ Reflect, Repr, Resolve, Show, StyleChain, Value,
+};
+use crate::layout::{Abs, Axes, Axis, Dir, Side};
+use crate::text::TextElem;
+
+/// Aligns content horizontally and vertically.
+///
+/// # Example
+/// Let's start with centering our content horizontally:
+/// ```example
+/// #set page(height: 120pt)
+/// #set align(center)
+///
+/// Centered text, a sight to see \
+/// In perfect balance, visually \
+/// Not left nor right, it stands alone \
+/// A work of art, a visual throne
+/// ```
+///
+/// To center something vertically, use _horizon_ alignment:
+/// ```example
+/// #set page(height: 120pt)
+/// #set align(horizon)
+///
+/// Vertically centered, \
+/// the stage had entered, \
+/// a new paragraph.
+/// ```
+///
+/// # Combining alignments
+/// You can combine two alignments with the `+` operator. Let's also only apply
+/// this to one piece of content by using the function form instead of a set
+/// rule:
+/// ```example
+/// #set page(height: 120pt)
+/// Though left in the beginning ...
+///
+/// #align(right + bottom)[
+/// ... they were right in the end, \
+/// and with addition had gotten, \
+/// the paragraph to the bottom!
+/// ]
+/// ```
+///
+/// # Nested alignment
+/// You can use varying alignments for layout containers and the elements within
+/// them. This way, you can create intricate layouts:
+///
+/// ```example
+/// #align(center, block[
+/// #set align(left)
+/// Though centered together \
+/// alone \
+/// we \
+/// are \
+/// left.
+/// ])
+/// ```
+///
+/// # Alignment within the same line
+/// The `align` function performs block-level alignment and thus always
+/// interrupts the current paragraph. To have different alignment for parts
+/// of the same line, you should use [fractional spacing]($h) instead:
+///
+/// ```example
+/// Start #h(1fr) End
+/// ```
+#[elem(Show)]
+pub struct AlignElem {
+ /// The [alignment] along both axes.
+ ///
+ /// ```example
+ /// #set page(height: 6cm)
+ /// #set text(lang: "ar")
+ ///
+ /// مثال
+ /// #align(
+ /// end + horizon,
+ /// rect(inset: 12pt)[ركن]
+ /// )
+ /// ```
+ #[positional]
+ #[fold]
+ #[default]
+ pub alignment: Alignment,
+
+ /// The content to align.
+ #[required]
+ pub body: Content,
+}
+
+impl Show for Packed<AlignElem> {
+ #[typst_macros::time(name = "align", span = self.span())]
+ fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
+ Ok(self.body().clone().aligned(self.alignment(styles)))
+ }
+}
+
+/// Where to [align] something along an axis.
+///
+/// Possible values are:
+/// - `start`: Aligns at the [start]($direction.start) of the [text
+/// direction]($text.dir).
+/// - `end`: Aligns at the [end]($direction.end) of the [text
+/// direction]($text.dir).
+/// - `left`: Align at the left.
+/// - `center`: Aligns in the middle, horizontally.
+/// - `right`: Aligns at the right.
+/// - `top`: Aligns at the top.
+/// - `horizon`: Aligns in the middle, vertically.
+/// - `bottom`: Align at the bottom.
+///
+/// These values are available globally and also in the alignment type's scope,
+/// so you can write either of the following two:
+///
+/// ```example
+/// #align(center)[Hi]
+/// #align(alignment.center)[Hi]
+/// ```
+///
+/// # 2D alignments
+/// To align along both axes at the same time, add the two alignments using the
+/// `+` operator. For example, `top + right` aligns the content to the top right
+/// corner.
+///
+/// ```example
+/// #set page(height: 3cm)
+/// #align(center + bottom)[Hi]
+/// ```
+///
+/// # Fields
+/// The `x` and `y` fields hold the alignment's horizontal and vertical
+/// components, respectively (as yet another `alignment`). They may be `{none}`.
+///
+/// ```example
+/// #(top + right).x \
+/// #left.x \
+/// #left.y (none)
+/// ```
+#[ty(scope)]
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub enum Alignment {
+ H(HAlignment),
+ V(VAlignment),
+ Both(HAlignment, VAlignment),
+}
+
+impl Alignment {
+ /// The horizontal component.
+ pub const fn x(self) -> Option<HAlignment> {
+ match self {
+ Self::H(h) | Self::Both(h, _) => Some(h),
+ Self::V(_) => None,
+ }
+ }
+
+ /// The vertical component.
+ pub const fn y(self) -> Option<VAlignment> {
+ match self {
+ Self::V(v) | Self::Both(_, v) => Some(v),
+ Self::H(_) => None,
+ }
+ }
+
+ /// Normalize the alignment to a LTR-TTB space.
+ pub fn fix(self, text_dir: Dir) -> Axes<FixedAlignment> {
+ Axes::new(
+ self.x().unwrap_or_default().fix(text_dir),
+ self.y().unwrap_or_default().fix(text_dir),
+ )
+ }
+}
+
+#[scope]
+impl Alignment {
+ pub const START: Self = Alignment::H(HAlignment::Start);
+ pub const LEFT: Self = Alignment::H(HAlignment::Left);
+ pub const CENTER: Self = Alignment::H(HAlignment::Center);
+ pub const RIGHT: Self = Alignment::H(HAlignment::Right);
+ pub const END: Self = Alignment::H(HAlignment::End);
+ pub const TOP: Self = Alignment::V(VAlignment::Top);
+ pub const HORIZON: Self = Alignment::V(VAlignment::Horizon);
+ pub const BOTTOM: Self = Alignment::V(VAlignment::Bottom);
+
+ /// The axis this alignment belongs to.
+ /// - `{"horizontal"}` for `start`, `left`, `center`, `right`, and `end`
+ /// - `{"vertical"}` for `top`, `horizon`, and `bottom`
+ /// - `{none}` for 2-dimensional alignments
+ ///
+ /// ```example
+ /// #left.axis() \
+ /// #bottom.axis()
+ /// ```
+ #[func]
+ pub const fn axis(self) -> Option<Axis> {
+ match self {
+ Self::H(_) => Some(Axis::X),
+ Self::V(_) => Some(Axis::Y),
+ Self::Both(..) => None,
+ }
+ }
+
+ /// The inverse alignment.
+ ///
+ /// ```example
+ /// #top.inv() \
+ /// #left.inv() \
+ /// #center.inv() \
+ /// #(left + bottom).inv()
+ /// ```
+ #[func(title = "Inverse")]
+ pub const fn inv(self) -> Alignment {
+ match self {
+ Self::H(h) => Self::H(h.inv()),
+ Self::V(v) => Self::V(v.inv()),
+ Self::Both(h, v) => Self::Both(h.inv(), v.inv()),
+ }
+ }
+}
+
+impl Default for Alignment {
+ fn default() -> Self {
+ HAlignment::default() + VAlignment::default()
+ }
+}
+
+impl Add for Alignment {
+ type Output = StrResult<Self>;
+
+ fn add(self, rhs: Self) -> Self::Output {
+ match (self, rhs) {
+ (Self::H(h), Self::V(v)) | (Self::V(v), Self::H(h)) => Ok(h + v),
+ (Self::H(_), Self::H(_)) => bail!("cannot add two horizontal alignments"),
+ (Self::V(_), Self::V(_)) => bail!("cannot add two vertical alignments"),
+ (Self::H(_), Self::Both(..)) | (Self::Both(..), Self::H(_)) => {
+ bail!("cannot add a horizontal and a 2D alignment")
+ }
+ (Self::V(_), Self::Both(..)) | (Self::Both(..), Self::V(_)) => {
+ bail!("cannot add a vertical and a 2D alignment")
+ }
+ (Self::Both(..), Self::Both(..)) => {
+ bail!("cannot add two 2D alignments")
+ }
+ }
+ }
+}
+
+impl Repr for Alignment {
+ fn repr(&self) -> EcoString {
+ match self {
+ Self::H(h) => h.repr(),
+ Self::V(v) => v.repr(),
+ Self::Both(h, v) => eco_format!("{} + {}", h.repr(), v.repr()),
+ }
+ }
+}
+
+impl Fold for Alignment {
+ fn fold(self, outer: Self) -> Self {
+ match (self, outer) {
+ (Self::H(h), Self::V(v) | Self::Both(_, v)) => Self::Both(h, v),
+ (Self::V(v), Self::H(h) | Self::Both(h, _)) => Self::Both(h, v),
+ _ => self,
+ }
+ }
+}
+
+impl Resolve for Alignment {
+ type Output = Axes<FixedAlignment>;
+
+ fn resolve(self, styles: StyleChain) -> Self::Output {
+ self.fix(TextElem::dir_in(styles))
+ }
+}
+
+impl From<Side> for Alignment {
+ fn from(side: Side) -> Self {
+ match side {
+ Side::Left => Self::LEFT,
+ Side::Top => Self::TOP,
+ Side::Right => Self::RIGHT,
+ Side::Bottom => Self::BOTTOM,
+ }
+ }
+}
+
+/// Alignment on this axis can be fixed to an absolute direction.
+pub trait FixAlignment {
+ /// Resolve to the absolute alignment.
+ fn fix(self, dir: Dir) -> FixedAlignment;
+}
+
+/// Where to align something horizontally.
+#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)]
+pub enum HAlignment {
+ #[default]
+ Start,
+ Left,
+ Center,
+ Right,
+ End,
+}
+
+impl HAlignment {
+ /// The inverse horizontal alignment.
+ pub const fn inv(self) -> Self {
+ match self {
+ Self::Start => Self::End,
+ Self::Left => Self::Right,
+ Self::Center => Self::Center,
+ Self::Right => Self::Left,
+ Self::End => Self::Start,
+ }
+ }
+}
+
+impl FixAlignment for HAlignment {
+ fn fix(self, dir: Dir) -> FixedAlignment {
+ match (self, dir.is_positive()) {
+ (Self::Start, true) | (Self::End, false) => FixedAlignment::Start,
+ (Self::Left, _) => FixedAlignment::Start,
+ (Self::Center, _) => FixedAlignment::Center,
+ (Self::Right, _) => FixedAlignment::End,
+ (Self::End, true) | (Self::Start, false) => FixedAlignment::End,
+ }
+ }
+}
+
+impl Repr for HAlignment {
+ fn repr(&self) -> EcoString {
+ match self {
+ Self::Start => "start".into(),
+ Self::Left => "left".into(),
+ Self::Center => "center".into(),
+ Self::Right => "right".into(),
+ Self::End => "end".into(),
+ }
+ }
+}
+
+impl Add<VAlignment> for HAlignment {
+ type Output = Alignment;
+
+ fn add(self, rhs: VAlignment) -> Self::Output {
+ Alignment::Both(self, rhs)
+ }
+}
+
+impl From<HAlignment> for Alignment {
+ fn from(align: HAlignment) -> Self {
+ Self::H(align)
+ }
+}
+
+impl TryFrom<Alignment> for HAlignment {
+ type Error = EcoString;
+
+ fn try_from(value: Alignment) -> StrResult<Self> {
+ match value {
+ Alignment::H(h) => Ok(h),
+ v => bail!(
+ "expected `start`, `left`, `center`, `right`, or `end`, found {}",
+ v.repr()
+ ),
+ }
+ }
+}
+
+impl Resolve for HAlignment {
+ type Output = FixedAlignment;
+
+ fn resolve(self, styles: StyleChain) -> Self::Output {
+ self.fix(TextElem::dir_in(styles))
+ }
+}
+
+cast! {
+ HAlignment,
+ self => Alignment::H(self).into_value(),
+ align: Alignment => Self::try_from(align)?,
+}
+
+/// A horizontal alignment which only allows `left`/`right` and `start`/`end`,
+/// thus excluding `center`.
+#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)]
+pub enum OuterHAlignment {
+ #[default]
+ Start,
+ Left,
+ Right,
+ End,
+}
+
+impl FixAlignment for OuterHAlignment {
+ fn fix(self, dir: Dir) -> FixedAlignment {
+ match (self, dir.is_positive()) {
+ (Self::Start, true) | (Self::End, false) => FixedAlignment::Start,
+ (Self::Left, _) => FixedAlignment::Start,
+ (Self::Right, _) => FixedAlignment::End,
+ (Self::End, true) | (Self::Start, false) => FixedAlignment::End,
+ }
+ }
+}
+
+impl Resolve for OuterHAlignment {
+ type Output = FixedAlignment;
+
+ fn resolve(self, styles: StyleChain) -> Self::Output {
+ self.fix(TextElem::dir_in(styles))
+ }
+}
+
+impl From<OuterHAlignment> for HAlignment {
+ fn from(value: OuterHAlignment) -> Self {
+ match value {
+ OuterHAlignment::Start => Self::Start,
+ OuterHAlignment::Left => Self::Left,
+ OuterHAlignment::Right => Self::Right,
+ OuterHAlignment::End => Self::End,
+ }
+ }
+}
+
+impl TryFrom<Alignment> for OuterHAlignment {
+ type Error = EcoString;
+
+ fn try_from(value: Alignment) -> StrResult<Self> {
+ match value {
+ Alignment::H(HAlignment::Start) => Ok(Self::Start),
+ Alignment::H(HAlignment::Left) => Ok(Self::Left),
+ Alignment::H(HAlignment::Right) => Ok(Self::Right),
+ Alignment::H(HAlignment::End) => Ok(Self::End),
+ v => bail!("expected `start`, `left`, `right`, or `end`, found {}", v.repr()),
+ }
+ }
+}
+
+cast! {
+ OuterHAlignment,
+ self => HAlignment::from(self).into_value(),
+ align: Alignment => Self::try_from(align)?,
+}
+
+/// Where to align something vertically.
+#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
+pub enum VAlignment {
+ #[default]
+ Top,
+ Horizon,
+ Bottom,
+}
+
+impl VAlignment {
+ /// The inverse vertical alignment.
+ pub const fn inv(self) -> Self {
+ match self {
+ Self::Top => Self::Bottom,
+ Self::Horizon => Self::Horizon,
+ Self::Bottom => Self::Top,
+ }
+ }
+
+ /// Returns the position of this alignment in a container with the given
+ /// extent.
+ pub fn position(self, extent: Abs) -> Abs {
+ match self {
+ Self::Top => Abs::zero(),
+ Self::Horizon => extent / 2.0,
+ Self::Bottom => extent,
+ }
+ }
+}
+
+impl FixAlignment for VAlignment {
+ fn fix(self, _: Dir) -> FixedAlignment {
+ // The vertical alignment does not depend on text direction.
+ match self {
+ Self::Top => FixedAlignment::Start,
+ Self::Horizon => FixedAlignment::Center,
+ Self::Bottom => FixedAlignment::End,
+ }
+ }
+}
+
+impl Repr for VAlignment {
+ fn repr(&self) -> EcoString {
+ match self {
+ Self::Top => "top".into(),
+ Self::Horizon => "horizon".into(),
+ Self::Bottom => "bottom".into(),
+ }
+ }
+}
+
+impl Add<HAlignment> for VAlignment {
+ type Output = Alignment;
+
+ fn add(self, rhs: HAlignment) -> Self::Output {
+ Alignment::Both(rhs, self)
+ }
+}
+
+impl Resolve for VAlignment {
+ type Output = FixedAlignment;
+
+ fn resolve(self, _: StyleChain) -> Self::Output {
+ self.fix(Dir::TTB)
+ }
+}
+
+impl From<VAlignment> for Alignment {
+ fn from(align: VAlignment) -> Self {
+ Self::V(align)
+ }
+}
+
+impl TryFrom<Alignment> for VAlignment {
+ type Error = EcoString;
+
+ fn try_from(value: Alignment) -> StrResult<Self> {
+ match value {
+ Alignment::V(v) => Ok(v),
+ v => bail!("expected `top`, `horizon`, or `bottom`, found {}", v.repr()),
+ }
+ }
+}
+
+cast! {
+ VAlignment,
+ self => Alignment::V(self).into_value(),
+ align: Alignment => Self::try_from(align)?,
+}
+
+/// A vertical alignment which only allows `top` and `bottom`, thus excluding
+/// `horizon`.
+#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
+pub enum OuterVAlignment {
+ #[default]
+ Top,
+ Bottom,
+}
+
+impl FixAlignment for OuterVAlignment {
+ fn fix(self, _: Dir) -> FixedAlignment {
+ // The vertical alignment does not depend on text direction.
+ match self {
+ Self::Top => FixedAlignment::Start,
+ Self::Bottom => FixedAlignment::End,
+ }
+ }
+}
+
+impl From<OuterVAlignment> for VAlignment {
+ fn from(value: OuterVAlignment) -> Self {
+ match value {
+ OuterVAlignment::Top => Self::Top,
+ OuterVAlignment::Bottom => Self::Bottom,
+ }
+ }
+}
+
+impl TryFrom<Alignment> for OuterVAlignment {
+ type Error = EcoString;
+
+ fn try_from(value: Alignment) -> StrResult<Self> {
+ match value {
+ Alignment::V(VAlignment::Top) => Ok(Self::Top),
+ Alignment::V(VAlignment::Bottom) => Ok(Self::Bottom),
+ v => bail!("expected `top` or `bottom`, found {}", v.repr()),
+ }
+ }
+}
+
+cast! {
+ OuterVAlignment,
+ self => VAlignment::from(self).into_value(),
+ align: Alignment => Self::try_from(align)?,
+}
+
+/// An internal representation that combines horizontal or vertical alignments. The
+/// allowed alignment positions are designated by the type parameter `H` and `V`.
+///
+/// This is not user-visible, but an internal type to impose type safety. For example,
+/// `SpecificAlignment<HAlignment, OuterVAlignment>` does not allow vertical alignment
+/// position "center", because `V = OuterVAlignment` doesn't have it.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub enum SpecificAlignment<H, V> {
+ H(H),
+ V(V),
+ Both(H, V),
+}
+
+impl<H, V> SpecificAlignment<H, V>
+where
+ H: Default + Copy + FixAlignment,
+ V: Default + Copy + FixAlignment,
+{
+ /// The horizontal component.
+ pub const fn x(self) -> Option<H> {
+ match self {
+ Self::H(h) | Self::Both(h, _) => Some(h),
+ Self::V(_) => None,
+ }
+ }
+
+ /// The vertical component.
+ pub const fn y(self) -> Option<V> {
+ match self {
+ Self::V(v) | Self::Both(_, v) => Some(v),
+ Self::H(_) => None,
+ }
+ }
+
+ /// Normalize the alignment to a LTR-TTB space.
+ pub fn fix(self, text_dir: Dir) -> Axes<FixedAlignment> {
+ Axes::new(
+ self.x().unwrap_or_default().fix(text_dir),
+ self.y().unwrap_or_default().fix(text_dir),
+ )
+ }
+}
+
+impl<H, V> Resolve for SpecificAlignment<H, V>
+where
+ H: Default + Copy + FixAlignment,
+ V: Default + Copy + FixAlignment,
+{
+ type Output = Axes<FixedAlignment>;
+
+ fn resolve(self, styles: StyleChain) -> Self::Output {
+ self.fix(TextElem::dir_in(styles))
+ }
+}
+
+impl<H, V> From<SpecificAlignment<H, V>> for Alignment
+where
+ HAlignment: From<H>,
+ VAlignment: From<V>,
+{
+ fn from(value: SpecificAlignment<H, V>) -> Self {
+ type FromType<H, V> = SpecificAlignment<H, V>;
+ match value {
+ FromType::H(h) => Self::H(HAlignment::from(h)),
+ FromType::V(v) => Self::V(VAlignment::from(v)),
+ FromType::Both(h, v) => Self::Both(HAlignment::from(h), VAlignment::from(v)),
+ }
+ }
+}
+
+impl<H, V> Reflect for SpecificAlignment<H, V>
+where
+ H: Reflect,
+ V: Reflect,
+{
+ fn input() -> CastInfo {
+ Alignment::input()
+ }
+
+ fn output() -> CastInfo {
+ Alignment::output()
+ }
+
+ fn castable(value: &Value) -> bool {
+ H::castable(value) || V::castable(value)
+ }
+}
+
+impl<H, V> IntoValue for SpecificAlignment<H, V>
+where
+ HAlignment: From<H>,
+ VAlignment: From<V>,
+{
+ fn into_value(self) -> Value {
+ Alignment::from(self).into_value()
+ }
+}
+
+impl<H, V> FromValue for SpecificAlignment<H, V>
+where
+ H: Reflect + TryFrom<Alignment, Error = EcoString>,
+ V: Reflect + TryFrom<Alignment, Error = EcoString>,
+{
+ fn from_value(value: Value) -> HintedStrResult<Self> {
+ if Alignment::castable(&value) {
+ let align = Alignment::from_value(value)?;
+ let result = match align {
+ Alignment::H(_) => Self::H(H::try_from(align)?),
+ Alignment::V(_) => Self::V(V::try_from(align)?),
+ Alignment::Both(h, v) => {
+ Self::Both(H::try_from(h.into())?, V::try_from(v.into())?)
+ }
+ };
+ return Ok(result);
+ }
+ Err(Self::error(&value))
+ }
+}
+
+/// A fixed alignment in the global coordinate space.
+///
+/// For horizontal alignment, start is globally left and for vertical alignment
+/// it is globally top.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
+pub enum FixedAlignment {
+ Start,
+ Center,
+ End,
+}
+
+impl FixedAlignment {
+ /// Returns the position of this alignment in a container with the given
+ /// extent.
+ pub fn position(self, extent: Abs) -> Abs {
+ match self {
+ Self::Start => Abs::zero(),
+ Self::Center => extent / 2.0,
+ Self::End => extent,
+ }
+ }
+
+ /// The inverse alignment.
+ pub const fn inv(self) -> Self {
+ match self {
+ Self::Start => Self::End,
+ Self::Center => Self::Center,
+ Self::End => Self::Start,
+ }
+ }
+}
+
+impl From<Side> for FixedAlignment {
+ fn from(side: Side) -> Self {
+ match side {
+ Side::Left => Self::Start,
+ Side::Top => Self::Start,
+ Side::Right => Self::End,
+ Side::Bottom => Self::End,
+ }
+ }
+}
diff --git a/crates/typst-library/src/layout/angle.rs b/crates/typst-library/src/layout/angle.rs
new file mode 100644
index 00000000..d1410786
--- /dev/null
+++ b/crates/typst-library/src/layout/angle.rs
@@ -0,0 +1,244 @@
+use std::f64::consts::PI;
+use std::fmt::{self, Debug, Formatter};
+use std::iter::Sum;
+use std::ops::{Add, Div, Mul, Neg};
+
+use ecow::EcoString;
+use typst_utils::{Numeric, Scalar};
+
+use crate::foundations::{func, repr, scope, ty, Repr};
+
+/// An angle describing a rotation.
+///
+/// Typst supports the following angular units:
+///
+/// - Degrees: `{180deg}`
+/// - Radians: `{3.14rad}`
+///
+/// # Example
+/// ```example
+/// #rotate(10deg)[Hello there!]
+/// ```
+#[ty(scope, cast)]
+#[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
+pub struct Angle(Scalar);
+
+impl Angle {
+ /// The zero angle.
+ pub const fn zero() -> Self {
+ Self(Scalar::ZERO)
+ }
+
+ /// Create an angle from a number of raw units.
+ pub const fn raw(raw: f64) -> Self {
+ Self(Scalar::new(raw))
+ }
+
+ /// Create an angle from a value in a unit.
+ pub fn with_unit(val: f64, unit: AngleUnit) -> Self {
+ Self(Scalar::new(val * unit.raw_scale()))
+ }
+
+ /// Create an angle from a number of radians.
+ pub fn rad(rad: f64) -> Self {
+ Self::with_unit(rad, AngleUnit::Rad)
+ }
+
+ /// Create an angle from a number of degrees.
+ pub fn deg(deg: f64) -> Self {
+ Self::with_unit(deg, AngleUnit::Deg)
+ }
+
+ /// Get the value of this angle in raw units.
+ pub const fn to_raw(self) -> f64 {
+ (self.0).get()
+ }
+
+ /// Get the value of this angle in a unit.
+ pub fn to_unit(self, unit: AngleUnit) -> f64 {
+ self.to_raw() / unit.raw_scale()
+ }
+
+ /// The absolute value of the this angle.
+ pub fn abs(self) -> Self {
+ Self::raw(self.to_raw().abs())
+ }
+
+ /// Get the sine of this angle in radians.
+ pub fn sin(self) -> f64 {
+ self.to_rad().sin()
+ }
+
+ /// Get the cosine of this angle in radians.
+ pub fn cos(self) -> f64 {
+ self.to_rad().cos()
+ }
+
+ /// Get the tangent of this angle in radians.
+ pub fn tan(self) -> f64 {
+ self.to_rad().tan()
+ }
+
+ /// Get the quadrant of the Cartesian plane that this angle lies in.
+ ///
+ /// The angle is automatically normalized to the range `0deg..=360deg`.
+ ///
+ /// The quadrants are defined as follows:
+ /// - First: `0deg..=90deg` (top-right)
+ /// - Second: `90deg..=180deg` (top-left)
+ /// - Third: `180deg..=270deg` (bottom-left)
+ /// - Fourth: `270deg..=360deg` (bottom-right)
+ pub fn quadrant(self) -> Quadrant {
+ let angle = self.to_deg().rem_euclid(360.0);
+ if angle <= 90.0 {
+ Quadrant::First
+ } else if angle <= 180.0 {
+ Quadrant::Second
+ } else if angle <= 270.0 {
+ Quadrant::Third
+ } else {
+ Quadrant::Fourth
+ }
+ }
+}
+
+#[scope]
+impl Angle {
+ /// Converts this angle to radians.
+ #[func(name = "rad", title = "Radians")]
+ pub fn to_rad(self) -> f64 {
+ self.to_unit(AngleUnit::Rad)
+ }
+
+ /// Converts this angle to degrees.
+ #[func(name = "deg", title = "Degrees")]
+ pub fn to_deg(self) -> f64 {
+ self.to_unit(AngleUnit::Deg)
+ }
+}
+
+impl Numeric for Angle {
+ fn zero() -> Self {
+ Self::zero()
+ }
+
+ fn is_finite(self) -> bool {
+ self.0.is_finite()
+ }
+}
+
+impl Debug for Angle {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ write!(f, "{:?}deg", self.to_deg())
+ }
+}
+
+impl Repr for Angle {
+ fn repr(&self) -> EcoString {
+ repr::format_float_with_unit(self.to_deg(), "deg")
+ }
+}
+
+impl Neg for Angle {
+ type Output = Self;
+
+ fn neg(self) -> Self {
+ Self(-self.0)
+ }
+}
+
+impl Add for Angle {
+ type Output = Self;
+
+ fn add(self, other: Self) -> Self {
+ Self(self.0 + other.0)
+ }
+}
+
+typst_utils::sub_impl!(Angle - Angle -> Angle);
+
+impl Mul<f64> for Angle {
+ type Output = Self;
+
+ fn mul(self, other: f64) -> Self {
+ Self(self.0 * other)
+ }
+}
+
+impl Mul<Angle> for f64 {
+ type Output = Angle;
+
+ fn mul(self, other: Angle) -> Angle {
+ other * self
+ }
+}
+
+impl Div for Angle {
+ type Output = f64;
+
+ fn div(self, other: Self) -> f64 {
+ self.to_raw() / other.to_raw()
+ }
+}
+
+impl Div<f64> for Angle {
+ type Output = Self;
+
+ fn div(self, other: f64) -> Self {
+ Self(self.0 / other)
+ }
+}
+
+typst_utils::assign_impl!(Angle += Angle);
+typst_utils::assign_impl!(Angle -= Angle);
+typst_utils::assign_impl!(Angle *= f64);
+typst_utils::assign_impl!(Angle /= f64);
+
+impl Sum for Angle {
+ fn sum<I: Iterator<Item = Angle>>(iter: I) -> Self {
+ Self(iter.map(|s| s.0).sum())
+ }
+}
+
+/// Different units of angular measurement.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub enum AngleUnit {
+ /// Radians.
+ Rad,
+ /// Degrees.
+ Deg,
+}
+
+impl AngleUnit {
+ /// How many raw units correspond to a value of `1.0` in this unit.
+ fn raw_scale(self) -> f64 {
+ match self {
+ Self::Rad => 1.0,
+ Self::Deg => PI / 180.0,
+ }
+ }
+}
+
+/// A quadrant of the Cartesian plane.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
+pub enum Quadrant {
+ /// The first quadrant, containing positive x and y values.
+ First,
+ /// The second quadrant, containing negative x and positive y values.
+ Second,
+ /// The third quadrant, containing negative x and y values.
+ Third,
+ /// The fourth quadrant, containing positive x and negative y values.
+ Fourth,
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_angle_unit_conversion() {
+ assert!((Angle::rad(2.0 * PI).to_deg() - 360.0) < 1e-4);
+ assert!((Angle::deg(45.0).to_rad() - std::f64::consts::FRAC_PI_4) < 1e-4);
+ }
+}
diff --git a/crates/typst-library/src/layout/axes.rs b/crates/typst-library/src/layout/axes.rs
new file mode 100644
index 00000000..7a73ba79
--- /dev/null
+++ b/crates/typst-library/src/layout/axes.rs
@@ -0,0 +1,320 @@
+use std::any::Any;
+use std::fmt::{self, Debug, Formatter};
+use std::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, Deref, Not};
+
+use typst_utils::Get;
+
+use crate::diag::bail;
+use crate::foundations::{array, cast, Array, Resolve, Smart, StyleChain};
+use crate::layout::{Abs, Dir, Length, Ratio, Rel, Size};
+
+/// A container with a horizontal and vertical component.
+#[derive(Default, Copy, Clone, Eq, PartialEq, Hash)]
+pub struct Axes<T> {
+ /// The horizontal component.
+ pub x: T,
+ /// The vertical component.
+ pub y: T,
+}
+
+impl<T> Axes<T> {
+ /// Create a new instance from the two components.
+ pub const fn new(x: T, y: T) -> Self {
+ Self { x, y }
+ }
+
+ /// Create a new instance with two equal components.
+ pub fn splat(v: T) -> Self
+ where
+ T: Clone,
+ {
+ Self { x: v.clone(), y: v }
+ }
+
+ /// Map the individual fields with `f`.
+ pub fn map<F, U>(self, mut f: F) -> Axes<U>
+ where
+ F: FnMut(T) -> U,
+ {
+ Axes { x: f(self.x), y: f(self.y) }
+ }
+
+ /// Convert from `&Axes<T>` to `Axes<&T>`.
+ pub fn as_ref(&self) -> Axes<&T> {
+ Axes { x: &self.x, y: &self.y }
+ }
+
+ /// Convert from `&Axes<T>` to `Axes<&<T as Deref>::Target>`.
+ pub fn as_deref(&self) -> Axes<&T::Target>
+ where
+ T: Deref,
+ {
+ Axes { x: &self.x, y: &self.y }
+ }
+
+ /// Convert from `&mut Axes<T>` to `Axes<&mut T>`.
+ pub fn as_mut(&mut self) -> Axes<&mut T> {
+ Axes { x: &mut self.x, y: &mut self.y }
+ }
+
+ /// Zip two instances into an instance over a tuple.
+ pub fn zip<U>(self, other: Axes<U>) -> Axes<(T, U)> {
+ Axes { x: (self.x, other.x), y: (self.y, other.y) }
+ }
+
+ /// Apply a function to this and another-instance componentwise.
+ pub fn zip_map<F, V, U>(self, other: Axes<V>, mut f: F) -> Axes<U>
+ where
+ F: FnMut(T, V) -> U,
+ {
+ Axes { x: f(self.x, other.x), y: f(self.y, other.y) }
+ }
+
+ /// Whether a condition is true for at least one of fields.
+ pub fn any<F>(self, mut f: F) -> bool
+ where
+ F: FnMut(&T) -> bool,
+ {
+ f(&self.x) || f(&self.y)
+ }
+
+ /// Whether a condition is true for both fields.
+ pub fn all<F>(self, mut f: F) -> bool
+ where
+ F: FnMut(&T) -> bool,
+ {
+ f(&self.x) && f(&self.y)
+ }
+}
+
+impl<T: Default> Axes<T> {
+ /// Create a new instance with y set to its default value.
+ pub fn with_x(x: T) -> Self {
+ Self { x, y: T::default() }
+ }
+
+ /// Create a new instance with x set to its default value.
+ pub fn with_y(y: T) -> Self {
+ Self { x: T::default(), y }
+ }
+}
+
+impl<T: Ord> Axes<T> {
+ /// The component-wise minimum of this and another instance.
+ pub fn min(self, other: Self) -> Self {
+ Self { x: self.x.min(other.x), y: self.y.min(other.y) }
+ }
+
+ /// The component-wise minimum of this and another instance.
+ pub fn max(self, other: Self) -> Self {
+ Self { x: self.x.max(other.x), y: self.y.max(other.y) }
+ }
+
+ /// The minimum of width and height.
+ pub fn min_by_side(self) -> T {
+ self.x.min(self.y)
+ }
+
+ /// The minimum of width and height.
+ pub fn max_by_side(self) -> T {
+ self.x.max(self.y)
+ }
+}
+
+impl Axes<Rel<Abs>> {
+ /// Evaluate the axes relative to the given `size`.
+ pub fn relative_to(&self, size: Size) -> Size {
+ Size {
+ x: self.x.relative_to(size.x),
+ y: self.y.relative_to(size.y),
+ }
+ }
+}
+
+impl<T> Get<Axis> for Axes<T> {
+ type Component = T;
+
+ fn get_ref(&self, axis: Axis) -> &T {
+ match axis {
+ Axis::X => &self.x,
+ Axis::Y => &self.y,
+ }
+ }
+
+ fn get_mut(&mut self, axis: Axis) -> &mut T {
+ match axis {
+ Axis::X => &mut self.x,
+ Axis::Y => &mut self.y,
+ }
+ }
+}
+
+impl<T> Debug for Axes<T>
+where
+ T: Debug + 'static,
+{
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ if (&self.x as &dyn Any).is::<Abs>() {
+ write!(f, "Size({:?}, {:?})", self.x, self.y)
+ } else {
+ write!(f, "Axes({:?}, {:?})", self.x, self.y)
+ }
+ }
+}
+
+/// The two layouting axes.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub enum Axis {
+ /// The horizontal axis.
+ X,
+ /// The vertical axis.
+ Y,
+}
+
+impl Axis {
+ /// The direction with the given positivity for this axis.
+ pub fn dir(self, positive: bool) -> Dir {
+ match (self, positive) {
+ (Self::X, true) => Dir::LTR,
+ (Self::X, false) => Dir::RTL,
+ (Self::Y, true) => Dir::TTB,
+ (Self::Y, false) => Dir::BTT,
+ }
+ }
+
+ /// The other axis.
+ pub fn other(self) -> Self {
+ match self {
+ Self::X => Self::Y,
+ Self::Y => Self::X,
+ }
+ }
+}
+
+cast! {
+ Axis,
+ self => match self {
+ Self::X => "horizontal".into_value(),
+ Self::Y => "vertical".into_value(),
+ },
+ "horizontal" => Self::X,
+ "vertical" => Self::Y,
+}
+
+impl<T> Axes<Smart<T>> {
+ /// Unwrap the individual fields.
+ pub fn unwrap_or(self, other: Axes<T>) -> Axes<T> {
+ Axes {
+ x: self.x.unwrap_or(other.x),
+ y: self.y.unwrap_or(other.y),
+ }
+ }
+}
+
+impl Axes<bool> {
+ /// Select `t.x` if `self.x` is true and `f.x` otherwise and same for `y`.
+ pub fn select<T>(self, t: Axes<T>, f: Axes<T>) -> Axes<T> {
+ Axes {
+ x: if self.x { t.x } else { f.x },
+ y: if self.y { t.y } else { f.y },
+ }
+ }
+}
+
+impl Not for Axes<bool> {
+ type Output = Self;
+
+ fn not(self) -> Self::Output {
+ Self { x: !self.x, y: !self.y }
+ }
+}
+
+impl BitOr for Axes<bool> {
+ type Output = Self;
+
+ fn bitor(self, rhs: Self) -> Self::Output {
+ Self { x: self.x | rhs.x, y: self.y | rhs.y }
+ }
+}
+
+impl BitOr<bool> for Axes<bool> {
+ type Output = Self;
+
+ fn bitor(self, rhs: bool) -> Self::Output {
+ Self { x: self.x | rhs, y: self.y | rhs }
+ }
+}
+
+impl BitAnd for Axes<bool> {
+ type Output = Self;
+
+ fn bitand(self, rhs: Self) -> Self::Output {
+ Self { x: self.x & rhs.x, y: self.y & rhs.y }
+ }
+}
+
+impl BitAnd<bool> for Axes<bool> {
+ type Output = Self;
+
+ fn bitand(self, rhs: bool) -> Self::Output {
+ Self { x: self.x & rhs, y: self.y & rhs }
+ }
+}
+
+impl BitOrAssign for Axes<bool> {
+ fn bitor_assign(&mut self, rhs: Self) {
+ self.x |= rhs.x;
+ self.y |= rhs.y;
+ }
+}
+
+impl BitAndAssign for Axes<bool> {
+ fn bitand_assign(&mut self, rhs: Self) {
+ self.x &= rhs.x;
+ self.y &= rhs.y;
+ }
+}
+
+cast! {
+ Axes<Rel<Length>>,
+ self => array![self.x, self.y].into_value(),
+ array: Array => {
+ let mut iter = array.into_iter();
+ match (iter.next(), iter.next(), iter.next()) {
+ (Some(a), Some(b), None) => Axes::new(a.cast()?, b.cast()?),
+ _ => bail!("point array must contain exactly two entries"),
+ }
+ },
+}
+
+cast! {
+ Axes<Ratio>,
+ self => array![self.x, self.y].into_value(),
+ array: Array => {
+ let mut iter = array.into_iter();
+ match (iter.next(), iter.next(), iter.next()) {
+ (Some(a), Some(b), None) => Axes::new(a.cast()?, b.cast()?),
+ _ => bail!("ratio array must contain exactly two entries"),
+ }
+ },
+}
+
+cast! {
+ Axes<Length>,
+ self => array![self.x, self.y].into_value(),
+ array: Array => {
+ let mut iter = array.into_iter();
+ match (iter.next(), iter.next(), iter.next()) {
+ (Some(a), Some(b), None) => Axes::new(a.cast()?, b.cast()?),
+ _ => bail!("length array must contain exactly two entries"),
+ }
+ },
+}
+
+impl<T: Resolve> Resolve for Axes<T> {
+ type Output = Axes<T::Output>;
+
+ fn resolve(self, styles: StyleChain) -> Self::Output {
+ self.map(|v| v.resolve(styles))
+ }
+}
diff --git a/crates/typst-library/src/layout/columns.rs b/crates/typst-library/src/layout/columns.rs
new file mode 100644
index 00000000..f2f36441
--- /dev/null
+++ b/crates/typst-library/src/layout/columns.rs
@@ -0,0 +1,98 @@
+use std::num::NonZeroUsize;
+
+use crate::diag::SourceResult;
+use crate::engine::Engine;
+use crate::foundations::{elem, Content, NativeElement, Packed, Show, StyleChain};
+use crate::layout::{BlockElem, Length, Ratio, Rel};
+
+/// Separates a region into multiple equally sized columns.
+///
+/// The `column` function lets you separate the interior of any container into
+/// multiple columns. It will currently not balance the height of the columns.
+/// Instead, the columns will take up the height of their container or the
+/// remaining height on the page. Support for balanced columns is planned for
+/// the future.
+///
+/// # Page-level columns { #page-level }
+/// If you need to insert columns across your whole document, use the `{page}`
+/// function's [`columns` parameter]($page.columns) instead. This will create
+/// the columns directly at the page-level rather than wrapping all of your
+/// content in a layout container. As a result, things like
+/// [pagebreaks]($pagebreak), [footnotes]($footnote), and [line
+/// numbers]($par.line) will continue to work as expected. For more information,
+/// also read the [relevant part of the page setup
+/// guide]($guides/page-setup-guide/#columns).
+///
+/// # Breaking out of columns { #breaking-out }
+/// To temporarily break out of columns (e.g. for a paper's title), use
+/// parent-scoped floating placement:
+///
+/// ```example:single
+/// #set page(columns: 2, height: 150pt)
+///
+/// #place(
+/// top + center,
+/// scope: "parent",
+/// float: true,
+/// text(1.4em, weight: "bold")[
+/// My document
+/// ],
+/// )
+///
+/// #lorem(40)
+/// ```
+#[elem(Show)]
+pub struct ColumnsElem {
+ /// The number of columns.
+ #[positional]
+ #[default(NonZeroUsize::new(2).unwrap())]
+ pub count: NonZeroUsize,
+
+ /// The size of the gutter space between each column.
+ #[resolve]
+ #[default(Ratio::new(0.04).into())]
+ pub gutter: Rel<Length>,
+
+ /// The content that should be layouted into the columns.
+ #[required]
+ pub body: Content,
+}
+
+impl Show for Packed<ColumnsElem> {
+ fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> {
+ Ok(BlockElem::multi_layouter(self.clone(), engine.routines.layout_columns)
+ .pack()
+ .spanned(self.span()))
+ }
+}
+
+/// Forces a column break.
+///
+/// The function will behave like a [page break]($pagebreak) when used in a
+/// single column layout or the last column on a page. Otherwise, content after
+/// the column break will be placed in the next column.
+///
+/// # Example
+/// ```example
+/// #set page(columns: 2)
+/// Preliminary findings from our
+/// ongoing research project have
+/// revealed a hitherto unknown
+/// phenomenon of extraordinary
+/// significance.
+///
+/// #colbreak()
+/// Through rigorous experimentation
+/// and analysis, we have discovered
+/// a hitherto uncharacterized process
+/// that defies our current
+/// understanding of the fundamental
+/// laws of nature.
+/// ```
+#[elem(title = "Column Break")]
+pub struct ColbreakElem {
+ /// If `{true}`, the column break is skipped if the current column is
+ /// already empty.
+ #[default(false)]
+ pub weak: bool,
+}
diff --git a/crates/typst-library/src/layout/container.rs b/crates/typst-library/src/layout/container.rs
new file mode 100644
index 00000000..266d1d88
--- /dev/null
+++ b/crates/typst-library/src/layout/container.rs
@@ -0,0 +1,563 @@
+use crate::diag::{bail, SourceResult};
+use crate::engine::Engine;
+use crate::foundations::{
+ cast, elem, Args, AutoValue, Construct, Content, NativeElement, Packed, Smart,
+ StyleChain, Value,
+};
+use crate::introspection::Locator;
+use crate::layout::{
+ Abs, Corners, Em, Fr, Fragment, Frame, Length, Region, Regions, Rel, Sides, Size,
+ Spacing,
+};
+use crate::visualize::{Paint, Stroke};
+
+/// An inline-level container that sizes content.
+///
+/// All elements except inline math, text, and boxes are block-level and cannot
+/// occur inside of a paragraph. The box function can be used to integrate such
+/// elements into a paragraph. Boxes take the size of their contents by default
+/// but can also be sized explicitly.
+///
+/// # Example
+/// ```example
+/// Refer to the docs
+/// #box(
+/// height: 9pt,
+/// image("docs.svg")
+/// )
+/// for more information.
+/// ```
+#[elem]
+pub struct BoxElem {
+ /// The width of the box.
+ ///
+ /// Boxes can have [fractional]($fraction) widths, as the example below
+ /// demonstrates.
+ ///
+ /// _Note:_ Currently, only boxes and only their widths might be fractionally
+ /// sized within paragraphs. Support for fractionally sized images, shapes,
+ /// and more might be added in the future.
+ ///
+ /// ```example
+ /// Line in #box(width: 1fr, line(length: 100%)) between.
+ /// ```
+ pub width: Sizing,
+
+ /// The height of the box.
+ pub height: Smart<Rel<Length>>,
+
+ /// An amount to shift the box's baseline by.
+ ///
+ /// ```example
+ /// Image: #box(baseline: 40%, image("tiger.jpg", width: 2cm)).
+ /// ```
+ #[resolve]
+ pub baseline: Rel<Length>,
+
+ /// The box's background color. See the
+ /// [rectangle's documentation]($rect.fill) for more details.
+ pub fill: Option<Paint>,
+
+ /// The box's border color. See the
+ /// [rectangle's documentation]($rect.stroke) for more details.
+ #[resolve]
+ #[fold]
+ pub stroke: Sides<Option<Option<Stroke>>>,
+
+ /// How much to round the box's corners. See the
+ /// [rectangle's documentation]($rect.radius) for more details.
+ #[resolve]
+ #[fold]
+ pub radius: Corners<Option<Rel<Length>>>,
+
+ /// How much to pad the box's content.
+ ///
+ /// _Note:_ When the box contains text, its exact size depends on the
+ /// current [text edges]($text.top-edge).
+ ///
+ /// ```example
+ /// #rect(inset: 0pt)[Tight]
+ /// ```
+ #[resolve]
+ #[fold]
+ pub inset: Sides<Option<Rel<Length>>>,
+
+ /// How much to expand the box's size without affecting the layout.
+ ///
+ /// This is useful to prevent padding from affecting line layout. For a
+ /// generalized version of the example below, see the documentation for the
+ /// [raw text's block parameter]($raw.block).
+ ///
+ /// ```example
+ /// An inline
+ /// #box(
+ /// fill: luma(235),
+ /// inset: (x: 3pt, y: 0pt),
+ /// outset: (y: 3pt),
+ /// radius: 2pt,
+ /// )[rectangle].
+ /// ```
+ #[resolve]
+ #[fold]
+ pub outset: Sides<Option<Rel<Length>>>,
+
+ /// Whether to clip the content inside the box.
+ ///
+ /// Clipping is useful when the box's content is larger than the box itself,
+ /// as any content that exceeds the box's bounds will be hidden.
+ ///
+ /// ```example
+ /// #box(
+ /// width: 50pt,
+ /// height: 50pt,
+ /// clip: true,
+ /// image("tiger.jpg", width: 100pt, height: 100pt)
+ /// )
+ /// ```
+ #[default(false)]
+ pub clip: bool,
+
+ /// The contents of the box.
+ #[positional]
+ #[borrowed]
+ pub body: Option<Content>,
+}
+
+/// An inline-level container that can produce arbitrary items that can break
+/// across lines.
+#[elem(Construct)]
+pub struct InlineElem {
+ /// A callback that is invoked with the regions to produce arbitrary
+ /// inline items.
+ #[required]
+ #[internal]
+ body: callbacks::InlineCallback,
+}
+
+impl Construct for InlineElem {
+ fn construct(_: &mut Engine, args: &mut Args) -> SourceResult<Content> {
+ bail!(args.span, "cannot be constructed manually");
+ }
+}
+
+impl InlineElem {
+ /// Create an inline-level item with a custom layouter.
+ #[allow(clippy::type_complexity)]
+ pub fn layouter<T: NativeElement>(
+ captured: Packed<T>,
+ callback: fn(
+ content: &Packed<T>,
+ engine: &mut Engine,
+ locator: Locator,
+ styles: StyleChain,
+ region: Size,
+ ) -> SourceResult<Vec<InlineItem>>,
+ ) -> Self {
+ Self::new(callbacks::InlineCallback::new(captured, callback))
+ }
+}
+
+impl Packed<InlineElem> {
+ /// Layout the element.
+ pub fn layout(
+ &self,
+ engine: &mut Engine,
+ locator: Locator,
+ styles: StyleChain,
+ region: Size,
+ ) -> SourceResult<Vec<InlineItem>> {
+ self.body().call(engine, locator, styles, region)
+ }
+}
+
+/// Layouted items suitable for placing in a paragraph.
+#[derive(Debug, Clone)]
+pub enum InlineItem {
+ /// Absolute spacing between other items, and whether it is weak.
+ Space(Abs, bool),
+ /// Layouted inline-level content.
+ Frame(Frame),
+}
+
+/// A block-level container.
+///
+/// Such a container can be used to separate content, size it, and give it a
+/// background or border.
+///
+/// # Examples
+/// With a block, you can give a background to content while still allowing it
+/// to break across multiple pages.
+/// ```example
+/// #set page(height: 100pt)
+/// #block(
+/// fill: luma(230),
+/// inset: 8pt,
+/// radius: 4pt,
+/// lorem(30),
+/// )
+/// ```
+///
+/// Blocks are also useful to force elements that would otherwise be inline to
+/// become block-level, especially when writing show rules.
+/// ```example
+/// #show heading: it => it.body
+/// = Blockless
+/// More text.
+///
+/// #show heading: it => block(it.body)
+/// = Blocky
+/// More text.
+/// ```
+#[elem]
+pub struct BlockElem {
+ /// The block's width.
+ ///
+ /// ```example
+ /// #set align(center)
+ /// #block(
+ /// width: 60%,
+ /// inset: 8pt,
+ /// fill: silver,
+ /// lorem(10),
+ /// )
+ /// ```
+ pub width: Smart<Rel<Length>>,
+
+ /// The block's height. When the height is larger than the remaining space
+ /// on a page and [`breakable`]($block.breakable) is `{true}`, the
+ /// block will continue on the next page with the remaining height.
+ ///
+ /// ```example
+ /// #set page(height: 80pt)
+ /// #set align(center)
+ /// #block(
+ /// width: 80%,
+ /// height: 150%,
+ /// fill: aqua,
+ /// )
+ /// ```
+ pub height: Sizing,
+
+ /// Whether the block can be broken and continue on the next page.
+ ///
+ /// ```example
+ /// #set page(height: 80pt)
+ /// The following block will
+ /// jump to its own page.
+ /// #block(
+ /// breakable: false,
+ /// lorem(15),
+ /// )
+ /// ```
+ #[default(true)]
+ pub breakable: bool,
+
+ /// The block's background color. See the
+ /// [rectangle's documentation]($rect.fill) for more details.
+ pub fill: Option<Paint>,
+
+ /// The block's border color. See the
+ /// [rectangle's documentation]($rect.stroke) for more details.
+ #[resolve]
+ #[fold]
+ pub stroke: Sides<Option<Option<Stroke>>>,
+
+ /// How much to round the block's corners. See the
+ /// [rectangle's documentation]($rect.radius) for more details.
+ #[resolve]
+ #[fold]
+ pub radius: Corners<Option<Rel<Length>>>,
+
+ /// How much to pad the block's content. See the
+ /// [box's documentation]($box.inset) for more details.
+ #[resolve]
+ #[fold]
+ pub inset: Sides<Option<Rel<Length>>>,
+
+ /// How much to expand the block's size without affecting the layout. See
+ /// the [box's documentation]($box.outset) for more details.
+ #[resolve]
+ #[fold]
+ pub outset: Sides<Option<Rel<Length>>>,
+
+ /// The spacing around the block. When `{auto}`, inherits the paragraph
+ /// [`spacing`]($par.spacing).
+ ///
+ /// For two adjacent blocks, the larger of the first block's `above` and the
+ /// second block's `below` spacing wins. Moreover, block spacing takes
+ /// precedence over paragraph [`spacing`]($par.spacing).
+ ///
+ /// Note that this is only a shorthand to set `above` and `below` to the
+ /// same value. Since the values for `above` and `below` might differ, a
+ /// [context] block only provides access to `{block.above}` and
+ /// `{block.below}`, not to `{block.spacing}` directly.
+ ///
+ /// This property can be used in combination with a show rule to adjust the
+ /// spacing around arbitrary block-level elements.
+ ///
+ /// ```example
+ /// #set align(center)
+ /// #show math.equation: set block(above: 8pt, below: 16pt)
+ ///
+ /// This sum of $x$ and $y$:
+ /// $ x + y = z $
+ /// A second paragraph.
+ /// ```
+ #[external]
+ #[default(Em::new(1.2).into())]
+ pub spacing: Spacing,
+
+ /// The spacing between this block and its predecessor.
+ #[parse(
+ let spacing = args.named("spacing")?;
+ args.named("above")?.or(spacing)
+ )]
+ pub above: Smart<Spacing>,
+
+ /// The spacing between this block and its successor.
+ #[parse(args.named("below")?.or(spacing))]
+ pub below: Smart<Spacing>,
+
+ /// Whether to clip the content inside the block.
+ ///
+ /// Clipping is useful when the block's content is larger than the block itself,
+ /// as any content that exceeds the block's bounds will be hidden.
+ ///
+ /// ```example
+ /// #block(
+ /// width: 50pt,
+ /// height: 50pt,
+ /// clip: true,
+ /// image("tiger.jpg", width: 100pt, height: 100pt)
+ /// )
+ /// ```
+ #[default(false)]
+ pub clip: bool,
+
+ /// Whether this block must stick to the following one, with no break in
+ /// between.
+ ///
+ /// This is, by default, set on heading blocks to prevent orphaned headings
+ /// at the bottom of the page.
+ ///
+ /// ```example
+ /// >>> #set page(height: 140pt)
+ /// // Disable stickiness of headings.
+ /// #show heading: set block(sticky: false)
+ /// #lorem(20)
+ ///
+ /// = Chapter
+ /// #lorem(10)
+ /// ```
+ #[default(false)]
+ pub sticky: bool,
+
+ /// The contents of the block.
+ #[positional]
+ #[borrowed]
+ pub body: Option<BlockBody>,
+}
+
+impl BlockElem {
+ /// Create a block with a custom single-region layouter.
+ ///
+ /// Such a block must have `breakable: false` (which is set by this
+ /// constructor).
+ pub fn single_layouter<T: NativeElement>(
+ captured: Packed<T>,
+ f: fn(
+ content: &Packed<T>,
+ engine: &mut Engine,
+ locator: Locator,
+ styles: StyleChain,
+ region: Region,
+ ) -> SourceResult<Frame>,
+ ) -> Self {
+ Self::new()
+ .with_breakable(false)
+ .with_body(Some(BlockBody::SingleLayouter(
+ callbacks::BlockSingleCallback::new(captured, f),
+ )))
+ }
+
+ /// Create a block with a custom multi-region layouter.
+ pub fn multi_layouter<T: NativeElement>(
+ captured: Packed<T>,
+ f: fn(
+ content: &Packed<T>,
+ engine: &mut Engine,
+ locator: Locator,
+ styles: StyleChain,
+ regions: Regions,
+ ) -> SourceResult<Fragment>,
+ ) -> Self {
+ Self::new().with_body(Some(BlockBody::MultiLayouter(
+ callbacks::BlockMultiCallback::new(captured, f),
+ )))
+ }
+}
+
+/// The contents of a block.
+#[derive(Debug, Clone, PartialEq, Hash)]
+pub enum BlockBody {
+ /// The block contains normal content.
+ Content(Content),
+ /// The block contains a layout callback that needs access to just one
+ /// base region.
+ SingleLayouter(callbacks::BlockSingleCallback),
+ /// The block contains a layout callback that needs access to the exact
+ /// regions.
+ MultiLayouter(callbacks::BlockMultiCallback),
+}
+
+impl Default for BlockBody {
+ fn default() -> Self {
+ Self::Content(Content::default())
+ }
+}
+
+cast! {
+ BlockBody,
+ self => match self {
+ Self::Content(content) => content.into_value(),
+ _ => Value::Auto,
+ },
+ v: Content => Self::Content(v),
+}
+
+/// Defines how to size something along an axis.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub enum Sizing {
+ /// A track that fits its item's contents.
+ Auto,
+ /// A size specified in absolute terms and relative to the parent's size.
+ Rel(Rel),
+ /// A size specified as a fraction of the remaining free space in the
+ /// parent.
+ Fr(Fr),
+}
+
+impl Sizing {
+ /// Whether this is an automatic sizing.
+ pub fn is_auto(self) -> bool {
+ matches!(self, Self::Auto)
+ }
+
+ /// Whether this is fractional sizing.
+ pub fn is_fractional(self) -> bool {
+ matches!(self, Self::Fr(_))
+ }
+}
+
+impl Default for Sizing {
+ fn default() -> Self {
+ Self::Auto
+ }
+}
+
+impl From<Smart<Rel>> for Sizing {
+ fn from(smart: Smart<Rel>) -> Self {
+ match smart {
+ Smart::Auto => Self::Auto,
+ Smart::Custom(rel) => Self::Rel(rel),
+ }
+ }
+}
+
+impl<T: Into<Spacing>> From<T> for Sizing {
+ fn from(spacing: T) -> Self {
+ match spacing.into() {
+ Spacing::Rel(rel) => Self::Rel(rel),
+ Spacing::Fr(fr) => Self::Fr(fr),
+ }
+ }
+}
+
+cast! {
+ Sizing,
+ self => match self {
+ Self::Auto => Value::Auto,
+ Self::Rel(rel) => rel.into_value(),
+ Self::Fr(fr) => fr.into_value(),
+ },
+ _: AutoValue => Self::Auto,
+ v: Rel<Length> => Self::Rel(v),
+ v: Fr => Self::Fr(v),
+}
+
+/// Manual closure implementations for layout callbacks.
+///
+/// Normal closures are not `Hash`, so we can't use them.
+mod callbacks {
+ use super::*;
+
+ macro_rules! callback {
+ ($name:ident = ($($param:ident: $param_ty:ty),* $(,)?) -> $ret:ty) => {
+ #[derive(Debug, Clone, PartialEq, Hash)]
+ pub struct $name {
+ captured: Content,
+ f: fn(&Content, $($param_ty),*) -> $ret,
+ }
+
+ impl $name {
+ pub fn new<T: NativeElement>(
+ captured: Packed<T>,
+ f: fn(&Packed<T>, $($param_ty),*) -> $ret,
+ ) -> Self {
+ Self {
+ // Type-erased the content.
+ captured: captured.pack(),
+ // Safety: The only difference between the two function
+ // pointer types is the type of the first parameter,
+ // which changes from `&Packed<T>` to `&Content`. This
+ // is safe because:
+ // - `Packed<T>` is a transparent wrapper around
+ // `Content`, so for any `T` it has the same memory
+ // representation as `Content`.
+ // - While `Packed<T>` imposes the additional constraint
+ // that the content is of type `T`, this constraint is
+ // upheld: It is initially the case because we store a
+ // `Packed<T>` above. It keeps being the case over the
+ // lifetime of the closure because `capture` is a
+ // private field and `Content`'s `Clone` impl is
+ // guaranteed to retain the type (if it didn't,
+ // literally everything would break).
+ #[allow(clippy::missing_transmute_annotations)]
+ f: unsafe { std::mem::transmute(f) },
+ }
+ }
+
+ pub fn call(&self, $($param: $param_ty),*) -> $ret {
+ (self.f)(&self.captured, $($param),*)
+ }
+ }
+ };
+ }
+
+ callback! {
+ InlineCallback = (
+ engine: &mut Engine,
+ locator: Locator,
+ styles: StyleChain,
+ region: Size,
+ ) -> SourceResult<Vec<InlineItem>>
+ }
+
+ callback! {
+ BlockSingleCallback = (
+ engine: &mut Engine,
+ locator: Locator,
+ styles: StyleChain,
+ region: Region,
+ ) -> SourceResult<Frame>
+ }
+
+ callback! {
+ BlockMultiCallback = (
+ engine: &mut Engine,
+ locator: Locator,
+ styles: StyleChain,
+ regions: Regions,
+ ) -> SourceResult<Fragment>
+ }
+}
diff --git a/crates/typst-library/src/layout/corners.rs b/crates/typst-library/src/layout/corners.rs
new file mode 100644
index 00000000..fe69126a
--- /dev/null
+++ b/crates/typst-library/src/layout/corners.rs
@@ -0,0 +1,315 @@
+use std::fmt::{self, Debug, Formatter};
+
+use typst_utils::Get;
+
+use crate::diag::HintedStrResult;
+use crate::foundations::{
+ AlternativeFold, CastInfo, Dict, Fold, FromValue, IntoValue, Reflect, Resolve,
+ StyleChain, Value,
+};
+use crate::layout::Side;
+
+/// A container with components for the four corners of a rectangle.
+#[derive(Default, Copy, Clone, Eq, PartialEq, Hash)]
+pub struct Corners<T> {
+ /// The value for the top left corner.
+ pub top_left: T,
+ /// The value for the top right corner.
+ pub top_right: T,
+ /// The value for the bottom right corner.
+ pub bottom_right: T,
+ /// The value for the bottom left corner.
+ pub bottom_left: T,
+}
+
+impl<T> Corners<T> {
+ /// Create a new instance from the four components.
+ pub const fn new(top_left: T, top_right: T, bottom_right: T, bottom_left: T) -> Self {
+ Self { top_left, top_right, bottom_right, bottom_left }
+ }
+
+ /// Create an instance with four equal components.
+ pub fn splat(value: T) -> Self
+ where
+ T: Clone,
+ {
+ Self {
+ top_left: value.clone(),
+ top_right: value.clone(),
+ bottom_right: value.clone(),
+ bottom_left: value,
+ }
+ }
+
+ /// Map the individual fields with `f`.
+ pub fn map<F, U>(self, mut f: F) -> Corners<U>
+ where
+ F: FnMut(T) -> U,
+ {
+ Corners {
+ top_left: f(self.top_left),
+ top_right: f(self.top_right),
+ bottom_right: f(self.bottom_right),
+ bottom_left: f(self.bottom_left),
+ }
+ }
+
+ /// Zip two instances into one.
+ pub fn zip<U>(self, other: Corners<U>) -> Corners<(T, U)> {
+ Corners {
+ top_left: (self.top_left, other.top_left),
+ top_right: (self.top_right, other.top_right),
+ bottom_right: (self.bottom_right, other.bottom_right),
+ bottom_left: (self.bottom_left, other.bottom_left),
+ }
+ }
+
+ /// An iterator over the corners, starting with the top left corner,
+ /// clockwise.
+ pub fn iter(&self) -> impl Iterator<Item = &T> {
+ [&self.top_left, &self.top_right, &self.bottom_right, &self.bottom_left]
+ .into_iter()
+ }
+
+ /// Whether all sides are equal.
+ pub fn is_uniform(&self) -> bool
+ where
+ T: PartialEq,
+ {
+ self.top_left == self.top_right
+ && self.top_right == self.bottom_right
+ && self.bottom_right == self.bottom_left
+ }
+}
+
+impl<T> Corners<Option<T>> {
+ /// Unwrap-or-default the individual corners.
+ pub fn unwrap_or_default(self) -> Corners<T>
+ where
+ T: Default,
+ {
+ self.map(Option::unwrap_or_default)
+ }
+}
+
+impl<T> Get<Corner> for Corners<T> {
+ type Component = T;
+
+ fn get_ref(&self, corner: Corner) -> &T {
+ match corner {
+ Corner::TopLeft => &self.top_left,
+ Corner::TopRight => &self.top_right,
+ Corner::BottomRight => &self.bottom_right,
+ Corner::BottomLeft => &self.bottom_left,
+ }
+ }
+
+ fn get_mut(&mut self, corner: Corner) -> &mut T {
+ match corner {
+ Corner::TopLeft => &mut self.top_left,
+ Corner::TopRight => &mut self.top_right,
+ Corner::BottomRight => &mut self.bottom_right,
+ Corner::BottomLeft => &mut self.bottom_left,
+ }
+ }
+}
+
+impl<T: Debug + PartialEq> Debug for Corners<T> {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ if self.is_uniform() {
+ f.write_str("Corners::splat(")?;
+ self.top_left.fmt(f)?;
+ f.write_str(")")
+ } else {
+ f.debug_struct("Corners")
+ .field("top_left", &self.top_left)
+ .field("top_right", &self.top_right)
+ .field("bottom_right", &self.bottom_right)
+ .field("bottom_left", &self.bottom_left)
+ .finish()
+ }
+ }
+}
+
+impl<T: Reflect> Reflect for Corners<Option<T>> {
+ fn input() -> CastInfo {
+ T::input() + Dict::input()
+ }
+
+ fn output() -> CastInfo {
+ T::output() + Dict::output()
+ }
+
+ fn castable(value: &Value) -> bool {
+ Dict::castable(value) || T::castable(value)
+ }
+}
+
+impl<T> IntoValue for Corners<Option<T>>
+where
+ T: PartialEq + IntoValue,
+{
+ fn into_value(self) -> Value {
+ if self.is_uniform() {
+ if let Some(top_left) = self.top_left {
+ return top_left.into_value();
+ }
+ }
+
+ let mut dict = Dict::new();
+ let mut handle = |key: &str, component: Option<T>| {
+ if let Some(c) = component {
+ dict.insert(key.into(), c.into_value());
+ }
+ };
+
+ handle("top-left", self.top_left);
+ handle("top-right", self.top_right);
+ handle("bottom-right", self.bottom_right);
+ handle("bottom-left", self.bottom_left);
+
+ Value::Dict(dict)
+ }
+}
+
+impl<T> FromValue for Corners<Option<T>>
+where
+ T: FromValue + Clone,
+{
+ fn from_value(mut value: Value) -> HintedStrResult<Self> {
+ let expected_keys = [
+ "top-left",
+ "top-right",
+ "bottom-right",
+ "bottom-left",
+ "left",
+ "top",
+ "right",
+ "bottom",
+ "rest",
+ ];
+
+ if let Value::Dict(dict) = &mut value {
+ if dict.is_empty() {
+ return Ok(Self::splat(None));
+ } else if dict.iter().any(|(key, _)| expected_keys.contains(&key.as_str())) {
+ let mut take = |key| dict.take(key).ok().map(T::from_value).transpose();
+ let rest = take("rest")?;
+ let left = take("left")?.or_else(|| rest.clone());
+ let top = take("top")?.or_else(|| rest.clone());
+ let right = take("right")?.or_else(|| rest.clone());
+ let bottom = take("bottom")?.or_else(|| rest.clone());
+ let corners = Corners {
+ top_left: take("top-left")?
+ .or_else(|| top.clone())
+ .or_else(|| left.clone()),
+ top_right: take("top-right")?
+ .or_else(|| top.clone())
+ .or_else(|| right.clone()),
+ bottom_right: take("bottom-right")?
+ .or_else(|| bottom.clone())
+ .or_else(|| right.clone()),
+ bottom_left: take("bottom-left")?
+ .or_else(|| bottom.clone())
+ .or_else(|| left.clone()),
+ };
+
+ dict.finish(&expected_keys)?;
+ return Ok(corners);
+ }
+ }
+
+ if T::castable(&value) {
+ Ok(Self::splat(Some(T::from_value(value)?)))
+ } else if let Value::Dict(dict) = &value {
+ let keys = dict.iter().map(|kv| kv.0.as_str()).collect();
+ // Do not hint at expected_keys, because T may be castable from Dict
+ // objects with other sets of expected keys.
+ Err(Dict::unexpected_keys(keys, None).into())
+ } else {
+ Err(Self::error(&value))
+ }
+ }
+}
+
+impl<T: Resolve> Resolve for Corners<T> {
+ type Output = Corners<T::Output>;
+
+ fn resolve(self, styles: StyleChain) -> Self::Output {
+ self.map(|v| v.resolve(styles))
+ }
+}
+
+impl<T: Fold> Fold for Corners<Option<T>> {
+ fn fold(self, outer: Self) -> Self {
+ // Usually, folding an inner `None` with an `outer` prefers the
+ // explicit `None`. However, here `None` means unspecified and thus
+ // we want `outer`, so we use `fold_or` to opt into such behavior.
+ self.zip(outer).map(|(inner, outer)| inner.fold_or(outer))
+ }
+}
+
+/// The four corners of a rectangle.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub enum Corner {
+ /// The top left corner.
+ TopLeft,
+ /// The top right corner.
+ TopRight,
+ /// The bottom right corner.
+ BottomRight,
+ /// The bottom left corner.
+ BottomLeft,
+}
+
+impl Corner {
+ /// The opposite corner.
+ pub fn inv(self) -> Self {
+ match self {
+ Self::TopLeft => Self::BottomRight,
+ Self::TopRight => Self::BottomLeft,
+ Self::BottomRight => Self::TopLeft,
+ Self::BottomLeft => Self::TopRight,
+ }
+ }
+
+ /// The next corner, clockwise.
+ pub fn next_cw(self) -> Self {
+ match self {
+ Self::TopLeft => Self::TopRight,
+ Self::TopRight => Self::BottomRight,
+ Self::BottomRight => Self::BottomLeft,
+ Self::BottomLeft => Self::TopLeft,
+ }
+ }
+
+ /// The next corner, counter-clockwise.
+ pub fn next_ccw(self) -> Self {
+ match self {
+ Self::TopLeft => Self::BottomLeft,
+ Self::TopRight => Self::TopLeft,
+ Self::BottomRight => Self::TopRight,
+ Self::BottomLeft => Self::BottomRight,
+ }
+ }
+
+ /// The next side, clockwise.
+ pub fn side_cw(self) -> Side {
+ match self {
+ Self::TopLeft => Side::Top,
+ Self::TopRight => Side::Right,
+ Self::BottomRight => Side::Bottom,
+ Self::BottomLeft => Side::Left,
+ }
+ }
+
+ /// The next side, counter-clockwise.
+ pub fn side_ccw(self) -> Side {
+ match self {
+ Self::TopLeft => Side::Left,
+ Self::TopRight => Side::Top,
+ Self::BottomRight => Side::Right,
+ Self::BottomLeft => Side::Bottom,
+ }
+ }
+}
diff --git a/crates/typst-library/src/layout/dir.rs b/crates/typst-library/src/layout/dir.rs
new file mode 100644
index 00000000..9a2e7710
--- /dev/null
+++ b/crates/typst-library/src/layout/dir.rs
@@ -0,0 +1,132 @@
+use ecow::EcoString;
+
+use crate::foundations::{func, scope, ty, Repr};
+use crate::layout::{Axis, Side};
+
+/// The four directions into which content can be laid out.
+///
+/// Possible values are:
+/// - `{ltr}`: Left to right.
+/// - `{rtl}`: Right to left.
+/// - `{ttb}`: Top to bottom.
+/// - `{btt}`: Bottom to top.
+///
+/// These values are available globally and
+/// also in the direction type's scope, so you can write either of the following
+/// two:
+/// ```example
+/// #stack(dir: rtl)[A][B][C]
+/// #stack(dir: direction.rtl)[A][B][C]
+/// ```
+#[ty(scope, name = "direction")]
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub enum Dir {
+ /// Left to right.
+ LTR,
+ /// Right to left.
+ RTL,
+ /// Top to bottom.
+ TTB,
+ /// Bottom to top.
+ BTT,
+}
+
+impl Dir {
+ /// Whether this direction points into the positive coordinate direction.
+ ///
+ /// The positive directions are left-to-right and top-to-bottom.
+ pub const fn is_positive(self) -> bool {
+ match self {
+ Self::LTR | Self::TTB => true,
+ Self::RTL | Self::BTT => false,
+ }
+ }
+}
+
+#[scope]
+impl Dir {
+ pub const LTR: Self = Self::LTR;
+ pub const RTL: Self = Self::RTL;
+ pub const TTB: Self = Self::TTB;
+ pub const BTT: Self = Self::BTT;
+
+ /// The axis this direction belongs to, either `{"horizontal"}` or
+ /// `{"vertical"}`.
+ ///
+ /// ```example
+ /// #ltr.axis() \
+ /// #ttb.axis()
+ /// ```
+ #[func]
+ pub const fn axis(self) -> Axis {
+ match self {
+ Self::LTR | Self::RTL => Axis::X,
+ Self::TTB | Self::BTT => Axis::Y,
+ }
+ }
+
+ /// The start point of this direction, as an alignment.
+ ///
+ /// ```example
+ /// #ltr.start() \
+ /// #rtl.start() \
+ /// #ttb.start() \
+ /// #btt.start()
+ /// ```
+ #[func]
+ pub const fn start(self) -> Side {
+ match self {
+ Self::LTR => Side::Left,
+ Self::RTL => Side::Right,
+ Self::TTB => Side::Top,
+ Self::BTT => Side::Bottom,
+ }
+ }
+
+ /// The end point of this direction, as an alignment.
+ ///
+ /// ```example
+ /// #ltr.end() \
+ /// #rtl.end() \
+ /// #ttb.end() \
+ /// #btt.end()
+ /// ```
+ #[func]
+ pub const fn end(self) -> Side {
+ match self {
+ Self::LTR => Side::Right,
+ Self::RTL => Side::Left,
+ Self::TTB => Side::Bottom,
+ Self::BTT => Side::Top,
+ }
+ }
+
+ /// The inverse direction.
+ ///
+ /// ```example
+ /// #ltr.inv() \
+ /// #rtl.inv() \
+ /// #ttb.inv() \
+ /// #btt.inv()
+ /// ```
+ #[func(title = "Inverse")]
+ pub const fn inv(self) -> Dir {
+ match self {
+ Self::LTR => Self::RTL,
+ Self::RTL => Self::LTR,
+ Self::TTB => Self::BTT,
+ Self::BTT => Self::TTB,
+ }
+ }
+}
+
+impl Repr for Dir {
+ fn repr(&self) -> EcoString {
+ match self {
+ Self::LTR => "ltr".into(),
+ Self::RTL => "rtl".into(),
+ Self::TTB => "ttb".into(),
+ Self::BTT => "btt".into(),
+ }
+ }
+}
diff --git a/crates/typst-library/src/layout/em.rs b/crates/typst-library/src/layout/em.rs
new file mode 100644
index 00000000..e2d8b120
--- /dev/null
+++ b/crates/typst-library/src/layout/em.rs
@@ -0,0 +1,168 @@
+use std::fmt::{self, Debug, Formatter};
+use std::iter::Sum;
+use std::ops::{Add, Div, Mul, Neg};
+
+use ecow::EcoString;
+use typst_utils::{Numeric, Scalar};
+
+use crate::foundations::{cast, repr, Repr, Resolve, StyleChain, Value};
+use crate::layout::Abs;
+use crate::text::TextElem;
+
+/// A length that is relative to the font size.
+///
+/// `1em` is the same as the font size.
+#[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
+pub struct Em(Scalar);
+
+impl Em {
+ /// The zero em length.
+ pub const fn zero() -> Self {
+ Self(Scalar::ZERO)
+ }
+
+ /// The font size.
+ pub const fn one() -> Self {
+ Self(Scalar::ONE)
+ }
+
+ /// Create a font-relative length.
+ pub const fn new(em: f64) -> Self {
+ Self(Scalar::new(em))
+ }
+
+ /// Create an em length from font units at the given units per em.
+ pub fn from_units(units: impl Into<f64>, units_per_em: f64) -> Self {
+ Self(Scalar::new(units.into() / units_per_em))
+ }
+
+ /// Create an em length from a length at the given font size.
+ pub fn from_length(length: Abs, font_size: Abs) -> Self {
+ let result = length / font_size;
+ if result.is_finite() {
+ Self(Scalar::new(result))
+ } else {
+ Self::zero()
+ }
+ }
+
+ /// The number of em units.
+ pub const fn get(self) -> f64 {
+ (self.0).get()
+ }
+
+ /// The absolute value of this em length.
+ pub fn abs(self) -> Self {
+ Self::new(self.get().abs())
+ }
+
+ /// Convert to an absolute length at the given font size.
+ pub fn at(self, font_size: Abs) -> Abs {
+ let resolved = font_size * self.get();
+ if resolved.is_finite() {
+ resolved
+ } else {
+ Abs::zero()
+ }
+ }
+}
+
+impl Numeric for Em {
+ fn zero() -> Self {
+ Self::zero()
+ }
+
+ fn is_finite(self) -> bool {
+ self.0.is_finite()
+ }
+}
+
+impl Debug for Em {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ write!(f, "{:?}em", self.get())
+ }
+}
+
+impl Repr for Em {
+ fn repr(&self) -> EcoString {
+ repr::format_float_with_unit(self.get(), "em")
+ }
+}
+
+impl Neg for Em {
+ type Output = Self;
+
+ fn neg(self) -> Self {
+ Self(-self.0)
+ }
+}
+
+impl Add for Em {
+ type Output = Self;
+
+ fn add(self, other: Self) -> Self {
+ Self(self.0 + other.0)
+ }
+}
+
+typst_utils::sub_impl!(Em - Em -> Em);
+
+impl Mul<f64> for Em {
+ type Output = Self;
+
+ fn mul(self, other: f64) -> Self {
+ Self(self.0 * other)
+ }
+}
+
+impl Mul<Em> for f64 {
+ type Output = Em;
+
+ fn mul(self, other: Em) -> Em {
+ other * self
+ }
+}
+
+impl Div<f64> for Em {
+ type Output = Self;
+
+ fn div(self, other: f64) -> Self {
+ Self(self.0 / other)
+ }
+}
+
+impl Div for Em {
+ type Output = f64;
+
+ fn div(self, other: Self) -> f64 {
+ self.get() / other.get()
+ }
+}
+
+typst_utils::assign_impl!(Em += Em);
+typst_utils::assign_impl!(Em -= Em);
+typst_utils::assign_impl!(Em *= f64);
+typst_utils::assign_impl!(Em /= f64);
+
+impl Sum for Em {
+ fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
+ Self(iter.map(|s| s.0).sum())
+ }
+}
+
+cast! {
+ Em,
+ self => Value::Length(self.into()),
+}
+
+impl Resolve for Em {
+ type Output = Abs;
+
+ fn resolve(self, styles: StyleChain) -> Self::Output {
+ if self.is_zero() {
+ Abs::zero()
+ } else {
+ self.at(TextElem::size_in(styles))
+ }
+ }
+}
diff --git a/crates/typst-library/src/layout/fr.rs b/crates/typst-library/src/layout/fr.rs
new file mode 100644
index 00000000..ff8a6e6b
--- /dev/null
+++ b/crates/typst-library/src/layout/fr.rs
@@ -0,0 +1,145 @@
+use std::fmt::{self, Debug, Formatter};
+use std::iter::Sum;
+use std::ops::{Add, Div, Mul, Neg};
+
+use ecow::EcoString;
+use typst_utils::{Numeric, Scalar};
+
+use crate::foundations::{repr, ty, Repr};
+use crate::layout::Abs;
+
+/// Defines how the remaining space in a layout is distributed.
+///
+/// Each fractionally sized element gets space based on the ratio of its
+/// fraction to the sum of all fractions.
+///
+/// For more details, also see the [h] and [v] functions and the
+/// [grid function]($grid).
+///
+/// # Example
+/// ```example
+/// Left #h(1fr) Left-ish #h(2fr) Right
+/// ```
+#[ty(cast, name = "fraction")]
+#[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
+pub struct Fr(Scalar);
+
+impl Fr {
+ /// Takes up zero space: `0fr`.
+ pub const fn zero() -> Self {
+ Self(Scalar::ZERO)
+ }
+
+ /// Takes up as much space as all other items with this fraction: `1fr`.
+ pub const fn one() -> Self {
+ Self(Scalar::ONE)
+ }
+
+ /// Create a new fraction.
+ pub const fn new(ratio: f64) -> Self {
+ Self(Scalar::new(ratio))
+ }
+
+ /// Get the underlying number.
+ pub const fn get(self) -> f64 {
+ (self.0).get()
+ }
+
+ /// The absolute value of this fraction.
+ pub fn abs(self) -> Self {
+ Self::new(self.get().abs())
+ }
+
+ /// Determine this fraction's share in the remaining space.
+ pub fn share(self, total: Self, remaining: Abs) -> Abs {
+ let ratio = self / total;
+ if ratio.is_finite() && remaining.is_finite() {
+ (ratio * remaining).max(Abs::zero())
+ } else {
+ Abs::zero()
+ }
+ }
+}
+
+impl Numeric for Fr {
+ fn zero() -> Self {
+ Self::zero()
+ }
+
+ fn is_finite(self) -> bool {
+ self.0.is_finite()
+ }
+}
+
+impl Debug for Fr {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ write!(f, "{:?}fr", self.get())
+ }
+}
+
+impl Repr for Fr {
+ fn repr(&self) -> EcoString {
+ repr::format_float_with_unit(self.get(), "fr")
+ }
+}
+
+impl Neg for Fr {
+ type Output = Self;
+
+ fn neg(self) -> Self {
+ Self(-self.0)
+ }
+}
+
+impl Add for Fr {
+ type Output = Self;
+
+ fn add(self, other: Self) -> Self {
+ Self(self.0 + other.0)
+ }
+}
+
+typst_utils::sub_impl!(Fr - Fr -> Fr);
+
+impl Mul<f64> for Fr {
+ type Output = Self;
+
+ fn mul(self, other: f64) -> Self {
+ Self(self.0 * other)
+ }
+}
+
+impl Mul<Fr> for f64 {
+ type Output = Fr;
+
+ fn mul(self, other: Fr) -> Fr {
+ other * self
+ }
+}
+
+impl Div for Fr {
+ type Output = f64;
+
+ fn div(self, other: Self) -> f64 {
+ self.get() / other.get()
+ }
+}
+
+impl Div<f64> for Fr {
+ type Output = Self;
+
+ fn div(self, other: f64) -> Self {
+ Self(self.0 / other)
+ }
+}
+
+typst_utils::assign_impl!(Fr += Fr);
+typst_utils::assign_impl!(Fr -= Fr);
+typst_utils::assign_impl!(Fr *= f64);
+typst_utils::assign_impl!(Fr /= f64);
+
+impl Sum for Fr {
+ fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
+ Self(iter.map(|s| s.0).sum())
+ }
+}
diff --git a/crates/typst-library/src/layout/fragment.rs b/crates/typst-library/src/layout/fragment.rs
new file mode 100644
index 00000000..c2666115
--- /dev/null
+++ b/crates/typst-library/src/layout/fragment.rs
@@ -0,0 +1,94 @@
+use std::fmt::{self, Debug, Formatter};
+
+use crate::layout::Frame;
+
+/// A partial layout result.
+#[derive(Clone)]
+pub struct Fragment(Vec<Frame>);
+
+impl Fragment {
+ /// Create a fragment from a single frame.
+ pub fn frame(frame: Frame) -> Self {
+ Self(vec![frame])
+ }
+
+ /// Create a fragment from multiple frames.
+ pub fn frames(frames: Vec<Frame>) -> Self {
+ Self(frames)
+ }
+
+ /// Return `true` if the length is 0.
+ pub fn is_empty(&self) -> bool {
+ self.0.is_empty()
+ }
+
+ /// The number of frames in the fragment.
+ pub fn len(&self) -> usize {
+ self.0.len()
+ }
+
+ /// Extract the first and only frame.
+ ///
+ /// Panics if there are multiple frames.
+ #[track_caller]
+ pub fn into_frame(self) -> Frame {
+ assert_eq!(self.0.len(), 1, "expected exactly one frame");
+ self.0.into_iter().next().unwrap()
+ }
+
+ /// Extract the frames.
+ pub fn into_frames(self) -> Vec<Frame> {
+ self.0
+ }
+
+ /// Extract a slice with the contained frames.
+ pub fn as_slice(&self) -> &[Frame] {
+ &self.0
+ }
+
+ /// Iterate over the contained frames.
+ pub fn iter(&self) -> std::slice::Iter<Frame> {
+ self.0.iter()
+ }
+
+ /// Iterate over the contained frames.
+ pub fn iter_mut(&mut self) -> std::slice::IterMut<Frame> {
+ self.0.iter_mut()
+ }
+}
+
+impl Debug for Fragment {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ match self.0.as_slice() {
+ [frame] => frame.fmt(f),
+ frames => frames.fmt(f),
+ }
+ }
+}
+
+impl IntoIterator for Fragment {
+ type Item = Frame;
+ type IntoIter = std::vec::IntoIter<Frame>;
+
+ fn into_iter(self) -> Self::IntoIter {
+ self.0.into_iter()
+ }
+}
+
+impl<'a> IntoIterator for &'a Fragment {
+ type Item = &'a Frame;
+ type IntoIter = std::slice::Iter<'a, Frame>;
+
+ fn into_iter(self) -> Self::IntoIter {
+ self.0.iter()
+ }
+}
+
+impl<'a> IntoIterator for &'a mut Fragment {
+ type Item = &'a mut Frame;
+ type IntoIter = std::slice::IterMut<'a, Frame>;
+
+ fn into_iter(self) -> Self::IntoIter {
+ self.0.iter_mut()
+ }
+}
diff --git a/crates/typst-library/src/layout/frame.rs b/crates/typst-library/src/layout/frame.rs
new file mode 100644
index 00000000..204584fc
--- /dev/null
+++ b/crates/typst-library/src/layout/frame.rs
@@ -0,0 +1,606 @@
+//! Finished documents.
+
+use std::fmt::{self, Debug, Formatter};
+use std::num::NonZeroUsize;
+use std::sync::Arc;
+
+use smallvec::SmallVec;
+use typst_syntax::Span;
+use typst_utils::{LazyHash, Numeric};
+
+use crate::foundations::{cast, dict, Dict, Label, StyleChain, Value};
+use crate::introspection::{Location, Tag};
+use crate::layout::{
+ Abs, Axes, FixedAlignment, HideElem, Length, Point, Size, Transform,
+};
+use crate::model::{Destination, LinkElem};
+use crate::text::TextItem;
+use crate::visualize::{Color, FixedStroke, Geometry, Image, Paint, Path, Shape};
+
+/// A finished layout with items at fixed positions.
+#[derive(Default, Clone, Hash)]
+pub struct Frame {
+ /// The size of the frame.
+ size: Size,
+ /// The baseline of the frame measured from the top. If this is `None`, the
+ /// frame's implicit baseline is at the bottom.
+ baseline: Option<Abs>,
+ /// The items composing this layout.
+ items: Arc<LazyHash<Vec<(Point, FrameItem)>>>,
+ /// The hardness of this frame.
+ ///
+ /// Determines whether it is a boundary for gradient drawing.
+ kind: FrameKind,
+}
+
+/// Constructor, accessors and setters.
+impl Frame {
+ /// Create a new, empty frame.
+ ///
+ /// Panics the size is not finite.
+ #[track_caller]
+ pub fn new(size: Size, kind: FrameKind) -> Self {
+ assert!(size.is_finite());
+ Self {
+ size,
+ baseline: None,
+ items: Arc::new(LazyHash::new(vec![])),
+ kind,
+ }
+ }
+
+ /// Create a new, empty soft frame.
+ ///
+ /// Panics if the size is not finite.
+ #[track_caller]
+ pub fn soft(size: Size) -> Self {
+ Self::new(size, FrameKind::Soft)
+ }
+
+ /// Create a new, empty hard frame.
+ ///
+ /// Panics if the size is not finite.
+ #[track_caller]
+ pub fn hard(size: Size) -> Self {
+ Self::new(size, FrameKind::Hard)
+ }
+
+ /// Sets the frame's hardness.
+ pub fn set_kind(&mut self, kind: FrameKind) {
+ self.kind = kind;
+ }
+
+ /// Sets the frame's hardness builder-style.
+ pub fn with_kind(mut self, kind: FrameKind) -> Self {
+ self.kind = kind;
+ self
+ }
+
+ /// Whether the frame is hard or soft.
+ pub fn kind(&self) -> FrameKind {
+ self.kind
+ }
+
+ /// Whether the frame contains no items.
+ pub fn is_empty(&self) -> bool {
+ self.items.is_empty()
+ }
+
+ /// The size of the frame.
+ pub fn size(&self) -> Size {
+ self.size
+ }
+
+ /// The size of the frame, mutably.
+ pub fn size_mut(&mut self) -> &mut Size {
+ &mut self.size
+ }
+
+ /// Set the size of the frame.
+ pub fn set_size(&mut self, size: Size) {
+ self.size = size;
+ }
+
+ /// The width of the frame.
+ pub fn width(&self) -> Abs {
+ self.size.x
+ }
+
+ /// The height of the frame.
+ pub fn height(&self) -> Abs {
+ self.size.y
+ }
+
+ /// The vertical position of the frame's baseline.
+ pub fn baseline(&self) -> Abs {
+ self.baseline.unwrap_or(self.size.y)
+ }
+
+ /// Whether the frame has a non-default baseline.
+ pub fn has_baseline(&self) -> bool {
+ self.baseline.is_some()
+ }
+
+ /// Set the frame's baseline from the top.
+ pub fn set_baseline(&mut self, baseline: Abs) {
+ self.baseline = Some(baseline);
+ }
+
+ /// The distance from the baseline to the top of the frame.
+ ///
+ /// This is the same as `baseline()`, but more in line with the terminology
+ /// used in math layout.
+ pub fn ascent(&self) -> Abs {
+ self.baseline()
+ }
+
+ /// The distance from the baseline to the bottom of the frame.
+ pub fn descent(&self) -> Abs {
+ self.size.y - self.baseline()
+ }
+
+ /// An iterator over the items inside this frame alongside their positions
+ /// relative to the top-left of the frame.
+ pub fn items(&self) -> std::slice::Iter<'_, (Point, FrameItem)> {
+ self.items.iter()
+ }
+}
+
+/// Insert items and subframes.
+impl Frame {
+ /// The layer the next item will be added on. This corresponds to the number
+ /// of items in the frame.
+ pub fn layer(&self) -> usize {
+ self.items.len()
+ }
+
+ /// Add an item at a position in the foreground.
+ pub fn push(&mut self, pos: Point, item: FrameItem) {
+ Arc::make_mut(&mut self.items).push((pos, item));
+ }
+
+ /// Add multiple items at a position in the foreground.
+ ///
+ /// The first item in the iterator will be the one that is most in the
+ /// background.
+ pub fn push_multiple<I>(&mut self, items: I)
+ where
+ I: IntoIterator<Item = (Point, FrameItem)>,
+ {
+ Arc::make_mut(&mut self.items).extend(items);
+ }
+
+ /// Add a frame at a position in the foreground.
+ ///
+ /// Automatically decides whether to inline the frame or to include it as a
+ /// group based on the number of items in it.
+ pub fn push_frame(&mut self, pos: Point, frame: Frame) {
+ if self.should_inline(&frame) {
+ self.inline(self.layer(), pos, frame);
+ } else {
+ self.push(pos, FrameItem::Group(GroupItem::new(frame)));
+ }
+ }
+
+ /// Insert an item at the given layer in the frame.
+ ///
+ /// This panics if the layer is greater than the number of layers present.
+ #[track_caller]
+ pub fn insert(&mut self, layer: usize, pos: Point, item: FrameItem) {
+ Arc::make_mut(&mut self.items).insert(layer, (pos, item));
+ }
+
+ /// Add an item at a position in the background.
+ pub fn prepend(&mut self, pos: Point, item: FrameItem) {
+ self.insert(0, pos, item);
+ }
+
+ /// Add multiple items at a position in the background.
+ ///
+ /// The first item in the iterator will be the one that is most in the
+ /// background.
+ pub fn prepend_multiple<I>(&mut self, items: I)
+ where
+ I: IntoIterator<Item = (Point, FrameItem)>,
+ {
+ Arc::make_mut(&mut self.items).splice(0..0, items);
+ }
+
+ /// Add a frame at a position in the background.
+ pub fn prepend_frame(&mut self, pos: Point, frame: Frame) {
+ if self.should_inline(&frame) {
+ self.inline(0, pos, frame);
+ } else {
+ self.prepend(pos, FrameItem::Group(GroupItem::new(frame)));
+ }
+ }
+
+ /// Whether the given frame should be inlined.
+ fn should_inline(&self, frame: &Frame) -> bool {
+ // We do not inline big frames and hard frames.
+ frame.kind().is_soft() && (self.items.is_empty() || frame.items.len() <= 5)
+ }
+
+ /// Inline a frame at the given layer.
+ fn inline(&mut self, layer: usize, pos: Point, frame: Frame) {
+ // Skip work if there's nothing to do.
+ if frame.items.is_empty() {
+ return;
+ }
+
+ // Try to just reuse the items.
+ if pos.is_zero() && self.items.is_empty() {
+ self.items = frame.items;
+ return;
+ }
+
+ // Try to transfer the items without adjusting the position.
+ // Also try to reuse the items if the Arc isn't shared.
+ let range = layer..layer;
+ if pos.is_zero() {
+ let sink = Arc::make_mut(&mut self.items);
+ match Arc::try_unwrap(frame.items) {
+ Ok(items) => {
+ sink.splice(range, items.into_inner());
+ }
+ Err(arc) => {
+ sink.splice(range, arc.iter().cloned());
+ }
+ }
+ return;
+ }
+
+ // We have to adjust the item positions.
+ // But still try to reuse the items if the Arc isn't shared.
+ let sink = Arc::make_mut(&mut self.items);
+ match Arc::try_unwrap(frame.items) {
+ Ok(items) => {
+ sink.splice(
+ range,
+ items.into_inner().into_iter().map(|(p, e)| (p + pos, e)),
+ );
+ }
+ Err(arc) => {
+ sink.splice(range, arc.iter().cloned().map(|(p, e)| (p + pos, e)));
+ }
+ }
+ }
+}
+
+/// Modify the frame.
+impl Frame {
+ /// Remove all items from the frame.
+ pub fn clear(&mut self) {
+ if Arc::strong_count(&self.items) == 1 {
+ Arc::make_mut(&mut self.items).clear();
+ } else {
+ self.items = Arc::new(LazyHash::new(vec![]));
+ }
+ }
+
+ /// Adjust the frame's size, translate the original content by an offset
+ /// computed according to the given alignments, and return the amount of
+ /// offset.
+ pub fn resize(&mut self, target: Size, align: Axes<FixedAlignment>) -> Point {
+ if self.size == target {
+ return Point::zero();
+ }
+ let offset =
+ align.zip_map(target - self.size, FixedAlignment::position).to_point();
+ self.size = target;
+ self.translate(offset);
+ offset
+ }
+
+ /// Move the baseline and contents of the frame by an offset.
+ pub fn translate(&mut self, offset: Point) {
+ if !offset.is_zero() {
+ if let Some(baseline) = &mut self.baseline {
+ *baseline += offset.y;
+ }
+ for (point, _) in Arc::make_mut(&mut self.items).iter_mut() {
+ *point += offset;
+ }
+ }
+ }
+
+ /// Apply late-stage properties from the style chain to this frame. This
+ /// includes:
+ /// - `HideElem::hidden`
+ /// - `LinkElem::dests`
+ ///
+ /// This must be called on all frames produced by elements
+ /// that manually handle styles (because their children can have varying
+ /// styles). This currently includes flow, par, and equation.
+ ///
+ /// Other elements don't manually need to handle it because their parents
+ /// that result from realization will take care of it and the styles can
+ /// only apply to them as a whole, not part of it (because they don't manage
+ /// styles).
+ pub fn post_processed(mut self, styles: StyleChain) -> Self {
+ self.post_process(styles);
+ self
+ }
+
+ /// Post process in place.
+ pub fn post_process(&mut self, styles: StyleChain) {
+ if !self.is_empty() {
+ self.post_process_raw(
+ LinkElem::dests_in(styles),
+ HideElem::hidden_in(styles),
+ );
+ }
+ }
+
+ /// Apply raw late-stage properties from the raw data.
+ pub fn post_process_raw(&mut self, dests: SmallVec<[Destination; 1]>, hide: bool) {
+ if !self.is_empty() {
+ let size = self.size;
+ self.push_multiple(
+ dests
+ .into_iter()
+ .map(|dest| (Point::zero(), FrameItem::Link(dest, size))),
+ );
+ if hide {
+ self.hide();
+ }
+ }
+ }
+
+ /// Hide all content in the frame, but keep metadata.
+ pub fn hide(&mut self) {
+ Arc::make_mut(&mut self.items).retain_mut(|(_, item)| match item {
+ FrameItem::Group(group) => {
+ group.frame.hide();
+ !group.frame.is_empty()
+ }
+ FrameItem::Tag(_) => true,
+ _ => false,
+ });
+ }
+
+ /// Add a background fill.
+ pub fn fill(&mut self, fill: impl Into<Paint>) {
+ self.prepend(
+ Point::zero(),
+ FrameItem::Shape(Geometry::Rect(self.size()).filled(fill), Span::detached()),
+ );
+ }
+
+ /// Arbitrarily transform the contents of the frame.
+ pub fn transform(&mut self, transform: Transform) {
+ if !self.is_empty() {
+ self.group(|g| g.transform = transform);
+ }
+ }
+
+ /// Clip the contents of a frame to a clip path.
+ ///
+ /// The clip path can be the size of the frame in the case of a
+ /// rectangular frame. In the case of a frame with rounded corner,
+ /// this should be a path that matches the frame's outline.
+ pub fn clip(&mut self, clip_path: Path) {
+ if !self.is_empty() {
+ self.group(|g| g.clip_path = Some(clip_path));
+ }
+ }
+
+ /// Add a label to the frame.
+ pub fn label(&mut self, label: Label) {
+ self.group(|g| g.label = Some(label));
+ }
+
+ /// Set a parent for the frame. As a result, all elements in the frame
+ /// become logically ordered immediately after the given location.
+ pub fn set_parent(&mut self, parent: Location) {
+ if !self.is_empty() {
+ self.group(|g| g.parent = Some(parent));
+ }
+ }
+
+ /// Wrap the frame's contents in a group and modify that group with `f`.
+ fn group<F>(&mut self, f: F)
+ where
+ F: FnOnce(&mut GroupItem),
+ {
+ let mut wrapper = Frame::soft(self.size);
+ wrapper.baseline = self.baseline;
+ let mut group = GroupItem::new(std::mem::take(self));
+ f(&mut group);
+ wrapper.push(Point::zero(), FrameItem::Group(group));
+ *self = wrapper;
+ }
+}
+
+/// Tools for debugging.
+impl Frame {
+ /// Add a full size aqua background and a red baseline for debugging.
+ pub fn mark_box(mut self) -> Self {
+ self.mark_box_in_place();
+ self
+ }
+
+ /// Debug in place. Add a full size aqua background and a red baseline for debugging.
+ pub fn mark_box_in_place(&mut self) {
+ self.insert(
+ 0,
+ Point::zero(),
+ FrameItem::Shape(
+ Geometry::Rect(self.size).filled(Color::TEAL.with_alpha(0.5)),
+ Span::detached(),
+ ),
+ );
+ self.insert(
+ 1,
+ Point::with_y(self.baseline()),
+ FrameItem::Shape(
+ Geometry::Line(Point::with_x(self.size.x))
+ .stroked(FixedStroke::from_pair(Color::RED, Abs::pt(1.0))),
+ Span::detached(),
+ ),
+ );
+ }
+
+ /// Add a green marker at a position for debugging.
+ pub fn mark_point(&mut self, pos: Point) {
+ let radius = Abs::pt(2.0);
+ self.push(
+ pos - Point::splat(radius),
+ FrameItem::Shape(
+ Geometry::Path(Path::ellipse(Size::splat(2.0 * radius)))
+ .filled(Color::GREEN),
+ Span::detached(),
+ ),
+ );
+ }
+
+ /// Add a green marker line at a position for debugging.
+ pub fn mark_line(&mut self, y: Abs) {
+ self.push(
+ Point::with_y(y),
+ FrameItem::Shape(
+ Geometry::Line(Point::with_x(self.size.x))
+ .stroked(FixedStroke::from_pair(Color::GREEN, Abs::pt(1.0))),
+ Span::detached(),
+ ),
+ );
+ }
+}
+
+impl Debug for Frame {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ f.write_str("Frame ")?;
+ f.debug_list()
+ .entries(self.items.iter().map(|(_, item)| item))
+ .finish()
+ }
+}
+
+/// The hardness of a frame.
+///
+/// This corresponds to whether or not the frame is considered to be the
+/// innermost parent of its contents. This is used to determine the coordinate
+/// reference system for gradients.
+#[derive(Default, Clone, Copy, PartialEq, Eq, Hash, Debug)]
+pub enum FrameKind {
+ /// A container which follows its parent's size.
+ ///
+ /// Soft frames are the default since they do not impact the layout of
+ /// a gradient set on one of its children.
+ #[default]
+ Soft,
+ /// A container which uses its own size.
+ ///
+ /// This is used for pages, blocks, and boxes.
+ Hard,
+}
+
+impl FrameKind {
+ /// Returns `true` if the frame is soft.
+ pub fn is_soft(self) -> bool {
+ matches!(self, Self::Soft)
+ }
+
+ /// Returns `true` if the frame is hard.
+ pub fn is_hard(self) -> bool {
+ matches!(self, Self::Hard)
+ }
+}
+
+/// The building block frames are composed of.
+#[derive(Clone, Hash)]
+pub enum FrameItem {
+ /// A subframe with optional transformation and clipping.
+ Group(GroupItem),
+ /// A run of shaped text.
+ Text(TextItem),
+ /// A geometric shape with optional fill and stroke.
+ Shape(Shape, Span),
+ /// An image and its size.
+ Image(Image, Size, Span),
+ /// An internal or external link to a destination.
+ Link(Destination, Size),
+ /// An introspectable element that produced something within this frame
+ /// alongside its key.
+ Tag(Tag),
+}
+
+impl Debug for FrameItem {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ match self {
+ Self::Group(group) => group.fmt(f),
+ Self::Text(text) => write!(f, "{text:?}"),
+ Self::Shape(shape, _) => write!(f, "{shape:?}"),
+ Self::Image(image, _, _) => write!(f, "{image:?}"),
+ Self::Link(dest, _) => write!(f, "Link({dest:?})"),
+ Self::Tag(tag) => write!(f, "{tag:?}"),
+ }
+ }
+}
+
+/// A subframe with optional transformation and clipping.
+#[derive(Clone, Hash)]
+pub struct GroupItem {
+ /// The group's frame.
+ pub frame: Frame,
+ /// A transformation to apply to the group.
+ pub transform: Transform,
+ /// Whether the frame should be a clipping boundary.
+ pub clip_path: Option<Path>,
+ /// The group's label.
+ pub label: Option<Label>,
+ /// The group's logical parent. All elements in this group are logically
+ /// ordered immediately after the parent's start location.
+ pub parent: Option<Location>,
+}
+
+impl GroupItem {
+ /// Create a new group with default settings.
+ pub fn new(frame: Frame) -> Self {
+ Self {
+ frame,
+ transform: Transform::identity(),
+ clip_path: None,
+ label: None,
+ parent: None,
+ }
+ }
+}
+
+impl Debug for GroupItem {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ f.write_str("Group ")?;
+ self.frame.fmt(f)
+ }
+}
+
+/// A physical position in a document.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub struct Position {
+ /// The page, starting at 1.
+ pub page: NonZeroUsize,
+ /// The exact coordinates on the page (from the top left, as usual).
+ pub point: Point,
+}
+
+cast! {
+ Position,
+ self => Value::Dict(self.into()),
+ mut dict: Dict => {
+ let page = dict.take("page")?.cast()?;
+ let x: Length = dict.take("x")?.cast()?;
+ let y: Length = dict.take("y")?.cast()?;
+ dict.finish(&["page", "x", "y"])?;
+ Self { page, point: Point::new(x.abs, y.abs) }
+ },
+}
+
+impl From<Position> for Dict {
+ fn from(pos: Position) -> Self {
+ dict! {
+ "page" => pos.page,
+ "x" => pos.point.x,
+ "y" => pos.point.y,
+ }
+ }
+}
diff --git a/crates/typst-library/src/layout/grid.rs b/crates/typst-library/src/layout/grid.rs
new file mode 100644
index 00000000..2e1e9abc
--- /dev/null
+++ b/crates/typst-library/src/layout/grid.rs
@@ -0,0 +1,924 @@
+use std::num::NonZeroUsize;
+use std::sync::Arc;
+
+use comemo::Track;
+use smallvec::{smallvec, SmallVec};
+use typst_utils::NonZeroExt;
+
+use crate::diag::{bail, At, HintedStrResult, HintedString, SourceResult};
+use crate::engine::Engine;
+use crate::foundations::{
+ cast, elem, scope, Array, CastInfo, Content, Context, Fold, FromValue, Func,
+ IntoValue, NativeElement, Packed, Reflect, Resolve, Show, Smart, StyleChain, Value,
+};
+use crate::layout::{
+ Alignment, BlockElem, Length, OuterHAlignment, OuterVAlignment, Rel, Sides, Sizing,
+};
+use crate::model::{TableCell, TableFooter, TableHLine, TableHeader, TableVLine};
+use crate::visualize::{Paint, Stroke};
+
+/// Arranges content in a grid.
+///
+/// The grid element allows you to arrange content in a grid. You can define the
+/// number of rows and columns, as well as the size of the gutters between them.
+/// There are multiple sizing modes for columns and rows that can be used to
+/// create complex layouts.
+///
+/// While the grid and table elements work very similarly, they are intended for
+/// different use cases and carry different semantics. The grid element is
+/// intended for presentational and layout purposes, while the
+/// [`{table}`]($table) element is intended for, in broad terms, presenting
+/// multiple related data points. In the future, Typst will annotate its output
+/// such that screenreaders will announce content in `table` as tabular while a
+/// grid's content will be announced no different than multiple content blocks
+/// in the document flow. Set and show rules on one of these elements do not
+/// affect the other.
+///
+/// A grid's sizing is determined by the track sizes specified in the arguments.
+/// Because each of the sizing parameters accepts the same values, we will
+/// explain them just once, here. Each sizing argument accepts an array of
+/// individual track sizes. A track size is either:
+///
+/// - `{auto}`: The track will be sized to fit its contents. It will be at most
+/// as large as the remaining space. If there is more than one `{auto}` track
+/// width, and together they claim more than the available space, the `{auto}`
+/// tracks will fairly distribute the available space among themselves.
+///
+/// - A fixed or relative length (e.g. `{10pt}` or `{20% - 1cm}`): The track
+/// will be exactly of this size.
+///
+/// - A fractional length (e.g. `{1fr}`): Once all other tracks have been sized,
+/// the remaining space will be divided among the fractional tracks according
+/// to their fractions. For example, if there are two fractional tracks, each
+/// with a fraction of `{1fr}`, they will each take up half of the remaining
+/// space.
+///
+/// To specify a single track, the array can be omitted in favor of a single
+/// value. To specify multiple `{auto}` tracks, enter the number of tracks
+/// instead of an array. For example, `columns:` `{3}` is equivalent to
+/// `columns:` `{(auto, auto, auto)}`.
+///
+/// # Examples
+/// The example below demonstrates the different track sizing options. It also
+/// shows how you can use [`grid.cell`]($grid.cell) to make an individual cell
+/// span two grid tracks.
+///
+/// ```example
+/// // We use `rect` to emphasize the
+/// // area of cells.
+/// #set rect(
+/// inset: 8pt,
+/// fill: rgb("e4e5ea"),
+/// width: 100%,
+/// )
+///
+/// #grid(
+/// columns: (60pt, 1fr, 2fr),
+/// rows: (auto, 60pt),
+/// gutter: 3pt,
+/// rect[Fixed width, auto height],
+/// rect[1/3 of the remains],
+/// rect[2/3 of the remains],
+/// rect(height: 100%)[Fixed height],
+/// grid.cell(
+/// colspan: 2,
+/// image("tiger.jpg", width: 100%),
+/// ),
+/// )
+/// ```
+///
+/// You can also [spread]($arguments/#spreading) an array of strings or content
+/// into a grid to populate its cells.
+///
+/// ```example
+/// #grid(
+/// columns: 5,
+/// gutter: 5pt,
+/// ..range(25).map(str)
+/// )
+/// ```
+///
+/// # Styling the grid
+/// The grid's appearance can be customized through different parameters. These
+/// are the most important ones:
+///
+/// - [`fill`]($grid.fill) to give all cells a background
+/// - [`align`]($grid.align) to change how cells are aligned
+/// - [`inset`]($grid.inset) to optionally add internal padding to each cell
+/// - [`stroke`]($grid.stroke) to optionally enable grid lines with a certain
+/// stroke
+///
+/// If you need to override one of the above options for a single cell, you can
+/// use the [`grid.cell`]($grid.cell) element. Likewise, you can override
+/// individual grid lines with the [`grid.hline`]($grid.hline) and
+/// [`grid.vline`]($grid.vline) elements.
+///
+/// Alternatively, if you need the appearance options to depend on a cell's
+/// position (column and row), you may specify a function to `fill` or `align`
+/// of the form `(column, row) => value`. You may also use a show rule on
+/// [`grid.cell`]($grid.cell) - see that element's examples or the examples
+/// below for more information.
+///
+/// Locating most of your styling in set and show rules is recommended, as it
+/// keeps the grid's or table's actual usages clean and easy to read. It also
+/// allows you to easily change the grid's appearance in one place.
+///
+/// ## Stroke styling precedence
+/// There are three ways to set the stroke of a grid cell: through
+/// [`{grid.cell}`'s `stroke` field]($grid.cell.stroke), by using
+/// [`{grid.hline}`]($grid.hline) and [`{grid.vline}`]($grid.vline), or by
+/// setting the [`{grid}`'s `stroke` field]($grid.stroke). When multiple of
+/// these settings are present and conflict, the `hline` and `vline` settings
+/// take the highest precedence, followed by the `cell` settings, and finally
+/// the `grid` settings.
+///
+/// Furthermore, strokes of a repeated grid header or footer will take
+/// precedence over regular cell strokes.
+#[elem(scope, Show)]
+pub struct GridElem {
+ /// The column sizes.
+ ///
+ /// Either specify a track size array or provide an integer to create a grid
+ /// with that many `{auto}`-sized columns. Note that opposed to rows and
+ /// gutters, providing a single track size will only ever create a single
+ /// column.
+ #[borrowed]
+ pub columns: TrackSizings,
+
+ /// The row sizes.
+ ///
+ /// If there are more cells than fit the defined rows, the last row is
+ /// repeated until there are no more cells.
+ #[borrowed]
+ pub rows: TrackSizings,
+
+ /// The gaps between rows and columns.
+ ///
+ /// If there are more gutters than defined sizes, the last gutter is
+ /// repeated.
+ ///
+ /// This is a shorthand to set `column-gutter` and `row-gutter` to the same
+ /// value.
+ #[external]
+ pub gutter: TrackSizings,
+
+ /// The gaps between columns.
+ #[parse(
+ let gutter = args.named("gutter")?;
+ args.named("column-gutter")?.or_else(|| gutter.clone())
+ )]
+ #[borrowed]
+ pub column_gutter: TrackSizings,
+
+ /// The gaps between rows.
+ #[parse(args.named("row-gutter")?.or_else(|| gutter.clone()))]
+ #[borrowed]
+ pub row_gutter: TrackSizings,
+
+ /// How to fill the cells.
+ ///
+ /// This can be a color or a function that returns a color. The function
+ /// receives the cells' column and row indices, starting from zero. This can
+ /// be used to implement striped grids.
+ ///
+ /// ```example
+ /// #grid(
+ /// fill: (x, y) =>
+ /// if calc.even(x + y) { luma(230) }
+ /// else { white },
+ /// align: center + horizon,
+ /// columns: 4,
+ /// inset: 2pt,
+ /// [X], [O], [X], [O],
+ /// [O], [X], [O], [X],
+ /// [X], [O], [X], [O],
+ /// [O], [X], [O], [X],
+ /// )
+ /// ```
+ #[borrowed]
+ pub fill: Celled<Option<Paint>>,
+
+ /// How to align the cells' content.
+ ///
+ /// This can either be a single alignment, an array of alignments
+ /// (corresponding to each column) or a function that returns an alignment.
+ /// The function receives the cells' column and row indices, starting from
+ /// zero. If set to `{auto}`, the outer alignment is used.
+ ///
+ /// You can find an example for this argument at the
+ /// [`table.align`]($table.align) parameter.
+ #[borrowed]
+ pub align: Celled<Smart<Alignment>>,
+
+ /// How to [stroke]($stroke) the cells.
+ ///
+ /// Grids have no strokes by default, which can be changed by setting this
+ /// option to the desired stroke.
+ ///
+ /// If it is necessary to place lines which can cross spacing between cells
+ /// produced by the `gutter` option, or to override the stroke between
+ /// multiple specific cells, consider specifying one or more of
+ /// [`grid.hline`]($grid.hline) and [`grid.vline`]($grid.vline) alongside
+ /// your grid cells.
+ ///
+ /// ```example
+ /// #set page(height: 13em, width: 26em)
+ ///
+ /// #let cv(..jobs) = grid(
+ /// columns: 2,
+ /// inset: 5pt,
+ /// stroke: (x, y) => if x == 0 and y > 0 {
+ /// (right: (
+ /// paint: luma(180),
+ /// thickness: 1.5pt,
+ /// dash: "dotted"
+ /// ))
+ /// },
+ /// grid.header(grid.cell(colspan: 2)[
+ /// *Professional Experience*
+ /// #box(width: 1fr, line(length: 100%, stroke: luma(180)))
+ /// ]),
+ /// ..{
+ /// let last = none
+ /// for job in jobs.pos() {
+ /// (
+ /// if job.year != last [*#job.year*],
+ /// [
+ /// *#job.company* - #job.role _(#job.timeframe)_ \
+ /// #job.details
+ /// ]
+ /// )
+ /// last = job.year
+ /// }
+ /// }
+ /// )
+ ///
+ /// #cv(
+ /// (
+ /// year: 2012,
+ /// company: [Pear Seed & Co.],
+ /// role: [Lead Engineer],
+ /// timeframe: [Jul - Dec],
+ /// details: [
+ /// - Raised engineers from 3x to 10x
+ /// - Did a great job
+ /// ],
+ /// ),
+ /// (
+ /// year: 2012,
+ /// company: [Mega Corp.],
+ /// role: [VP of Sales],
+ /// timeframe: [Mar - Jun],
+ /// details: [- Closed tons of customers],
+ /// ),
+ /// (
+ /// year: 2013,
+ /// company: [Tiny Co.],
+ /// role: [CEO],
+ /// timeframe: [Jan - Dec],
+ /// details: [- Delivered 4x more shareholder value],
+ /// ),
+ /// (
+ /// year: 2014,
+ /// company: [Glorbocorp Ltd],
+ /// role: [CTO],
+ /// timeframe: [Jan - Mar],
+ /// details: [- Drove containerization forward],
+ /// ),
+ /// )
+ /// ```
+ #[resolve]
+ #[fold]
+ pub stroke: Celled<Sides<Option<Option<Arc<Stroke>>>>>,
+
+ /// How much to pad the cells' content.
+ ///
+ /// You can find an example for this argument at the
+ /// [`table.inset`]($table.inset) parameter.
+ #[fold]
+ pub inset: Celled<Sides<Option<Rel<Length>>>>,
+
+ /// The contents of the grid cells, plus any extra grid lines specified
+ /// with the [`grid.hline`]($grid.hline) and [`grid.vline`]($grid.vline)
+ /// elements.
+ ///
+ /// The cells are populated in row-major order.
+ #[variadic]
+ pub children: Vec<GridChild>,
+}
+
+#[scope]
+impl GridElem {
+ #[elem]
+ type GridCell;
+
+ #[elem]
+ type GridHLine;
+
+ #[elem]
+ type GridVLine;
+
+ #[elem]
+ type GridHeader;
+
+ #[elem]
+ type GridFooter;
+}
+
+impl Show for Packed<GridElem> {
+ fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> {
+ Ok(BlockElem::multi_layouter(self.clone(), engine.routines.layout_grid)
+ .pack()
+ .spanned(self.span()))
+ }
+}
+
+/// Track sizing definitions.
+#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
+pub struct TrackSizings(pub SmallVec<[Sizing; 4]>);
+
+cast! {
+ TrackSizings,
+ self => self.0.into_value(),
+ sizing: Sizing => Self(smallvec![sizing]),
+ count: NonZeroUsize => Self(smallvec![Sizing::Auto; count.get()]),
+ values: Array => Self(values.into_iter().map(Value::cast).collect::<HintedStrResult<_>>()?),
+}
+
+/// Any child of a grid element.
+#[derive(Debug, PartialEq, Clone, Hash)]
+pub enum GridChild {
+ Header(Packed<GridHeader>),
+ Footer(Packed<GridFooter>),
+ Item(GridItem),
+}
+
+cast! {
+ GridChild,
+ self => match self {
+ Self::Header(header) => header.into_value(),
+ Self::Footer(footer) => footer.into_value(),
+ Self::Item(item) => item.into_value(),
+ },
+ v: Content => {
+ v.try_into()?
+ },
+}
+
+impl TryFrom<Content> for GridChild {
+ type Error = HintedString;
+ fn try_from(value: Content) -> HintedStrResult<Self> {
+ if value.is::<TableHeader>() {
+ bail!(
+ "cannot use `table.header` as a grid header";
+ hint: "use `grid.header` instead"
+ )
+ }
+ if value.is::<TableFooter>() {
+ bail!(
+ "cannot use `table.footer` as a grid footer";
+ hint: "use `grid.footer` instead"
+ )
+ }
+
+ value
+ .into_packed::<GridHeader>()
+ .map(Self::Header)
+ .or_else(|value| value.into_packed::<GridFooter>().map(Self::Footer))
+ .or_else(|value| GridItem::try_from(value).map(Self::Item))
+ }
+}
+
+/// A grid item, which is the basic unit of grid specification.
+#[derive(Debug, PartialEq, Clone, Hash)]
+pub enum GridItem {
+ HLine(Packed<GridHLine>),
+ VLine(Packed<GridVLine>),
+ Cell(Packed<GridCell>),
+}
+
+cast! {
+ GridItem,
+ self => match self {
+ Self::HLine(hline) => hline.into_value(),
+ Self::VLine(vline) => vline.into_value(),
+ Self::Cell(cell) => cell.into_value(),
+ },
+ v: Content => {
+ v.try_into()?
+ }
+}
+
+impl TryFrom<Content> for GridItem {
+ type Error = HintedString;
+ fn try_from(value: Content) -> HintedStrResult<Self> {
+ if value.is::<GridHeader>() {
+ bail!("cannot place a grid header within another header or footer");
+ }
+ if value.is::<TableHeader>() {
+ bail!("cannot place a table header within another header or footer");
+ }
+ if value.is::<GridFooter>() {
+ bail!("cannot place a grid footer within another footer or header");
+ }
+ if value.is::<TableFooter>() {
+ bail!("cannot place a table footer within another footer or header");
+ }
+ if value.is::<TableCell>() {
+ bail!(
+ "cannot use `table.cell` as a grid cell";
+ hint: "use `grid.cell` instead"
+ );
+ }
+ if value.is::<TableHLine>() {
+ bail!(
+ "cannot use `table.hline` as a grid line";
+ hint: "use `grid.hline` instead"
+ );
+ }
+ if value.is::<TableVLine>() {
+ bail!(
+ "cannot use `table.vline` as a grid line";
+ hint: "use `grid.vline` instead"
+ );
+ }
+
+ Ok(value
+ .into_packed::<GridHLine>()
+ .map(Self::HLine)
+ .or_else(|value| value.into_packed::<GridVLine>().map(Self::VLine))
+ .or_else(|value| value.into_packed::<GridCell>().map(Self::Cell))
+ .unwrap_or_else(|value| {
+ let span = value.span();
+ Self::Cell(Packed::new(GridCell::new(value)).spanned(span))
+ }))
+ }
+}
+
+/// A repeatable grid header.
+///
+/// If `repeat` is set to `true`, the header will be repeated across pages. For
+/// an example, refer to the [`table.header`]($table.header) element and the
+/// [`grid.stroke`]($grid.stroke) parameter.
+#[elem(name = "header", title = "Grid Header")]
+pub struct GridHeader {
+ /// Whether this header should be repeated across pages.
+ #[default(true)]
+ pub repeat: bool,
+
+ /// The cells and lines within the header.
+ #[variadic]
+ pub children: Vec<GridItem>,
+}
+
+/// A repeatable grid footer.
+///
+/// Just like the [`grid.header`]($grid.header) element, the footer can repeat
+/// itself on every page of the table.
+///
+/// No other grid cells may be placed after the footer.
+#[elem(name = "footer", title = "Grid Footer")]
+pub struct GridFooter {
+ /// Whether this footer should be repeated across pages.
+ #[default(true)]
+ pub repeat: bool,
+
+ /// The cells and lines within the footer.
+ #[variadic]
+ pub children: Vec<GridItem>,
+}
+
+/// A horizontal line in the grid.
+///
+/// Overrides any per-cell stroke, including stroke specified through the grid's
+/// `stroke` field. Can cross spacing between cells created through the grid's
+/// `column-gutter` option.
+///
+/// An example for this function can be found at the
+/// [`table.hline`]($table.hline) element.
+#[elem(name = "hline", title = "Grid Horizontal Line")]
+pub struct GridHLine {
+ /// The row above which the horizontal line is placed (zero-indexed).
+ /// If the `position` field is set to `{bottom}`, the line is placed below
+ /// the row with the given index instead (see that field's docs for
+ /// details).
+ ///
+ /// Specifying `{auto}` causes the line to be placed at the row below the
+ /// last automatically positioned cell (that is, cell without coordinate
+ /// overrides) before the line among the grid's children. If there is no
+ /// such cell before the line, it is placed at the top of the grid (row 0).
+ /// Note that specifying for this option exactly the total amount of rows
+ /// in the grid causes this horizontal line to override the bottom border
+ /// of the grid, while a value of 0 overrides the top border.
+ pub y: Smart<usize>,
+
+ /// The column at which the horizontal line starts (zero-indexed, inclusive).
+ pub start: usize,
+
+ /// The column before which the horizontal line ends (zero-indexed,
+ /// exclusive).
+ /// Therefore, the horizontal line will be drawn up to and across column
+ /// `end - 1`.
+ ///
+ /// A value equal to `{none}` or to the amount of columns causes it to
+ /// extend all the way towards the end of the grid.
+ pub end: Option<NonZeroUsize>,
+
+ /// The line's stroke.
+ ///
+ /// Specifying `{none}` removes any lines previously placed across this
+ /// line's range, including hlines or per-cell stroke below it.
+ #[resolve]
+ #[fold]
+ #[default(Some(Arc::new(Stroke::default())))]
+ pub stroke: Option<Arc<Stroke>>,
+
+ /// The position at which the line is placed, given its row (`y`) - either
+ /// `{top}` to draw above it or `{bottom}` to draw below it.
+ ///
+ /// This setting is only relevant when row gutter is enabled (and
+ /// shouldn't be used otherwise - prefer just increasing the `y` field by
+ /// one instead), since then the position below a row becomes different
+ /// from the position above the next row due to the spacing between both.
+ #[default(OuterVAlignment::Top)]
+ pub position: OuterVAlignment,
+}
+
+/// A vertical line in the grid.
+///
+/// Overrides any per-cell stroke, including stroke specified through the
+/// grid's `stroke` field. Can cross spacing between cells created through
+/// the grid's `row-gutter` option.
+#[elem(name = "vline", title = "Grid Vertical Line")]
+pub struct GridVLine {
+ /// The column before which the horizontal line is placed (zero-indexed).
+ /// If the `position` field is set to `{end}`, the line is placed after the
+ /// column with the given index instead (see that field's docs for
+ /// details).
+ ///
+ /// Specifying `{auto}` causes the line to be placed at the column after
+ /// the last automatically positioned cell (that is, cell without
+ /// coordinate overrides) before the line among the grid's children. If
+ /// there is no such cell before the line, it is placed before the grid's
+ /// first column (column 0).
+ /// Note that specifying for this option exactly the total amount of
+ /// columns in the grid causes this vertical line to override the end
+ /// border of the grid (right in LTR, left in RTL), while a value of 0
+ /// overrides the start border (left in LTR, right in RTL).
+ pub x: Smart<usize>,
+
+ /// The row at which the vertical line starts (zero-indexed, inclusive).
+ pub start: usize,
+
+ /// The row on top of which the vertical line ends (zero-indexed,
+ /// exclusive).
+ /// Therefore, the vertical line will be drawn up to and across row
+ /// `end - 1`.
+ ///
+ /// A value equal to `{none}` or to the amount of rows causes it to extend
+ /// all the way towards the bottom of the grid.
+ pub end: Option<NonZeroUsize>,
+
+ /// The line's stroke.
+ ///
+ /// Specifying `{none}` removes any lines previously placed across this
+ /// line's range, including vlines or per-cell stroke below it.
+ #[resolve]
+ #[fold]
+ #[default(Some(Arc::new(Stroke::default())))]
+ pub stroke: Option<Arc<Stroke>>,
+
+ /// The position at which the line is placed, given its column (`x`) -
+ /// either `{start}` to draw before it or `{end}` to draw after it.
+ ///
+ /// The values `{left}` and `{right}` are also accepted, but discouraged as
+ /// they cause your grid to be inconsistent between left-to-right and
+ /// right-to-left documents.
+ ///
+ /// This setting is only relevant when column gutter is enabled (and
+ /// shouldn't be used otherwise - prefer just increasing the `x` field by
+ /// one instead), since then the position after a column becomes different
+ /// from the position before the next column due to the spacing between
+ /// both.
+ #[default(OuterHAlignment::Start)]
+ pub position: OuterHAlignment,
+}
+
+/// A cell in the grid. You can use this function in the argument list of a grid
+/// to override grid style properties for an individual cell or manually
+/// positioning it within the grid. You can also use this function in show rules
+/// to apply certain styles to multiple cells at once.
+///
+/// For example, you can override the position and stroke for a single cell:
+///
+/// ```example
+/// >>> #set page(width: auto)
+/// >>> #set text(15pt, font: "Noto Sans Symbols 2", bottom-edge: -.2em)
+/// <<< #set text(15pt, font: "Noto Sans Symbols 2")
+/// #show regex("[♚-♟︎]"): set text(fill: rgb("21212A"))
+/// #show regex("[♔-♙]"): set text(fill: rgb("111015"))
+///
+/// #grid(
+/// fill: (x, y) => rgb(
+/// if calc.odd(x + y) { "7F8396" }
+/// else { "EFF0F3" }
+/// ),
+/// columns: (1em,) * 8,
+/// rows: 1em,
+/// align: center + horizon,
+///
+/// [♖], [♘], [♗], [♕], [♔], [♗], [♘], [♖],
+/// [♙], [♙], [♙], [♙], [], [♙], [♙], [♙],
+/// grid.cell(
+/// x: 4, y: 3,
+/// stroke: blue.transparentize(60%)
+/// )[♙],
+///
+/// ..(grid.cell(y: 6)[♟],) * 8,
+/// ..([♜], [♞], [♝], [♛], [♚], [♝], [♞], [♜])
+/// .map(grid.cell.with(y: 7)),
+/// )
+/// ```
+///
+/// You may also apply a show rule on `grid.cell` to style all cells at once,
+/// which allows you, for example, to apply styles based on a cell's position.
+/// Refer to the examples of the [`table.cell`]($table.cell) element to learn
+/// more about this.
+#[elem(name = "cell", title = "Grid Cell", Show)]
+pub struct GridCell {
+ /// The cell's body.
+ #[required]
+ pub body: Content,
+
+ /// The cell's column (zero-indexed).
+ /// This field may be used in show rules to style a cell depending on its
+ /// column.
+ ///
+ /// You may override this field to pick in which column the cell must
+ /// be placed. If no row (`y`) is chosen, the cell will be placed in the
+ /// first row (starting at row 0) with that column available (or a new row
+ /// if none). If both `x` and `y` are chosen, however, the cell will be
+ /// placed in that exact position. An error is raised if that position is
+ /// not available (thus, it is usually wise to specify cells with a custom
+ /// position before cells with automatic positions).
+ ///
+ /// ```example
+ /// #let circ(c) = circle(
+ /// fill: c, width: 5mm
+ /// )
+ ///
+ /// #grid(
+ /// columns: 4,
+ /// rows: 7mm,
+ /// stroke: .5pt + blue,
+ /// align: center + horizon,
+ /// inset: 1mm,
+ ///
+ /// grid.cell(x: 2, y: 2, circ(aqua)),
+ /// circ(yellow),
+ /// grid.cell(x: 3, circ(green)),
+ /// circ(black),
+ /// )
+ /// ```
+ pub x: Smart<usize>,
+
+ /// The cell's row (zero-indexed).
+ /// This field may be used in show rules to style a cell depending on its
+ /// row.
+ ///
+ /// You may override this field to pick in which row the cell must be
+ /// placed. If no column (`x`) is chosen, the cell will be placed in the
+ /// first column (starting at column 0) available in the chosen row. If all
+ /// columns in the chosen row are already occupied, an error is raised.
+ ///
+ /// ```example
+ /// #let tri(c) = polygon.regular(
+ /// fill: c,
+ /// size: 5mm,
+ /// vertices: 3,
+ /// )
+ ///
+ /// #grid(
+ /// columns: 2,
+ /// stroke: blue,
+ /// inset: 1mm,
+ ///
+ /// tri(black),
+ /// grid.cell(y: 1, tri(teal)),
+ /// grid.cell(y: 1, tri(red)),
+ /// grid.cell(y: 2, tri(orange))
+ /// )
+ /// ```
+ pub y: Smart<usize>,
+
+ /// The amount of columns spanned by this cell.
+ #[default(NonZeroUsize::ONE)]
+ pub colspan: NonZeroUsize,
+
+ /// The amount of rows spanned by this cell.
+ #[default(NonZeroUsize::ONE)]
+ pub rowspan: NonZeroUsize,
+
+ /// The cell's [fill]($grid.fill) override.
+ pub fill: Smart<Option<Paint>>,
+
+ /// The cell's [alignment]($grid.align) override.
+ pub align: Smart<Alignment>,
+
+ /// The cell's [inset]($grid.inset) override.
+ pub inset: Smart<Sides<Option<Rel<Length>>>>,
+
+ /// The cell's [stroke]($grid.stroke) override.
+ #[resolve]
+ #[fold]
+ pub stroke: Sides<Option<Option<Arc<Stroke>>>>,
+
+ /// Whether rows spanned by this cell can be placed in different pages.
+ /// When equal to `{auto}`, a cell spanning only fixed-size rows is
+ /// unbreakable, while a cell spanning at least one `{auto}`-sized row is
+ /// breakable.
+ pub breakable: Smart<bool>,
+}
+
+cast! {
+ GridCell,
+ v: Content => v.into(),
+}
+
+impl Show for Packed<GridCell> {
+ fn show(&self, _engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
+ show_grid_cell(self.body().clone(), self.inset(styles), self.align(styles))
+ }
+}
+
+impl Default for Packed<GridCell> {
+ fn default() -> Self {
+ Packed::new(GridCell::new(Content::default()))
+ }
+}
+
+impl From<Content> for GridCell {
+ fn from(value: Content) -> Self {
+ #[allow(clippy::unwrap_or_default)]
+ value.unpack::<Self>().unwrap_or_else(Self::new)
+ }
+}
+
+/// Function with common code to display a grid cell or table cell.
+pub(crate) fn show_grid_cell(
+ mut body: Content,
+ inset: Smart<Sides<Option<Rel<Length>>>>,
+ align: Smart<Alignment>,
+) -> SourceResult<Content> {
+ let inset = inset.unwrap_or_default().map(Option::unwrap_or_default);
+
+ if inset != Sides::default() {
+ // Only pad if some inset is not 0pt.
+ // Avoids a bug where using .padded() in any way inside Show causes
+ // alignment in align(...) to break.
+ body = body.padded(inset);
+ }
+
+ if let Smart::Custom(alignment) = align {
+ body = body.aligned(alignment);
+ }
+
+ Ok(body)
+}
+
+/// A value that can be configured per cell.
+#[derive(Debug, Clone, PartialEq, Hash)]
+pub enum Celled<T> {
+ /// A bare value, the same for all cells.
+ Value(T),
+ /// A closure mapping from cell coordinates to a value.
+ Func(Func),
+ /// An array of alignment values corresponding to each column.
+ Array(Vec<T>),
+}
+
+impl<T: Default + Clone + FromValue> Celled<T> {
+ /// Resolve the value based on the cell position.
+ pub fn resolve(
+ &self,
+ engine: &mut Engine,
+ styles: StyleChain,
+ x: usize,
+ y: usize,
+ ) -> SourceResult<T> {
+ Ok(match self {
+ Self::Value(value) => value.clone(),
+ Self::Func(func) => func
+ .call(engine, Context::new(None, Some(styles)).track(), [x, y])?
+ .cast()
+ .at(func.span())?,
+ Self::Array(array) => x
+ .checked_rem(array.len())
+ .and_then(|i| array.get(i))
+ .cloned()
+ .unwrap_or_default(),
+ })
+ }
+}
+
+impl<T: Default> Default for Celled<T> {
+ fn default() -> Self {
+ Self::Value(T::default())
+ }
+}
+
+impl<T: Reflect> Reflect for Celled<T> {
+ fn input() -> CastInfo {
+ T::input() + Array::input() + Func::input()
+ }
+
+ fn output() -> CastInfo {
+ T::output() + Array::output() + Func::output()
+ }
+
+ fn castable(value: &Value) -> bool {
+ Array::castable(value) || Func::castable(value) || T::castable(value)
+ }
+}
+
+impl<T: IntoValue> IntoValue for Celled<T> {
+ fn into_value(self) -> Value {
+ match self {
+ Self::Value(value) => value.into_value(),
+ Self::Func(func) => func.into_value(),
+ Self::Array(arr) => arr.into_value(),
+ }
+ }
+}
+
+impl<T: FromValue> FromValue for Celled<T> {
+ fn from_value(value: Value) -> HintedStrResult<Self> {
+ match value {
+ Value::Func(v) => Ok(Self::Func(v)),
+ Value::Array(array) => Ok(Self::Array(
+ array.into_iter().map(T::from_value).collect::<HintedStrResult<_>>()?,
+ )),
+ v if T::castable(&v) => Ok(Self::Value(T::from_value(v)?)),
+ v => Err(Self::error(&v)),
+ }
+ }
+}
+
+impl<T: Fold> Fold for Celled<T> {
+ fn fold(self, outer: Self) -> Self {
+ match (self, outer) {
+ (Self::Value(inner), Self::Value(outer)) => Self::Value(inner.fold(outer)),
+ (self_, _) => self_,
+ }
+ }
+}
+
+impl<T: Resolve> Resolve for Celled<T> {
+ type Output = ResolvedCelled<T>;
+
+ fn resolve(self, styles: StyleChain) -> Self::Output {
+ match self {
+ Self::Value(value) => ResolvedCelled(Celled::Value(value.resolve(styles))),
+ Self::Func(func) => ResolvedCelled(Celled::Func(func)),
+ Self::Array(values) => ResolvedCelled(Celled::Array(
+ values.into_iter().map(|value| value.resolve(styles)).collect(),
+ )),
+ }
+ }
+}
+
+/// The result of resolving a Celled's value according to styles.
+/// Holds resolved values which depend on each grid cell's position.
+/// When it is a closure, however, it is only resolved when the closure is
+/// called.
+#[derive(Default, Clone)]
+pub struct ResolvedCelled<T: Resolve>(Celled<T::Output>);
+
+impl<T> ResolvedCelled<T>
+where
+ T: FromValue + Resolve,
+ <T as Resolve>::Output: Default + Clone,
+{
+ /// Resolve the value based on the cell position.
+ pub fn resolve(
+ &self,
+ engine: &mut Engine,
+ styles: StyleChain,
+ x: usize,
+ y: usize,
+ ) -> SourceResult<T::Output> {
+ Ok(match &self.0 {
+ Celled::Value(value) => value.clone(),
+ Celled::Func(func) => func
+ .call(engine, Context::new(None, Some(styles)).track(), [x, y])?
+ .cast::<T>()
+ .at(func.span())?
+ .resolve(styles),
+ Celled::Array(array) => x
+ .checked_rem(array.len())
+ .and_then(|i| array.get(i))
+ .cloned()
+ .unwrap_or_default(),
+ })
+ }
+}
diff --git a/crates/typst-library/src/layout/hide.rs b/crates/typst-library/src/layout/hide.rs
new file mode 100644
index 00000000..1b8b9bd5
--- /dev/null
+++ b/crates/typst-library/src/layout/hide.rs
@@ -0,0 +1,34 @@
+use crate::diag::SourceResult;
+use crate::engine::Engine;
+use crate::foundations::{elem, Content, Packed, Show, StyleChain};
+
+/// Hides content without affecting layout.
+///
+/// The `hide` function allows you to hide content while the layout still 'sees'
+/// it. This is useful to create whitespace that is exactly as large as some
+/// content. It may also be useful to redact content because its arguments are
+/// not included in the output.
+///
+/// # Example
+/// ```example
+/// Hello Jane \
+/// #hide[Hello] Joe
+/// ```
+#[elem(Show)]
+pub struct HideElem {
+ /// The content to hide.
+ #[required]
+ pub body: Content,
+
+ /// This style is set on the content contained in the `hide` element.
+ #[internal]
+ #[ghost]
+ pub hidden: bool,
+}
+
+impl Show for Packed<HideElem> {
+ #[typst_macros::time(name = "hide", span = self.span())]
+ fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
+ Ok(self.body().clone().styled(HideElem::set_hidden(true)))
+ }
+}
diff --git a/crates/typst-library/src/layout/layout.rs b/crates/typst-library/src/layout/layout.rs
new file mode 100644
index 00000000..c3d112e1
--- /dev/null
+++ b/crates/typst-library/src/layout/layout.rs
@@ -0,0 +1,107 @@
+use comemo::Track;
+use typst_syntax::Span;
+
+use crate::diag::SourceResult;
+use crate::engine::Engine;
+use crate::foundations::{
+ dict, elem, func, Content, Context, Func, NativeElement, Packed, Show, StyleChain,
+};
+use crate::introspection::Locatable;
+use crate::layout::{BlockElem, Size};
+
+/// Provides access to the current outer container's (or page's, if none)
+/// dimensions (width and height).
+///
+/// Accepts a function that receives a single parameter, which is a dictionary
+/// with keys `width` and `height`, both of type [`length`]. The function is
+/// provided [context], meaning you don't need to use it in combination with the
+/// `context` keyword. This is why [`measure`] can be called in the example
+/// below.
+///
+/// ```example
+/// #let text = lorem(30)
+/// #layout(size => [
+/// #let (height,) = measure(
+/// block(width: size.width, text),
+/// )
+/// This text is #height high with
+/// the current page width: \
+/// #text
+/// ])
+/// ```
+///
+/// Note that the `layout` function forces its contents into a [block]-level
+/// container, so placement relative to the page or pagebreaks are not possible
+/// within it.
+///
+/// If the `layout` call is placed inside a box with a width of `{800pt}` and a
+/// height of `{400pt}`, then the specified function will be given the argument
+/// `{(width: 800pt, height: 400pt)}`. If it is placed directly into the page, it
+/// receives the page's dimensions minus its margins. This is mostly useful in
+/// combination with [measurement]($measure).
+///
+/// You can also use this function to resolve [`ratio`] to fixed lengths. This
+/// might come in handy if you're building your own layout abstractions.
+///
+/// ```example
+/// #layout(size => {
+/// let half = 50% * size.width
+/// [Half a page is #half wide.]
+/// })
+/// ```
+///
+/// Note that the width or height provided by `layout` will be infinite if the
+/// corresponding page dimension is set to `{auto}`.
+#[func]
+pub fn layout(
+ /// The call span of this function.
+ span: Span,
+ /// A function to call with the outer container's size. Its return value is
+ /// displayed in the document.
+ ///
+ /// The container's size is given as a [dictionary] with the keys `width`
+ /// and `height`.
+ ///
+ /// This function is called once for each time the content returned by
+ /// `layout` appears in the document. This makes it possible to generate
+ /// content that depends on the dimensions of its container.
+ func: Func,
+) -> Content {
+ LayoutElem::new(func).pack().spanned(span)
+}
+
+/// Executes a `layout` call.
+#[elem(Locatable, Show)]
+struct LayoutElem {
+ /// The function to call with the outer container's (or page's) size.
+ #[required]
+ func: Func,
+}
+
+impl Show for Packed<LayoutElem> {
+ fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
+ Ok(BlockElem::multi_layouter(
+ self.clone(),
+ |elem, engine, locator, styles, regions| {
+ // Gets the current region's base size, which will be the size of the
+ // outer container, or of the page if there is no such container.
+ let Size { x, y } = regions.base();
+ let loc = elem.location().unwrap();
+ let context = Context::new(Some(loc), Some(styles));
+ let result = elem
+ .func()
+ .call(
+ engine,
+ context.track(),
+ [dict! { "width" => x, "height" => y }],
+ )?
+ .display();
+ (engine.routines.layout_fragment)(
+ engine, &result, locator, styles, regions,
+ )
+ },
+ )
+ .pack()
+ .spanned(self.span()))
+ }
+}
diff --git a/crates/typst-library/src/layout/length.rs b/crates/typst-library/src/layout/length.rs
new file mode 100644
index 00000000..800140c4
--- /dev/null
+++ b/crates/typst-library/src/layout/length.rs
@@ -0,0 +1,276 @@
+use std::cmp::Ordering;
+use std::fmt::{self, Debug, Formatter};
+use std::ops::{Add, Div, Mul, Neg};
+
+use comemo::Tracked;
+use ecow::{eco_format, EcoString};
+use typst_syntax::Span;
+use typst_utils::Numeric;
+
+use crate::diag::{bail, HintedStrResult, SourceResult};
+use crate::foundations::{func, scope, ty, Context, Fold, Repr, Resolve, StyleChain};
+use crate::layout::{Abs, Em};
+
+/// A size or distance, possibly expressed with contextual units.
+///
+/// Typst supports the following length units:
+///
+/// - Points: `{72pt}`
+/// - Millimeters: `{254mm}`
+/// - Centimeters: `{2.54cm}`
+/// - Inches: `{1in}`
+/// - Relative to font size: `{2.5em}`
+///
+/// You can multiply lengths with and divide them by integers and floats.
+///
+/// # Example
+/// ```example
+/// #rect(width: 20pt)
+/// #rect(width: 2em)
+/// #rect(width: 1in)
+///
+/// #(3em + 5pt).em \
+/// #(20pt).em \
+/// #(40em + 2pt).abs \
+/// #(5em).abs
+/// ```
+///
+/// # Fields
+/// - `abs`: A length with just the absolute component of the current length
+/// (that is, excluding the `em` component).
+/// - `em`: The amount of `em` units in this length, as a [float].
+#[ty(scope, cast)]
+#[derive(Default, Copy, Clone, Eq, PartialEq, Hash)]
+pub struct Length {
+ /// The absolute part.
+ pub abs: Abs,
+ /// The font-relative part.
+ pub em: Em,
+}
+
+impl Length {
+ /// The zero length.
+ pub const fn zero() -> Self {
+ Self { abs: Abs::zero(), em: Em::zero() }
+ }
+
+ /// Try to compute the absolute value of the length.
+ pub fn try_abs(self) -> Option<Self> {
+ (self.abs.is_zero() || self.em.is_zero())
+ .then(|| Self { abs: self.abs.abs(), em: self.em.abs() })
+ }
+
+ /// Try to divide two lengths.
+ pub fn try_div(self, other: Self) -> Option<f64> {
+ if self.abs.is_zero() && other.abs.is_zero() {
+ Some(self.em / other.em)
+ } else if self.em.is_zero() && other.em.is_zero() {
+ Some(self.abs / other.abs)
+ } else {
+ None
+ }
+ }
+
+ /// Convert to an absolute length at the given font size.
+ pub fn at(self, font_size: Abs) -> Abs {
+ self.abs + self.em.at(font_size)
+ }
+
+ /// Fails with an error if the length has a non-zero font-relative part.
+ fn ensure_that_em_is_zero(&self, span: Span, unit: &str) -> SourceResult<()> {
+ if self.em == Em::zero() {
+ return Ok(());
+ }
+
+ bail!(
+ span,
+ "cannot convert a length with non-zero em units (`{}`) to {unit}",
+ self.repr();
+ hint: "use `length.to-absolute()` to resolve its em component \
+ (requires context)";
+ hint: "or use `length.abs.{unit}()` instead to ignore its em component"
+ )
+ }
+}
+
+#[scope]
+impl Length {
+ /// Converts this length to points.
+ ///
+ /// Fails with an error if this length has non-zero `em` units (such as
+ /// `5em + 2pt` instead of just `2pt`). Use the `abs` field (such as in
+ /// `(5em + 2pt).abs.pt()`) to ignore the `em` component of the length (thus
+ /// converting only its absolute component).
+ #[func(name = "pt", title = "Points")]
+ pub fn to_pt(&self, span: Span) -> SourceResult<f64> {
+ self.ensure_that_em_is_zero(span, "pt")?;
+ Ok(self.abs.to_pt())
+ }
+
+ /// Converts this length to millimeters.
+ ///
+ /// Fails with an error if this length has non-zero `em` units. See the
+ /// [`pt`]($length.pt) method for more details.
+ #[func(name = "mm", title = "Millimeters")]
+ pub fn to_mm(&self, span: Span) -> SourceResult<f64> {
+ self.ensure_that_em_is_zero(span, "mm")?;
+ Ok(self.abs.to_mm())
+ }
+
+ /// Converts this length to centimeters.
+ ///
+ /// Fails with an error if this length has non-zero `em` units. See the
+ /// [`pt`]($length.pt) method for more details.
+ #[func(name = "cm", title = "Centimeters")]
+ pub fn to_cm(&self, span: Span) -> SourceResult<f64> {
+ self.ensure_that_em_is_zero(span, "cm")?;
+ Ok(self.abs.to_cm())
+ }
+
+ /// Converts this length to inches.
+ ///
+ /// Fails with an error if this length has non-zero `em` units. See the
+ /// [`pt`]($length.pt) method for more details.
+ #[func(name = "inches")]
+ pub fn to_inches(&self, span: Span) -> SourceResult<f64> {
+ self.ensure_that_em_is_zero(span, "inches")?;
+ Ok(self.abs.to_inches())
+ }
+
+ /// Resolve this length to an absolute length.
+ ///
+ /// ```example
+ /// #set text(size: 12pt)
+ /// #context [
+ /// #(6pt).to-absolute() \
+ /// #(6pt + 10em).to-absolute() \
+ /// #(10em).to-absolute()
+ /// ]
+ ///
+ /// #set text(size: 6pt)
+ /// #context [
+ /// #(6pt).to-absolute() \
+ /// #(6pt + 10em).to-absolute() \
+ /// #(10em).to-absolute()
+ /// ]
+ /// ```
+ #[func]
+ pub fn to_absolute(&self, context: Tracked<Context>) -> HintedStrResult<Length> {
+ Ok(self.resolve(context.styles()?).into())
+ }
+}
+
+impl Debug for Length {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ match (self.abs.is_zero(), self.em.is_zero()) {
+ (false, false) => write!(f, "{:?} + {:?}", self.abs, self.em),
+ (true, false) => self.em.fmt(f),
+ (_, true) => self.abs.fmt(f),
+ }
+ }
+}
+
+impl Repr for Length {
+ fn repr(&self) -> EcoString {
+ match (self.abs.is_zero(), self.em.is_zero()) {
+ (false, false) => eco_format!("{} + {}", self.abs.repr(), self.em.repr()),
+ (true, false) => self.em.repr(),
+ (_, true) => self.abs.repr(),
+ }
+ }
+}
+
+impl Numeric for Length {
+ fn zero() -> Self {
+ Self::zero()
+ }
+
+ fn is_finite(self) -> bool {
+ self.abs.is_finite() && self.em.is_finite()
+ }
+}
+
+impl PartialOrd for Length {
+ fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+ if self.em.is_zero() && other.em.is_zero() {
+ self.abs.partial_cmp(&other.abs)
+ } else if self.abs.is_zero() && other.abs.is_zero() {
+ self.em.partial_cmp(&other.em)
+ } else {
+ None
+ }
+ }
+}
+
+impl From<Abs> for Length {
+ fn from(abs: Abs) -> Self {
+ Self { abs, em: Em::zero() }
+ }
+}
+
+impl From<Em> for Length {
+ fn from(em: Em) -> Self {
+ Self { abs: Abs::zero(), em }
+ }
+}
+
+impl Neg for Length {
+ type Output = Self;
+
+ fn neg(self) -> Self::Output {
+ Self { abs: -self.abs, em: -self.em }
+ }
+}
+
+impl Add for Length {
+ type Output = Self;
+
+ fn add(self, rhs: Self) -> Self::Output {
+ Self { abs: self.abs + rhs.abs, em: self.em + rhs.em }
+ }
+}
+
+typst_utils::sub_impl!(Length - Length -> Length);
+
+impl Mul<f64> for Length {
+ type Output = Self;
+
+ fn mul(self, rhs: f64) -> Self::Output {
+ Self { abs: self.abs * rhs, em: self.em * rhs }
+ }
+}
+
+impl Mul<Length> for f64 {
+ type Output = Length;
+
+ fn mul(self, rhs: Length) -> Self::Output {
+ rhs * self
+ }
+}
+
+impl Div<f64> for Length {
+ type Output = Self;
+
+ fn div(self, rhs: f64) -> Self::Output {
+ Self { abs: self.abs / rhs, em: self.em / rhs }
+ }
+}
+
+typst_utils::assign_impl!(Length += Length);
+typst_utils::assign_impl!(Length -= Length);
+typst_utils::assign_impl!(Length *= f64);
+typst_utils::assign_impl!(Length /= f64);
+
+impl Resolve for Length {
+ type Output = Abs;
+
+ fn resolve(self, styles: StyleChain) -> Self::Output {
+ self.abs + self.em.resolve(styles)
+ }
+}
+
+impl Fold for Length {
+ fn fold(self, _: Self) -> Self {
+ self
+ }
+}
diff --git a/crates/typst-library/src/layout/measure.rs b/crates/typst-library/src/layout/measure.rs
new file mode 100644
index 00000000..2fa51b2d
--- /dev/null
+++ b/crates/typst-library/src/layout/measure.rs
@@ -0,0 +1,115 @@
+use comemo::Tracked;
+use typst_syntax::Span;
+
+use crate::diag::{warning, At, SourceResult};
+use crate::engine::Engine;
+use crate::foundations::{
+ dict, func, Content, Context, Dict, Resolve, Smart, StyleChain, Styles,
+};
+use crate::introspection::{Locator, LocatorLink};
+use crate::layout::{Abs, Axes, Length, Region, Size};
+
+/// Measures the layouted size of content.
+///
+/// The `measure` function lets you determine the layouted size of content.
+/// By default an infinite space is assumed, so the measured dimensions may
+/// not necessarily match the final dimensions of the content.
+/// If you want to measure in the current layout dimensions, you can combine
+/// `measure` and [`layout`].
+///
+/// # Example
+/// The same content can have a different size depending on the [context] that
+/// it is placed into. In the example below, the `[#content]` is of course
+/// bigger when we increase the font size.
+///
+/// ```example
+/// #let content = [Hello!]
+/// #content
+/// #set text(14pt)
+/// #content
+/// ```
+///
+/// For this reason, you can only measure when context is available.
+///
+/// ```example
+/// #let thing(body) = context {
+/// let size = measure(body)
+/// [Width of "#body" is #size.width]
+/// }
+///
+/// #thing[Hey] \
+/// #thing[Welcome]
+/// ```
+///
+/// The measure function returns a dictionary with the entries `width` and
+/// `height`, both of type [`length`].
+#[func(contextual)]
+pub fn measure(
+ /// The engine.
+ engine: &mut Engine,
+ /// The callsite context.
+ context: Tracked<Context>,
+ /// The callsite span.
+ span: Span,
+ /// The width available to layout the content.
+ ///
+ /// Setting this to `{auto}` indicates infinite available width.
+ ///
+ /// Note that using the `width` and `height` parameters of this function is
+ /// different from measuring a sized [`block`] containing the content. In
+ /// the following example, the former will get the dimensions of the inner
+ /// content instead of the dimensions of the block.
+ ///
+ /// ```example
+ /// #context measure(lorem(100), width: 400pt)
+ ///
+ /// #context measure(block(lorem(100), width: 400pt))
+ /// ```
+ #[named]
+ #[default(Smart::Auto)]
+ width: Smart<Length>,
+ /// The height available to layout the content.
+ ///
+ /// Setting this to `{auto}` indicates infinite available height.
+ #[named]
+ #[default(Smart::Auto)]
+ height: Smart<Length>,
+ /// The content whose size to measure.
+ content: Content,
+ /// _Compatibility:_ This argument is deprecated. It only exists for
+ /// compatibility with Typst 0.10 and lower and shouldn't be used anymore.
+ #[default]
+ styles: Option<Styles>,
+) -> SourceResult<Dict> {
+ let styles = match &styles {
+ Some(styles) => {
+ engine.sink.warn(warning!(
+ span, "calling `measure` with a styles argument is deprecated";
+ hint: "try removing the styles argument"
+ ));
+ StyleChain::new(styles)
+ }
+ None => context.styles().at(span)?,
+ };
+
+ // Create a pod region with the available space.
+ let pod = Region::new(
+ Axes::new(
+ width.resolve(styles).unwrap_or(Abs::inf()),
+ height.resolve(styles).unwrap_or(Abs::inf()),
+ ),
+ Axes::splat(false),
+ );
+
+ // We put the locator into a special "measurement mode" to ensure that
+ // introspection-driven features within the content continue to work. Read
+ // the "Dealing with measurement" section of the [`Locator`] docs for more
+ // details.
+ let here = context.location().at(span)?;
+ let link = LocatorLink::measure(here);
+ let locator = Locator::link(&link);
+
+ let frame = (engine.routines.layout_frame)(engine, &content, locator, styles, pod)?;
+ let Size { x, y } = frame.size();
+ Ok(dict! { "width" => x, "height" => y })
+}
diff --git a/crates/typst-library/src/layout/mod.rs b/crates/typst-library/src/layout/mod.rs
new file mode 100644
index 00000000..b54d6906
--- /dev/null
+++ b/crates/typst-library/src/layout/mod.rs
@@ -0,0 +1,106 @@
+//! Composable layouts.
+
+mod abs;
+mod align;
+mod angle;
+mod axes;
+mod columns;
+mod container;
+mod corners;
+mod dir;
+mod em;
+mod fr;
+mod fragment;
+mod frame;
+mod grid;
+mod hide;
+#[path = "layout.rs"]
+mod layout_;
+mod length;
+#[path = "measure.rs"]
+mod measure_;
+mod pad;
+mod page;
+mod place;
+mod point;
+mod ratio;
+mod regions;
+mod rel;
+mod repeat;
+mod sides;
+mod size;
+mod spacing;
+mod stack;
+mod transform;
+
+pub use self::abs::*;
+pub use self::align::*;
+pub use self::angle::*;
+pub use self::axes::*;
+pub use self::columns::*;
+pub use self::container::*;
+pub use self::corners::*;
+pub use self::dir::*;
+pub use self::em::*;
+pub use self::fr::*;
+pub use self::fragment::*;
+pub use self::frame::*;
+pub use self::grid::*;
+pub use self::hide::*;
+pub use self::layout_::*;
+pub use self::length::*;
+pub use self::measure_::*;
+pub use self::pad::*;
+pub use self::page::*;
+pub use self::place::*;
+pub use self::point::*;
+pub use self::ratio::*;
+pub use self::regions::*;
+pub use self::rel::*;
+pub use self::repeat::*;
+pub use self::sides::*;
+pub use self::size::*;
+pub use self::spacing::*;
+pub use self::stack::*;
+pub use self::transform::*;
+
+use crate::foundations::{category, Category, Scope};
+
+/// Arranging elements on the page in different ways.
+///
+/// By combining layout functions, you can create complex and automatic layouts.
+#[category]
+pub static LAYOUT: Category;
+
+/// Hook up all `layout` definitions.
+pub fn define(global: &mut Scope) {
+ global.category(LAYOUT);
+ global.define_type::<Length>();
+ global.define_type::<Angle>();
+ global.define_type::<Ratio>();
+ global.define_type::<Rel<Length>>();
+ global.define_type::<Fr>();
+ global.define_type::<Dir>();
+ global.define_type::<Alignment>();
+ global.define_elem::<PageElem>();
+ global.define_elem::<PagebreakElem>();
+ global.define_elem::<VElem>();
+ global.define_elem::<HElem>();
+ global.define_elem::<BoxElem>();
+ global.define_elem::<BlockElem>();
+ global.define_elem::<StackElem>();
+ global.define_elem::<GridElem>();
+ global.define_elem::<ColumnsElem>();
+ global.define_elem::<ColbreakElem>();
+ global.define_elem::<PlaceElem>();
+ global.define_elem::<AlignElem>();
+ global.define_elem::<PadElem>();
+ global.define_elem::<RepeatElem>();
+ global.define_elem::<MoveElem>();
+ global.define_elem::<ScaleElem>();
+ global.define_elem::<RotateElem>();
+ global.define_elem::<SkewElem>();
+ global.define_elem::<HideElem>();
+ global.define_func::<measure>();
+ global.define_func::<layout>();
+}
diff --git a/crates/typst-library/src/layout/pad.rs b/crates/typst-library/src/layout/pad.rs
new file mode 100644
index 00000000..1dc6d131
--- /dev/null
+++ b/crates/typst-library/src/layout/pad.rs
@@ -0,0 +1,65 @@
+use crate::diag::SourceResult;
+use crate::engine::Engine;
+use crate::foundations::{elem, Content, NativeElement, Packed, Show, StyleChain};
+use crate::layout::{BlockElem, Length, Rel};
+
+/// Adds spacing around content.
+///
+/// The spacing can be specified for each side individually, or for all sides at
+/// once by specifying a positional argument.
+///
+/// # Example
+/// ```example
+/// #set align(center)
+///
+/// #pad(x: 16pt, image("typing.jpg"))
+/// _Typing speeds can be
+/// measured in words per minute._
+/// ```
+#[elem(title = "Padding", Show)]
+pub struct PadElem {
+ /// The padding at the left side.
+ #[parse(
+ let all = args.named("rest")?.or(args.find()?);
+ let x = args.named("x")?.or(all);
+ let y = args.named("y")?.or(all);
+ args.named("left")?.or(x)
+ )]
+ pub left: Rel<Length>,
+
+ /// The padding at the top side.
+ #[parse(args.named("top")?.or(y))]
+ pub top: Rel<Length>,
+
+ /// The padding at the right side.
+ #[parse(args.named("right")?.or(x))]
+ pub right: Rel<Length>,
+
+ /// The padding at the bottom side.
+ #[parse(args.named("bottom")?.or(y))]
+ pub bottom: Rel<Length>,
+
+ /// A shorthand to set `left` and `right` to the same value.
+ #[external]
+ pub x: Rel<Length>,
+
+ /// A shorthand to set `top` and `bottom` to the same value.
+ #[external]
+ pub y: Rel<Length>,
+
+ /// A shorthand to set all four sides to the same value.
+ #[external]
+ pub rest: Rel<Length>,
+
+ /// The content to pad at the sides.
+ #[required]
+ pub body: Content,
+}
+
+impl Show for Packed<PadElem> {
+ fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> {
+ Ok(BlockElem::multi_layouter(self.clone(), engine.routines.layout_pad)
+ .pack()
+ .spanned(self.span()))
+ }
+}
diff --git a/crates/typst-library/src/layout/page.rs b/crates/typst-library/src/layout/page.rs
new file mode 100644
index 00000000..de278cb6
--- /dev/null
+++ b/crates/typst-library/src/layout/page.rs
@@ -0,0 +1,924 @@
+use std::borrow::Cow;
+use std::num::NonZeroUsize;
+use std::ops::RangeInclusive;
+use std::str::FromStr;
+
+use comemo::Track;
+use typst_utils::{singleton, NonZeroExt, Scalar};
+
+use crate::diag::{bail, SourceResult};
+use crate::engine::Engine;
+use crate::foundations::{
+ cast, elem, Args, AutoValue, Cast, Construct, Content, Context, Dict, Fold, Func,
+ NativeElement, Set, Smart, StyleChain, Value,
+};
+use crate::layout::{
+ Abs, Alignment, FlushElem, Frame, HAlignment, Length, OuterVAlignment, Ratio, Rel,
+ Sides, SpecificAlignment,
+};
+use crate::model::Numbering;
+use crate::visualize::{Color, Paint};
+
+/// Layouts its child onto one or multiple pages.
+///
+/// Although this function is primarily used in set rules to affect page
+/// properties, it can also be used to explicitly render its argument onto
+/// a set of pages of its own.
+///
+/// Pages can be set to use `{auto}` as their width or height. In this case, the
+/// pages will grow to fit their content on the respective axis.
+///
+/// The [Guide for Page Setup]($guides/page-setup-guide) explains how to use
+/// this and related functions to set up a document with many examples.
+///
+/// # Example
+/// ```example
+/// >>> #set page(margin: auto)
+/// #set page("us-letter")
+///
+/// There you go, US friends!
+/// ```
+#[elem(Construct)]
+pub struct PageElem {
+ /// A standard paper size to set width and height.
+ ///
+ /// This is just a shorthand for setting `width` and `height` and, as such,
+ /// cannot be retrieved in a context expression.
+ #[external]
+ #[default(Paper::A4)]
+ pub paper: Paper,
+
+ /// The width of the page.
+ ///
+ /// ```example
+ /// #set page(
+ /// width: 3cm,
+ /// margin: (x: 0cm),
+ /// )
+ ///
+ /// #for i in range(3) {
+ /// box(square(width: 1cm))
+ /// }
+ /// ```
+ #[resolve]
+ #[parse(
+ let paper = args.named_or_find::<Paper>("paper")?;
+ args.named("width")?
+ .or_else(|| paper.map(|paper| Smart::Custom(paper.width().into())))
+ )]
+ #[default(Smart::Custom(Paper::A4.width().into()))]
+ #[ghost]
+ pub width: Smart<Length>,
+
+ /// The height of the page.
+ ///
+ /// If this is set to `{auto}`, page breaks can only be triggered manually
+ /// by inserting a [page break]($pagebreak). Most examples throughout this
+ /// documentation use `{auto}` for the height of the page to dynamically
+ /// grow and shrink to fit their content.
+ #[resolve]
+ #[parse(
+ args.named("height")?
+ .or_else(|| paper.map(|paper| Smart::Custom(paper.height().into())))
+ )]
+ #[default(Smart::Custom(Paper::A4.height().into()))]
+ #[ghost]
+ pub height: Smart<Length>,
+
+ /// Whether the page is flipped into landscape orientation.
+ ///
+ /// ```example
+ /// #set page(
+ /// "us-business-card",
+ /// flipped: true,
+ /// fill: rgb("f2e5dd"),
+ /// )
+ ///
+ /// #set align(bottom + end)
+ /// #text(14pt)[*Sam H. Richards*] \
+ /// _Procurement Manager_
+ ///
+ /// #set text(10pt)
+ /// 17 Main Street \
+ /// New York, NY 10001 \
+ /// +1 555 555 5555
+ /// ```
+ #[default(false)]
+ #[ghost]
+ pub flipped: bool,
+
+ /// The page's margins.
+ ///
+ /// - `{auto}`: The margins are set automatically to 2.5/21 times the smaller
+ /// dimension of the page. This results in 2.5cm margins for an A4 page.
+ /// - A single length: The same margin on all sides.
+ /// - A dictionary: With a dictionary, the margins can be set individually.
+ /// The dictionary can contain the following keys in order of precedence:
+ /// - `top`: The top margin.
+ /// - `right`: The right margin.
+ /// - `bottom`: The bottom margin.
+ /// - `left`: The left margin.
+ /// - `inside`: The margin at the inner side of the page (where the
+ /// [binding]($page.binding) is).
+ /// - `outside`: The margin at the outer side of the page (opposite to the
+ /// [binding]($page.binding)).
+ /// - `x`: The horizontal margins.
+ /// - `y`: The vertical margins.
+ /// - `rest`: The margins on all sides except those for which the
+ /// dictionary explicitly sets a size.
+ ///
+ /// The values for `left` and `right` are mutually exclusive with
+ /// the values for `inside` and `outside`.
+ ///
+ /// ```example
+ /// #set page(
+ /// width: 3cm,
+ /// height: 4cm,
+ /// margin: (x: 8pt, y: 4pt),
+ /// )
+ ///
+ /// #rect(
+ /// width: 100%,
+ /// height: 100%,
+ /// fill: aqua,
+ /// )
+ /// ```
+ #[fold]
+ #[ghost]
+ pub margin: Margin,
+
+ /// On which side the pages will be bound.
+ ///
+ /// - `{auto}`: Equivalent to `left` if the [text direction]($text.dir)
+ /// is left-to-right and `right` if it is right-to-left.
+ /// - `left`: Bound on the left side.
+ /// - `right`: Bound on the right side.
+ ///
+ /// This affects the meaning of the `inside` and `outside` options for
+ /// margins.
+ #[ghost]
+ pub binding: Smart<Binding>,
+
+ /// How many columns the page has.
+ ///
+ /// If you need to insert columns into a page or other container, you can
+ /// also use the [`columns` function]($columns).
+ ///
+ /// ```example:single
+ /// #set page(columns: 2, height: 4.8cm)
+ /// Climate change is one of the most
+ /// pressing issues of our time, with
+ /// the potential to devastate
+ /// communities, ecosystems, and
+ /// economies around the world. It's
+ /// clear that we need to take urgent
+ /// action to reduce our carbon
+ /// emissions and mitigate the impacts
+ /// of a rapidly changing climate.
+ /// ```
+ #[default(NonZeroUsize::ONE)]
+ #[ghost]
+ pub columns: NonZeroUsize,
+
+ /// The page's background fill.
+ ///
+ /// Setting this to something non-transparent instructs the printer to color
+ /// the complete page. If you are considering larger production runs, it may
+ /// be more environmentally friendly and cost-effective to source pre-dyed
+ /// pages and not set this property.
+ ///
+ /// When set to `{none}`, the background becomes transparent. Note that PDF
+ /// pages will still appear with a (usually white) background in viewers,
+ /// but they are actually transparent. (If you print them, no color is used
+ /// for the background.)
+ ///
+ /// The default of `{auto}` results in `{none}` for PDF output, and
+ /// `{white}` for PNG and SVG.
+ ///
+ /// ```example
+ /// #set page(fill: rgb("444352"))
+ /// #set text(fill: rgb("fdfdfd"))
+ /// *Dark mode enabled.*
+ /// ```
+ #[borrowed]
+ #[ghost]
+ pub fill: Smart<Option<Paint>>,
+
+ /// How to [number]($numbering) the pages.
+ ///
+ /// If an explicit `footer` (or `header` for top-aligned numbering) is
+ /// given, the numbering is ignored.
+ ///
+ /// ```example
+ /// #set page(
+ /// height: 100pt,
+ /// margin: (top: 16pt, bottom: 24pt),
+ /// numbering: "1 / 1",
+ /// )
+ ///
+ /// #lorem(48)
+ /// ```
+ #[borrowed]
+ #[ghost]
+ pub numbering: Option<Numbering>,
+
+ /// The alignment of the page numbering.
+ ///
+ /// If the vertical component is `top`, the numbering is placed into the
+ /// header and if it is `bottom`, it is placed in the footer. Horizon
+ /// alignment is forbidden. If an explicit matching `header` or `footer` is
+ /// given, the numbering is ignored.
+ ///
+ /// ```example
+ /// #set page(
+ /// margin: (top: 16pt, bottom: 24pt),
+ /// numbering: "1",
+ /// number-align: right,
+ /// )
+ ///
+ /// #lorem(30)
+ /// ```
+ #[default(SpecificAlignment::Both(HAlignment::Center, OuterVAlignment::Bottom))]
+ #[ghost]
+ pub number_align: SpecificAlignment<HAlignment, OuterVAlignment>,
+
+ /// The page's header. Fills the top margin of each page.
+ ///
+ /// - Content: Shows the content as the header.
+ /// - `{auto}`: Shows the page number if a `numbering` is set and
+ /// `number-align` is `top`.
+ /// - `{none}`: Suppresses the header.
+ ///
+ /// ```example
+ /// #set par(justify: true)
+ /// #set page(
+ /// margin: (top: 32pt, bottom: 20pt),
+ /// header: [
+ /// #set text(8pt)
+ /// #smallcaps[Typst Academcy]
+ /// #h(1fr) _Exercise Sheet 3_
+ /// ],
+ /// )
+ ///
+ /// #lorem(19)
+ /// ```
+ #[borrowed]
+ #[ghost]
+ pub header: Smart<Option<Content>>,
+
+ /// The amount the header is raised into the top margin.
+ #[resolve]
+ #[default(Ratio::new(0.3).into())]
+ #[ghost]
+ pub header_ascent: Rel<Length>,
+
+ /// The page's footer. Fills the bottom margin of each page.
+ ///
+ /// - Content: Shows the content as the footer.
+ /// - `{auto}`: Shows the page number if a `numbering` is set and
+ /// `number-align` is `bottom`.
+ /// - `{none}`: Suppresses the footer.
+ ///
+ /// For just a page number, the `numbering` property typically suffices. If
+ /// you want to create a custom footer but still display the page number,
+ /// you can directly access the [page counter]($counter).
+ ///
+ /// ```example
+ /// #set par(justify: true)
+ /// #set page(
+ /// height: 100pt,
+ /// margin: 20pt,
+ /// footer: context [
+ /// #set align(right)
+ /// #set text(8pt)
+ /// #counter(page).display(
+ /// "1 of I",
+ /// both: true,
+ /// )
+ /// ]
+ /// )
+ ///
+ /// #lorem(48)
+ /// ```
+ #[borrowed]
+ #[ghost]
+ pub footer: Smart<Option<Content>>,
+
+ /// The amount the footer is lowered into the bottom margin.
+ #[resolve]
+ #[default(Ratio::new(0.3).into())]
+ #[ghost]
+ pub footer_descent: Rel<Length>,
+
+ /// Content in the page's background.
+ ///
+ /// This content will be placed behind the page's body. It can be
+ /// used to place a background image or a watermark.
+ ///
+ /// ```example
+ /// #set page(background: rotate(24deg,
+ /// text(18pt, fill: rgb("FFCBC4"))[
+ /// *CONFIDENTIAL*
+ /// ]
+ /// ))
+ ///
+ /// = Typst's secret plans
+ /// In the year 2023, we plan to take
+ /// over the world (of typesetting).
+ /// ```
+ #[borrowed]
+ #[ghost]
+ pub background: Option<Content>,
+
+ /// Content in the page's foreground.
+ ///
+ /// This content will overlay the page's body.
+ ///
+ /// ```example
+ /// #set page(foreground: text(24pt)[🥸])
+ ///
+ /// Reviewer 2 has marked our paper
+ /// "Weak Reject" because they did
+ /// not understand our approach...
+ /// ```
+ #[borrowed]
+ #[ghost]
+ pub foreground: Option<Content>,
+
+ /// The contents of the page(s).
+ ///
+ /// Multiple pages will be created if the content does not fit on a single
+ /// page. A new page with the page properties prior to the function invocation
+ /// will be created after the body has been typeset.
+ #[external]
+ #[required]
+ pub body: Content,
+}
+
+impl Construct for PageElem {
+ fn construct(engine: &mut Engine, args: &mut Args) -> SourceResult<Content> {
+ // The page constructor is special: It doesn't create a page element.
+ // Instead, it just ensures that the passed content lives in a separate
+ // page and styles it.
+ let styles = Self::set(engine, args)?;
+ let body = args.expect::<Content>("body")?;
+ Ok(Content::sequence([
+ PagebreakElem::shared_weak().clone(),
+ // We put an effectless, invisible non-tag element on the page.
+ // This has two desirable consequences:
+ // - The page is kept even if the body is empty
+ // - The page doesn't inherit shared styles from the body
+ FlushElem::new().pack(),
+ body,
+ PagebreakElem::shared_boundary().clone(),
+ ])
+ .styled_with_map(styles))
+ }
+}
+
+/// A manual page break.
+///
+/// Must not be used inside any containers.
+///
+/// # Example
+/// ```example
+/// The next page contains
+/// more details on compound theory.
+/// #pagebreak()
+///
+/// == Compound Theory
+/// In 1984, the first ...
+/// ```
+#[elem(title = "Page Break")]
+pub struct PagebreakElem {
+ /// If `{true}`, the page break is skipped if the current page is already
+ /// empty.
+ #[default(false)]
+ pub weak: bool,
+
+ /// If given, ensures that the next page will be an even/odd page, with an
+ /// empty page in between if necessary.
+ ///
+ /// ```example
+ /// #set page(height: 30pt)
+ ///
+ /// First.
+ /// #pagebreak(to: "odd")
+ /// Third.
+ /// ```
+ pub to: Option<Parity>,
+
+ /// Whether this pagebreak designates an end boundary of a page run. This is
+ /// an even weaker version of pagebreak `weak` because it not only doesn't
+ /// force an empty page, but also doesn't force its initial styles onto a
+ /// staged empty page.
+ #[internal]
+ #[parse(None)]
+ #[default(false)]
+ pub boundary: bool,
+}
+
+impl PagebreakElem {
+ /// Get the globally shared weak pagebreak element.
+ pub fn shared_weak() -> &'static Content {
+ singleton!(Content, PagebreakElem::new().with_weak(true).pack())
+ }
+
+ /// Get the globally shared boundary pagebreak element.
+ pub fn shared_boundary() -> &'static Content {
+ singleton!(
+ Content,
+ PagebreakElem::new().with_weak(true).with_boundary(true).pack()
+ )
+ }
+}
+
+/// A finished page.
+#[derive(Debug, Clone)]
+pub struct Page {
+ /// The frame that defines the page.
+ pub frame: Frame,
+ /// How the page is filled.
+ ///
+ /// - When `None`, the background is transparent.
+ /// - When `Auto`, the background is transparent for PDF and white
+ /// for raster and SVG targets.
+ ///
+ /// Exporters should access the resolved value of this property through
+ /// `fill_or_transparent()` or `fill_or_white()`.
+ pub fill: Smart<Option<Paint>>,
+ /// The page's numbering.
+ pub numbering: Option<Numbering>,
+ /// The logical page number (controlled by `counter(page)` and may thus not
+ /// match the physical number).
+ pub number: usize,
+}
+
+impl Page {
+ /// Get the configured background or `None` if it is `Auto`.
+ ///
+ /// This is used in PDF export.
+ pub fn fill_or_transparent(&self) -> Option<Paint> {
+ self.fill.clone().unwrap_or(None)
+ }
+
+ /// Get the configured background or white if it is `Auto`.
+ ///
+ /// This is used in raster and SVG export.
+ pub fn fill_or_white(&self) -> Option<Paint> {
+ self.fill.clone().unwrap_or_else(|| Some(Color::WHITE.into()))
+ }
+}
+
+/// Specification of the page's margins.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub struct Margin {
+ /// The margins for each side.
+ pub sides: Sides<Option<Smart<Rel<Length>>>>,
+ /// Whether to swap `left` and `right` to make them `inside` and `outside`
+ /// (when to swap depends on the binding).
+ pub two_sided: Option<bool>,
+}
+
+impl Margin {
+ /// Create an instance with four equal components.
+ pub fn splat(value: Option<Smart<Rel<Length>>>) -> Self {
+ Self { sides: Sides::splat(value), two_sided: None }
+ }
+}
+
+impl Default for Margin {
+ fn default() -> Self {
+ Self {
+ sides: Sides::splat(Some(Smart::Auto)),
+ two_sided: None,
+ }
+ }
+}
+
+impl Fold for Margin {
+ fn fold(self, outer: Self) -> Self {
+ Margin {
+ sides: self.sides.fold(outer.sides),
+ two_sided: self.two_sided.fold(outer.two_sided),
+ }
+ }
+}
+
+cast! {
+ Margin,
+ self => {
+ let two_sided = self.two_sided.unwrap_or(false);
+ if !two_sided && self.sides.is_uniform() {
+ if let Some(left) = self.sides.left {
+ return left.into_value();
+ }
+ }
+
+ let mut dict = Dict::new();
+ let mut handle = |key: &str, component: Option<Smart<Rel<Length>>>| {
+ if let Some(c) = component {
+ dict.insert(key.into(), c.into_value());
+ }
+ };
+
+ handle("top", self.sides.top);
+ handle("bottom", self.sides.bottom);
+ if two_sided {
+ handle("inside", self.sides.left);
+ handle("outside", self.sides.right);
+ } else {
+ handle("left", self.sides.left);
+ handle("right", self.sides.right);
+ }
+
+ Value::Dict(dict)
+ },
+ _: AutoValue => Self::splat(Some(Smart::Auto)),
+ v: Rel<Length> => Self::splat(Some(Smart::Custom(v))),
+ mut dict: Dict => {
+ let mut take = |key| dict.take(key).ok().map(Value::cast).transpose();
+
+ let rest = take("rest")?;
+ let x = take("x")?.or(rest);
+ let y = take("y")?.or(rest);
+ let top = take("top")?.or(y);
+ let bottom = take("bottom")?.or(y);
+ let outside = take("outside")?;
+ let inside = take("inside")?;
+ let left = take("left")?;
+ let right = take("right")?;
+
+ let implicitly_two_sided = outside.is_some() || inside.is_some();
+ let implicitly_not_two_sided = left.is_some() || right.is_some();
+ if implicitly_two_sided && implicitly_not_two_sided {
+ bail!("`inside` and `outside` are mutually exclusive with `left` and `right`");
+ }
+
+ // - If 'implicitly_two_sided' is false here, then
+ // 'implicitly_not_two_sided' will be guaranteed to be true
+ // due to the previous two 'if' conditions.
+ // - If both are false, this means that this margin change does not
+ // affect lateral margins, and thus shouldn't make a difference on
+ // the 'two_sided' attribute of this margin.
+ let two_sided = (implicitly_two_sided || implicitly_not_two_sided)
+ .then_some(implicitly_two_sided);
+
+ dict.finish(&[
+ "left", "top", "right", "bottom", "outside", "inside", "x", "y", "rest",
+ ])?;
+
+ Margin {
+ sides: Sides {
+ left: inside.or(left).or(x),
+ top,
+ right: outside.or(right).or(x),
+ bottom,
+ },
+ two_sided,
+ }
+ }
+}
+
+/// Specification of the page's binding.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub enum Binding {
+ /// Bound on the left, as customary in LTR languages.
+ Left,
+ /// Bound on the right, as customary in RTL languages.
+ Right,
+}
+
+impl Binding {
+ /// Whether to swap left and right margin for the page with this number.
+ pub fn swap(self, number: NonZeroUsize) -> bool {
+ match self {
+ // Left-bound must swap on even pages
+ // (because it is correct on the first page).
+ Self::Left => number.get() % 2 == 0,
+ // Right-bound must swap on odd pages
+ // (because it is wrong on the first page).
+ Self::Right => number.get() % 2 == 1,
+ }
+ }
+}
+
+cast! {
+ Binding,
+ self => match self {
+ Self::Left => Alignment::LEFT.into_value(),
+ Self::Right => Alignment::RIGHT.into_value(),
+ },
+ v: Alignment => match v {
+ Alignment::LEFT => Self::Left,
+ Alignment::RIGHT => Self::Right,
+ _ => bail!("must be `left` or `right`"),
+ },
+}
+
+/// A header, footer, foreground or background definition.
+#[derive(Debug, Clone, Hash)]
+pub enum Marginal {
+ /// Bare content.
+ Content(Content),
+ /// A closure mapping from a page number to content.
+ Func(Func),
+}
+
+impl Marginal {
+ /// Resolve the marginal based on the page number.
+ pub fn resolve(
+ &self,
+ engine: &mut Engine,
+ styles: StyleChain,
+ page: usize,
+ ) -> SourceResult<Cow<'_, Content>> {
+ Ok(match self {
+ Self::Content(content) => Cow::Borrowed(content),
+ Self::Func(func) => Cow::Owned(
+ func.call(engine, Context::new(None, Some(styles)).track(), [page])?
+ .display(),
+ ),
+ })
+ }
+}
+
+cast! {
+ Marginal,
+ self => match self {
+ Self::Content(v) => v.into_value(),
+ Self::Func(v) => v.into_value(),
+ },
+ v: Content => Self::Content(v),
+ v: Func => Self::Func(v),
+}
+
+/// A list of page ranges to be exported.
+#[derive(Debug, Clone)]
+pub struct PageRanges(Vec<PageRange>);
+
+/// A range of pages to export.
+///
+/// The range is one-indexed. For example, `1..=3` indicates the first, second
+/// and third pages should be exported.
+pub type PageRange = RangeInclusive<Option<NonZeroUsize>>;
+
+impl PageRanges {
+ /// Create new page ranges.
+ pub fn new(ranges: Vec<PageRange>) -> Self {
+ Self(ranges)
+ }
+
+ /// Check if a page, given its number, should be included when exporting the
+ /// document while restricting the exported pages to these page ranges.
+ /// This is the one-indexed version of 'includes_page_index'.
+ pub fn includes_page(&self, page: NonZeroUsize) -> bool {
+ self.includes_page_index(page.get() - 1)
+ }
+
+ /// Check if a page, given its index, should be included when exporting the
+ /// document while restricting the exported pages to these page ranges.
+ /// This is the zero-indexed version of 'includes_page'.
+ pub fn includes_page_index(&self, page: usize) -> bool {
+ let page = NonZeroUsize::try_from(page + 1).unwrap();
+ self.0.iter().any(|range| match (range.start(), range.end()) {
+ (Some(start), Some(end)) => (start..=end).contains(&&page),
+ (Some(start), None) => (start..).contains(&&page),
+ (None, Some(end)) => (..=end).contains(&&page),
+ (None, None) => true,
+ })
+ }
+}
+
+/// Whether something should be even or odd.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
+pub enum Parity {
+ /// Next page will be an even page.
+ Even,
+ /// Next page will be an odd page.
+ Odd,
+}
+
+impl Parity {
+ /// Whether the given number matches the parity.
+ pub fn matches(self, number: usize) -> bool {
+ match self {
+ Self::Even => number % 2 == 0,
+ Self::Odd => number % 2 == 1,
+ }
+ }
+}
+
+/// Specification of a paper.
+#[derive(Debug, Copy, Clone, Hash)]
+pub struct Paper {
+ /// The name of the paper.
+ name: &'static str,
+ /// The width of the paper in millimeters.
+ width: Scalar,
+ /// The height of the paper in millimeters.
+ height: Scalar,
+}
+
+impl Paper {
+ /// The width of the paper.
+ pub fn width(self) -> Abs {
+ Abs::mm(self.width.get())
+ }
+
+ /// The height of the paper.
+ pub fn height(self) -> Abs {
+ Abs::mm(self.height.get())
+ }
+}
+
+/// Defines paper constants and a paper parsing implementation.
+macro_rules! papers {
+ ($(($var:ident: $width:expr, $height: expr, $name:literal))*) => {
+ /// Predefined papers.
+ ///
+ /// Each paper is parsable from its name in kebab-case.
+ impl Paper {
+ $(pub const $var: Self = Self {
+ name: $name,
+ width: Scalar::new($width),
+ height: Scalar::new($height),
+ };)*
+ }
+
+ impl FromStr for Paper {
+ type Err = &'static str;
+
+ fn from_str(name: &str) -> Result<Self, Self::Err> {
+ match name.to_lowercase().as_str() {
+ $($name => Ok(Self::$var),)*
+ _ => Err("unknown paper size"),
+ }
+ }
+ }
+
+ cast! {
+ Paper,
+ self => self.name.into_value(),
+ $(
+ /// Produces a paper of the respective size.
+ $name => Self::$var,
+ )*
+ }
+ };
+}
+
+// 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/crates/typst-library/src/layout/place.rs b/crates/typst-library/src/layout/place.rs
new file mode 100644
index 00000000..bedeb507
--- /dev/null
+++ b/crates/typst-library/src/layout/place.rs
@@ -0,0 +1,208 @@
+use crate::foundations::{elem, scope, Cast, Content, Packed, Smart};
+use crate::introspection::{Locatable, Unqueriable};
+use crate::layout::{Alignment, Em, Length, Rel};
+
+/// Places content relatively to its parent container.
+///
+/// Placed content can be either overlaid (the default) or floating. Overlaid
+/// content is aligned with the parent container according to the given
+/// [`alignment`]($place.alignment), and shown over any other content added so
+/// far in the container. Floating content is placed at the top or bottom of
+/// the container, displacing other content down or up respectively. In both
+/// cases, the content position can be adjusted with [`dx`]($place.dx) and
+/// [`dy`]($place.dy) offsets without affecting the layout.
+///
+/// The parent can be any container such as a [`block`], [`box`],
+/// [`rect`], etc. A top level `place` call will place content directly
+/// in the text area of the current page. This can be used for absolute
+/// positioning on the page: with a `top + left`
+/// [`alignment`]($place.alignment), the offsets `dx` and `dy` will set the
+/// position of the element's top left corner relatively to the top left corner
+/// of the text area. For absolute positioning on the full page including
+/// margins, you can use `place` in [`page.foreground`]($page.foreground) or
+/// [`page.background`]($page.background).
+///
+/// # Examples
+/// ```example
+/// #set page(height: 120pt)
+/// Hello, world!
+///
+/// #rect(
+/// width: 100%,
+/// height: 2cm,
+/// place(horizon + right, square()),
+/// )
+///
+/// #place(
+/// top + left,
+/// dx: -5pt,
+/// square(size: 5pt, fill: red),
+/// )
+/// ```
+///
+/// # Effect on the position of other elements { #effect-on-other-elements }
+/// Overlaid elements don't take space in the flow of content, but a `place`
+/// call inserts an invisible block-level element in the flow. This can
+/// affect the layout by breaking the current paragraph. To avoid this,
+/// you can wrap the `place` call in a [`box`] when the call is made
+/// in the middle of a paragraph. The alignment and offsets will then be
+/// relative to this zero-size box. To make sure it doesn't interfere with
+/// spacing, the box should be attached to a word using a word joiner.
+///
+/// For example, the following defines a function for attaching an annotation
+/// to the following word:
+///
+/// ```example
+/// >>> #set page(height: 70pt)
+/// #let annotate(..args) = {
+/// box(place(..args))
+/// sym.wj
+/// h(0pt, weak: true)
+/// }
+///
+/// A placed #annotate(square(), dy: 2pt)
+/// square in my text.
+/// ```
+///
+/// The zero-width weak spacing serves to discard spaces between the function
+/// call and the next word.
+#[elem(scope, Locatable, Unqueriable)]
+pub struct PlaceElem {
+ /// Relative to which position in the parent container to place the content.
+ ///
+ /// - If `float` is `{false}`, then this can be any alignment other than `{auto}`.
+ /// - If `float` is `{true}`, then this must be `{auto}`, `{top}`, or `{bottom}`.
+ ///
+ /// When `float` is `{false}` and no vertical alignment is specified, the
+ /// content is placed at the current position on the vertical axis.
+ #[positional]
+ #[default(Smart::Custom(Alignment::START))]
+ pub alignment: Smart<Alignment>,
+
+ /// Relative to which containing scope something is placed.
+ ///
+ /// The parent scope is primarily used with figures and, for
+ /// this reason, the figure function has a mirrored [`scope`
+ /// parameter]($figure.scope). Nonetheless, it can also be more generally
+ /// useful to break out of the columns. A typical example would be to
+ /// [create a single-column title section]($guides/page-setup-guide/#columns)
+ /// in a two-column document.
+ ///
+ /// Note that parent-scoped placement is currently only supported if `float`
+ /// is `{true}`. This may change in the future.
+ ///
+ /// ```example
+ /// #set page(height: 150pt, columns: 2)
+ /// #place(
+ /// top + center,
+ /// scope: "parent",
+ /// float: true,
+ /// rect(width: 80%, fill: aqua),
+ /// )
+ ///
+ /// #lorem(25)
+ /// ```
+ pub scope: PlacementScope,
+
+ /// Whether the placed element has floating layout.
+ ///
+ /// Floating elements are positioned at the top or bottom of the parent
+ /// container, displacing in-flow content. They are always placed in the
+ /// in-flow order relative to each other, as well as before any content
+ /// following a later [`place.flush`] element.
+ ///
+ /// ```example
+ /// #set page(height: 150pt)
+ /// #let note(where, body) = place(
+ /// center + where,
+ /// float: true,
+ /// clearance: 6pt,
+ /// rect(body),
+ /// )
+ ///
+ /// #lorem(10)
+ /// #note(bottom)[Bottom 1]
+ /// #note(bottom)[Bottom 2]
+ /// #lorem(40)
+ /// #note(top)[Top]
+ /// #lorem(10)
+ /// ```
+ pub float: bool,
+
+ /// The spacing between the placed element and other elements in a floating
+ /// layout.
+ ///
+ /// Has no effect if `float` is `{false}`.
+ #[default(Em::new(1.5).into())]
+ #[resolve]
+ pub clearance: Length,
+
+ /// The horizontal displacement of the placed content.
+ ///
+ /// ```example
+ /// #set page(height: 100pt)
+ /// #for i in range(16) {
+ /// let amount = i * 4pt
+ /// place(center, dx: amount - 32pt, dy: amount)[A]
+ /// }
+ /// ```
+ ///
+ /// This does not affect the layout of in-flow content.
+ /// In other words, the placed content is treated as if it
+ /// were wrapped in a [`move`] element.
+ pub dx: Rel<Length>,
+
+ /// The vertical displacement of the placed content.
+ ///
+ /// This does not affect the layout of in-flow content.
+ /// In other words, the placed content is treated as if it
+ /// were wrapped in a [`move`] element.
+ pub dy: Rel<Length>,
+
+ /// The content to place.
+ #[required]
+ pub body: Content,
+}
+
+/// `PlaceElem` must be locatable to support logical ordering of floats, but I
+/// do not want to expose `query(place)` for now.
+impl Unqueriable for Packed<PlaceElem> {}
+
+#[scope]
+impl PlaceElem {
+ #[elem]
+ type FlushElem;
+}
+
+/// Relative to which containing scope something shall be placed.
+#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash, Cast)]
+pub enum PlacementScope {
+ /// Place into the current column.
+ #[default]
+ Column,
+ /// Place relative to the parent, letting the content span over all columns.
+ Parent,
+}
+
+/// Asks the layout algorithm to place pending floating elements before
+/// continuing with the content.
+///
+/// This is useful for preventing floating figures from spilling
+/// into the next section.
+///
+/// ```example
+/// >>> #set page(height: 160pt, width: 150pt)
+/// #lorem(15)
+///
+/// #figure(
+/// rect(width: 100%, height: 50pt),
+/// placement: auto,
+/// caption: [A rectangle],
+/// )
+///
+/// #place.flush()
+///
+/// This text appears after the figure.
+/// ```
+#[elem]
+pub struct FlushElem {}
diff --git a/crates/typst-library/src/layout/point.rs b/crates/typst-library/src/layout/point.rs
new file mode 100644
index 00000000..c3d1594e
--- /dev/null
+++ b/crates/typst-library/src/layout/point.rs
@@ -0,0 +1,168 @@
+use std::fmt::{self, Debug, Formatter};
+use std::ops::{Add, Div, Mul, Neg};
+
+use typst_utils::{Get, Numeric};
+
+use crate::layout::{Abs, Axis, Size, Transform};
+
+/// A point in 2D.
+#[derive(Default, Copy, Clone, Eq, PartialEq, Hash)]
+pub struct Point {
+ /// The x coordinate.
+ pub x: Abs,
+ /// The y coordinate.
+ pub y: Abs,
+}
+
+impl Point {
+ /// The origin point.
+ pub const fn zero() -> Self {
+ Self { x: Abs::zero(), y: Abs::zero() }
+ }
+
+ /// Create a new point from x and y coordinates.
+ pub const fn new(x: Abs, y: Abs) -> Self {
+ Self { x, y }
+ }
+
+ /// Create an instance with two equal components.
+ pub const fn splat(value: Abs) -> Self {
+ Self { x: value, y: value }
+ }
+
+ /// Create a new point with y set to zero.
+ pub const fn with_x(x: Abs) -> Self {
+ Self { x, y: Abs::zero() }
+ }
+
+ /// Create a new point with x set to zero.
+ pub const fn with_y(y: Abs) -> Self {
+ Self { x: Abs::zero(), y }
+ }
+
+ /// The component-wise minimum of this and another point.
+ pub fn min(self, other: Self) -> Self {
+ Self { x: self.x.min(other.x), y: self.y.min(other.y) }
+ }
+
+ /// The component-wise minimum of this and another point.
+ pub fn max(self, other: Self) -> Self {
+ Self { x: self.x.max(other.x), y: self.y.max(other.y) }
+ }
+
+ /// Maps the point with the given function.
+ pub fn map(self, f: impl Fn(Abs) -> Abs) -> Self {
+ Self { x: f(self.x), y: f(self.y) }
+ }
+
+ /// The distance between this point and the origin.
+ pub fn hypot(self) -> Abs {
+ Abs::raw(self.x.to_raw().hypot(self.y.to_raw()))
+ }
+
+ /// Transform the point with the given transformation.
+ ///
+ /// In the event that one of the coordinates is infinite, the result will
+ /// be zero.
+ pub fn transform(self, ts: Transform) -> Self {
+ Self::new(
+ ts.sx.of(self.x) + ts.kx.of(self.y) + ts.tx,
+ ts.ky.of(self.x) + ts.sy.of(self.y) + ts.ty,
+ )
+ }
+
+ /// Transforms the point with the given transformation, without accounting
+ /// for infinite values.
+ pub fn transform_inf(self, ts: Transform) -> Self {
+ Self::new(
+ ts.sx.get() * self.x + ts.kx.get() * self.y + ts.tx,
+ ts.ky.get() * self.x + ts.sy.get() * self.y + ts.ty,
+ )
+ }
+
+ /// Convert to a size.
+ pub fn to_size(self) -> Size {
+ Size::new(self.x, self.y)
+ }
+}
+
+impl Numeric for Point {
+ fn zero() -> Self {
+ Self::zero()
+ }
+
+ fn is_finite(self) -> bool {
+ self.x.is_finite() && self.y.is_finite()
+ }
+}
+
+impl Get<Axis> for Point {
+ type Component = Abs;
+
+ fn get_ref(&self, axis: Axis) -> &Abs {
+ match axis {
+ Axis::X => &self.x,
+ Axis::Y => &self.y,
+ }
+ }
+
+ fn get_mut(&mut self, axis: Axis) -> &mut Abs {
+ match axis {
+ Axis::X => &mut self.x,
+ Axis::Y => &mut self.y,
+ }
+ }
+}
+
+impl Debug for Point {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ write!(f, "Point({:?}, {:?})", self.x, self.y)
+ }
+}
+
+impl Neg for Point {
+ type Output = Self;
+
+ fn neg(self) -> Self {
+ Self { x: -self.x, y: -self.y }
+ }
+}
+
+impl Add for Point {
+ type Output = Self;
+
+ fn add(self, other: Self) -> Self {
+ Self { x: self.x + other.x, y: self.y + other.y }
+ }
+}
+
+typst_utils::sub_impl!(Point - Point -> Point);
+
+impl Mul<f64> for Point {
+ type Output = Self;
+
+ fn mul(self, other: f64) -> Self {
+ Self { x: self.x * other, y: self.y * other }
+ }
+}
+
+impl Mul<Point> for f64 {
+ type Output = Point;
+
+ fn mul(self, other: Point) -> Point {
+ other * self
+ }
+}
+
+impl Div<f64> for Point {
+ type Output = Self;
+
+ fn div(self, other: f64) -> Self {
+ Self { x: self.x / other, y: self.y / other }
+ }
+}
+
+typst_utils::assign_impl!(Point += Point);
+typst_utils::assign_impl!(Point -= Point);
+typst_utils::assign_impl!(Point *= f64);
+typst_utils::assign_impl!(Point /= f64);
diff --git a/crates/typst-library/src/layout/ratio.rs b/crates/typst-library/src/layout/ratio.rs
new file mode 100644
index 00000000..1c0dcd29
--- /dev/null
+++ b/crates/typst-library/src/layout/ratio.rs
@@ -0,0 +1,153 @@
+use std::fmt::{self, Debug, Formatter};
+use std::ops::{Add, Div, Mul, Neg};
+
+use ecow::EcoString;
+use typst_utils::{Numeric, Scalar};
+
+use crate::foundations::{repr, ty, Repr};
+
+/// A ratio of a whole.
+///
+/// Written as a number, followed by a percent sign.
+///
+/// # Example
+/// ```example
+/// #set align(center)
+/// #scale(x: 150%)[
+/// Scaled apart.
+/// ]
+/// ```
+#[ty(cast)]
+#[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
+pub struct Ratio(Scalar);
+
+impl Ratio {
+ /// A ratio of `0%` represented as `0.0`.
+ pub const fn zero() -> Self {
+ Self(Scalar::ZERO)
+ }
+
+ /// A ratio of `100%` represented as `1.0`.
+ pub const fn one() -> Self {
+ Self(Scalar::ONE)
+ }
+
+ /// Create a new ratio from a value, where `1.0` means `100%`.
+ pub const fn new(ratio: f64) -> Self {
+ Self(Scalar::new(ratio))
+ }
+
+ /// Get the underlying ratio.
+ pub const fn get(self) -> f64 {
+ (self.0).get()
+ }
+
+ /// Whether the ratio is zero.
+ pub fn is_zero(self) -> bool {
+ self.0 == 0.0
+ }
+
+ /// Whether the ratio is one.
+ pub fn is_one(self) -> bool {
+ self.0 == 1.0
+ }
+
+ /// The absolute value of this ratio.
+ pub fn abs(self) -> Self {
+ Self::new(self.get().abs())
+ }
+
+ /// Return the ratio of the given `whole`.
+ pub fn of<T: Numeric>(self, whole: T) -> T {
+ let resolved = whole * self.get();
+ if resolved.is_finite() {
+ resolved
+ } else {
+ T::zero()
+ }
+ }
+}
+
+impl Debug for Ratio {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ write!(f, "{:?}%", self.get() * 100.0)
+ }
+}
+
+impl Repr for Ratio {
+ fn repr(&self) -> EcoString {
+ repr::format_float_with_unit(self.get() * 100.0, "%")
+ }
+}
+
+impl Neg for Ratio {
+ type Output = Self;
+
+ fn neg(self) -> Self {
+ Self(-self.0)
+ }
+}
+
+impl Add for Ratio {
+ type Output = Self;
+
+ fn add(self, other: Self) -> Self {
+ Self(self.0 + other.0)
+ }
+}
+
+typst_utils::sub_impl!(Ratio - Ratio -> Ratio);
+
+impl Mul for Ratio {
+ type Output = Self;
+
+ fn mul(self, other: Self) -> Self {
+ Self(self.0 * other.0)
+ }
+}
+
+impl Mul<f64> for Ratio {
+ type Output = Self;
+
+ fn mul(self, other: f64) -> Self {
+ Self(self.0 * other)
+ }
+}
+
+impl Mul<Ratio> for f64 {
+ type Output = Ratio;
+
+ fn mul(self, other: Ratio) -> Ratio {
+ other * self
+ }
+}
+
+impl Div for Ratio {
+ type Output = f64;
+
+ fn div(self, other: Self) -> f64 {
+ self.get() / other.get()
+ }
+}
+
+impl Div<f64> for Ratio {
+ type Output = Self;
+
+ fn div(self, other: f64) -> Self {
+ Self(self.0 / other)
+ }
+}
+
+impl Div<Ratio> for f64 {
+ type Output = Self;
+
+ fn div(self, other: Ratio) -> Self {
+ self / other.get()
+ }
+}
+
+typst_utils::assign_impl!(Ratio += Ratio);
+typst_utils::assign_impl!(Ratio -= Ratio);
+typst_utils::assign_impl!(Ratio *= Ratio);
+typst_utils::assign_impl!(Ratio *= f64);
+typst_utils::assign_impl!(Ratio /= f64);
diff --git a/crates/typst-library/src/layout/regions.rs b/crates/typst-library/src/layout/regions.rs
new file mode 100644
index 00000000..385664bb
--- /dev/null
+++ b/crates/typst-library/src/layout/regions.rs
@@ -0,0 +1,159 @@
+use std::fmt::{self, Debug, Formatter};
+
+use crate::layout::{Abs, Axes, Size};
+
+/// A single region to layout into.
+#[derive(Debug, Copy, Clone, Hash)]
+pub struct Region {
+ /// The size of the region.
+ pub size: Size,
+ /// Whether elements should expand to fill the regions instead of shrinking
+ /// to fit the content.
+ pub expand: Axes<bool>,
+}
+
+impl Region {
+ /// Create a new region.
+ pub fn new(size: Size, expand: Axes<bool>) -> Self {
+ Self { size, expand }
+ }
+}
+
+impl From<Region> for Regions<'_> {
+ fn from(region: Region) -> Self {
+ Regions {
+ size: region.size,
+ expand: region.expand,
+ full: region.size.y,
+ backlog: &[],
+ last: None,
+ }
+ }
+}
+
+/// A sequence of regions to layout into.
+///
+/// A *region* is a contiguous rectangular space in which elements
+/// can be laid out. All regions within a `Regions` object have the
+/// same width, namely `self.size.x`. This means that it is not
+/// currently possible to, for instance, have content wrap to the
+/// side of a floating element.
+#[derive(Copy, Clone, Hash)]
+pub struct Regions<'a> {
+ /// The remaining size of the first region.
+ pub size: Size,
+ /// Whether elements should expand to fill the regions instead of shrinking
+ /// to fit the content.
+ pub expand: Axes<bool>,
+ /// The full height of the region for relative sizing.
+ pub full: Abs,
+ /// The height of followup regions. The width is the same for all regions.
+ pub backlog: &'a [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>,
+}
+
+impl Regions<'_> {
+ /// Create a new sequence of same-size regions that repeats indefinitely.
+ pub fn repeat(size: Size, expand: Axes<bool>) -> Self {
+ Self {
+ size,
+ full: size.y,
+ backlog: &[],
+ last: Some(size.y),
+ expand,
+ }
+ }
+
+ /// The base size, which doesn't take into account that the regions is
+ /// already partially used up.
+ ///
+ /// This is also used for relative sizing.
+ pub fn base(&self) -> Size {
+ Size::new(self.size.x, self.full)
+ }
+
+ /// 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<'v, F>(&self, backlog: &'v mut Vec<Abs>, mut f: F) -> Regions<'v>
+ where
+ F: FnMut(Size) -> Size,
+ {
+ let x = self.size.x;
+ backlog.clear();
+ backlog.extend(self.backlog.iter().map(|&y| f(Size::new(x, y)).y));
+ Regions {
+ size: f(self.size),
+ full: f(Size::new(x, self.full)).y,
+ backlog,
+ 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.size.y) && self.may_progress()
+ }
+
+ /// Whether a region break is permitted.
+ pub fn may_break(&self) -> bool {
+ !self.backlog.is_empty() || self.last.is_some()
+ }
+
+ /// Whether calling `next()` may improve a situation where there is a lack
+ /// of space.
+ pub fn may_progress(&self) -> bool {
+ !self.backlog.is_empty() || self.last.is_some_and(|height| self.size.y != height)
+ }
+
+ /// Advance to the next region if there is any.
+ pub fn next(&mut self) {
+ if let Some(height) = self
+ .backlog
+ .split_first()
+ .map(|(first, tail)| {
+ self.backlog = tail;
+ *first
+ })
+ .or(self.last)
+ {
+ self.size.y = height;
+ self.full = 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 iterator may be infinite.
+ pub fn iter(&self) -> impl Iterator<Item = Size> + '_ {
+ let first = std::iter::once(self.size);
+ let backlog = self.backlog.iter();
+ let last = self.last.iter().cycle();
+ first.chain(backlog.chain(last).map(|&h| Size::new(self.size.x, h)))
+ }
+}
+
+impl Debug for Regions<'_> {
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ f.write_str("Regions ")?;
+ let mut list = f.debug_list();
+ let mut prev = self.size.y;
+ list.entry(&self.size);
+ for &height in self.backlog {
+ list.entry(&Size::new(self.size.x, height));
+ prev = height;
+ }
+ if let Some(last) = self.last {
+ if last != prev {
+ list.entry(&Size::new(self.size.x, last));
+ }
+ list.entry(&(..));
+ }
+ list.finish()
+ }
+}
diff --git a/crates/typst-library/src/layout/rel.rs b/crates/typst-library/src/layout/rel.rs
new file mode 100644
index 00000000..30441a42
--- /dev/null
+++ b/crates/typst-library/src/layout/rel.rs
@@ -0,0 +1,276 @@
+use std::cmp::Ordering;
+use std::fmt::{self, Debug, Formatter};
+use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign};
+
+use ecow::{eco_format, EcoString};
+use typst_utils::Numeric;
+
+use crate::foundations::{cast, ty, Fold, Repr, Resolve, StyleChain};
+use crate::layout::{Abs, Em, Length, Ratio};
+
+/// A length in relation to some known length.
+///
+/// This type is a combination of a [length] with a [ratio]. It results from
+/// addition and subtraction of a length and a ratio. Wherever a relative length
+/// is expected, you can also use a bare length or ratio.
+///
+/// # Example
+/// ```example
+/// #rect(width: 100% - 50pt)
+///
+/// #(100% - 50pt).length \
+/// #(100% - 50pt).ratio
+/// ```
+///
+/// A relative length has the following fields:
+/// - `length`: Its length component.
+/// - `ratio`: Its ratio component.
+#[ty(cast, name = "relative", title = "Relative Length")]
+#[derive(Default, Copy, Clone, Eq, PartialEq, Hash)]
+pub struct Rel<T: Numeric = Length> {
+ /// The relative part.
+ pub rel: Ratio,
+ /// The absolute part.
+ pub abs: T,
+}
+
+impl<T: Numeric> Rel<T> {
+ /// The zero relative.
+ pub fn zero() -> Self {
+ Self { rel: Ratio::zero(), abs: T::zero() }
+ }
+
+ /// A relative with a ratio of `100%` and no absolute part.
+ pub fn one() -> Self {
+ Self { rel: Ratio::one(), abs: T::zero() }
+ }
+
+ /// Create a new relative from its parts.
+ pub fn new(rel: Ratio, abs: T) -> Self {
+ Self { rel, abs }
+ }
+
+ /// Whether both parts are zero.
+ pub fn is_zero(self) -> bool {
+ self.rel.is_zero() && self.abs == T::zero()
+ }
+
+ /// Whether the relative part is one and the absolute part is zero.
+ pub fn is_one(self) -> bool {
+ self.rel.is_one() && self.abs == T::zero()
+ }
+
+ /// Evaluate this relative to the given `whole`.
+ pub fn relative_to(self, whole: T) -> T {
+ self.rel.of(whole) + self.abs
+ }
+
+ /// Map the absolute part with `f`.
+ pub fn map<F, U>(self, f: F) -> Rel<U>
+ where
+ F: FnOnce(T) -> U,
+ U: Numeric,
+ {
+ Rel { rel: self.rel, abs: f(self.abs) }
+ }
+}
+
+impl Rel<Length> {
+ /// Try to divide two relative lengths.
+ pub fn try_div(self, other: Self) -> Option<f64> {
+ if self.rel.is_zero() && other.rel.is_zero() {
+ self.abs.try_div(other.abs)
+ } else if self.abs.is_zero() && other.abs.is_zero() {
+ Some(self.rel / other.rel)
+ } else {
+ None
+ }
+ }
+
+ /// Convert to a relative length with the absolute part resolved at the
+ /// given font size.
+ pub fn at(self, font_size: Abs) -> Rel<Abs> {
+ self.map(|abs| abs.at(font_size))
+ }
+}
+
+impl<T: Numeric + Debug> Debug for Rel<T> {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ match (self.rel.is_zero(), self.abs.is_zero()) {
+ (false, false) => write!(f, "{:?} + {:?}", self.rel, self.abs),
+ (false, true) => self.rel.fmt(f),
+ (true, _) => self.abs.fmt(f),
+ }
+ }
+}
+
+impl<T: Numeric + Repr> Repr for Rel<T> {
+ fn repr(&self) -> EcoString {
+ eco_format!("{} + {}", self.rel.repr(), self.abs.repr())
+ }
+}
+
+impl From<Abs> for Rel<Length> {
+ fn from(abs: Abs) -> Self {
+ Rel::from(Length::from(abs))
+ }
+}
+
+impl From<Em> for Rel<Length> {
+ fn from(em: Em) -> Self {
+ Rel::from(Length::from(em))
+ }
+}
+
+impl<T: Numeric> From<T> for Rel<T> {
+ fn from(abs: T) -> Self {
+ Self { rel: Ratio::zero(), abs }
+ }
+}
+
+impl<T: Numeric> From<Ratio> for Rel<T> {
+ fn from(rel: Ratio) -> Self {
+ Self { rel, abs: T::zero() }
+ }
+}
+
+impl<T: Numeric + PartialOrd> PartialOrd for Rel<T> {
+ fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+ if self.rel.is_zero() && other.rel.is_zero() {
+ self.abs.partial_cmp(&other.abs)
+ } else if self.abs.is_zero() && other.abs.is_zero() {
+ self.rel.partial_cmp(&other.rel)
+ } else {
+ None
+ }
+ }
+}
+
+impl<T: Numeric> Neg for Rel<T> {
+ type Output = Self;
+
+ fn neg(self) -> Self {
+ Self { rel: -self.rel, abs: -self.abs }
+ }
+}
+
+impl<T: Numeric> Add for Rel<T> {
+ type Output = Self;
+
+ fn add(self, other: Self) -> Self::Output {
+ Self {
+ rel: self.rel + other.rel,
+ abs: self.abs + other.abs,
+ }
+ }
+}
+
+impl<T: Numeric> Sub for Rel<T> {
+ type Output = Self;
+
+ fn sub(self, other: Self) -> Self::Output {
+ self + -other
+ }
+}
+
+impl<T: Numeric> Mul<f64> for Rel<T> {
+ type Output = Self;
+
+ fn mul(self, other: f64) -> Self::Output {
+ Self { rel: self.rel * other, abs: self.abs * other }
+ }
+}
+
+impl<T: Numeric> Mul<Rel<T>> for f64 {
+ type Output = Rel<T>;
+
+ fn mul(self, other: Rel<T>) -> Self::Output {
+ other * self
+ }
+}
+
+impl<T: Numeric> Div<f64> for Rel<T> {
+ type Output = Self;
+
+ fn div(self, other: f64) -> Self::Output {
+ Self { rel: self.rel / other, abs: self.abs / other }
+ }
+}
+
+impl<T: Numeric + AddAssign> AddAssign for Rel<T> {
+ fn add_assign(&mut self, other: Self) {
+ self.rel += other.rel;
+ self.abs += other.abs;
+ }
+}
+
+impl<T: Numeric + SubAssign> SubAssign for Rel<T> {
+ fn sub_assign(&mut self, other: Self) {
+ self.rel -= other.rel;
+ self.abs -= other.abs;
+ }
+}
+
+impl<T: Numeric + MulAssign<f64>> MulAssign<f64> for Rel<T> {
+ fn mul_assign(&mut self, other: f64) {
+ self.rel *= other;
+ self.abs *= other;
+ }
+}
+
+impl<T: Numeric + DivAssign<f64>> DivAssign<f64> for Rel<T> {
+ fn div_assign(&mut self, other: f64) {
+ self.rel /= other;
+ self.abs /= other;
+ }
+}
+
+impl<T: Numeric> Add<T> for Ratio {
+ type Output = Rel<T>;
+
+ fn add(self, other: T) -> Self::Output {
+ Rel::from(self) + Rel::from(other)
+ }
+}
+
+impl<T: Numeric> Add<T> for Rel<T> {
+ type Output = Self;
+
+ fn add(self, other: T) -> Self::Output {
+ self + Rel::from(other)
+ }
+}
+
+impl<T: Numeric> Add<Ratio> for Rel<T> {
+ type Output = Self;
+
+ fn add(self, other: Ratio) -> Self::Output {
+ self + Rel::from(other)
+ }
+}
+
+impl<T> Resolve for Rel<T>
+where
+ T: Resolve + Numeric,
+ <T as Resolve>::Output: Numeric,
+{
+ type Output = Rel<<T as Resolve>::Output>;
+
+ fn resolve(self, styles: StyleChain) -> Self::Output {
+ self.map(|abs| abs.resolve(styles))
+ }
+}
+
+impl<T> Fold for Rel<T>
+where
+ T: Numeric + Fold,
+{
+ fn fold(self, outer: Self) -> Self {
+ Self { rel: self.rel, abs: self.abs.fold(outer.abs) }
+ }
+}
+
+cast! {
+ Rel<Abs>,
+ self => self.map(Length::from).into_value(),
+}
diff --git a/crates/typst-library/src/layout/repeat.rs b/crates/typst-library/src/layout/repeat.rs
new file mode 100644
index 00000000..e423410a
--- /dev/null
+++ b/crates/typst-library/src/layout/repeat.rs
@@ -0,0 +1,49 @@
+use crate::diag::SourceResult;
+use crate::engine::Engine;
+use crate::foundations::{elem, Content, NativeElement, Packed, Show, StyleChain};
+use crate::layout::{BlockElem, Length};
+
+/// Repeats content to the available space.
+///
+/// This can be useful when implementing a custom index, reference, or outline.
+///
+/// Space may be inserted between the instances of the body parameter, so be
+/// sure to adjust the [`justify`]($repeat.justify) parameter accordingly.
+///
+/// Errors if there no bounds on the available space, as it would create
+/// infinite content.
+///
+/// # Example
+/// ```example
+/// Sign on the dotted line:
+/// #box(width: 1fr, repeat[.])
+///
+/// #set text(10pt)
+/// #v(8pt, weak: true)
+/// #align(right)[
+/// Berlin, the 22nd of December, 2022
+/// ]
+/// ```
+#[elem(Show)]
+pub struct RepeatElem {
+ /// The content to repeat.
+ #[required]
+ pub body: Content,
+
+ /// The gap between each instance of the body.
+ #[default]
+ pub gap: Length,
+
+ /// Whether to increase the gap between instances to completely fill the
+ /// available space.
+ #[default(true)]
+ pub justify: bool,
+}
+
+impl Show for Packed<RepeatElem> {
+ fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> {
+ Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_repeat)
+ .pack()
+ .spanned(self.span()))
+ }
+}
diff --git a/crates/typst-library/src/layout/sides.rs b/crates/typst-library/src/layout/sides.rs
new file mode 100644
index 00000000..e04b63d9
--- /dev/null
+++ b/crates/typst-library/src/layout/sides.rs
@@ -0,0 +1,343 @@
+use std::fmt::{self, Debug, Formatter};
+use std::ops::Add;
+
+use typst_utils::Get;
+
+use crate::diag::{bail, HintedStrResult};
+use crate::foundations::{
+ cast, AlternativeFold, CastInfo, Dict, Fold, FromValue, IntoValue, Reflect, Resolve,
+ StyleChain, Value,
+};
+use crate::layout::{Abs, Alignment, Axes, Axis, Corner, Rel, Size};
+
+/// A container with left, top, right and bottom components.
+#[derive(Default, Copy, Clone, Eq, PartialEq, Hash)]
+pub struct Sides<T> {
+ /// The value for the left side.
+ pub left: T,
+ /// The value for the top side.
+ pub top: T,
+ /// The value for the right side.
+ pub right: T,
+ /// The value for the bottom side.
+ pub bottom: T,
+}
+
+impl<T> Sides<T> {
+ /// Create a new instance from the four components.
+ pub const fn new(left: T, top: T, right: T, bottom: T) -> Self {
+ Self { left, top, right, bottom }
+ }
+
+ /// Create an instance with four equal components.
+ pub fn splat(value: T) -> Self
+ where
+ T: Clone,
+ {
+ Self {
+ left: value.clone(),
+ top: value.clone(),
+ right: value.clone(),
+ bottom: value,
+ }
+ }
+
+ /// Map the individual fields with `f`.
+ pub fn map<F, U>(self, mut f: F) -> Sides<U>
+ where
+ F: FnMut(T) -> U,
+ {
+ Sides {
+ left: f(self.left),
+ top: f(self.top),
+ right: f(self.right),
+ bottom: f(self.bottom),
+ }
+ }
+
+ /// Convert from `&Sides<T>` to `Sides<&T>`.
+ pub fn as_ref(&self) -> Sides<&T> {
+ Sides {
+ left: &self.left,
+ top: &self.top,
+ right: &self.right,
+ bottom: &self.bottom,
+ }
+ }
+
+ /// Zip two instances into one.
+ pub fn zip<U>(self, other: Sides<U>) -> Sides<(T, U)> {
+ Sides {
+ left: (self.left, other.left),
+ top: (self.top, other.top),
+ right: (self.right, other.right),
+ bottom: (self.bottom, other.bottom),
+ }
+ }
+
+ /// An iterator over the sides, starting with the left side, clockwise.
+ pub fn iter(&self) -> impl Iterator<Item = &T> {
+ [&self.left, &self.top, &self.right, &self.bottom].into_iter()
+ }
+
+ /// Whether all sides are equal.
+ pub fn is_uniform(&self) -> bool
+ where
+ T: PartialEq,
+ {
+ self.left == self.top && self.top == self.right && self.right == self.bottom
+ }
+}
+
+impl<T: Add> Sides<T> {
+ /// Sums up `left` and `right` into `x`, and `top` and `bottom` into `y`.
+ pub fn sum_by_axis(self) -> Axes<T::Output> {
+ Axes::new(self.left + self.right, self.top + self.bottom)
+ }
+}
+
+impl<T> Sides<Option<T>> {
+ /// Unwrap-or-default the individual sides.
+ pub fn unwrap_or_default(self) -> Sides<T>
+ where
+ T: Default,
+ {
+ self.map(Option::unwrap_or_default)
+ }
+}
+
+impl Sides<Rel<Abs>> {
+ /// Evaluate the sides relative to the given `size`.
+ pub fn relative_to(&self, size: Size) -> Sides<Abs> {
+ Sides {
+ left: self.left.relative_to(size.x),
+ top: self.top.relative_to(size.y),
+ right: self.right.relative_to(size.x),
+ bottom: self.bottom.relative_to(size.y),
+ }
+ }
+
+ /// Whether all sides are zero.
+ pub fn is_zero(&self) -> bool {
+ self.left.is_zero()
+ && self.top.is_zero()
+ && self.right.is_zero()
+ && self.bottom.is_zero()
+ }
+}
+
+impl<T> Get<Side> for Sides<T> {
+ type Component = T;
+
+ fn get_ref(&self, side: Side) -> &T {
+ match side {
+ Side::Left => &self.left,
+ Side::Top => &self.top,
+ Side::Right => &self.right,
+ Side::Bottom => &self.bottom,
+ }
+ }
+
+ fn get_mut(&mut self, side: Side) -> &mut T {
+ match side {
+ Side::Left => &mut self.left,
+ Side::Top => &mut self.top,
+ Side::Right => &mut self.right,
+ Side::Bottom => &mut self.bottom,
+ }
+ }
+}
+
+impl<T: Debug + PartialEq> Debug for Sides<T> {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ if self.is_uniform() {
+ f.write_str("Sides::splat(")?;
+ self.left.fmt(f)?;
+ f.write_str(")")
+ } else {
+ f.debug_struct("Sides")
+ .field("left", &self.left)
+ .field("top", &self.top)
+ .field("right", &self.right)
+ .field("bottom", &self.bottom)
+ .finish()
+ }
+ }
+}
+
+impl<T: Reflect> Reflect for Sides<Option<T>> {
+ fn input() -> CastInfo {
+ T::input() + Dict::input()
+ }
+
+ fn output() -> CastInfo {
+ T::output() + Dict::output()
+ }
+
+ fn castable(value: &Value) -> bool {
+ Dict::castable(value) || T::castable(value)
+ }
+}
+
+impl<T> IntoValue for Sides<Option<T>>
+where
+ T: PartialEq + IntoValue,
+{
+ fn into_value(self) -> Value {
+ if self.is_uniform() {
+ if let Some(left) = self.left {
+ return left.into_value();
+ }
+ }
+
+ let mut dict = Dict::new();
+ let mut handle = |key: &str, component: Option<T>| {
+ if let Some(c) = component {
+ dict.insert(key.into(), c.into_value());
+ }
+ };
+
+ handle("left", self.left);
+ handle("top", self.top);
+ handle("right", self.right);
+ handle("bottom", self.bottom);
+
+ Value::Dict(dict)
+ }
+}
+
+impl<T> FromValue for Sides<Option<T>>
+where
+ T: Default + FromValue + Clone,
+{
+ fn from_value(mut value: Value) -> HintedStrResult<Self> {
+ let expected_keys = ["left", "top", "right", "bottom", "x", "y", "rest"];
+ if let Value::Dict(dict) = &mut value {
+ if dict.is_empty() {
+ return Ok(Self::splat(None));
+ } else if dict.iter().any(|(key, _)| expected_keys.contains(&key.as_str())) {
+ let mut take = |key| dict.take(key).ok().map(T::from_value).transpose();
+ let rest = take("rest")?;
+ let x = take("x")?.or_else(|| rest.clone());
+ let y = take("y")?.or_else(|| rest.clone());
+ let sides = Sides {
+ left: take("left")?.or_else(|| x.clone()),
+ top: take("top")?.or_else(|| y.clone()),
+ right: take("right")?.or_else(|| x.clone()),
+ bottom: take("bottom")?.or_else(|| y.clone()),
+ };
+
+ dict.finish(&expected_keys)?;
+ return Ok(sides);
+ }
+ }
+
+ if T::castable(&value) {
+ Ok(Self::splat(Some(T::from_value(value)?)))
+ } else if let Value::Dict(dict) = &value {
+ let keys = dict.iter().map(|kv| kv.0.as_str()).collect();
+ // Do not hint at expected_keys, because T may be castable from Dict
+ // objects with other sets of expected keys.
+ Err(Dict::unexpected_keys(keys, None).into())
+ } else {
+ Err(Self::error(&value))
+ }
+ }
+}
+
+impl<T: Resolve> Resolve for Sides<T> {
+ type Output = Sides<T::Output>;
+
+ fn resolve(self, styles: StyleChain) -> Self::Output {
+ self.map(|v| v.resolve(styles))
+ }
+}
+
+impl<T: Fold> Fold for Sides<Option<T>> {
+ fn fold(self, outer: Self) -> Self {
+ // Usually, folding an inner `None` with an `outer` prefers the
+ // explicit `None`. However, here `None` means unspecified and thus
+ // we want `outer`, so we use `fold_or` to opt into such behavior.
+ self.zip(outer).map(|(inner, outer)| inner.fold_or(outer))
+ }
+}
+
+/// The four sides of objects.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub enum Side {
+ /// The left side.
+ Left,
+ /// The top side.
+ Top,
+ /// The right side.
+ Right,
+ /// The bottom side.
+ Bottom,
+}
+
+impl Side {
+ /// The opposite side.
+ pub fn inv(self) -> Self {
+ match self {
+ Self::Left => Self::Right,
+ Self::Top => Self::Bottom,
+ Self::Right => Self::Left,
+ Self::Bottom => Self::Top,
+ }
+ }
+
+ /// The next side, clockwise.
+ pub fn next_cw(self) -> Self {
+ match self {
+ Self::Left => Self::Top,
+ Self::Top => Self::Right,
+ Self::Right => Self::Bottom,
+ Self::Bottom => Self::Left,
+ }
+ }
+
+ /// The next side, counter-clockwise.
+ pub fn next_ccw(self) -> Self {
+ match self {
+ Self::Left => Self::Bottom,
+ Self::Top => Self::Left,
+ Self::Right => Self::Top,
+ Self::Bottom => Self::Right,
+ }
+ }
+
+ /// The first corner of the side in clockwise order.
+ pub fn start_corner(self) -> Corner {
+ match self {
+ Self::Left => Corner::BottomLeft,
+ Self::Top => Corner::TopLeft,
+ Self::Right => Corner::TopRight,
+ Self::Bottom => Corner::BottomRight,
+ }
+ }
+
+ /// The second corner of the side in clockwise order.
+ pub fn end_corner(self) -> Corner {
+ self.next_cw().start_corner()
+ }
+
+ /// Return the corresponding axis.
+ pub fn axis(self) -> Axis {
+ match self {
+ Self::Left | Self::Right => Axis::Y,
+ Self::Top | Self::Bottom => Axis::X,
+ }
+ }
+}
+
+cast! {
+ Side,
+ self => Alignment::from(self).into_value(),
+ align: Alignment => match align {
+ Alignment::LEFT => Self::Left,
+ Alignment::RIGHT => Self::Right,
+ Alignment::TOP => Self::Top,
+ Alignment::BOTTOM => Self::Bottom,
+ _ => bail!("cannot convert this alignment to a side"),
+ },
+}
diff --git a/crates/typst-library/src/layout/size.rs b/crates/typst-library/src/layout/size.rs
new file mode 100644
index 00000000..294eec66
--- /dev/null
+++ b/crates/typst-library/src/layout/size.rs
@@ -0,0 +1,87 @@
+use std::ops::{Add, Div, Mul, Neg};
+
+use typst_utils::Numeric;
+
+use crate::layout::{Abs, Axes, Point, Ratio};
+
+/// A size in 2D.
+pub type Size = Axes<Abs>;
+
+impl Size {
+ /// The zero value.
+ pub const fn zero() -> Self {
+ Self { x: Abs::zero(), y: Abs::zero() }
+ }
+
+ /// Whether the other size fits into this one (smaller width and height).
+ pub fn fits(self, other: Self) -> bool {
+ self.x.fits(other.x) && self.y.fits(other.y)
+ }
+
+ /// Convert to a point.
+ pub fn to_point(self) -> Point {
+ Point::new(self.x, self.y)
+ }
+
+ /// Converts to a ratio of width to height.
+ pub fn aspect_ratio(self) -> Ratio {
+ Ratio::new(self.x / self.y)
+ }
+}
+
+impl Numeric for Size {
+ fn zero() -> Self {
+ Self::zero()
+ }
+
+ fn is_finite(self) -> bool {
+ self.x.is_finite() && self.y.is_finite()
+ }
+}
+
+impl Neg for Size {
+ type Output = Self;
+
+ fn neg(self) -> Self {
+ Self { x: -self.x, y: -self.y }
+ }
+}
+
+impl Add for Size {
+ type Output = Self;
+
+ fn add(self, other: Self) -> Self {
+ Self { x: self.x + other.x, y: self.y + other.y }
+ }
+}
+
+typst_utils::sub_impl!(Size - Size -> Size);
+
+impl Mul<f64> for Size {
+ type Output = Self;
+
+ fn mul(self, other: f64) -> Self {
+ Self { x: self.x * other, y: self.y * other }
+ }
+}
+
+impl Mul<Size> for f64 {
+ type Output = Size;
+
+ fn mul(self, other: Size) -> Size {
+ other * self
+ }
+}
+
+impl Div<f64> for Size {
+ type Output = Self;
+
+ fn div(self, other: f64) -> Self {
+ Self { x: self.x / other, y: self.y / other }
+ }
+}
+
+typst_utils::assign_impl!(Size -= Size);
+typst_utils::assign_impl!(Size += Size);
+typst_utils::assign_impl!(Size *= f64);
+typst_utils::assign_impl!(Size /= f64);
diff --git a/crates/typst-library/src/layout/spacing.rs b/crates/typst-library/src/layout/spacing.rs
new file mode 100644
index 00000000..b3ca1e81
--- /dev/null
+++ b/crates/typst-library/src/layout/spacing.rs
@@ -0,0 +1,193 @@
+use typst_utils::Numeric;
+
+use crate::foundations::{cast, elem, Content};
+use crate::layout::{Abs, Em, Fr, Length, Ratio, Rel};
+
+/// Inserts horizontal spacing into a paragraph.
+///
+/// The spacing can be absolute, relative, or fractional. In the last case, the
+/// remaining space on the line is distributed among all fractional spacings
+/// according to their relative fractions.
+///
+/// # Example
+/// ```example
+/// First #h(1cm) Second \
+/// First #h(30%) Second
+/// ```
+///
+/// # Fractional spacing
+/// With fractional spacing, you can align things within a line without forcing
+/// a paragraph break (like [`align`] would). Each fractionally sized element
+/// gets space based on the ratio of its fraction to the sum of all fractions.
+///
+/// ```example
+/// First #h(1fr) Second \
+/// First #h(1fr) Second #h(1fr) Third \
+/// First #h(2fr) Second #h(1fr) Third
+/// ```
+///
+/// # Mathematical Spacing { #math-spacing }
+/// In [mathematical formulas]($category/math), you can additionally use these
+/// constants to add spacing between elements: `thin` (1/6 em), `med` (2/9 em),
+/// `thick` (5/18 em), `quad` (1 em), `wide` (2 em).
+#[elem(title = "Spacing (H)")]
+pub struct HElem {
+ /// How much spacing to insert.
+ #[required]
+ pub amount: Spacing,
+
+ /// If `{true}`, the spacing collapses at the start or end of a paragraph.
+ /// Moreover, from multiple adjacent weak spacings all but the largest one
+ /// collapse.
+ ///
+ /// Weak spacing in markup also causes all adjacent markup spaces to be
+ /// removed, regardless of the amount of spacing inserted. To force a space
+ /// next to weak spacing, you can explicitly write `[#" "]` (for a normal
+ /// space) or `[~]` (for a non-breaking space). The latter can be useful to
+ /// create a construct that always attaches to the preceding word with one
+ /// non-breaking space, independently of whether a markup space existed in
+ /// front or not.
+ ///
+ /// ```example
+ /// #h(1cm, weak: true)
+ /// We identified a group of _weak_
+ /// specimens that fail to manifest
+ /// in most cases. However, when
+ /// #h(8pt, weak: true) supported
+ /// #h(8pt, weak: true) on both sides,
+ /// they do show up.
+ ///
+ /// Further #h(0pt, weak: true) more,
+ /// even the smallest of them swallow
+ /// adjacent markup spaces.
+ /// ```
+ #[default(false)]
+ pub weak: bool,
+}
+
+impl HElem {
+ /// Zero-width horizontal weak spacing that eats surrounding spaces.
+ pub fn hole() -> Self {
+ Self::new(Abs::zero().into()).with_weak(true)
+ }
+}
+
+/// Inserts vertical spacing into a flow of blocks.
+///
+/// The spacing can be absolute, relative, or fractional. In the last case,
+/// the remaining space on the page is distributed among all fractional spacings
+/// according to their relative fractions.
+///
+/// # Example
+/// ```example
+/// #grid(
+/// rows: 3cm,
+/// columns: 6,
+/// gutter: 1fr,
+/// [A #parbreak() B],
+/// [A #v(0pt) B],
+/// [A #v(10pt) B],
+/// [A #v(0pt, weak: true) B],
+/// [A #v(40%, weak: true) B],
+/// [A #v(1fr) B],
+/// )
+/// ```
+#[elem(title = "Spacing (V)")]
+pub struct VElem {
+ /// How much spacing to insert.
+ #[required]
+ pub amount: Spacing,
+
+ /// If `{true}`, the spacing collapses at the start or end of a flow.
+ /// Moreover, from multiple adjacent weak spacings all but the largest one
+ /// collapse. Weak spacings will always collapse adjacent paragraph spacing,
+ /// even if the paragraph spacing is larger.
+ ///
+ /// ```example
+ /// The following theorem is
+ /// foundational to the field:
+ /// #v(4pt, weak: true)
+ /// $ x^2 + y^2 = r^2 $
+ /// #v(4pt, weak: true)
+ /// The proof is simple:
+ /// ```
+ pub weak: bool,
+
+ /// Whether the spacing collapses if not immediately preceded by a
+ /// paragraph.
+ #[internal]
+ #[parse(Some(false))]
+ pub attach: bool,
+}
+
+cast! {
+ VElem,
+ v: Content => v.unpack::<Self>().map_err(|_| "expected `v` element")?,
+}
+
+/// 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.
+ Rel(Rel<Length>),
+ /// Spacing specified as a fraction of the remaining free space in the
+ /// parent.
+ Fr(Fr),
+}
+
+impl Spacing {
+ /// Whether this is fractional spacing.
+ pub fn is_fractional(self) -> bool {
+ matches!(self, Self::Fr(_))
+ }
+
+ /// Whether the spacing is actually no spacing.
+ pub fn is_zero(&self) -> bool {
+ match self {
+ Self::Rel(rel) => rel.is_zero(),
+ Self::Fr(fr) => fr.is_zero(),
+ }
+ }
+}
+
+impl From<Abs> for Spacing {
+ fn from(abs: Abs) -> Self {
+ Self::Rel(abs.into())
+ }
+}
+
+impl From<Em> for Spacing {
+ fn from(em: Em) -> Self {
+ Self::Rel(Rel::new(Ratio::zero(), em.into()))
+ }
+}
+
+impl From<Length> for Spacing {
+ fn from(length: Length) -> Self {
+ Self::Rel(length.into())
+ }
+}
+
+impl From<Fr> for Spacing {
+ fn from(fr: Fr) -> Self {
+ Self::Fr(fr)
+ }
+}
+
+cast! {
+ Spacing,
+ self => match self {
+ Self::Rel(rel) => {
+ if rel.rel.is_zero() {
+ rel.abs.into_value()
+ } else if rel.abs.is_zero() {
+ rel.rel.into_value()
+ } else {
+ rel.into_value()
+ }
+ }
+ Self::Fr(fr) => fr.into_value(),
+ },
+ v: Rel<Length> => Self::Rel(v),
+ v: Fr => Self::Fr(v),
+}
diff --git a/crates/typst-library/src/layout/stack.rs b/crates/typst-library/src/layout/stack.rs
new file mode 100644
index 00000000..5fc78480
--- /dev/null
+++ b/crates/typst-library/src/layout/stack.rs
@@ -0,0 +1,84 @@
+use std::fmt::{self, Debug, Formatter};
+
+use crate::diag::SourceResult;
+use crate::engine::Engine;
+use crate::foundations::{cast, elem, Content, NativeElement, Packed, Show, StyleChain};
+use crate::layout::{BlockElem, Dir, Spacing};
+
+/// Arranges content and spacing horizontally or vertically.
+///
+/// The stack places a list of items along an axis, with optional spacing
+/// between each item.
+///
+/// # Example
+/// ```example
+/// #stack(
+/// dir: ttb,
+/// rect(width: 40pt),
+/// rect(width: 120pt),
+/// rect(width: 90pt),
+/// )
+/// ```
+#[elem(Show)]
+pub struct StackElem {
+ /// The direction along which the items are stacked. Possible values are:
+ ///
+ /// - `{ltr}`: Left to right.
+ /// - `{rtl}`: Right to left.
+ /// - `{ttb}`: Top to bottom.
+ /// - `{btt}`: Bottom to top.
+ ///
+ /// You can use the `start` and `end` methods to obtain the initial and
+ /// final points (respectively) of a direction, as `alignment`. You can also
+ /// use the `axis` method to determine whether a direction is
+ /// `{"horizontal"}` or `{"vertical"}`. The `inv` method returns a
+ /// direction's inverse direction.
+ ///
+ /// For example, `{ttb.start()}` is `top`, `{ttb.end()}` is `bottom`,
+ /// `{ttb.axis()}` is `{"vertical"}` and `{ttb.inv()}` is equal to `btt`.
+ #[default(Dir::TTB)]
+ pub dir: Dir,
+
+ /// Spacing to insert between items where no explicit spacing was provided.
+ pub spacing: Option<Spacing>,
+
+ /// The children to stack along the axis.
+ #[variadic]
+ pub children: Vec<StackChild>,
+}
+
+impl Show for Packed<StackElem> {
+ fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> {
+ Ok(BlockElem::multi_layouter(self.clone(), engine.routines.layout_stack)
+ .pack()
+ .spanned(self.span()))
+ }
+}
+
+/// A child of a stack element.
+#[derive(Clone, PartialEq, 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),
+ }
+ }
+}
+
+cast! {
+ StackChild,
+ self => match self {
+ Self::Spacing(spacing) => spacing.into_value(),
+ Self::Block(content) => content.into_value(),
+ },
+ v: Spacing => Self::Spacing(v),
+ v: Content => Self::Block(v),
+}
diff --git a/crates/typst-library/src/layout/transform.rs b/crates/typst-library/src/layout/transform.rs
new file mode 100644
index 00000000..183df609
--- /dev/null
+++ b/crates/typst-library/src/layout/transform.rs
@@ -0,0 +1,408 @@
+use crate::diag::SourceResult;
+use crate::engine::Engine;
+use crate::foundations::{
+ cast, elem, Content, NativeElement, Packed, Show, Smart, StyleChain,
+};
+use crate::layout::{
+ Abs, Alignment, Angle, BlockElem, HAlignment, Length, Ratio, Rel, VAlignment,
+};
+
+/// Moves content without affecting layout.
+///
+/// The `move` function allows you to move content while the layout still 'sees'
+/// it at the original positions. Containers will still be sized as if the
+/// content was not moved.
+///
+/// # Example
+/// ```example
+/// #rect(inset: 0pt, move(
+/// dx: 6pt, dy: 6pt,
+/// rect(
+/// inset: 8pt,
+/// fill: white,
+/// stroke: black,
+/// [Abra cadabra]
+/// )
+/// ))
+/// ```
+#[elem(Show)]
+pub struct MoveElem {
+ /// The horizontal displacement of the content.
+ pub dx: Rel<Length>,
+
+ /// The vertical displacement of the content.
+ pub dy: Rel<Length>,
+
+ /// The content to move.
+ #[required]
+ pub body: Content,
+}
+
+impl Show for Packed<MoveElem> {
+ fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> {
+ Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_move)
+ .pack()
+ .spanned(self.span()))
+ }
+}
+
+/// Rotates content without affecting layout.
+///
+/// Rotates an element by a given angle. The layout will act as if the element
+/// was not rotated unless you specify `{reflow: true}`.
+///
+/// # Example
+/// ```example
+/// #stack(
+/// dir: ltr,
+/// spacing: 1fr,
+/// ..range(16)
+/// .map(i => rotate(24deg * i)[X]),
+/// )
+/// ```
+#[elem(Show)]
+pub struct RotateElem {
+ /// The amount of rotation.
+ ///
+ /// ```example
+ /// #rotate(-1.571rad)[Space!]
+ /// ```
+ ///
+ #[positional]
+ pub angle: Angle,
+
+ /// The origin of the rotation.
+ ///
+ /// If, for instance, you wanted the bottom left corner of the rotated
+ /// element to stay aligned with the baseline, you would set it to `bottom +
+ /// left` instead.
+ ///
+ /// ```example
+ /// #set text(spacing: 8pt)
+ /// #let square = square.with(width: 8pt)
+ ///
+ /// #box(square())
+ /// #box(rotate(30deg, origin: center, square()))
+ /// #box(rotate(30deg, origin: top + left, square()))
+ /// #box(rotate(30deg, origin: bottom + right, square()))
+ /// ```
+ #[fold]
+ #[default(HAlignment::Center + VAlignment::Horizon)]
+ pub origin: Alignment,
+
+ /// Whether the rotation impacts the layout.
+ ///
+ /// If set to `{false}`, the rotated content will retain the bounding box of
+ /// the original content. If set to `{true}`, the bounding box will take the
+ /// rotation of the content into account and adjust the layout accordingly.
+ ///
+ /// ```example
+ /// Hello #rotate(90deg, reflow: true)[World]!
+ /// ```
+ #[default(false)]
+ pub reflow: bool,
+
+ /// The content to rotate.
+ #[required]
+ pub body: Content,
+}
+
+impl Show for Packed<RotateElem> {
+ fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> {
+ Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_rotate)
+ .pack()
+ .spanned(self.span()))
+ }
+}
+
+/// Scales content without affecting layout.
+///
+/// Lets you mirror content by specifying a negative scale on a single axis.
+///
+/// # Example
+/// ```example
+/// #set align(center)
+/// #scale(x: -100%)[This is mirrored.]
+/// #scale(x: -100%, reflow: true)[This is mirrored.]
+/// ```
+#[elem(Show)]
+pub struct ScaleElem {
+ /// The scaling factor for both axes, as a positional argument. This is just
+ /// an optional shorthand notation for setting `x` and `y` to the same
+ /// value.
+ #[external]
+ #[positional]
+ #[default(Smart::Custom(ScaleAmount::Ratio(Ratio::one())))]
+ pub factor: Smart<ScaleAmount>,
+
+ /// The horizontal scaling factor.
+ ///
+ /// The body will be mirrored horizontally if the parameter is negative.
+ #[parse(
+ let all = args.find()?;
+ args.named("x")?.or(all)
+ )]
+ #[default(Smart::Custom(ScaleAmount::Ratio(Ratio::one())))]
+ pub x: Smart<ScaleAmount>,
+
+ /// The vertical scaling factor.
+ ///
+ /// The body will be mirrored vertically if the parameter is negative.
+ #[parse(args.named("y")?.or(all))]
+ #[default(Smart::Custom(ScaleAmount::Ratio(Ratio::one())))]
+ pub y: Smart<ScaleAmount>,
+
+ /// The origin of the transformation.
+ ///
+ /// ```example
+ /// A#box(scale(75%)[A])A \
+ /// B#box(scale(75%, origin: bottom + left)[B])B
+ /// ```
+ #[fold]
+ #[default(HAlignment::Center + VAlignment::Horizon)]
+ pub origin: Alignment,
+
+ /// Whether the scaling impacts the layout.
+ ///
+ /// If set to `{false}`, the scaled content will be allowed to overlap
+ /// other content. If set to `{true}`, it will compute the new size of
+ /// the scaled content and adjust the layout accordingly.
+ ///
+ /// ```example
+ /// Hello #scale(x: 20%, y: 40%, reflow: true)[World]!
+ /// ```
+ #[default(false)]
+ pub reflow: bool,
+
+ /// The content to scale.
+ #[required]
+ pub body: Content,
+}
+
+impl Show for Packed<ScaleElem> {
+ fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> {
+ Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_scale)
+ .pack()
+ .spanned(self.span()))
+ }
+}
+
+/// To what size something shall be scaled.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub enum ScaleAmount {
+ Ratio(Ratio),
+ Length(Length),
+}
+
+cast! {
+ ScaleAmount,
+ self => match self {
+ ScaleAmount::Ratio(ratio) => ratio.into_value(),
+ ScaleAmount::Length(length) => length.into_value(),
+ },
+ ratio: Ratio => ScaleAmount::Ratio(ratio),
+ length: Length => ScaleAmount::Length(length),
+}
+
+/// Skews content.
+///
+/// Skews an element in horizontal and/or vertical direction. The layout will
+/// act as if the element was not skewed unless you specify `{reflow: true}`.
+///
+/// # Example
+/// ```example
+/// #skew(ax: -12deg)[
+/// This is some fake italic text.
+/// ]
+/// ```
+#[elem(Show)]
+pub struct SkewElem {
+ /// The horizontal skewing angle.
+ ///
+ /// ```example
+ /// #skew(ax: 30deg)[Skewed]
+ /// ```
+ ///
+ #[default(Angle::zero())]
+ pub ax: Angle,
+
+ /// The vertical skewing angle.
+ ///
+ /// ```example
+ /// #skew(ay: 30deg)[Skewed]
+ /// ```
+ ///
+ #[default(Angle::zero())]
+ pub ay: Angle,
+
+ /// The origin of the skew transformation.
+ ///
+ /// The origin will stay fixed during the operation.
+ ///
+ /// ```example
+ /// X #box(skew(ax: -30deg, origin: center + horizon)[X]) X \
+ /// X #box(skew(ax: -30deg, origin: bottom + left)[X]) X \
+ /// X #box(skew(ax: -30deg, origin: top + right)[X]) X
+ /// ```
+ #[fold]
+ #[default(HAlignment::Center + VAlignment::Horizon)]
+ pub origin: Alignment,
+
+ /// Whether the skew transformation impacts the layout.
+ ///
+ /// If set to `{false}`, the skewed content will retain the bounding box of
+ /// the original content. If set to `{true}`, the bounding box will take the
+ /// transformation of the content into account and adjust the layout accordingly.
+ ///
+ /// ```example
+ /// Hello #skew(ay: 30deg, reflow: true, "World")!
+ /// ```
+ #[default(false)]
+ pub reflow: bool,
+
+ /// The content to skew.
+ #[required]
+ pub body: Content,
+}
+
+impl Show for Packed<SkewElem> {
+ fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> {
+ Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_skew)
+ .pack()
+ .spanned(self.span()))
+ }
+}
+
+/// A scale-skew-translate transformation.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub struct Transform {
+ pub sx: Ratio,
+ pub ky: Ratio,
+ pub kx: Ratio,
+ pub sy: Ratio,
+ pub tx: Abs,
+ pub ty: Abs,
+}
+
+impl Transform {
+ /// The identity transformation.
+ pub const fn identity() -> Self {
+ Self {
+ sx: Ratio::one(),
+ ky: Ratio::zero(),
+ kx: Ratio::zero(),
+ sy: Ratio::one(),
+ tx: Abs::zero(),
+ ty: Abs::zero(),
+ }
+ }
+
+ /// A translate transform.
+ pub const fn translate(tx: Abs, ty: Abs) -> Self {
+ Self { tx, ty, ..Self::identity() }
+ }
+
+ /// A scale transform.
+ pub const fn scale(sx: Ratio, sy: Ratio) -> Self {
+ Self { sx, sy, ..Self::identity() }
+ }
+
+ /// A rotate transform.
+ pub fn rotate(angle: Angle) -> Self {
+ let cos = Ratio::new(angle.cos());
+ let sin = Ratio::new(angle.sin());
+ Self {
+ sx: cos,
+ ky: sin,
+ kx: -sin,
+ sy: cos,
+ ..Self::default()
+ }
+ }
+
+ /// A skew transform.
+ pub fn skew(ax: Angle, ay: Angle) -> Self {
+ Self {
+ kx: Ratio::new(ax.tan()),
+ ky: Ratio::new(ay.tan()),
+ ..Self::identity()
+ }
+ }
+
+ /// Whether this is the identity transformation.
+ pub fn is_identity(self) -> bool {
+ self == Self::identity()
+ }
+
+ /// Pre-concatenate another transformation.
+ pub fn pre_concat(self, prev: Self) -> Self {
+ Transform {
+ sx: self.sx * prev.sx + self.kx * prev.ky,
+ ky: self.ky * prev.sx + self.sy * prev.ky,
+ kx: self.sx * prev.kx + self.kx * prev.sy,
+ sy: self.ky * prev.kx + self.sy * prev.sy,
+ tx: self.sx.of(prev.tx) + self.kx.of(prev.ty) + self.tx,
+ ty: self.ky.of(prev.tx) + self.sy.of(prev.ty) + self.ty,
+ }
+ }
+
+ /// Post-concatenate another transformation.
+ pub fn post_concat(self, next: Self) -> Self {
+ next.pre_concat(self)
+ }
+
+ /// Inverts the transformation.
+ ///
+ /// Returns `None` if the determinant of the matrix is zero.
+ pub fn invert(self) -> Option<Self> {
+ // Allow the trivial case to be inlined.
+ if self.is_identity() {
+ return Some(self);
+ }
+
+ // Fast path for scale-translate-only transforms.
+ if self.kx.is_zero() && self.ky.is_zero() {
+ if self.sx.is_zero() || self.sy.is_zero() {
+ return Some(Self::translate(-self.tx, -self.ty));
+ }
+
+ let inv_x = 1.0 / self.sx;
+ let inv_y = 1.0 / self.sy;
+ return Some(Self {
+ sx: Ratio::new(inv_x),
+ ky: Ratio::zero(),
+ kx: Ratio::zero(),
+ sy: Ratio::new(inv_y),
+ tx: -self.tx * inv_x,
+ ty: -self.ty * inv_y,
+ });
+ }
+
+ let det = self.sx * self.sy - self.kx * self.ky;
+ if det.get().abs() < 1e-12 {
+ return None;
+ }
+
+ let inv_det = 1.0 / det;
+ Some(Self {
+ sx: (self.sy * inv_det),
+ ky: (-self.ky * inv_det),
+ kx: (-self.kx * inv_det),
+ sy: (self.sx * inv_det),
+ tx: Abs::pt(
+ (self.kx.get() * self.ty.to_pt() - self.sy.get() * self.tx.to_pt())
+ * inv_det,
+ ),
+ ty: Abs::pt(
+ (self.ky.get() * self.tx.to_pt() - self.sx.get() * self.ty.to_pt())
+ * inv_det,
+ ),
+ })
+ }
+}
+
+impl Default for Transform {
+ fn default() -> Self {
+ Self::identity()
+ }
+}