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