summaryrefslogtreecommitdiff
path: root/crates/typst-library/src/layout/length.rs
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2024-10-27 19:04:55 +0100
committerGitHub <noreply@github.com>2024-10-27 18:04:55 +0000
commitbe7cfc85d08c545abfac08098b7b33b4bd71f37e (patch)
treef4137fa2aaa57babae1f7603a9b2ed7e688f43d8 /crates/typst-library/src/layout/length.rs
parentb8034a343831e8609aec2ec81eb7eeda57aa5d81 (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.rs276
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
+ }
+}