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