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};
8use crate::text::{is_default_ignorable, Font, Lang, Region};
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
40impl Debug for TextItem {
41 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
42 f.write_str("Text(")?;
43 self.text.fmt(f)?;
44 f.write_str(")")
45 }
46}
47
48/// A glyph in a run of shaped text.
49#[derive(Debug, Clone, Eq, PartialEq, Hash)]
50pub struct Glyph {
51 /// The glyph's index in the font.
52 pub id: u16,
53 /// The advance width of the glyph.
54 pub x_advance: Em,
55 /// The horizontal offset of the glyph.
56 pub x_offset: Em,
57 /// The range of the glyph in its item's text. The range's length may
58 /// be more than one due to multi-byte UTF-8 encoding or ligatures.
59 pub range: Range<u16>,
60 /// The source code location of the text.
61 pub span: (Span, u16),
62}
63
64impl Glyph {
65 /// The range of the glyph in its item's text.
66 pub fn range(&self) -> Range<usize> {
67 usize::from(self.range.start)..usize::from(self.range.end)
68 }
69}
70
71/// A slice of a [`TextItem`].
72pub struct TextItemView<'a> {
73 /// The whole item this is a part of
74 pub item: &'a TextItem,
75 /// The glyphs of this slice
76 pub glyph_range: Range<usize>,
77}
78
79impl<'a> TextItemView<'a> {
80 /// Build a TextItemView for the whole contents of a TextItem.
81 pub fn full(text: &'a TextItem) -> Self {
82 Self::from_glyph_range(text, 0..text.glyphs.len())
83 }
84
85 /// Build a new [`TextItemView`] from a [`TextItem`] and a range of glyphs.
86 pub fn from_glyph_range(text: &'a TextItem, glyph_range: Range<usize>) -> Self {
87 TextItemView { item: text, glyph_range }
88 }
89
90 /// Returns an iterator over the glyphs of the slice.
91 ///
92 /// Note that the ranges are not remapped. They still point into the
93 /// original text.
94 pub fn glyphs(&self) -> &[Glyph] {
95 &self.item.glyphs[self.glyph_range.clone()]
96 }
97
98 /// The plain text for the given glyph from `glyphs()`. This is an
99 /// approximation since glyphs do not correspond 1-1 with codepoints.
100 pub fn glyph_text(&self, glyph: &Glyph) -> EcoString {
101 // Trim default ignorables which might have ended up in the glyph's
102 // cluster. Keep interior ones so that joined emojis work. All of this
103 // is a hack and needs to be reworked. See
104 // https://github.com/typst/typst/pull/5099
105 self.item.text[glyph.range()]
106 .trim_matches(is_default_ignorable)
107 .into()
108 }
109
110 /// The total width of this text slice
111 pub fn width(&self) -> Abs {
112 self.glyphs()
113 .iter()
114 .map(|g| g.x_advance)
115 .sum::<Em>()
116 .at(self.item.size)
117 }
118}