typst_library/text/deco.rs
1use smallvec::smallvec;
2
3use crate::diag::SourceResult;
4use crate::engine::Engine;
5use crate::foundations::{elem, Content, Packed, Show, Smart, StyleChain};
6use crate::layout::{Abs, Corners, Length, Rel, Sides};
7use crate::text::{BottomEdge, BottomEdgeMetric, TextElem, TopEdge, TopEdgeMetric};
8use crate::visualize::{Color, FixedStroke, Paint, Stroke};
9
10/// Underlines text.
11///
12/// # Example
13/// ```example
14/// This is #underline[important].
15/// ```
16#[elem(Show)]
17pub struct UnderlineElem {
18 /// How to [stroke] the line.
19 ///
20 /// If set to `{auto}`, takes on the text's color and a thickness defined in
21 /// the current font.
22 ///
23 /// ```example
24 /// Take #underline(
25 /// stroke: 1.5pt + red,
26 /// offset: 2pt,
27 /// [care],
28 /// )
29 /// ```
30 #[resolve]
31 #[fold]
32 pub stroke: Smart<Stroke>,
33
34 /// The position of the line relative to the baseline, read from the font
35 /// tables if `{auto}`.
36 ///
37 /// ```example
38 /// #underline(offset: 5pt)[
39 /// The Tale Of A Faraway Line I
40 /// ]
41 /// ```
42 #[resolve]
43 pub offset: Smart<Length>,
44
45 /// The amount by which to extend the line beyond (or within if negative)
46 /// the content.
47 ///
48 /// ```example
49 /// #align(center,
50 /// underline(extent: 2pt)[Chapter 1]
51 /// )
52 /// ```
53 #[resolve]
54 pub extent: Length,
55
56 /// Whether the line skips sections in which it would collide with the
57 /// glyphs.
58 ///
59 /// ```example
60 /// This #underline(evade: true)[is great].
61 /// This #underline(evade: false)[is less great].
62 /// ```
63 #[default(true)]
64 pub evade: bool,
65
66 /// Whether the line is placed behind the content it underlines.
67 ///
68 /// ```example
69 /// #set underline(stroke: (thickness: 1em, paint: maroon, cap: "round"))
70 /// #underline(background: true)[This is stylized.] \
71 /// #underline(background: false)[This is partially hidden.]
72 /// ```
73 #[default(false)]
74 pub background: bool,
75
76 /// The content to underline.
77 #[required]
78 pub body: Content,
79}
80
81impl Show for Packed<UnderlineElem> {
82 #[typst_macros::time(name = "underline", span = self.span())]
83 fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
84 Ok(self.body.clone().styled(TextElem::set_deco(smallvec![Decoration {
85 line: DecoLine::Underline {
86 stroke: self.stroke(styles).unwrap_or_default(),
87 offset: self.offset(styles),
88 evade: self.evade(styles),
89 background: self.background(styles),
90 },
91 extent: self.extent(styles),
92 }])))
93 }
94}
95
96/// Adds a line over text.
97///
98/// # Example
99/// ```example
100/// #overline[A line over text.]
101/// ```
102#[elem(Show)]
103pub struct OverlineElem {
104 /// How to [stroke] the line.
105 ///
106 /// If set to `{auto}`, takes on the text's color and a thickness defined in
107 /// the current font.
108 ///
109 /// ```example
110 /// #set text(fill: olive)
111 /// #overline(
112 /// stroke: green.darken(20%),
113 /// offset: -12pt,
114 /// [The Forest Theme],
115 /// )
116 /// ```
117 #[resolve]
118 #[fold]
119 pub stroke: Smart<Stroke>,
120
121 /// The position of the line relative to the baseline. Read from the font
122 /// tables if `{auto}`.
123 ///
124 /// ```example
125 /// #overline(offset: -1.2em)[
126 /// The Tale Of A Faraway Line II
127 /// ]
128 /// ```
129 #[resolve]
130 pub offset: Smart<Length>,
131
132 /// The amount by which to extend the line beyond (or within if negative)
133 /// the content.
134 ///
135 /// ```example
136 /// #set overline(extent: 4pt)
137 /// #set underline(extent: 4pt)
138 /// #overline(underline[Typography Today])
139 /// ```
140 #[resolve]
141 pub extent: Length,
142
143 /// Whether the line skips sections in which it would collide with the
144 /// glyphs.
145 ///
146 /// ```example
147 /// #overline(
148 /// evade: false,
149 /// offset: -7.5pt,
150 /// stroke: 1pt,
151 /// extent: 3pt,
152 /// [Temple],
153 /// )
154 /// ```
155 #[default(true)]
156 pub evade: bool,
157
158 /// Whether the line is placed behind the content it overlines.
159 ///
160 /// ```example
161 /// #set overline(stroke: (thickness: 1em, paint: maroon, cap: "round"))
162 /// #overline(background: true)[This is stylized.] \
163 /// #overline(background: false)[This is partially hidden.]
164 /// ```
165 #[default(false)]
166 pub background: bool,
167
168 /// The content to add a line over.
169 #[required]
170 pub body: Content,
171}
172
173impl Show for Packed<OverlineElem> {
174 #[typst_macros::time(name = "overline", span = self.span())]
175 fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
176 Ok(self.body.clone().styled(TextElem::set_deco(smallvec![Decoration {
177 line: DecoLine::Overline {
178 stroke: self.stroke(styles).unwrap_or_default(),
179 offset: self.offset(styles),
180 evade: self.evade(styles),
181 background: self.background(styles),
182 },
183 extent: self.extent(styles),
184 }])))
185 }
186}
187
188/// Strikes through text.
189///
190/// # Example
191/// ```example
192/// This is #strike[not] relevant.
193/// ```
194#[elem(title = "Strikethrough", Show)]
195pub struct StrikeElem {
196 /// How to [stroke] the line.
197 ///
198 /// If set to `{auto}`, takes on the text's color and a thickness defined in
199 /// the current font.
200 ///
201 /// _Note:_ Please don't use this for real redaction as you can still copy
202 /// paste the text.
203 ///
204 /// ```example
205 /// This is #strike(stroke: 1.5pt + red)[very stricken through]. \
206 /// This is #strike(stroke: 10pt)[redacted].
207 /// ```
208 #[resolve]
209 #[fold]
210 pub stroke: Smart<Stroke>,
211
212 /// The position of the line relative to the baseline. Read from the font
213 /// tables if `{auto}`.
214 ///
215 /// This is useful if you are unhappy with the offset your font provides.
216 ///
217 /// ```example
218 /// #set text(font: "Inria Serif")
219 /// This is #strike(offset: auto)[low-ish]. \
220 /// This is #strike(offset: -3.5pt)[on-top].
221 /// ```
222 #[resolve]
223 pub offset: Smart<Length>,
224
225 /// The amount by which to extend the line beyond (or within if negative)
226 /// the content.
227 ///
228 /// ```example
229 /// This #strike(extent: -2pt)[skips] parts of the word.
230 /// This #strike(extent: 2pt)[extends] beyond the word.
231 /// ```
232 #[resolve]
233 pub extent: Length,
234
235 /// Whether the line is placed behind the content.
236 ///
237 /// ```example
238 /// #set strike(stroke: red)
239 /// #strike(background: true)[This is behind.] \
240 /// #strike(background: false)[This is in front.]
241 /// ```
242 #[default(false)]
243 pub background: bool,
244
245 /// The content to strike through.
246 #[required]
247 pub body: Content,
248}
249
250impl Show for Packed<StrikeElem> {
251 #[typst_macros::time(name = "strike", span = self.span())]
252 fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
253 Ok(self.body.clone().styled(TextElem::set_deco(smallvec![Decoration {
254 // Note that we do not support evade option for strikethrough.
255 line: DecoLine::Strikethrough {
256 stroke: self.stroke(styles).unwrap_or_default(),
257 offset: self.offset(styles),
258 background: self.background(styles),
259 },
260 extent: self.extent(styles),
261 }])))
262 }
263}
264
265/// Highlights text with a background color.
266///
267/// # Example
268/// ```example
269/// This is #highlight[important].
270/// ```
271#[elem(Show)]
272pub struct HighlightElem {
273 /// The color to highlight the text with.
274 ///
275 /// ```example
276 /// This is #highlight(
277 /// fill: blue
278 /// )[highlighted with blue].
279 /// ```
280 #[default(Some(Color::from_u8(0xFF, 0xFD, 0x11, 0xA1).into()))]
281 pub fill: Option<Paint>,
282
283 /// The highlight's border color. See the
284 /// [rectangle's documentation]($rect.stroke) for more details.
285 ///
286 /// ```example
287 /// This is a #highlight(
288 /// stroke: fuchsia
289 /// )[stroked highlighting].
290 /// ```
291 #[resolve]
292 #[fold]
293 pub stroke: Sides<Option<Option<Stroke>>>,
294
295 /// The top end of the background rectangle.
296 ///
297 /// ```example
298 /// #set highlight(top-edge: "ascender")
299 /// #highlight[a] #highlight[aib]
300 ///
301 /// #set highlight(top-edge: "x-height")
302 /// #highlight[a] #highlight[aib]
303 /// ```
304 #[default(TopEdge::Metric(TopEdgeMetric::Ascender))]
305 pub top_edge: TopEdge,
306
307 /// The bottom end of the background rectangle.
308 ///
309 /// ```example
310 /// #set highlight(bottom-edge: "descender")
311 /// #highlight[a] #highlight[ap]
312 ///
313 /// #set highlight(bottom-edge: "baseline")
314 /// #highlight[a] #highlight[ap]
315 /// ```
316 #[default(BottomEdge::Metric(BottomEdgeMetric::Descender))]
317 pub bottom_edge: BottomEdge,
318
319 /// The amount by which to extend the background to the sides beyond
320 /// (or within if negative) the content.
321 ///
322 /// ```example
323 /// A long #highlight(extent: 4pt)[background].
324 /// ```
325 #[resolve]
326 pub extent: Length,
327
328 /// How much to round the highlight's corners. See the
329 /// [rectangle's documentation]($rect.radius) for more details.
330 ///
331 /// ```example
332 /// Listen #highlight(
333 /// radius: 5pt, extent: 2pt
334 /// )[carefully], it will be on the test.
335 /// ```
336 #[resolve]
337 #[fold]
338 pub radius: Corners<Option<Rel<Length>>>,
339
340 /// The content that should be highlighted.
341 #[required]
342 pub body: Content,
343}
344
345impl Show for Packed<HighlightElem> {
346 #[typst_macros::time(name = "highlight", span = self.span())]
347 fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
348 Ok(self.body.clone().styled(TextElem::set_deco(smallvec![Decoration {
349 line: DecoLine::Highlight {
350 fill: self.fill(styles),
351 stroke: self
352 .stroke(styles)
353 .unwrap_or_default()
354 .map(|stroke| stroke.map(Stroke::unwrap_or_default)),
355 top_edge: self.top_edge(styles),
356 bottom_edge: self.bottom_edge(styles),
357 radius: self.radius(styles).unwrap_or_default(),
358 },
359 extent: self.extent(styles),
360 }])))
361 }
362}
363
364/// A text decoration.
365///
366/// Can be positioned over, under, or on top of text, or highlight the text with
367/// a background.
368#[derive(Debug, Clone, Eq, PartialEq, Hash)]
369pub struct Decoration {
370 pub line: DecoLine,
371 pub extent: Abs,
372}
373
374/// A kind of decorative line.
375#[derive(Debug, Clone, Eq, PartialEq, Hash)]
376pub enum DecoLine {
377 Underline {
378 stroke: Stroke<Abs>,
379 offset: Smart<Abs>,
380 evade: bool,
381 background: bool,
382 },
383 Strikethrough {
384 stroke: Stroke<Abs>,
385 offset: Smart<Abs>,
386 background: bool,
387 },
388 Overline {
389 stroke: Stroke<Abs>,
390 offset: Smart<Abs>,
391 evade: bool,
392 background: bool,
393 },
394 Highlight {
395 fill: Option<Paint>,
396 stroke: Sides<Option<FixedStroke>>,
397 top_edge: TopEdge,
398 bottom_edge: BottomEdge,
399 radius: Corners<Rel<Abs>>,
400 },
401}