typst_library/layout/
spacing.rs

1use typst_utils::Numeric;
2
3use crate::foundations::{cast, elem, Content};
4use crate::layout::{Abs, Em, Fr, Length, Ratio, Rel};
5
6/// Inserts horizontal spacing into a paragraph.
7///
8/// The spacing can be absolute, relative, or fractional. In the last case, the
9/// remaining space on the line is distributed among all fractional spacings
10/// according to their relative fractions.
11///
12/// # Example
13/// ```example
14/// First #h(1cm) Second \
15/// First #h(30%) Second
16/// ```
17///
18/// # Fractional spacing
19/// With fractional spacing, you can align things within a line without forcing
20/// a paragraph break (like [`align`] would). Each fractionally sized element
21/// gets space based on the ratio of its fraction to the sum of all fractions.
22///
23/// ```example
24/// First #h(1fr) Second \
25/// First #h(1fr) Second #h(1fr) Third \
26/// First #h(2fr) Second #h(1fr) Third
27/// ```
28///
29/// # Mathematical Spacing { #math-spacing }
30/// In [mathematical formulas]($category/math), you can additionally use these
31/// constants to add spacing between elements: `thin` (1/6 em), `med` (2/9 em),
32/// `thick` (5/18 em), `quad` (1 em), `wide` (2 em).
33#[elem(title = "Spacing (H)")]
34pub struct HElem {
35    /// How much spacing to insert.
36    #[required]
37    pub amount: Spacing,
38
39    /// If `{true}`, the spacing collapses at the start or end of a paragraph.
40    /// Moreover, from multiple adjacent weak spacings all but the largest one
41    /// collapse.
42    ///
43    /// Weak spacing in markup also causes all adjacent markup spaces to be
44    /// removed, regardless of the amount of spacing inserted. To force a space
45    /// next to weak spacing, you can explicitly write `[#" "]` (for a normal
46    /// space) or `[~]` (for a non-breaking space). The latter can be useful to
47    /// create a construct that always attaches to the preceding word with one
48    /// non-breaking space, independently of whether a markup space existed in
49    /// front or not.
50    ///
51    /// ```example
52    /// #h(1cm, weak: true)
53    /// We identified a group of _weak_
54    /// specimens that fail to manifest
55    /// in most cases. However, when
56    /// #h(8pt, weak: true) supported
57    /// #h(8pt, weak: true) on both sides,
58    /// they do show up.
59    ///
60    /// Further #h(0pt, weak: true) more,
61    /// even the smallest of them swallow
62    /// adjacent markup spaces.
63    /// ```
64    #[default(false)]
65    pub weak: bool,
66}
67
68impl HElem {
69    /// Zero-width horizontal weak spacing that eats surrounding spaces.
70    pub fn hole() -> Self {
71        Self::new(Abs::zero().into()).with_weak(true)
72    }
73}
74
75/// Inserts vertical spacing into a flow of blocks.
76///
77/// The spacing can be absolute, relative, or fractional. In the last case,
78/// the remaining space on the page is distributed among all fractional spacings
79/// according to their relative fractions.
80///
81/// # Example
82/// ```example
83/// #grid(
84///   rows: 3cm,
85///   columns: 6,
86///   gutter: 1fr,
87///   [A #parbreak() B],
88///   [A #v(0pt) B],
89///   [A #v(10pt) B],
90///   [A #v(0pt, weak: true) B],
91///   [A #v(40%, weak: true) B],
92///   [A #v(1fr) B],
93/// )
94/// ```
95#[elem(title = "Spacing (V)")]
96pub struct VElem {
97    /// How much spacing to insert.
98    #[required]
99    pub amount: Spacing,
100
101    /// If `{true}`, the spacing collapses at the start or end of a flow.
102    /// Moreover, from multiple adjacent weak spacings all but the largest one
103    /// collapse. Weak spacings will always collapse adjacent paragraph spacing,
104    /// even if the paragraph spacing is larger.
105    ///
106    /// ```example
107    /// The following theorem is
108    /// foundational to the field:
109    /// #v(4pt, weak: true)
110    /// $ x^2 + y^2 = r^2 $
111    /// #v(4pt, weak: true)
112    /// The proof is simple:
113    /// ```
114    pub weak: bool,
115
116    /// Whether the spacing collapses if not immediately preceded by a
117    /// paragraph.
118    #[internal]
119    #[parse(Some(false))]
120    pub attach: bool,
121}
122
123cast! {
124    VElem,
125    v: Content => v.unpack::<Self>().map_err(|_| "expected `v` element")?,
126}
127
128/// Kinds of spacing.
129#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
130pub enum Spacing {
131    /// Spacing specified in absolute terms and relative to the parent's size.
132    Rel(Rel<Length>),
133    /// Spacing specified as a fraction of the remaining free space in the
134    /// parent.
135    Fr(Fr),
136}
137
138impl Spacing {
139    /// Whether this is fractional spacing.
140    pub fn is_fractional(self) -> bool {
141        matches!(self, Self::Fr(_))
142    }
143
144    /// Whether the spacing is actually no spacing.
145    pub fn is_zero(&self) -> bool {
146        match self {
147            Self::Rel(rel) => rel.is_zero(),
148            Self::Fr(fr) => fr.is_zero(),
149        }
150    }
151}
152
153impl From<Abs> for Spacing {
154    fn from(abs: Abs) -> Self {
155        Self::Rel(abs.into())
156    }
157}
158
159impl From<Em> for Spacing {
160    fn from(em: Em) -> Self {
161        Self::Rel(Rel::new(Ratio::zero(), em.into()))
162    }
163}
164
165impl From<Length> for Spacing {
166    fn from(length: Length) -> Self {
167        Self::Rel(length.into())
168    }
169}
170
171impl From<Fr> for Spacing {
172    fn from(fr: Fr) -> Self {
173        Self::Fr(fr)
174    }
175}
176
177cast! {
178    Spacing,
179    self => match self {
180        Self::Rel(rel) => {
181            if rel.rel.is_zero() {
182                rel.abs.into_value()
183            } else if rel.abs.is_zero() {
184                rel.rel.into_value()
185            } else {
186                rel.into_value()
187            }
188        }
189        Self::Fr(fr) => fr.into_value(),
190    },
191    v: Rel<Length> => Self::Rel(v),
192    v: Fr => Self::Fr(v),
193}