Skip to main content

i_slint_core/textlayout/
fragments.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 core::ops::Range;
5
6use euclid::num::Zero;
7
8use super::glyphclusters::GlyphClusterIterator;
9use super::{BreakOpportunity, CheckedAdd, LineBreakIterator, ShapeBuffer};
10
11#[derive(Debug, PartialEq, Eq, Default)]
12pub struct TextFragment<Length> {
13    pub byte_range: Range<usize>,
14    pub glyph_range: Range<usize>,
15    pub width: Length,
16    pub trailing_whitespace_width: Length,
17    pub trailing_whitespace_bytes: usize,
18    pub trailing_mandatory_break: bool,
19}
20
21#[derive(Clone)]
22pub struct TextFragmentIterator<'a, Length> {
23    line_breaks: LineBreakIterator<'a>,
24    glyph_clusters: GlyphClusterIterator<'a, Length>,
25    text_len: usize,
26    pub break_anywhere: bool,
27    /// Set when the last emitted fragment was cut short because its width no longer fit the
28    /// coordinate type; the glyphs past that point cannot be displayed anyway.
29    pub truncated: bool,
30}
31
32impl<'a, Length> TextFragmentIterator<'a, Length> {
33    pub fn new(text: &'a str, shape_buffer: &'a ShapeBuffer<Length>) -> Self {
34        Self {
35            line_breaks: LineBreakIterator::new(text),
36            glyph_clusters: GlyphClusterIterator::new(text, shape_buffer),
37            text_len: text.len(),
38            break_anywhere: false,
39            truncated: false,
40        }
41    }
42}
43
44impl<Length: Clone + Default + core::ops::AddAssign + CheckedAdd + Zero + Copy> Iterator
45    for TextFragmentIterator<'_, Length>
46{
47    type Item = TextFragment<Length>;
48
49    fn next(&mut self) -> Option<Self::Item> {
50        let first_glyph_cluster = self.glyph_clusters.next()?;
51
52        let mut fragment = Self::Item::default();
53        self.truncated = false;
54
55        let next_break_offset = if self.break_anywhere {
56            if first_glyph_cluster.is_line_or_paragraph_separator {
57                fragment.trailing_mandatory_break = true;
58            }
59            0
60        } else if let Some((next_break_offset, break_type)) = self.line_breaks.next() {
61            if matches!(break_type, BreakOpportunity::Mandatory) {
62                fragment.trailing_mandatory_break = true;
63            }
64            next_break_offset
65        } else {
66            self.text_len
67        };
68
69        if first_glyph_cluster.is_whitespace {
70            fragment.trailing_whitespace_width = first_glyph_cluster.width;
71            fragment.trailing_whitespace_bytes = first_glyph_cluster.byte_range.len();
72            fragment.byte_range.start = first_glyph_cluster.byte_range.start;
73            fragment.byte_range.end = first_glyph_cluster.byte_range.start;
74        } else {
75            fragment.width = first_glyph_cluster.width;
76            fragment.byte_range = first_glyph_cluster.byte_range.clone();
77        }
78
79        let start = first_glyph_cluster.glyph_range.start;
80        let mut last_glyph_cluster = first_glyph_cluster;
81
82        while last_glyph_cluster.byte_range.end < next_break_offset {
83            let next_glyph_cluster = match self.glyph_clusters.next() {
84                Some(cluster) => cluster,
85                None => break,
86            };
87
88            if next_glyph_cluster.is_line_or_paragraph_separator {
89                break;
90            }
91
92            if next_glyph_cluster.is_whitespace {
93                let Some(width) =
94                    fragment.trailing_whitespace_width.checked_add(next_glyph_cluster.width)
95                else {
96                    self.truncated = true;
97                    break;
98                };
99                fragment.trailing_whitespace_width = width;
100                fragment.trailing_whitespace_bytes += next_glyph_cluster.byte_range.len();
101            } else {
102                // transition from whitespace to characters by treating previous trailing whitespace
103                // as regular characters
104                let folded_whitespace = if last_glyph_cluster.is_whitespace {
105                    fragment.trailing_whitespace_width
106                } else {
107                    Length::zero()
108                };
109                // The line is wider than the coordinate type can represent; the rest is off any
110                // possible viewport, so stop here rather than overflowing.
111                let Some(width) = fragment
112                    .width
113                    .checked_add(folded_whitespace)
114                    .and_then(|width| width.checked_add(next_glyph_cluster.width))
115                else {
116                    self.truncated = true;
117                    break;
118                };
119                fragment.width = width;
120                fragment.byte_range.end = next_glyph_cluster.byte_range.end;
121                if last_glyph_cluster.is_whitespace {
122                    fragment.trailing_whitespace_width = Length::zero();
123                    fragment.trailing_whitespace_bytes = 0;
124                }
125            }
126
127            last_glyph_cluster = next_glyph_cluster.clone();
128        }
129
130        fragment.glyph_range = Range { start, end: last_glyph_cluster.glyph_range.end };
131
132        // Make sure that adjacent fragments are advanced in their byte range:
133        // this assertion should hold: fragment.byte_range.end + fragment.trailing_whitespace_bytes == next_fragment.byte_range.start
134        // That means characters causing mandatory breaks need to be included.
135        if fragment.trailing_mandatory_break && !self.break_anywhere {
136            fragment.trailing_whitespace_bytes = next_break_offset - fragment.byte_range.end;
137        }
138
139        Some(fragment)
140    }
141}
142
143#[cfg(test)]
144use super::{FixedTestFont, TextLayout};
145#[cfg(test)]
146use std::{vec, vec::Vec};
147
148#[test]
149fn fragment_iterator_simple() {
150    let font = FixedTestFont;
151    let text = "H WX";
152    let shape_buffer = ShapeBuffer::new(
153        &TextLayout { font: &font, letter_spacing: None, line_height: None },
154        text,
155    );
156    let fragments = TextFragmentIterator::new(text, &shape_buffer).collect::<Vec<_>>();
157    let expected = vec![
158        TextFragment {
159            byte_range: Range { start: 0, end: 1 },
160            glyph_range: Range { start: 0, end: 2 },
161            width: 10.,
162            trailing_whitespace_width: 10.,
163            trailing_mandatory_break: false,
164            trailing_whitespace_bytes: 1,
165        },
166        TextFragment {
167            byte_range: Range { start: 2, end: text.len() },
168            glyph_range: Range { start: 2, end: text.len() },
169            width: 20.,
170            trailing_whitespace_width: 0.,
171            trailing_mandatory_break: false,
172            trailing_whitespace_bytes: 0,
173        },
174    ];
175    assert_eq!(fragments, expected);
176}
177
178#[test]
179fn fragment_iterator_simple_v2() {
180    let font = FixedTestFont;
181    let text = "Hello World";
182    let shape_buffer = ShapeBuffer::new(
183        &TextLayout { font: &font, letter_spacing: None, line_height: None },
184        text,
185    );
186    let fragments = TextFragmentIterator::new(text, &shape_buffer).collect::<Vec<_>>();
187    let expected = vec![
188        TextFragment {
189            byte_range: Range { start: 0, end: 5 },
190            glyph_range: Range { start: 0, end: 6 },
191            width: 50.,
192            trailing_whitespace_width: 10.,
193            trailing_mandatory_break: false,
194            trailing_whitespace_bytes: 1,
195        },
196        TextFragment {
197            byte_range: Range { start: 6, end: text.len() },
198            glyph_range: Range { start: 6, end: text.len() },
199            width: 10. * (text.len() - 6) as f32,
200            trailing_whitespace_width: 0.,
201            trailing_whitespace_bytes: 0,
202            trailing_mandatory_break: false,
203        },
204    ];
205    assert_eq!(fragments, expected);
206}
207
208#[test]
209fn fragment_iterator_forced_break() {
210    let font = FixedTestFont;
211    let text = "H\nW";
212    let shape_buffer = ShapeBuffer::new(
213        &TextLayout { font: &font, letter_spacing: None, line_height: None },
214        text,
215    );
216    let fragments = TextFragmentIterator::new(text, &shape_buffer).collect::<Vec<_>>();
217    assert_eq!(
218        fragments,
219        vec![
220            TextFragment {
221                byte_range: Range { start: 0, end: 1 },
222                glyph_range: Range { start: 0, end: 1 },
223                width: 10.,
224                trailing_whitespace_width: 0.,
225                trailing_whitespace_bytes: 1,
226                trailing_mandatory_break: true,
227            },
228            TextFragment {
229                byte_range: Range { start: 2, end: 3 },
230                glyph_range: Range { start: 2, end: 3 },
231                width: 10.,
232                trailing_whitespace_width: 0.,
233                trailing_whitespace_bytes: 0,
234                trailing_mandatory_break: false,
235            },
236        ]
237    );
238}
239
240#[test]
241fn fragment_iterator_forced_break_multi() {
242    let font = FixedTestFont;
243    let text = "H\n\n\nW";
244    let shape_buffer = ShapeBuffer::new(
245        &TextLayout { font: &font, letter_spacing: None, line_height: None },
246        text,
247    );
248    let fragments = TextFragmentIterator::new(text, &shape_buffer).collect::<Vec<_>>();
249    assert_eq!(
250        fragments,
251        vec![
252            TextFragment {
253                byte_range: Range { start: 0, end: 1 },
254                glyph_range: Range { start: 0, end: 1 },
255                width: 10.,
256                trailing_whitespace_width: 0.,
257                trailing_whitespace_bytes: 1,
258                trailing_mandatory_break: true,
259            },
260            TextFragment {
261                byte_range: Range { start: 2, end: 2 },
262                glyph_range: Range { start: 2, end: 3 },
263                width: 0.,
264                trailing_whitespace_width: 10.,
265                trailing_whitespace_bytes: 1,
266                trailing_mandatory_break: true,
267            },
268            TextFragment {
269                byte_range: Range { start: 3, end: 3 },
270                glyph_range: Range { start: 3, end: 4 },
271                width: 0.,
272                trailing_whitespace_width: 10.,
273                trailing_whitespace_bytes: 1,
274                trailing_mandatory_break: true,
275            },
276            TextFragment {
277                byte_range: Range { start: 4, end: 5 },
278                glyph_range: Range { start: 4, end: 5 },
279                width: 10.,
280                trailing_whitespace_width: 0.,
281                trailing_whitespace_bytes: 0,
282                trailing_mandatory_break: false,
283            },
284        ]
285    );
286}
287
288#[test]
289fn fragment_iterator_nbsp() {
290    let font = FixedTestFont;
291    let text = "X H\u{00a0}W";
292    let shape_buffer = ShapeBuffer::new(
293        &TextLayout { font: &font, letter_spacing: None, line_height: None },
294        text,
295    );
296    let fragments = TextFragmentIterator::new(text, &shape_buffer).collect::<Vec<_>>();
297    assert_eq!(
298        fragments,
299        vec![
300            TextFragment {
301                byte_range: Range { start: 0, end: 1 },
302                glyph_range: Range { start: 0, end: 2 },
303                width: 10.,
304                trailing_whitespace_width: 10.,
305                trailing_whitespace_bytes: 1,
306                trailing_mandatory_break: false,
307            },
308            TextFragment {
309                byte_range: Range { start: 2, end: 6 },
310                glyph_range: Range { start: 2, end: 5 },
311                width: 30.,
312                trailing_whitespace_width: 0.,
313                trailing_whitespace_bytes: 0,
314                trailing_mandatory_break: false,
315            }
316        ]
317    );
318}
319
320#[test]
321fn fragment_iterator_break_anywhere() {
322    let font = FixedTestFont;
323    let text = "AB\nCD\nEF";
324    let shape_buffer = ShapeBuffer::new(
325        &TextLayout { font: &font, letter_spacing: None, line_height: None },
326        text,
327    );
328    let mut fragments = TextFragmentIterator::new(text, &shape_buffer);
329    assert_eq!(
330        fragments.next(),
331        Some(TextFragment {
332            byte_range: Range { start: 0, end: 2 },
333            glyph_range: Range { start: 0, end: 2 },
334            width: 20.,
335            trailing_whitespace_width: 0.,
336            trailing_whitespace_bytes: 1,
337            trailing_mandatory_break: true,
338        })
339    );
340    assert_eq!(
341        fragments.next(),
342        Some(TextFragment {
343            byte_range: Range { start: 3, end: 5 },
344            glyph_range: Range { start: 3, end: 5 },
345            width: 20.,
346            trailing_whitespace_width: 0.,
347            trailing_whitespace_bytes: 1,
348            trailing_mandatory_break: true,
349        },)
350    );
351    fragments.break_anywhere = true;
352    let last_two = fragments.by_ref().take(2).collect::<Vec<_>>();
353    assert_eq!(
354        last_two,
355        vec![
356            TextFragment {
357                byte_range: Range { start: 6, end: 7 },
358                glyph_range: Range { start: 6, end: 7 },
359                width: 10.,
360                trailing_whitespace_width: 0.,
361                trailing_whitespace_bytes: 0,
362                trailing_mandatory_break: false,
363            },
364            TextFragment {
365                byte_range: Range { start: 7, end: 8 },
366                glyph_range: Range { start: 7, end: 8 },
367                width: 10.,
368                trailing_whitespace_width: 0.,
369                trailing_whitespace_bytes: 0,
370                trailing_mandatory_break: false,
371            },
372        ]
373    );
374}
375
376#[test]
377fn fragment_iterator_leading_nbsp() {
378    let font = FixedTestFont;
379    let text = "A\n\u{00a0}\u{00a0}AB";
380    let shape_buffer = ShapeBuffer::new(
381        &TextLayout { font: &font, letter_spacing: None, line_height: None },
382        text,
383    );
384    let fragments = TextFragmentIterator::new(text, &shape_buffer).collect::<Vec<_>>();
385    assert_eq!(
386        fragments,
387        vec![
388            TextFragment {
389                byte_range: Range { start: 0, end: 1 },
390                glyph_range: Range { start: 0, end: 1 },
391                width: 10.,
392                trailing_whitespace_width: 0.,
393                trailing_whitespace_bytes: 1,
394                trailing_mandatory_break: true,
395            },
396            TextFragment {
397                byte_range: Range { start: 2, end: 8 },
398                glyph_range: Range { start: 2, end: 6 },
399                width: 40.,
400                trailing_whitespace_width: 0.,
401                trailing_whitespace_bytes: 0,
402                trailing_mandatory_break: false,
403            }
404        ]
405    );
406}