summaryrefslogtreecommitdiff
path: root/crates/typst-library/src/text/item.rs
blob: 518b082b9071231303137135fd8976641e58d1bf (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
use std::fmt::{self, Debug, Formatter};
use std::ops::Range;

use ecow::EcoString;
use typst_syntax::Span;

use crate::layout::{Abs, Em};
use crate::text::{is_default_ignorable, Font, Lang, Region};
use crate::visualize::{FixedStroke, Paint};

/// A run of shaped text.
#[derive(Clone, Eq, PartialEq, Hash)]
pub struct TextItem {
    /// The font the glyphs are contained in.
    pub font: Font,
    /// The font size.
    pub size: Abs,
    /// Glyph color.
    pub fill: Paint,
    /// Glyph stroke.
    pub stroke: Option<FixedStroke>,
    /// The natural language of the text.
    pub lang: Lang,
    /// The region of the text.
    pub region: Option<Region>,
    /// The item's plain text.
    pub text: EcoString,
    /// The glyphs. The number of glyphs may be different from the number of
    /// characters in the plain text due to e.g. ligatures.
    pub glyphs: Vec<Glyph>,
}

impl TextItem {
    /// The width of the text run.
    pub fn width(&self) -> Abs {
        self.glyphs.iter().map(|g| g.x_advance).sum::<Em>().at(self.size)
    }

    /// The height of the text run.
    pub fn height(&self) -> Abs {
        self.glyphs.iter().map(|g| g.y_advance).sum::<Em>().at(self.size)
    }
}

impl Debug for TextItem {
    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
        f.write_str("Text(")?;
        self.text.fmt(f)?;
        f.write_str(")")
    }
}

/// A glyph in a run of shaped text.
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct Glyph {
    /// The glyph's index in the font.
    pub id: u16,
    /// The advance width of the glyph.
    pub x_advance: Em,
    /// The horizontal offset of the glyph.
    pub x_offset: Em,
    /// The advance height (Y-up) of the glyph.
    pub y_advance: Em,
    /// The vertical offset (Y-up) of the glyph.
    pub y_offset: Em,
    /// The range of the glyph in its item's text. The range's length may
    /// be more than one due to multi-byte UTF-8 encoding or ligatures.
    pub range: Range<u16>,
    /// The source code location of the text.
    pub span: (Span, u16),
}

impl Glyph {
    /// The range of the glyph in its item's text.
    pub fn range(&self) -> Range<usize> {
        usize::from(self.range.start)..usize::from(self.range.end)
    }
}

/// A slice of a [`TextItem`].
pub struct TextItemView<'a> {
    /// The whole item this is a part of
    pub item: &'a TextItem,
    /// The glyphs of this slice
    pub glyph_range: Range<usize>,
}

impl<'a> TextItemView<'a> {
    /// Build a TextItemView for the whole contents of a TextItem.
    pub fn full(text: &'a TextItem) -> Self {
        Self::from_glyph_range(text, 0..text.glyphs.len())
    }

    /// Build a new [`TextItemView`] from a [`TextItem`] and a range of glyphs.
    pub fn from_glyph_range(text: &'a TextItem, glyph_range: Range<usize>) -> Self {
        TextItemView { item: text, glyph_range }
    }

    /// Returns an iterator over the glyphs of the slice.
    ///
    /// Note that the ranges are not remapped. They still point into the
    /// original text.
    pub fn glyphs(&self) -> &[Glyph] {
        &self.item.glyphs[self.glyph_range.clone()]
    }

    /// The plain text for the given glyph from `glyphs()`. This is an
    /// approximation since glyphs do not correspond 1-1 with codepoints.
    pub fn glyph_text(&self, glyph: &Glyph) -> EcoString {
        // Trim default ignorables which might have ended up in the glyph's
        // cluster. Keep interior ones so that joined emojis work. All of this
        // is a hack and needs to be reworked. See
        // https://github.com/typst/typst/pull/5099
        self.item.text[glyph.range()]
            .trim_matches(is_default_ignorable)
            .into()
    }

    /// The total width of this text slice
    pub fn width(&self) -> Abs {
        self.glyphs()
            .iter()
            .map(|g| g.x_advance)
            .sum::<Em>()
            .at(self.item.size)
    }

    /// The total height of this text slice
    pub fn height(&self) -> Abs {
        self.glyphs()
            .iter()
            .map(|g| g.y_advance)
            .sum::<Em>()
            .at(self.item.size)
    }
}