Skip to main content

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