typst_library/text/
item.rs

1use std::fmt::{self, Debug, Formatter};
2use std::ops::Range;
3
4use ecow::EcoString;
5use typst_syntax::Span;
6
7use crate::layout::{Abs, Em, Point, Rect};
8use crate::text::{Font, Lang, Region, is_default_ignorable};
9use crate::visualize::{FixedStroke, Paint};
10
11/// A run of shaped text.
12#[derive(Clone, Eq, PartialEq, Hash)]
13pub struct TextItem {
14    /// The font the glyphs are contained in.
15    pub font: Font,
16    /// The font size.
17    pub size: Abs,
18    /// Glyph color.
19    pub fill: Paint,
20    /// Glyph stroke.
21    pub stroke: Option<FixedStroke>,
22    /// The natural language of the text.
23    pub lang: Lang,
24    /// The region of the text.
25    pub region: Option<Region>,
26    /// The item's plain text.
27    pub text: EcoString,
28    /// The glyphs. The number of glyphs may be different from the number of
29    /// characters in the plain text due to e.g. ligatures.
30    pub glyphs: Vec<Glyph>,
31}
32
33impl TextItem {
34    /// The width of the text run.
35    pub fn width(&self) -> Abs {
36        self.glyphs.iter().map(|g| g.x_advance).sum::<Em>().at(self.size)
37    }
38
39    /// The height of the text run.
40    pub fn height(&self) -> Abs {
41        self.glyphs.iter().map(|g| g.y_advance).sum::<Em>().at(self.size)
42    }
43
44    /// The bounding box of the text run.
45    #[comemo::memoize]
46    pub fn bbox(&self) -> Rect {
47        let mut min = Point::splat(Abs::inf());
48        let mut max = Point::splat(-Abs::inf());
49        let mut cursor = Point::zero();
50
51        for glyph in self.glyphs.iter() {
52            let advance =
53                Point::new(glyph.x_advance.at(self.size), glyph.y_advance.at(self.size));
54            let offset =
55                Point::new(glyph.x_offset.at(self.size), glyph.y_offset.at(self.size));
56            if let Some(rect) =
57                self.font.ttf().glyph_bounding_box(ttf_parser::GlyphId(glyph.id))
58            {
59                let pos = cursor + offset;
60                let a = pos
61                    + Point::new(
62                        self.font.to_em(rect.x_min).at(self.size),
63                        self.font.to_em(rect.y_min).at(self.size),
64                    );
65                let b = pos
66                    + Point::new(
67                        self.font.to_em(rect.x_max).at(self.size),
68                        self.font.to_em(rect.y_max).at(self.size),
69                    );
70                min = min.min(a).min(b);
71                max = max.max(a).max(b);
72            }
73            cursor += advance;
74        }
75
76        // Text runs use a y-up coordinate system, in contrast to the default
77        // frame orientation.
78        min.y *= -1.0;
79        max.y *= -1.0;
80        Rect::new(min, max)
81    }
82}
83
84impl Debug for TextItem {
85    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
86        f.write_str("Text(")?;
87        self.text.fmt(f)?;
88        f.write_str(")")
89    }
90}
91
92/// A glyph in a run of shaped text.
93#[derive(Debug, Clone, Eq, PartialEq, Hash)]
94pub struct Glyph {
95    /// The glyph's index in the font.
96    pub id: u16,
97    /// The advance width of the glyph.
98    pub x_advance: Em,
99    /// The horizontal offset of the glyph.
100    pub x_offset: Em,
101    /// The advance height (Y-up) of the glyph.
102    pub y_advance: Em,
103    /// The vertical offset (Y-up) of the glyph.
104    pub y_offset: Em,
105    /// The range of the glyph in its item's text. The range's length may
106    /// be more than one due to multi-byte UTF-8 encoding or ligatures.
107    pub range: Range<u16>,
108    /// The source code location of the text.
109    pub span: (Span, u16),
110}
111
112impl Glyph {
113    /// The range of the glyph in its item's text.
114    pub fn range(&self) -> Range<usize> {
115        usize::from(self.range.start)..usize::from(self.range.end)
116    }
117}
118
119/// A slice of a [`TextItem`].
120pub struct TextItemView<'a> {
121    /// The whole item this is a part of
122    pub item: &'a TextItem,
123    /// The glyphs of this slice
124    pub glyph_range: Range<usize>,
125}
126
127impl<'a> TextItemView<'a> {
128    /// Build a TextItemView for the whole contents of a TextItem.
129    pub fn full(text: &'a TextItem) -> Self {
130        Self::from_glyph_range(text, 0..text.glyphs.len())
131    }
132
133    /// Build a new [`TextItemView`] from a [`TextItem`] and a range of glyphs.
134    pub fn from_glyph_range(text: &'a TextItem, glyph_range: Range<usize>) -> Self {
135        TextItemView { item: text, glyph_range }
136    }
137
138    /// Returns an iterator over the glyphs of the slice.
139    ///
140    /// Note that the ranges are not remapped. They still point into the
141    /// original text.
142    pub fn glyphs(&self) -> &[Glyph] {
143        &self.item.glyphs[self.glyph_range.clone()]
144    }
145
146    /// The plain text for the given glyph from `glyphs()`. This is an
147    /// approximation since glyphs do not correspond 1-1 with codepoints.
148    pub fn glyph_text(&self, glyph: &Glyph) -> EcoString {
149        // Trim default ignorables which might have ended up in the glyph's
150        // cluster. Keep interior ones so that joined emojis work. All of this
151        // is a hack and needs to be reworked. See
152        // https://github.com/typst/typst/pull/5099
153        self.item.text[glyph.range()]
154            .trim_matches(is_default_ignorable)
155            .into()
156    }
157
158    /// The total width of this text slice
159    pub fn width(&self) -> Abs {
160        self.glyphs()
161            .iter()
162            .map(|g| g.x_advance)
163            .sum::<Em>()
164            .at(self.item.size)
165    }
166
167    /// The total height of this text slice
168    pub fn height(&self) -> Abs {
169        self.glyphs()
170            .iter()
171            .map(|g| g.y_advance)
172            .sum::<Em>()
173            .at(self.item.size)
174    }
175}