i_slint_core/textlayout/
shaping.rs

1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3
4use alloc::vec::Vec;
5use core::ops::Range;
6
7use super::TextLayout;
8
9/// This struct describes a glyph from shaping to rendering. This includes the relative shaping
10/// offsets, advance (in abstract lengths) and platform specific glyph data.
11#[derive(Clone, Default, Debug)]
12pub struct Glyph<Length> {
13    pub advance: Length,
14    pub offset_x: Length,
15    pub offset_y: Length,
16    /// Glyph IDs are font specific identifiers. In TrueType fonts zero indicates the missing glyph, which
17    /// is mapped to an Option here.
18    pub glyph_id: Option<core::num::NonZeroU16>,
19    /// The byte offset back in the original (Rust) string to the character that
20    /// "produced" this glyph. When one character produces multiple glyphs (for example
21    /// decomposed ligature), then all glyphs have the same offset.
22    pub text_byte_offset: usize,
23}
24
25/// This trait defines the interface between the text layout and the platform specific
26/// mapping of text to glyphs. An implementation of the TextShaper trait must provide
27/// metric types (Length, LengthPrimitive), which is used for the line breaking calculation
28/// and glyph positioning, as well as an opaque platform specific glyph data type.
29///
30/// Functionality wise it provides the ability to convert a string into a set of glyphs,
31/// each of which has basic metric fields as well as an offset back into the original string.
32/// Typically this is implemented by using a general text shaper, which performs an M:N mapping
33/// from unicode characters to glyphs, via glyph substitutions and script specific rules. In addition
34/// the glyphs may be positioned for the required appearance (such as stacked diacritics).
35///
36/// Finally, for convenience the TextShaper also provides a single glyph_for_char function, for example
37/// used to lookup single glyphs (such as the elision character) as well as additional metrics
38/// used for text paragraph layout.
39pub trait TextShaper {
40    type LengthPrimitive: core::ops::Mul
41        + core::ops::Div
42        + core::ops::Add<Output = Self::LengthPrimitive>
43        + core::ops::AddAssign
44        + euclid::num::Zero
45        + euclid::num::One
46        + core::convert::From<i16>
47        + Copy
48        + core::fmt::Debug;
49    type Length: euclid::num::Zero
50        + core::ops::AddAssign
51        + core::ops::Add<Output = Self::Length>
52        + core::ops::Sub<Output = Self::Length>
53        + Default
54        + Clone
55        + Copy
56        + core::cmp::PartialOrd
57        + core::ops::Mul<Self::LengthPrimitive, Output = Self::Length>
58        + core::ops::Div<Self::LengthPrimitive, Output = Self::Length>
59        + core::fmt::Debug;
60    // Shapes the given string and emits the result into the given glyphs buffer.
61    fn shape_text<GlyphStorage: core::iter::Extend<Glyph<Self::Length>>>(
62        &self,
63        text: &str,
64        glyphs: &mut GlyphStorage,
65    );
66    fn glyph_for_char(&self, ch: char) -> Option<Glyph<Self::Length>>;
67    fn max_lines(&self, max_height: Self::Length) -> usize;
68}
69
70pub trait FontMetrics<Length: Copy + core::ops::Sub<Output = Length>> {
71    fn height(&self) -> Length {
72        self.ascent() - self.descent()
73    }
74    fn ascent(&self) -> Length;
75    fn descent(&self) -> Length;
76    fn x_height(&self) -> Length;
77    fn cap_height(&self) -> Length;
78}
79
80pub trait AbstractFont: TextShaper + FontMetrics<<Self as TextShaper>::Length> {}
81
82impl<T> AbstractFont for T where T: TextShaper + FontMetrics<<Self as TextShaper>::Length> {}
83
84pub struct ShapeBoundaries<'a> {
85    text: &'a str,
86    #[cfg(feature = "unicode-script")]
87    // TODO: We should do a better analysis to find boundaries for text shaping; including
88    // boundaries when the bidi level changes or an explicit separator like
89    // paragraph/lineseparator/space is encountered.
90    chars: core::str::CharIndices<'a>,
91    next_boundary_start: Option<usize>,
92    #[cfg(feature = "unicode-script")]
93    last_script: Option<unicode_script::Script>,
94}
95
96impl<'a> ShapeBoundaries<'a> {
97    pub fn new(text: &'a str) -> Self {
98        let next_boundary_start = if !text.is_empty() { Some(0) } else { None };
99        Self {
100            text,
101            #[cfg(feature = "unicode-script")]
102            chars: text.char_indices(),
103            next_boundary_start,
104            #[cfg(feature = "unicode-script")]
105            last_script: None,
106        }
107    }
108}
109
110impl Iterator for ShapeBoundaries<'_> {
111    type Item = usize;
112
113    #[cfg(feature = "unicode-script")]
114    fn next(&mut self) -> Option<Self::Item> {
115        self.next_boundary_start?;
116
117        let (next_offset, script) = loop {
118            match self.chars.next() {
119                Some((byte_offset, ch)) => {
120                    use unicode_script::UnicodeScript;
121                    let next_script = ch.script();
122                    let previous_script = *self.last_script.get_or_insert(next_script);
123
124                    if next_script == previous_script {
125                        continue;
126                    }
127                    if matches!(
128                        next_script,
129                        unicode_script::Script::Unknown
130                            | unicode_script::Script::Common
131                            | unicode_script::Script::Inherited,
132                    ) {
133                        continue;
134                    }
135
136                    break (Some(byte_offset), Some(next_script));
137                }
138                None => {
139                    break (None, None);
140                }
141            }
142        };
143
144        self.last_script = script;
145        self.next_boundary_start = next_offset;
146
147        Some(self.next_boundary_start.unwrap_or(self.text.len()))
148    }
149
150    #[cfg(not(feature = "unicode-script"))]
151    fn next(&mut self) -> Option<Self::Item> {
152        match self.next_boundary_start {
153            Some(_) => {
154                self.next_boundary_start = None;
155                Some(self.text.len())
156            }
157            None => None,
158        }
159    }
160}
161
162#[derive(Debug)]
163pub struct TextRun {
164    pub byte_range: Range<usize>,
165    pub glyph_range: Range<usize>,
166    // TODO: direction, etc.
167}
168
169pub struct ShapeBuffer<Length> {
170    pub glyphs: Vec<Glyph<Length>>,
171    pub text_runs: Vec<TextRun>,
172}
173
174impl<Length> ShapeBuffer<Length> {
175    pub fn new<Font>(layout: &TextLayout<Font>, text: &str) -> Self
176    where
177        Font: AbstractFont<Length = Length>,
178        Length: Copy + core::ops::AddAssign,
179    {
180        let mut glyphs = Vec::new();
181        let text_runs = ShapeBoundaries::new(text)
182            .scan(0, |run_start, run_end| {
183                let glyphs_start = glyphs.len();
184
185                layout.font.shape_text(&text[*run_start..run_end], &mut glyphs);
186
187                if let Some(letter_spacing) = layout.letter_spacing {
188                    if glyphs.len() > glyphs_start {
189                        let mut last_byte_offset = glyphs[glyphs_start].text_byte_offset;
190                        for index in glyphs_start + 1..glyphs.len() {
191                            let current_glyph_byte_offset = glyphs[index].text_byte_offset;
192                            if current_glyph_byte_offset != last_byte_offset {
193                                let previous_glyph = &mut glyphs[index - 1];
194                                previous_glyph.advance += letter_spacing;
195                            }
196                            last_byte_offset = current_glyph_byte_offset;
197                        }
198
199                        glyphs.last_mut().unwrap().advance += letter_spacing;
200                    }
201                }
202
203                let run = TextRun {
204                    byte_range: Range { start: *run_start, end: run_end },
205                    glyph_range: Range { start: glyphs_start, end: glyphs.len() },
206                };
207                *run_start = run_end;
208
209                Some(run)
210            })
211            .collect();
212
213        Self { glyphs, text_runs }
214    }
215}
216
217#[test]
218fn test_shape_boundaries_simple() {
219    {
220        let simple_text = "Hello World";
221        let mut itemizer = ShapeBoundaries::new(simple_text);
222        assert_eq!(itemizer.next(), Some(simple_text.len()));
223        assert_eq!(itemizer.next(), None);
224    }
225}
226
227#[test]
228fn test_shape_boundaries_empty() {
229    {
230        let mut itemizer = ShapeBoundaries::new("");
231        assert_eq!(itemizer.next(), None);
232    }
233}
234
235#[test]
236#[cfg_attr(
237    not(feature = "unicode-script"),
238    ignore = "Not supported without the unicode-script feature"
239)]
240fn test_shape_boundaries_script_change() {
241    {
242        let text = "abc🍌🐒defதோசை.";
243        let mut itemizer = ShapeBoundaries::new(text).scan(0, |start, end| {
244            let str = &text[*start..end];
245            *start = end;
246            Some(str)
247        });
248        assert_eq!(itemizer.next(), Some("abc🍌🐒def"));
249        assert_eq!(itemizer.next(), Some("தோசை."));
250        assert_eq!(itemizer.next(), None);
251    }
252}
253
254#[cfg(test)]
255impl TextShaper for &rustybuzz::Face<'_> {
256    type LengthPrimitive = f32;
257    type Length = f32;
258    fn shape_text<GlyphStorage: std::iter::Extend<Glyph<f32>>>(
259        &self,
260        text: &str,
261        glyphs: &mut GlyphStorage,
262    ) {
263        let mut buffer = rustybuzz::UnicodeBuffer::new();
264        buffer.push_str(text);
265        let glyph_buffer = rustybuzz::shape(self, &[], buffer);
266
267        let output_glyph_generator =
268            glyph_buffer.glyph_infos().iter().zip(glyph_buffer.glyph_positions().iter()).map(
269                |(info, position)| {
270                    let mut out_glyph = Glyph::default();
271                    out_glyph.glyph_id = core::num::NonZeroU16::new(info.glyph_id as u16);
272                    out_glyph.offset_x = position.x_offset as _;
273                    out_glyph.offset_y = position.y_offset as _;
274                    out_glyph.advance = position.x_advance as _;
275                    out_glyph.text_byte_offset = info.cluster as usize;
276                    out_glyph
277                },
278            );
279
280        // Cannot return impl Iterator, so extend argument instead
281        glyphs.extend(output_glyph_generator);
282    }
283
284    fn glyph_for_char(&self, _ch: char) -> Option<Glyph<f32>> {
285        todo!()
286    }
287
288    fn max_lines(&self, max_height: f32) -> usize {
289        (max_height / self.height()).floor() as _
290    }
291}
292
293#[cfg(test)]
294impl FontMetrics<f32> for &rustybuzz::Face<'_> {
295    fn ascent(&self) -> f32 {
296        self.ascender() as _
297    }
298
299    fn descent(&self) -> f32 {
300        self.descender() as _
301    }
302
303    fn x_height(&self) -> f32 {
304        rustybuzz::ttf_parser::Face::x_height(self).unwrap_or_default() as _
305    }
306
307    fn cap_height(&self) -> f32 {
308        rustybuzz::ttf_parser::Face::capital_height(self).unwrap_or_default() as _
309    }
310}
311
312#[cfg(test)]
313fn with_dejavu_font<R>(mut callback: impl FnMut(&rustybuzz::Face<'_>) -> R) -> R {
314    let mut collection = fontique::Collection::default();
315    let dejavu_path: std::path::PathBuf =
316        [env!("CARGO_MANIFEST_DIR"), "..", "common", "sharedfontique", "DejaVuSans.ttf"]
317            .iter()
318            .collect();
319    let registered_fonts =
320        collection.register_fonts(std::fs::read(&dejavu_path).unwrap().into(), None);
321    let mut cache = fontique::SourceCache::default();
322    let mut query = collection.query(&mut cache);
323    query.set_families(std::iter::once(fontique::QueryFamily::from(registered_fonts[0].0)));
324    let mut font = None;
325    query.matches_with(|query_font| {
326        font = Some(query_font.clone());
327        fontique::QueryStatus::Stop
328    });
329    let font = font.unwrap();
330    let face = rustybuzz::Face::from_slice(font.blob.data(), font.index)
331        .expect("unable to parse dejavu font");
332    callback(&face)
333}
334
335#[test]
336fn test_shaping() {
337    use std::num::NonZeroU16;
338    use TextShaper;
339
340    with_dejavu_font(|face| {
341        {
342            let mut shaped_glyphs = Vec::new();
343            // two glyph clusters: ā́b
344            face.shape_text("a\u{0304}\u{0301}b", &mut shaped_glyphs);
345
346            assert_eq!(shaped_glyphs.len(), 3);
347            assert_eq!(shaped_glyphs[0].glyph_id, NonZeroU16::new(195));
348            assert_eq!(shaped_glyphs[0].text_byte_offset, 0);
349
350            assert_eq!(shaped_glyphs[1].glyph_id, NonZeroU16::new(690));
351            assert_eq!(shaped_glyphs[1].text_byte_offset, 0);
352
353            assert_eq!(shaped_glyphs[2].glyph_id, NonZeroU16::new(69));
354            assert_eq!(shaped_glyphs[2].text_byte_offset, 5);
355        }
356
357        {
358            let mut shaped_glyphs = Vec::new();
359            // two glyph clusters: ā́b
360            face.shape_text("a b", &mut shaped_glyphs);
361
362            assert_eq!(shaped_glyphs.len(), 3);
363            assert_eq!(shaped_glyphs[0].glyph_id, NonZeroU16::new(68));
364            assert_eq!(shaped_glyphs[0].text_byte_offset, 0);
365
366            assert_eq!(shaped_glyphs[1].text_byte_offset, 1);
367
368            assert_eq!(shaped_glyphs[2].glyph_id, NonZeroU16::new(69));
369            assert_eq!(shaped_glyphs[2].text_byte_offset, 2);
370        }
371    });
372}
373
374#[test]
375fn test_letter_spacing() {
376    use TextShaper;
377
378    with_dejavu_font(|face| {
379        // two glyph clusters: ā́b
380        let text = "a\u{0304}\u{0301}b";
381        let advances = {
382            let mut shaped_glyphs = Vec::new();
383            face.shape_text(text, &mut shaped_glyphs);
384
385            assert_eq!(shaped_glyphs.len(), 3);
386
387            shaped_glyphs.iter().map(|g| g.advance).collect::<Vec<_>>()
388        };
389
390        let layout = TextLayout { font: &face, letter_spacing: Some(20.) };
391        let buffer = ShapeBuffer::new(&layout, text);
392
393        assert_eq!(buffer.glyphs.len(), advances.len());
394
395        let mut expected_advances = advances;
396        expected_advances[1] += layout.letter_spacing.unwrap();
397        *expected_advances.last_mut().unwrap() += layout.letter_spacing.unwrap();
398
399        assert_eq!(
400            buffer.glyphs.iter().map(|glyph| glyph.advance).collect::<Vec<_>>(),
401            expected_advances
402        );
403    });
404}