Skip to main content

typst_library/layout/
length.rs

1use std::cmp::Ordering;
2use std::fmt::{self, Debug, Formatter};
3use std::ops::{Add, Div, Mul, Neg};
4
5use comemo::Tracked;
6use ecow::{EcoString, eco_format};
7use typst_syntax::Span;
8use typst_utils::{Numeric, NumericLength};
9
10use crate::diag::{HintedStrResult, SourceResult, bail};
11use crate::foundations::{Context, Fold, Repr, Resolve, StyleChain, func, scope, ty};
12use crate::layout::{Abs, Em};
13
14/// A size or distance, possibly expressed with contextual units.
15///
16/// Typst supports the following length units:
17///
18/// - Points: `{72pt}`
19/// - Millimeters: `{254mm}`
20/// - Centimeters: `{2.54cm}`
21/// - Inches: `{1in}`
22/// - Relative to font size: `{2.5em}`
23///
24/// You can multiply lengths with and divide them by integers and floats.
25///
26/// = Example <example>
27/// ```example
28/// #rect(width: 20pt)
29/// #rect(width: 2em)
30/// #rect(width: 1in)
31///
32/// #(3em + 5pt).em \
33/// #(20pt).em \
34/// #(40em + 2pt).abs \
35/// #(5em).abs
36/// ```
37///
38/// = Fields <fields>
39/// - `abs`: A length with just the absolute component of the current length
40///   (that is, excluding the `em` component).
41/// - `em`: The amount of `em` units in this length, as a @float[float].
42#[ty(scope, cast)]
43#[derive(Default, Copy, Clone, Eq, PartialEq, Hash)]
44pub struct Length {
45    /// The absolute part.
46    pub abs: Abs,
47    /// The font-relative part.
48    pub em: Em,
49}
50
51impl Length {
52    /// The zero length.
53    pub const fn zero() -> Self {
54        Self { abs: Abs::zero(), em: Em::zero() }
55    }
56
57    /// Try to compute the absolute value of the length.
58    pub fn try_abs(self) -> Option<Self> {
59        (self.abs.is_zero() || self.em.is_zero())
60            .then(|| Self { abs: self.abs.abs(), em: self.em.abs() })
61    }
62
63    /// Try to divide two lengths.
64    pub fn try_div(self, other: Self) -> Option<f64> {
65        if self.abs.is_zero() && other.abs.is_zero() {
66            Some(self.em / other.em)
67        } else if self.em.is_zero() && other.em.is_zero() {
68            Some(self.abs / other.abs)
69        } else {
70            None
71        }
72    }
73
74    /// Convert to an absolute length at the given font size.
75    pub fn at(self, font_size: Abs) -> Abs {
76        self.abs + self.em.at(font_size)
77    }
78
79    /// Fails with an error if the length has a non-zero font-relative part.
80    fn ensure_that_em_is_zero(&self, span: Span, unit: &str) -> SourceResult<()> {
81        if self.em == Em::zero() {
82            return Ok(());
83        }
84
85        bail!(
86            span,
87            "cannot convert a length with non-zero em units (`{}`) to {unit}",
88            self.repr();
89            hint: "use `length.to-absolute()` to resolve its em component \
90                   (requires context)";
91            hint: "or use `length.abs.{unit}()` instead to ignore its em component";
92        )
93    }
94}
95
96#[scope]
97impl Length {
98    /// Converts this length to points.
99    ///
100    /// Fails with an error if this length has non-zero `em` units (such as
101    /// `5em + 2pt` instead of just `2pt`). Use the `abs` field (such as in
102    /// `(5em + 2pt).abs.pt()`) to ignore the `em` component of the length (thus
103    /// converting only its absolute component).
104    #[func(name = "pt", title = "Points")]
105    pub fn to_pt(&self, span: Span) -> SourceResult<f64> {
106        self.ensure_that_em_is_zero(span, "pt")?;
107        Ok(self.abs.to_pt())
108    }
109
110    /// Converts this length to millimeters.
111    ///
112    /// Fails with an error if this length has non-zero `em` units. See the
113    /// @length.pt[`pt`] method for more details.
114    #[func(name = "mm", title = "Millimeters")]
115    pub fn to_mm(&self, span: Span) -> SourceResult<f64> {
116        self.ensure_that_em_is_zero(span, "mm")?;
117        Ok(self.abs.to_mm())
118    }
119
120    /// Converts this length to centimeters.
121    ///
122    /// Fails with an error if this length has non-zero `em` units. See the
123    /// @length.pt[`pt`] method for more details.
124    #[func(name = "cm", title = "Centimeters")]
125    pub fn to_cm(&self, span: Span) -> SourceResult<f64> {
126        self.ensure_that_em_is_zero(span, "cm")?;
127        Ok(self.abs.to_cm())
128    }
129
130    /// Converts this length to inches.
131    ///
132    /// Fails with an error if this length has non-zero `em` units. See the
133    /// @length.pt[`pt`] method for more details.
134    #[func(name = "inches")]
135    pub fn to_inches(&self, span: Span) -> SourceResult<f64> {
136        self.ensure_that_em_is_zero(span, "inches")?;
137        Ok(self.abs.to_inches())
138    }
139
140    /// Resolve this length to an absolute length.
141    ///
142    /// ```example
143    /// #set text(size: 12pt)
144    /// #context [
145    ///   #(6pt).to-absolute() \
146    ///   #(6pt + 10em).to-absolute() \
147    ///   #(10em).to-absolute()
148    /// ]
149    ///
150    /// #set text(size: 6pt)
151    /// #context [
152    ///   #(6pt).to-absolute() \
153    ///   #(6pt + 10em).to-absolute() \
154    ///   #(10em).to-absolute()
155    /// ]
156    /// ```
157    #[func]
158    pub fn to_absolute(&self, context: Tracked<Context>) -> HintedStrResult<Length> {
159        Ok(self.resolve(context.styles()?).into())
160    }
161}
162
163impl Debug for Length {
164    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
165        match (self.abs.is_zero(), self.em.is_zero()) {
166            (false, false) => write!(f, "{:?} + {:?}", self.abs, self.em),
167            (true, false) => self.em.fmt(f),
168            (_, true) => self.abs.fmt(f),
169        }
170    }
171}
172
173impl Repr for Length {
174    fn repr(&self) -> EcoString {
175        match (self.abs.is_zero(), self.em.is_zero()) {
176            (false, false) => eco_format!("{} + {}", self.abs.repr(), self.em.repr()),
177            (true, false) => self.em.repr(),
178            (_, true) => self.abs.repr(),
179        }
180    }
181}
182
183impl NumericLength for Length {}
184
185impl Numeric for Length {
186    fn zero() -> Self {
187        Self::zero()
188    }
189
190    fn is_finite(self) -> bool {
191        self.abs.is_finite() && self.em.is_finite()
192    }
193}
194
195impl PartialOrd for Length {
196    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
197        if self.em.is_zero() && other.em.is_zero() {
198            self.abs.partial_cmp(&other.abs)
199        } else if self.abs.is_zero() && other.abs.is_zero() {
200            self.em.partial_cmp(&other.em)
201        } else {
202            None
203        }
204    }
205}
206
207impl From<Abs> for Length {
208    fn from(abs: Abs) -> Self {
209        Self { abs, em: Em::zero() }
210    }
211}
212
213impl From<Em> for Length {
214    fn from(em: Em) -> Self {
215        Self { abs: Abs::zero(), em }
216    }
217}
218
219impl Neg for Length {
220    type Output = Self;
221
222    fn neg(self) -> Self::Output {
223        Self { abs: -self.abs, em: -self.em }
224    }
225}
226
227impl Add for Length {
228    type Output = Self;
229
230    fn add(self, rhs: Self) -> Self::Output {
231        Self { abs: self.abs + rhs.abs, em: self.em + rhs.em }
232    }
233}
234
235typst_utils::sub_impl!(Length - Length -> Length);
236
237impl Mul<f64> for Length {
238    type Output = Self;
239
240    fn mul(self, rhs: f64) -> Self::Output {
241        Self { abs: self.abs * rhs, em: self.em * rhs }
242    }
243}
244
245impl Mul<Length> for f64 {
246    type Output = Length;
247
248    fn mul(self, rhs: Length) -> Self::Output {
249        rhs * self
250    }
251}
252
253impl Div<f64> for Length {
254    type Output = Self;
255
256    fn div(self, rhs: f64) -> Self::Output {
257        Self { abs: self.abs / rhs, em: self.em / rhs }
258    }
259}
260
261typst_utils::assign_impl!(Length += Length);
262typst_utils::assign_impl!(Length -= Length);
263typst_utils::assign_impl!(Length *= f64);
264typst_utils::assign_impl!(Length /= f64);
265
266impl Resolve for Length {
267    type Output = Abs;
268
269    fn resolve(self, styles: StyleChain) -> Self::Output {
270        self.abs + self.em.resolve(styles)
271    }
272}
273
274impl Fold for Length {
275    fn fold(self, _: Self) -> Self {
276        self
277    }
278}