From be7cfc85d08c545abfac08098b7b33b4bd71f37e Mon Sep 17 00:00:00 2001 From: Laurenz Date: Sun, 27 Oct 2024 19:04:55 +0100 Subject: Split out four new crates (#5302) --- crates/typst-library/src/layout/abs.rs | 279 ++++++++ crates/typst-library/src/layout/align.rs | 748 ++++++++++++++++++++++ crates/typst-library/src/layout/angle.rs | 244 +++++++ crates/typst-library/src/layout/axes.rs | 320 ++++++++++ crates/typst-library/src/layout/columns.rs | 98 +++ crates/typst-library/src/layout/container.rs | 563 ++++++++++++++++ crates/typst-library/src/layout/corners.rs | 315 +++++++++ crates/typst-library/src/layout/dir.rs | 132 ++++ crates/typst-library/src/layout/em.rs | 168 +++++ crates/typst-library/src/layout/fr.rs | 145 +++++ crates/typst-library/src/layout/fragment.rs | 94 +++ crates/typst-library/src/layout/frame.rs | 606 ++++++++++++++++++ crates/typst-library/src/layout/grid.rs | 924 +++++++++++++++++++++++++++ crates/typst-library/src/layout/hide.rs | 34 + crates/typst-library/src/layout/layout.rs | 107 ++++ crates/typst-library/src/layout/length.rs | 276 ++++++++ crates/typst-library/src/layout/measure.rs | 115 ++++ crates/typst-library/src/layout/mod.rs | 106 +++ crates/typst-library/src/layout/pad.rs | 65 ++ crates/typst-library/src/layout/page.rs | 924 +++++++++++++++++++++++++++ crates/typst-library/src/layout/place.rs | 208 ++++++ crates/typst-library/src/layout/point.rs | 168 +++++ crates/typst-library/src/layout/ratio.rs | 153 +++++ crates/typst-library/src/layout/regions.rs | 159 +++++ crates/typst-library/src/layout/rel.rs | 276 ++++++++ crates/typst-library/src/layout/repeat.rs | 49 ++ crates/typst-library/src/layout/sides.rs | 343 ++++++++++ crates/typst-library/src/layout/size.rs | 87 +++ crates/typst-library/src/layout/spacing.rs | 193 ++++++ crates/typst-library/src/layout/stack.rs | 84 +++ crates/typst-library/src/layout/transform.rs | 408 ++++++++++++ 31 files changed, 8391 insertions(+) create mode 100644 crates/typst-library/src/layout/abs.rs create mode 100644 crates/typst-library/src/layout/align.rs create mode 100644 crates/typst-library/src/layout/angle.rs create mode 100644 crates/typst-library/src/layout/axes.rs create mode 100644 crates/typst-library/src/layout/columns.rs create mode 100644 crates/typst-library/src/layout/container.rs create mode 100644 crates/typst-library/src/layout/corners.rs create mode 100644 crates/typst-library/src/layout/dir.rs create mode 100644 crates/typst-library/src/layout/em.rs create mode 100644 crates/typst-library/src/layout/fr.rs create mode 100644 crates/typst-library/src/layout/fragment.rs create mode 100644 crates/typst-library/src/layout/frame.rs create mode 100644 crates/typst-library/src/layout/grid.rs create mode 100644 crates/typst-library/src/layout/hide.rs create mode 100644 crates/typst-library/src/layout/layout.rs create mode 100644 crates/typst-library/src/layout/length.rs create mode 100644 crates/typst-library/src/layout/measure.rs create mode 100644 crates/typst-library/src/layout/mod.rs create mode 100644 crates/typst-library/src/layout/pad.rs create mode 100644 crates/typst-library/src/layout/page.rs create mode 100644 crates/typst-library/src/layout/place.rs create mode 100644 crates/typst-library/src/layout/point.rs create mode 100644 crates/typst-library/src/layout/ratio.rs create mode 100644 crates/typst-library/src/layout/regions.rs create mode 100644 crates/typst-library/src/layout/rel.rs create mode 100644 crates/typst-library/src/layout/repeat.rs create mode 100644 crates/typst-library/src/layout/sides.rs create mode 100644 crates/typst-library/src/layout/size.rs create mode 100644 crates/typst-library/src/layout/spacing.rs create mode 100644 crates/typst-library/src/layout/stack.rs create mode 100644 crates/typst-library/src/layout/transform.rs (limited to 'crates/typst-library/src/layout') 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 for Abs { + type Output = Self; + + fn mul(self, other: f64) -> Self { + Self(self.0 * other) + } +} + +impl Mul for f64 { + type Output = Abs; + + fn mul(self, other: Abs) -> Abs { + other * self + } +} + +impl Div 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>(iter: I) -> Self { + Self(iter.map(|s| s.0).sum()) + } +} + +impl<'a> Sum<&'a Self> for Abs { + fn sum>(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 { + #[typst_macros::time(name = "align", span = self.span())] + fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { + 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 { + match self { + Self::H(h) | Self::Both(h, _) => Some(h), + Self::V(_) => None, + } + } + + /// The vertical component. + pub const fn y(self) -> Option { + 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 { + 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 { + 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; + + 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; + + fn resolve(self, styles: StyleChain) -> Self::Output { + self.fix(TextElem::dir_in(styles)) + } +} + +impl From 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 for HAlignment { + type Output = Alignment; + + fn add(self, rhs: VAlignment) -> Self::Output { + Alignment::Both(self, rhs) + } +} + +impl From for Alignment { + fn from(align: HAlignment) -> Self { + Self::H(align) + } +} + +impl TryFrom for HAlignment { + type Error = EcoString; + + fn try_from(value: Alignment) -> StrResult { + 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 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 for OuterHAlignment { + type Error = EcoString; + + fn try_from(value: Alignment) -> StrResult { + 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 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 for Alignment { + fn from(align: VAlignment) -> Self { + Self::V(align) + } +} + +impl TryFrom for VAlignment { + type Error = EcoString; + + fn try_from(value: Alignment) -> StrResult { + 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 for VAlignment { + fn from(value: OuterVAlignment) -> Self { + match value { + OuterVAlignment::Top => Self::Top, + OuterVAlignment::Bottom => Self::Bottom, + } + } +} + +impl TryFrom for OuterVAlignment { + type Error = EcoString; + + fn try_from(value: Alignment) -> StrResult { + 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` 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(H), + V(V), + Both(H, V), +} + +impl SpecificAlignment +where + H: Default + Copy + FixAlignment, + V: Default + Copy + FixAlignment, +{ + /// The horizontal component. + pub const fn x(self) -> Option { + match self { + Self::H(h) | Self::Both(h, _) => Some(h), + Self::V(_) => None, + } + } + + /// The vertical component. + pub const fn y(self) -> Option { + 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 { + Axes::new( + self.x().unwrap_or_default().fix(text_dir), + self.y().unwrap_or_default().fix(text_dir), + ) + } +} + +impl Resolve for SpecificAlignment +where + H: Default + Copy + FixAlignment, + V: Default + Copy + FixAlignment, +{ + type Output = Axes; + + fn resolve(self, styles: StyleChain) -> Self::Output { + self.fix(TextElem::dir_in(styles)) + } +} + +impl From> for Alignment +where + HAlignment: From, + VAlignment: From, +{ + fn from(value: SpecificAlignment) -> Self { + type FromType = SpecificAlignment; + 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 Reflect for SpecificAlignment +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 IntoValue for SpecificAlignment +where + HAlignment: From, + VAlignment: From, +{ + fn into_value(self) -> Value { + Alignment::from(self).into_value() + } +} + +impl FromValue for SpecificAlignment +where + H: Reflect + TryFrom, + V: Reflect + TryFrom, +{ + fn from_value(value: Value) -> HintedStrResult { + 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 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 for Angle { + type Output = Self; + + fn mul(self, other: f64) -> Self { + Self(self.0 * other) + } +} + +impl Mul 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 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>(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 { + /// The horizontal component. + pub x: T, + /// The vertical component. + pub y: T, +} + +impl Axes { + /// 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(self, mut f: F) -> Axes + where + F: FnMut(T) -> U, + { + Axes { x: f(self.x), y: f(self.y) } + } + + /// Convert from `&Axes` to `Axes<&T>`. + pub fn as_ref(&self) -> Axes<&T> { + Axes { x: &self.x, y: &self.y } + } + + /// Convert from `&Axes` to `Axes<&::Target>`. + pub fn as_deref(&self) -> Axes<&T::Target> + where + T: Deref, + { + Axes { x: &self.x, y: &self.y } + } + + /// Convert from `&mut Axes` 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(self, other: Axes) -> 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(self, other: Axes, mut f: F) -> Axes + 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(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(self, mut f: F) -> bool + where + F: FnMut(&T) -> bool, + { + f(&self.x) && f(&self.y) + } +} + +impl Axes { + /// 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 Axes { + /// 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> { + /// 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 Get for Axes { + 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 Debug for Axes +where + T: Debug + 'static, +{ + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + if (&self.x as &dyn Any).is::() { + 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 Axes> { + /// Unwrap the individual fields. + pub fn unwrap_or(self, other: Axes) -> Axes { + Axes { + x: self.x.unwrap_or(other.x), + y: self.y.unwrap_or(other.y), + } + } +} + +impl Axes { + /// Select `t.x` if `self.x` is true and `f.x` otherwise and same for `y`. + pub fn select(self, t: Axes, f: Axes) -> Axes { + Axes { + x: if self.x { t.x } else { f.x }, + y: if self.y { t.y } else { f.y }, + } + } +} + +impl Not for Axes { + type Output = Self; + + fn not(self) -> Self::Output { + Self { x: !self.x, y: !self.y } + } +} + +impl BitOr for Axes { + type Output = Self; + + fn bitor(self, rhs: Self) -> Self::Output { + Self { x: self.x | rhs.x, y: self.y | rhs.y } + } +} + +impl BitOr for Axes { + type Output = Self; + + fn bitor(self, rhs: bool) -> Self::Output { + Self { x: self.x | rhs, y: self.y | rhs } + } +} + +impl BitAnd for Axes { + type Output = Self; + + fn bitand(self, rhs: Self) -> Self::Output { + Self { x: self.x & rhs.x, y: self.y & rhs.y } + } +} + +impl BitAnd for Axes { + type Output = Self; + + fn bitand(self, rhs: bool) -> Self::Output { + Self { x: self.x & rhs, y: self.y & rhs } + } +} + +impl BitOrAssign for Axes { + fn bitor_assign(&mut self, rhs: Self) { + self.x |= rhs.x; + self.y |= rhs.y; + } +} + +impl BitAndAssign for Axes { + fn bitand_assign(&mut self, rhs: Self) { + self.x &= rhs.x; + self.y &= rhs.y; + } +} + +cast! { + Axes>, + 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, + 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, + 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 Resolve for Axes { + type Output = Axes; + + 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, + + /// The content that should be layouted into the columns. + #[required] + pub body: Content, +} + +impl Show for Packed { + fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult { + 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>, + + /// An amount to shift the box's baseline by. + /// + /// ```example + /// Image: #box(baseline: 40%, image("tiger.jpg", width: 2cm)). + /// ``` + #[resolve] + pub baseline: Rel, + + /// The box's background color. See the + /// [rectangle's documentation]($rect.fill) for more details. + pub fill: Option, + + /// The box's border color. See the + /// [rectangle's documentation]($rect.stroke) for more details. + #[resolve] + #[fold] + pub stroke: Sides>>, + + /// How much to round the box's corners. See the + /// [rectangle's documentation]($rect.radius) for more details. + #[resolve] + #[fold] + pub radius: Corners>>, + + /// 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>>, + + /// 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>>, + + /// 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, +} + +/// 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 { + 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( + captured: Packed, + callback: fn( + content: &Packed, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + region: Size, + ) -> SourceResult>, + ) -> Self { + Self::new(callbacks::InlineCallback::new(captured, callback)) + } +} + +impl Packed { + /// Layout the element. + pub fn layout( + &self, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + region: Size, + ) -> SourceResult> { + 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>, + + /// 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, + + /// The block's border color. See the + /// [rectangle's documentation]($rect.stroke) for more details. + #[resolve] + #[fold] + pub stroke: Sides>>, + + /// How much to round the block's corners. See the + /// [rectangle's documentation]($rect.radius) for more details. + #[resolve] + #[fold] + pub radius: Corners>>, + + /// How much to pad the block's content. See the + /// [box's documentation]($box.inset) for more details. + #[resolve] + #[fold] + pub inset: Sides>>, + + /// 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>>, + + /// 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, + + /// The spacing between this block and its successor. + #[parse(args.named("below")?.or(spacing))] + pub below: Smart, + + /// 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, +} + +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( + captured: Packed, + f: fn( + content: &Packed, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + region: Region, + ) -> SourceResult, + ) -> 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( + captured: Packed, + f: fn( + content: &Packed, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + regions: Regions, + ) -> SourceResult, + ) -> 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> for Sizing { + fn from(smart: Smart) -> Self { + match smart { + Smart::Auto => Self::Auto, + Smart::Custom(rel) => Self::Rel(rel), + } + } +} + +impl> From 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 => 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( + captured: Packed, + f: fn(&Packed, $($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` to `&Content`. This + // is safe because: + // - `Packed` is a transparent wrapper around + // `Content`, so for any `T` it has the same memory + // representation as `Content`. + // - While `Packed` 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` 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> + } + + callback! { + BlockSingleCallback = ( + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + region: Region, + ) -> SourceResult + } + + callback! { + BlockMultiCallback = ( + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + regions: Regions, + ) -> SourceResult + } +} 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 { + /// 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 Corners { + /// 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(self, mut f: F) -> Corners + 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(self, other: Corners) -> 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 { + [&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 Corners> { + /// Unwrap-or-default the individual corners. + pub fn unwrap_or_default(self) -> Corners + where + T: Default, + { + self.map(Option::unwrap_or_default) + } +} + +impl Get for Corners { + 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 Debug for Corners { + 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 Reflect for Corners> { + 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 IntoValue for Corners> +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| { + 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 FromValue for Corners> +where + T: FromValue + Clone, +{ + fn from_value(mut value: Value) -> HintedStrResult { + 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 Resolve for Corners { + type Output = Corners; + + fn resolve(self, styles: StyleChain) -> Self::Output { + self.map(|v| v.resolve(styles)) + } +} + +impl Fold for Corners> { + 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, 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 for Em { + type Output = Self; + + fn mul(self, other: f64) -> Self { + Self(self.0 * other) + } +} + +impl Mul for f64 { + type Output = Em; + + fn mul(self, other: Em) -> Em { + other * self + } +} + +impl Div 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>(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 for Fr { + type Output = Self; + + fn mul(self, other: f64) -> Self { + Self(self.0 * other) + } +} + +impl Mul 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 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>(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); + +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) -> 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 { + 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 { + self.0.iter() + } + + /// Iterate over the contained frames. + pub fn iter_mut(&mut self) -> std::slice::IterMut { + 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; + + 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, + /// The items composing this layout. + items: Arc>>, + /// 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(&mut self, items: I) + where + I: IntoIterator, + { + 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(&mut self, items: I) + where + I: IntoIterator, + { + 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) -> 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) { + 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(&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, + /// The group's label. + pub label: Option