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/// Adds two widths, returning `None` when the result would not fit the coordinate type. A line
27/// wider than that cannot be positioned or displayed anyway, so the layout stops there instead of
28/// overflowing (which would panic in a debug build). Floats never overflow, so they always add.
29pub trait CheckedAdd: Copy {
30    /// Adds, returning `None` if the result would not fit the coordinate type.
31    fn checked_add(self, other: Self) -> Option<Self>;
32
33    /// Adds, clamping to the coordinate type's maximum instead of overflowing.
34    fn saturating_add(self, other: Self) -> Self;
35}
36
37impl CheckedAdd for f32 {
38    fn checked_add(self, other: Self) -> Option<Self> {
39        Some(self + other)
40    }
41
42    fn saturating_add(self, other: Self) -> Self {
43        self + other
44    }
45}
46
47impl<U> CheckedAdd for euclid::Length<i16, U> {
48    fn checked_add(self, other: Self) -> Option<Self> {
49        self.get().checked_add(other.get()).map(euclid::Length::new)
50    }
51
52    fn saturating_add(self, other: Self) -> Self {
53        euclid::Length::new(self.get().saturating_add(other.get()))
54    }
55}
56
57/// This trait defines the interface between the text layout and the platform specific
58/// mapping of text to glyphs. An implementation of the TextShaper trait must provide
59/// metric types (Length, LengthPrimitive), which is used for the line breaking calculation
60/// and glyph positioning, as well as an opaque platform specific glyph data type.
61///
62/// Functionality wise it provides the ability to convert a string into a set of glyphs,
63/// each of which has basic metric fields as well as an offset back into the original string.
64/// Typically this is implemented by using a general text shaper, which performs an M:N mapping
65/// from unicode characters to glyphs, via glyph substitutions and script specific rules. In addition
66/// the glyphs may be positioned for the required appearance (such as stacked diacritics).
67///
68/// Finally, for convenience the TextShaper also provides a single glyph_for_char function, for example
69/// used to lookup single glyphs (such as the elision character) as well as additional metrics
70/// used for text paragraph layout.
71pub trait TextShaper {
72    type LengthPrimitive: core::ops::Mul
73        + core::ops::Div
74        + core::ops::Add<Output = Self::LengthPrimitive>
75        + core::ops::AddAssign
76        + euclid::num::Zero
77        + euclid::num::One
78        + core::convert::From<i16>
79        + Copy
80        + core::fmt::Debug;
81    type Length: euclid::num::Zero
82        + CheckedAdd
83        + core::ops::AddAssign
84        + core::ops::Add<Output = Self::Length>
85        + core::ops::Sub<Output = Self::Length>
86        + Default
87        + Clone
88        + Copy
89        + core::cmp::PartialOrd
90        + core::ops::Mul<Self::LengthPrimitive, Output = Self::Length>
91        + core::ops::Div<Self::LengthPrimitive, Output = Self::Length>
92        + DivCount
93        + core::fmt::Debug;
94    // Shapes the given string and emits the result into the given glyphs buffer.
95    fn shape_text<GlyphStorage: core::iter::Extend<Glyph<Self::Length>>>(
96        &self,
97        text: &str,
98        glyphs: &mut GlyphStorage,
99    );
100    fn glyph_for_char(&self, ch: char) -> Option<Glyph<Self::Length>>;
101}
102
103/// How many times one length fits into another of the same unit.
104/// euclid's `Length / Length` produces a `Scale` rather than a plain number, so the division the
105/// paragraph layout needs is spelled out here instead of as a `core::ops::Div` bound.
106pub trait DivCount {
107    /// The number of whole `divisor`s in `self`, truncated, and zero when that count is negative.
108    /// `divisor` must not be zero.
109    fn div_count(self, divisor: Self) -> usize;
110}
111
112impl DivCount for f32 {
113    fn div_count(self, divisor: Self) -> usize {
114        // Casts from float saturate, so a negative or NaN ratio ends up at zero.
115        (self / divisor) as usize
116    }
117}
118
119impl DivCount for i16 {
120    fn div_count(self, divisor: Self) -> usize {
121        // Widen so that i16::MIN / -1 can't overflow.
122        (i32::from(self) / i32::from(divisor)).max(0) as usize
123    }
124}
125
126impl<T: DivCount + Clone, U> DivCount for euclid::Length<T, U> {
127    fn div_count(self, divisor: Self) -> usize {
128        self.get().div_count(divisor.get())
129    }
130}
131
132pub trait FontMetrics<Length: Copy + core::ops::Sub<Output = Length>> {
133    fn height(&self) -> Length {
134        self.ascent() - self.descent()
135    }
136    fn ascent(&self) -> Length;
137    fn descent(&self) -> Length;
138    fn x_height(&self) -> Length;
139    fn cap_height(&self) -> Length;
140}
141
142pub trait AbstractFont: TextShaper + FontMetrics<<Self as TextShaper>::Length> {}
143
144impl<T> AbstractFont for T where T: TextShaper + FontMetrics<<Self as TextShaper>::Length> {}
145
146pub struct ShapeBoundaries<'a> {
147    text: &'a str,
148    #[cfg(feature = "unicode-script")]
149    // TODO: We should do a better analysis to find boundaries for text shaping; including
150    // boundaries when the bidi level changes or an explicit separator like
151    // paragraph/lineseparator/space is encountered.
152    chars: core::str::CharIndices<'a>,
153    next_boundary_start: Option<usize>,
154    #[cfg(feature = "unicode-script")]
155    last_script: Option<unicode_script::Script>,
156}
157
158impl<'a> ShapeBoundaries<'a> {
159    pub fn new(text: &'a str) -> Self {
160        let next_boundary_start = if !text.is_empty() { Some(0) } else { None };
161        Self {
162            text,
163            #[cfg(feature = "unicode-script")]
164            chars: text.char_indices(),
165            next_boundary_start,
166            #[cfg(feature = "unicode-script")]
167            last_script: None,
168        }
169    }
170}
171
172impl Iterator for ShapeBoundaries<'_> {
173    type Item = usize;
174
175    #[cfg(feature = "unicode-script")]
176    fn next(&mut self) -> Option<Self::Item> {
177        self.next_boundary_start?;
178
179        let (next_offset, script) = loop {
180            match self.chars.next() {
181                Some((byte_offset, ch)) => {
182                    use unicode_script::UnicodeScript;
183                    let next_script = ch.script();
184                    let previous_script = *self.last_script.get_or_insert(next_script);
185
186                    if next_script == previous_script {
187                        continue;
188                    }
189                    if matches!(
190                        next_script,
191                        unicode_script::Script::Unknown
192                            | unicode_script::Script::Common
193                            | unicode_script::Script::Inherited,
194                    ) {
195                        continue;
196                    }
197
198                    break (Some(byte_offset), Some(next_script));
199                }
200                None => {
201                    break (None, None);
202                }
203            }
204        };
205
206        self.last_script = script;
207        self.next_boundary_start = next_offset;
208
209        Some(self.next_boundary_start.unwrap_or(self.text.len()))
210    }
211
212    #[cfg(not(feature = "unicode-script"))]
213    fn next(&mut self) -> Option<Self::Item> {
214        match self.next_boundary_start {
215            Some(_) => {
216                self.next_boundary_start = None;
217                Some(self.text.len())
218            }
219            None => None,
220        }
221    }
222}
223
224#[derive(Debug)]
225pub struct TextRun {
226    pub byte_range: Range<usize>,
227    pub glyph_range: Range<usize>,
228    // TODO: direction, etc.
229}
230
231pub struct ShapeBuffer<Length> {
232    pub glyphs: Vec<Glyph<Length>>,
233    pub text_runs: Vec<TextRun>,
234}
235
236impl<Length> ShapeBuffer<Length> {
237    pub fn new<Font>(layout: &TextLayout<Font>, text: &str) -> Self
238    where
239        Font: AbstractFont<Length = Length>,
240        Length: Copy + core::ops::AddAssign,
241    {
242        let mut glyphs = Vec::new();
243        let text_runs = ShapeBoundaries::new(text)
244            .scan(0, |run_start, run_end| {
245                let glyphs_start = glyphs.len();
246
247                layout.font.shape_text(&text[*run_start..run_end], &mut glyphs);
248
249                // Make the cluster index absolute.
250                //
251                // A shaper sees one run's slice, so the offset it reports is
252                // relative to that slice. Everything downstream compares these
253                // against indices into the whole string: TextLine::byte_range,
254                // a selection range, and the byte offset a click maps to. This
255                // is the one place the run's start is still in hand, and
256                // folding it in here makes TextRun::byte_range and the glyph
257                // offsets mean the same thing.
258                for glyph in &mut glyphs[glyphs_start..] {
259                    glyph.text_byte_offset += *run_start;
260                }
261
262                if let Some(letter_spacing) = layout.letter_spacing
263                    && glyphs.len() > glyphs_start
264                {
265                    let mut last_byte_offset = glyphs[glyphs_start].text_byte_offset;
266                    for index in glyphs_start + 1..glyphs.len() {
267                        let current_glyph_byte_offset = glyphs[index].text_byte_offset;
268                        if current_glyph_byte_offset != last_byte_offset {
269                            let previous_glyph = &mut glyphs[index - 1];
270                            previous_glyph.advance += letter_spacing;
271                        }
272                        last_byte_offset = current_glyph_byte_offset;
273                    }
274
275                    glyphs.last_mut().unwrap().advance += letter_spacing;
276                }
277
278                let run = TextRun {
279                    byte_range: Range { start: *run_start, end: run_end },
280                    glyph_range: Range { start: glyphs_start, end: glyphs.len() },
281                };
282                *run_start = run_end;
283
284                Some(run)
285            })
286            .collect();
287
288        Self { glyphs, text_runs }
289    }
290}
291
292#[test]
293fn test_div_count() {
294    assert_eq!(9.0_f32.div_count(3.0), 3);
295    assert_eq!(10.0_f32.div_count(3.0), 3);
296    assert_eq!((-10.0_f32).div_count(3.0), 0);
297    assert_eq!(3.0_f32.div_count(10.0), 0);
298    assert_eq!(f32::NAN.div_count(16.0), 0);
299
300    assert_eq!(i16::MIN.div_count(-1), 32768);
301
302    type IntLen = euclid::Length<i16, euclid::UnknownUnit>;
303    assert_eq!(IntLen::new(10).div_count(IntLen::new(3)), 3);
304    assert_eq!(IntLen::new(-10).div_count(IntLen::new(3)), 0);
305
306    type FloatLen = euclid::Length<f32, euclid::UnknownUnit>;
307    assert_eq!(FloatLen::new(10.).div_count(FloatLen::new(3.)), 3);
308}
309
310#[test]
311fn test_shape_boundaries_simple() {
312    {
313        let simple_text = "Hello World";
314        let mut itemizer = ShapeBoundaries::new(simple_text);
315        assert_eq!(itemizer.next(), Some(simple_text.len()));
316        assert_eq!(itemizer.next(), None);
317    }
318}
319
320#[test]
321fn test_shape_boundaries_empty() {
322    {
323        let mut itemizer = ShapeBoundaries::new("");
324        assert_eq!(itemizer.next(), None);
325    }
326}
327
328#[test]
329#[cfg_attr(
330    not(feature = "unicode-script"),
331    ignore = "Not supported without the unicode-script feature"
332)]
333fn test_shape_boundaries_script_change() {
334    {
335        let text = "abc🍌🐒defதோசை.";
336        let mut itemizer = ShapeBoundaries::new(text).scan(0, |start, end| {
337            let str = &text[*start..end];
338            *start = end;
339            Some(str)
340        });
341        assert_eq!(itemizer.next(), Some("abc🍌🐒def"));
342        assert_eq!(itemizer.next(), Some("தோசை."));
343        assert_eq!(itemizer.next(), None);
344    }
345}
346
347#[cfg(test)]
348impl TextShaper for &rustybuzz::Face<'_> {
349    type LengthPrimitive = f32;
350    type Length = f32;
351    fn shape_text<GlyphStorage: std::iter::Extend<Glyph<f32>>>(
352        &self,
353        text: &str,
354        glyphs: &mut GlyphStorage,
355    ) {
356        let mut buffer = rustybuzz::UnicodeBuffer::new();
357        buffer.push_str(text);
358        let glyph_buffer = rustybuzz::shape(self, &[], buffer);
359
360        let output_glyph_generator =
361            glyph_buffer.glyph_infos().iter().zip(glyph_buffer.glyph_positions().iter()).map(
362                |(info, position)| {
363                    let mut out_glyph = Glyph::default();
364                    out_glyph.glyph_id = core::num::NonZeroU16::new(info.glyph_id as u16);
365                    out_glyph.offset_x = position.x_offset as _;
366                    out_glyph.offset_y = position.y_offset as _;
367                    out_glyph.advance = position.x_advance as _;
368                    out_glyph.text_byte_offset = info.cluster as usize;
369                    out_glyph
370                },
371            );
372
373        // Cannot return impl Iterator, so extend argument instead
374        glyphs.extend(output_glyph_generator);
375    }
376
377    fn glyph_for_char(&self, _ch: char) -> Option<Glyph<f32>> {
378        todo!()
379    }
380}
381
382#[cfg(test)]
383impl FontMetrics<f32> for &rustybuzz::Face<'_> {
384    fn ascent(&self) -> f32 {
385        self.ascender() as _
386    }
387
388    fn descent(&self) -> f32 {
389        self.descender() as _
390    }
391
392    fn x_height(&self) -> f32 {
393        rustybuzz::ttf_parser::Face::x_height(self).unwrap_or_default() as _
394    }
395
396    fn cap_height(&self) -> f32 {
397        rustybuzz::ttf_parser::Face::capital_height(self).unwrap_or_default() as _
398    }
399}
400
401#[cfg(test)]
402fn with_default_font<R>(mut callback: impl FnMut(&rustybuzz::Face<'_>) -> R) -> R {
403    let mut collection = fontique::Collection::new(fontique::CollectionOptions {
404        system_fonts: false,
405        ..Default::default()
406    });
407    let font_path: std::path::PathBuf =
408        [env!("CARGO_MANIFEST_DIR"), "..", "common", "sharedfontique", "Inter-VariableFont.ttf"]
409            .iter()
410            .collect();
411    let registered_fonts =
412        collection.register_fonts(std::fs::read(&font_path).unwrap().into(), None);
413    let mut cache = fontique::SourceCache::default();
414    let mut query = collection.query(&mut cache);
415    query.set_families(std::iter::once(fontique::QueryFamily::from(registered_fonts[0].0)));
416    let mut font = None;
417    query.matches_with(|query_font| {
418        font = Some(query_font.clone());
419        fontique::QueryStatus::Stop
420    });
421    let font = font.unwrap();
422    let face =
423        rustybuzz::Face::from_slice(font.blob.data(), font.index).expect("unable to parse font");
424    callback(&face)
425}
426
427#[test]
428fn test_shaping() {
429    use TextShaper;
430
431    with_default_font(|face| {
432        {
433            let mut shaped_glyphs = Vec::new();
434            // two glyph clusters: ā́b
435            face.shape_text("a\u{0304}\u{0301}b", &mut shaped_glyphs);
436
437            assert_eq!(shaped_glyphs.len(), 3);
438            assert!(shaped_glyphs[0].glyph_id.is_some());
439            assert_eq!(shaped_glyphs[0].text_byte_offset, 0);
440
441            assert!(shaped_glyphs[1].glyph_id.is_some());
442            assert_eq!(shaped_glyphs[1].text_byte_offset, 0);
443
444            assert!(shaped_glyphs[2].glyph_id.is_some());
445            assert_eq!(shaped_glyphs[2].text_byte_offset, 5);
446        }
447
448        {
449            let mut shaped_glyphs = Vec::new();
450            // two glyph clusters: ā́b
451            face.shape_text("a b", &mut shaped_glyphs);
452
453            assert_eq!(shaped_glyphs.len(), 3);
454            assert!(shaped_glyphs[0].glyph_id.is_some());
455            assert_eq!(shaped_glyphs[0].text_byte_offset, 0);
456
457            assert_eq!(shaped_glyphs[1].text_byte_offset, 1);
458
459            assert!(shaped_glyphs[2].glyph_id.is_some());
460            assert_eq!(shaped_glyphs[2].text_byte_offset, 2);
461        }
462    });
463}
464
465/// The byte offset on a glyph is an index into the whole string, on every run.
466///
467/// A shaper sees one run's slice, so the offset it reports is relative to that slice.
468/// Text that changes script part way through is more than one run,
469/// and a line that opens with a bracketed number is exactly that shape.
470/// Line breaking, selection and the byte offset a click maps to
471/// all compare these offsets against indices into the whole string.
472#[test]
473#[cfg_attr(
474    not(feature = "unicode-script"),
475    ignore = "Not supported without the unicode-script feature"
476)]
477fn test_byte_offsets_are_absolute() {
478    with_default_font(|face| {
479        // `Common` script up to the space, `Latin` from `a` on.
480        let text = "[01] abc";
481        let layout = TextLayout { font: &face, letter_spacing: None, line_height: None };
482        let buffer = ShapeBuffer::new(&layout, text);
483
484        assert_eq!(buffer.text_runs.len(), 2, "expected a run boundary at the script change");
485        let second_run = &buffer.text_runs[1];
486        assert_eq!(second_run.byte_range.start, 5);
487
488        // Every offset is an index into `text`, and the first glyph of the second
489        // run points at the character that run starts with.
490        for glyph in &buffer.glyphs {
491            assert!(
492                text.is_char_boundary(glyph.text_byte_offset),
493                "{} is not an index into {text:?}",
494                glyph.text_byte_offset
495            );
496        }
497        assert_eq!(buffer.glyphs[second_run.glyph_range.start].text_byte_offset, 5);
498        assert_eq!(buffer.glyphs.last().unwrap().text_byte_offset, text.len() - 1);
499    });
500}
501
502#[test]
503fn test_letter_spacing() {
504    use TextShaper;
505
506    with_default_font(|face| {
507        // two glyph clusters: ā́b
508        let text = "a\u{0304}\u{0301}b";
509        let advances = {
510            let mut shaped_glyphs = Vec::new();
511            face.shape_text(text, &mut shaped_glyphs);
512
513            assert_eq!(shaped_glyphs.len(), 3);
514
515            shaped_glyphs.iter().map(|g| g.advance).collect::<Vec<_>>()
516        };
517
518        let layout = TextLayout { font: &face, letter_spacing: Some(20.), line_height: None };
519        let buffer = ShapeBuffer::new(&layout, text);
520
521        assert_eq!(buffer.glyphs.len(), advances.len());
522
523        let mut expected_advances = advances;
524        expected_advances[1] += layout.letter_spacing.unwrap();
525        *expected_advances.last_mut().unwrap() += layout.letter_spacing.unwrap();
526
527        assert_eq!(
528            buffer.glyphs.iter().map(|glyph| glyph.advance).collect::<Vec<_>>(),
529            expected_advances
530        );
531    });
532}