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/length.rs | |
| parent | b8034a343831e8609aec2ec81eb7eeda57aa5d81 (diff) | |
Split out four new crates (#5302)
Diffstat (limited to 'crates/typst-library/src/layout/length.rs')
| -rw-r--r-- | crates/typst-library/src/layout/length.rs | 276 |
1 files changed, 276 insertions, 0 deletions
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 + } +} |
