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}