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}