i_slint_core/textlayout/
linebreaker.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 crate::items::TextWrap;
9
10use super::fragments::{TextFragment, TextFragmentIterator};
11use super::{ShapeBuffer, TextShaper};
12
13#[derive(Clone, Default, Debug)]
14pub struct TextLine<Length: Default + Clone> {
15    // The range excludes trailing whitespace
16    pub byte_range: Range<usize>,
17    // number of bytes in text after byte_range occupied by trailing whitespace
18    pub trailing_whitespace_bytes: usize,
19    pub(crate) glyph_range: Range<usize>,
20    trailing_whitespace: Length,
21    pub(crate) text_width: Length, // with as occupied by the glyphs
22}
23
24impl<
25        Length: Default + Copy + Clone + Zero + core::ops::Add<Output = Length> + core::cmp::PartialOrd,
26    > TextLine<Length>
27{
28    pub fn line_text<'a>(&self, paragraph: &'a str) -> &'a str {
29        &paragraph[self.byte_range.clone()]
30    }
31
32    pub fn width_including_trailing_whitespace(&self) -> Length {
33        if self.text_width > Length::zero() {
34            self.text_width + self.trailing_whitespace
35        } else {
36            Length::zero()
37        }
38    }
39
40    pub fn is_empty(&self) -> bool {
41        self.byte_range.is_empty()
42    }
43}
44
45impl<Length: Clone + Copy + Default + core::ops::AddAssign> TextLine<Length> {
46    pub fn add_fragment(&mut self, fragment: &TextFragment<Length>) {
47        if self.byte_range.is_empty() {
48            self.byte_range = fragment.byte_range.clone();
49        } else if !fragment.byte_range.is_empty() {
50            self.byte_range.end = fragment.byte_range.end;
51        }
52        if self.glyph_range.is_empty() {
53            self.glyph_range = fragment.glyph_range.clone();
54        } else {
55            self.glyph_range.end = fragment.glyph_range.end;
56        }
57        if !fragment.byte_range.is_empty() {
58            self.text_width += self.trailing_whitespace;
59            self.trailing_whitespace = Length::default();
60            self.trailing_whitespace_bytes = 0;
61        }
62        self.text_width += fragment.width;
63        self.trailing_whitespace += fragment.trailing_whitespace_width;
64        self.trailing_whitespace_bytes += fragment.trailing_whitespace_bytes;
65    }
66}
67
68pub struct TextLineBreaker<'a, Font: TextShaper> {
69    fragments: TextFragmentIterator<'a, Font::Length>,
70    available_width: Option<Font::Length>,
71    current_line: TextLine<Font::Length>,
72    num_emitted_lines: usize,
73    mandatory_line_break_on_next_iteration: bool,
74    max_lines: Option<usize>,
75    text_wrap: TextWrap,
76}
77
78impl<'a, Font: TextShaper> TextLineBreaker<'a, Font> {
79    pub fn new(
80        text: &'a str,
81        shape_buffer: &'a ShapeBuffer<Font::Length>,
82        available_width: Option<Font::Length>,
83        max_lines: Option<usize>,
84        text_wrap: TextWrap,
85    ) -> Self {
86        Self {
87            fragments: TextFragmentIterator::new(text, shape_buffer),
88            available_width,
89            current_line: Default::default(),
90            num_emitted_lines: 0,
91            mandatory_line_break_on_next_iteration: false,
92            max_lines,
93            text_wrap,
94        }
95    }
96}
97
98impl<Font: TextShaper> Iterator for TextLineBreaker<'_, Font> {
99    type Item = TextLine<Font::Length>;
100
101    fn next(&mut self) -> Option<Self::Item> {
102        if let Some(max_lines) = self.max_lines {
103            if self.num_emitted_lines >= max_lines {
104                return None;
105            }
106        }
107
108        if core::mem::take(&mut self.mandatory_line_break_on_next_iteration) {
109            self.num_emitted_lines += 1;
110            return Some(core::mem::take(&mut self.current_line));
111        }
112
113        self.fragments.break_anywhere = false;
114
115        let mut next_line = loop {
116            // Clone the fragment iterator so that we can roll back in case we must break down the first
117            // word with `break_anywhere = true`.
118            let mut fragments = self.fragments.clone();
119
120            let fragment = match fragments.next() {
121                Some(fragment) => fragment,
122                None => {
123                    break None;
124                }
125            };
126
127            // As trailing_mandatory_break is only set if break_anywhere is false, the fragment must
128            // be first processed with break_anywhere = false and if no mandatory break is found, the
129            // loop is re-run with break_anywhere = true when using CharWrap.
130            if self.text_wrap == TextWrap::CharWrap
131                && !fragment.trailing_mandatory_break
132                && !self.fragments.break_anywhere
133            {
134                self.fragments.break_anywhere = true;
135                continue;
136            }
137
138            if let Some(available_width) = self.available_width {
139                if self.current_line.width_including_trailing_whitespace() + fragment.width
140                    > available_width
141                {
142                    if self.current_line.is_empty() {
143                        if !self.fragments.break_anywhere {
144                            // Try again but break anywhere this time. self.fragments is cloned at the beginning
145                            // of the loop.
146                            self.fragments.break_anywhere = true;
147                            continue;
148                        } else {
149                            // Even if we allow to break anywhere, there is still no room for the next fragment.
150                            // Just use it anywhere otherwise we would return many empty lines
151                            self.fragments = fragments;
152                            self.current_line.add_fragment(&fragment);
153                            break Some(core::mem::take(&mut self.current_line));
154                        }
155                    }
156
157                    let next_line = core::mem::take(&mut self.current_line);
158                    self.mandatory_line_break_on_next_iteration = fragment.trailing_mandatory_break;
159
160                    if self.text_wrap != TextWrap::CharWrap
161                        && !fragments.break_anywhere
162                        && fragment.width < available_width
163                    {
164                        self.current_line.add_fragment(&fragment);
165                        self.fragments = fragments;
166                    }
167
168                    break Some(next_line);
169                };
170            }
171
172            self.fragments = fragments;
173            self.current_line.add_fragment(&fragment);
174
175            if fragment.trailing_mandatory_break {
176                break Some(core::mem::take(&mut self.current_line));
177            }
178        };
179
180        // Emit at least one single line
181        if next_line.is_none()
182            && (!self.current_line.byte_range.is_empty() || self.num_emitted_lines == 0)
183        {
184            next_line = Some(core::mem::take(&mut self.current_line));
185        }
186
187        if next_line.is_some() {
188            self.num_emitted_lines += 1;
189        }
190
191        next_line
192    }
193}
194
195#[cfg(test)]
196use super::{FixedTestFont, TextLayout};
197
198#[test]
199fn test_empty_line_break() {
200    let font = FixedTestFont;
201    let text = "";
202    let shape_buffer = ShapeBuffer::new(&TextLayout { font: &font, letter_spacing: None }, text);
203    let lines = TextLineBreaker::<FixedTestFont>::new(
204        text,
205        &shape_buffer,
206        Some(50.),
207        None,
208        TextWrap::WordWrap,
209    )
210    .collect::<std::vec::Vec<_>>();
211    assert_eq!(lines.len(), 1);
212    assert_eq!(lines[0].line_text(text), "");
213}
214
215#[test]
216fn test_basic_line_break_char_wrap() {
217    // The available width is half-way into the next word
218    let font = FixedTestFont;
219    let text = "Hello World";
220    let shape_buffer = ShapeBuffer::new(&TextLayout { font: &font, letter_spacing: None }, text);
221    let lines = TextLineBreaker::<FixedTestFont>::new(
222        text,
223        &shape_buffer,
224        Some(80.),
225        None,
226        TextWrap::CharWrap,
227    )
228    .collect::<std::vec::Vec<_>>();
229    assert_eq!(lines.len(), 2);
230    assert_eq!(lines[0].line_text(text), "Hello Wo");
231    assert_eq!(lines[1].line_text(text), "rld");
232}
233
234#[test]
235fn test_basic_line_break() {
236    let font = FixedTestFont;
237    let text = "Hello World";
238    let shape_buffer = ShapeBuffer::new(&TextLayout { font: &font, letter_spacing: None }, text);
239    let lines = TextLineBreaker::<FixedTestFont>::new(
240        text,
241        &shape_buffer,
242        Some(50.),
243        None,
244        TextWrap::WordWrap,
245    )
246    .collect::<std::vec::Vec<_>>();
247    assert_eq!(lines.len(), 2);
248    assert_eq!(lines[0].line_text(text), "Hello");
249    assert_eq!(lines[1].line_text(text), "World");
250}
251
252#[test]
253fn test_basic_line_break_max_lines() {
254    let font = FixedTestFont;
255    let text = "Hello World";
256    let shape_buffer = ShapeBuffer::new(&TextLayout { font: &font, letter_spacing: None }, text);
257    let lines = TextLineBreaker::<FixedTestFont>::new(
258        text,
259        &shape_buffer,
260        Some(50.),
261        Some(1),
262        TextWrap::WordWrap,
263    )
264    .collect::<std::vec::Vec<_>>();
265    assert_eq!(lines.len(), 1);
266    assert_eq!(lines[0].line_text(text), "Hello");
267}
268
269#[test]
270fn test_linebreak_trailing_space() {
271    let font = FixedTestFont;
272    let text = "Hello              ";
273    let shape_buffer = ShapeBuffer::new(&TextLayout { font: &font, letter_spacing: None }, text);
274    let lines = TextLineBreaker::<FixedTestFont>::new(
275        text,
276        &shape_buffer,
277        Some(50.),
278        None,
279        TextWrap::WordWrap,
280    )
281    .collect::<std::vec::Vec<_>>();
282    assert_eq!(lines.len(), 1);
283    assert_eq!(lines[0].line_text(text), "Hello");
284}
285
286#[test]
287fn test_forced_break() {
288    let font = FixedTestFont;
289    let text = "Hello\nWorld";
290    let shape_buffer = ShapeBuffer::new(&TextLayout { font: &font, letter_spacing: None }, text);
291    let lines =
292        TextLineBreaker::<FixedTestFont>::new(text, &shape_buffer, None, None, TextWrap::WordWrap)
293            .collect::<std::vec::Vec<_>>();
294    assert_eq!(lines.len(), 2);
295    assert_eq!(lines[0].line_text(text), "Hello");
296    assert_eq!(lines[1].line_text(text), "World");
297}
298
299#[test]
300fn test_forced_break_multi() {
301    let font = FixedTestFont;
302    let text = "Hello\n\n\nWorld";
303    let shape_buffer = ShapeBuffer::new(&TextLayout { font: &font, letter_spacing: None }, text);
304    let lines =
305        TextLineBreaker::<FixedTestFont>::new(text, &shape_buffer, None, None, TextWrap::WordWrap)
306            .collect::<std::vec::Vec<_>>();
307    assert_eq!(lines.len(), 4);
308    assert_eq!(lines[0].line_text(text), "Hello");
309    assert_eq!(lines[1].line_text(text), "");
310    assert_eq!(lines[2].line_text(text), "");
311    assert_eq!(lines[3].line_text(text), "World");
312}
313
314#[test]
315fn test_forced_break_multi_char_wrap() {
316    let font = FixedTestFont;
317    let text = "Hello\n\n\nWorld";
318    let shape_buffer = ShapeBuffer::new(&TextLayout { font: &font, letter_spacing: None }, text);
319    let lines = TextLineBreaker::<FixedTestFont>::new(
320        text,
321        &shape_buffer,
322        Some(30.),
323        None,
324        TextWrap::CharWrap,
325    )
326    .collect::<std::vec::Vec<_>>();
327    assert_eq!(lines.len(), 6);
328    assert_eq!(lines[0].line_text(text), "Hel");
329    assert_eq!(lines[1].line_text(text), "lo");
330    assert_eq!(lines[2].line_text(text), "");
331    assert_eq!(lines[3].line_text(text), "");
332    assert_eq!(lines[4].line_text(text), "Wor");
333    assert_eq!(lines[5].line_text(text), "ld");
334}
335
336#[test]
337fn test_forced_break_max_lines() {
338    let font = FixedTestFont;
339    let text = "Hello\n\n\nWorld";
340    let shape_buffer = ShapeBuffer::new(&TextLayout { font: &font, letter_spacing: None }, text);
341    let lines = TextLineBreaker::<FixedTestFont>::new(
342        text,
343        &shape_buffer,
344        None,
345        Some(2),
346        TextWrap::WordWrap,
347    )
348    .collect::<std::vec::Vec<_>>();
349    assert_eq!(lines.len(), 2);
350    assert_eq!(lines[0].line_text(text), "Hello");
351    assert_eq!(lines[1].line_text(text), "");
352}
353
354#[test]
355fn test_nbsp_break() {
356    let font = FixedTestFont;
357    let text = "Ok Hello\u{00a0}World";
358    let shape_buffer = ShapeBuffer::new(&TextLayout { font: &font, letter_spacing: None }, text);
359    let lines = TextLineBreaker::<FixedTestFont>::new(
360        text,
361        &shape_buffer,
362        Some(110.),
363        None,
364        TextWrap::WordWrap,
365    )
366    .collect::<std::vec::Vec<_>>();
367    assert_eq!(lines.len(), 2);
368    assert_eq!(lines[0].line_text(text), "Ok");
369    assert_eq!(lines[1].line_text(text), "Hello\u{00a0}World");
370}
371
372#[test]
373fn test_single_line_multi_break_opportunity() {
374    let font = FixedTestFont;
375    let text = "a b c";
376    let shape_buffer = ShapeBuffer::new(&TextLayout { font: &font, letter_spacing: None }, text);
377    let lines =
378        TextLineBreaker::<FixedTestFont>::new(text, &shape_buffer, None, None, TextWrap::WordWrap)
379            .collect::<std::vec::Vec<_>>();
380    assert_eq!(lines.len(), 1);
381    assert_eq!(lines[0].line_text(text), "a b c");
382}
383
384#[test]
385fn test_basic_line_break_anywhere_fallback() {
386    let font = FixedTestFont;
387    let text = "HelloWorld";
388    let shape_buffer = ShapeBuffer::new(&TextLayout { font: &font, letter_spacing: None }, text);
389    let lines = TextLineBreaker::<FixedTestFont>::new(
390        text,
391        &shape_buffer,
392        Some(50.),
393        None,
394        TextWrap::WordWrap,
395    )
396    .collect::<std::vec::Vec<_>>();
397    assert_eq!(lines.len(), 2);
398    assert_eq!(lines[0].line_text(text), "Hello");
399    assert_eq!(lines[1].line_text(text), "World");
400}
401
402#[test]
403fn test_basic_line_break_anywhere_fallback_multi_line() {
404    let font = FixedTestFont;
405    let text = "HelloWorld\nHelloWorld";
406    let shape_buffer = ShapeBuffer::new(&TextLayout { font: &font, letter_spacing: None }, text);
407    let lines = TextLineBreaker::<FixedTestFont>::new(
408        text,
409        &shape_buffer,
410        Some(50.),
411        None,
412        TextWrap::WordWrap,
413    )
414    .collect::<std::vec::Vec<_>>();
415    assert_eq!(lines.len(), 4);
416    assert_eq!(lines[0].line_text(text), "Hello");
417    assert_eq!(lines[1].line_text(text), "World");
418    assert_eq!(lines[2].line_text(text), "Hello");
419    assert_eq!(lines[3].line_text(text), "World");
420}
421
422#[test]
423fn test_basic_line_break_anywhere_fallback_multi_line_char_wrap() {
424    let font = FixedTestFont;
425    let text = "HelloWorld\nHelloWorld";
426    let shape_buffer = ShapeBuffer::new(&TextLayout { font: &font, letter_spacing: None }, text);
427    let lines = TextLineBreaker::<FixedTestFont>::new(
428        text,
429        &shape_buffer,
430        Some(50.),
431        None,
432        TextWrap::CharWrap,
433    )
434    .collect::<std::vec::Vec<_>>();
435    assert_eq!(lines.len(), 4);
436    assert_eq!(lines[0].line_text(text), "Hello");
437    assert_eq!(lines[1].line_text(text), "World");
438    assert_eq!(lines[2].line_text(text), "Hello");
439    assert_eq!(lines[3].line_text(text), "World");
440}
441
442#[test]
443fn test_basic_line_break_anywhere_fallback_multi_line_v2() {
444    let font = FixedTestFont;
445    let text = "HelloW orldHellow";
446    let shape_buffer = ShapeBuffer::new(&TextLayout { font: &font, letter_spacing: None }, text);
447    let lines = TextLineBreaker::<FixedTestFont>::new(
448        text,
449        &shape_buffer,
450        Some(50.),
451        None,
452        TextWrap::WordWrap,
453    )
454    .collect::<std::vec::Vec<_>>();
455    assert_eq!(lines.len(), 4);
456    assert_eq!(lines[0].line_text(text), "Hello");
457    assert_eq!(lines[1].line_text(text), "W");
458    assert_eq!(lines[2].line_text(text), "orldH");
459    assert_eq!(lines[3].line_text(text), "ellow");
460}
461
462#[test]
463fn test_basic_line_break_anywhere_fallback_max_lines() {
464    let font = FixedTestFont;
465    let text = "HelloW orldHellow";
466    let shape_buffer = ShapeBuffer::new(&TextLayout { font: &font, letter_spacing: None }, text);
467    let lines = TextLineBreaker::<FixedTestFont>::new(
468        text,
469        &shape_buffer,
470        Some(50.),
471        Some(3),
472        TextWrap::WordWrap,
473    )
474    .collect::<std::vec::Vec<_>>();
475    assert_eq!(lines.len(), 3);
476    assert_eq!(lines[0].line_text(text), "Hello");
477    assert_eq!(lines[1].line_text(text), "W");
478    assert_eq!(lines[2].line_text(text), "orldH");
479}
480
481#[test]
482fn test_basic_line_break_space() {
483    // The available width is half-way into the trailing "W"
484    let font = FixedTestFont;
485    let text = "H W";
486    let shape_buffer = ShapeBuffer::new(&TextLayout { font: &font, letter_spacing: None }, text);
487    let lines = TextLineBreaker::<FixedTestFont>::new(
488        text,
489        &shape_buffer,
490        Some(25.),
491        None,
492        TextWrap::WordWrap,
493    )
494    .collect::<std::vec::Vec<_>>();
495    assert_eq!(lines.len(), 2);
496    assert_eq!(lines[0].line_text(text), "H");
497    assert_eq!(lines[1].line_text(text), "W");
498}
499
500#[test]
501fn test_basic_line_break_space_char_wrap() {
502    // The available width is half-way into the trailing "W"
503    let font = FixedTestFont;
504    let text = "H W";
505    let shape_buffer = ShapeBuffer::new(&TextLayout { font: &font, letter_spacing: None }, text);
506    let lines = TextLineBreaker::<FixedTestFont>::new(
507        text,
508        &shape_buffer,
509        Some(25.),
510        None,
511        TextWrap::CharWrap,
512    )
513    .collect::<std::vec::Vec<_>>();
514    assert_eq!(lines.len(), 2);
515    assert_eq!(lines[0].line_text(text), "H");
516    assert_eq!(lines[1].line_text(text), "W");
517}
518
519#[test]
520fn test_basic_line_break_space_v2() {
521    // The available width is half-way into the trailing "W"
522    let font = FixedTestFont;
523    let text = "B B W";
524    let shape_buffer = ShapeBuffer::new(&TextLayout { font: &font, letter_spacing: None }, text);
525    let lines = TextLineBreaker::<FixedTestFont>::new(
526        text,
527        &shape_buffer,
528        Some(45.),
529        None,
530        TextWrap::WordWrap,
531    )
532    .collect::<std::vec::Vec<_>>();
533    assert_eq!(lines.len(), 2);
534    assert_eq!(lines[0].line_text(text), "B B");
535    assert_eq!(lines[1].line_text(text), "W");
536}
537
538#[test]
539fn test_basic_line_break_space_v3() {
540    // The available width is half-way into the trailing "W"
541    let font = FixedTestFont;
542    let text = "H   W";
543    let shape_buffer = ShapeBuffer::new(&TextLayout { font: &font, letter_spacing: None }, text);
544    let lines = TextLineBreaker::<FixedTestFont>::new(
545        text,
546        &shape_buffer,
547        Some(15.),
548        None,
549        TextWrap::WordWrap,
550    )
551    .collect::<std::vec::Vec<_>>();
552    assert_eq!(lines.len(), 2);
553    assert_eq!(lines[0].line_text(text), "H");
554    assert_eq!(lines[1].line_text(text), "W");
555}
556
557#[test]
558fn test_basic_line_break_space_v4() {
559    // The available width is half-way into the trailing space
560    let font = FixedTestFont;
561    let text = "H W  H  ";
562    let shape_buffer = ShapeBuffer::new(&TextLayout { font: &font, letter_spacing: None }, text);
563    let lines = TextLineBreaker::<FixedTestFont>::new(
564        text,
565        &shape_buffer,
566        Some(65.),
567        None,
568        TextWrap::WordWrap,
569    )
570    .collect::<std::vec::Vec<_>>();
571    assert_eq!(lines.len(), 1);
572    assert_eq!(lines[0].line_text(text), "H W  H");
573}
574
575#[test]
576fn test_line_width_with_whitespace() {
577    let font = FixedTestFont;
578    let text = "Hello World";
579    let shape_buffer = ShapeBuffer::new(&TextLayout { font: &font, letter_spacing: None }, text);
580    let lines = TextLineBreaker::<FixedTestFont>::new(
581        text,
582        &shape_buffer,
583        Some(200.),
584        None,
585        TextWrap::WordWrap,
586    )
587    .collect::<std::vec::Vec<_>>();
588    assert_eq!(lines.len(), 1);
589    assert_eq!(lines[0].text_width, text.len() as f32 * 10.);
590}
591
592#[test]
593fn zero_width() {
594    let font = FixedTestFont;
595    let text = "He\nHe o";
596    let shape_buffer = ShapeBuffer::new(&TextLayout { font: &font, letter_spacing: None }, text);
597    let lines = TextLineBreaker::<FixedTestFont>::new(
598        text,
599        &shape_buffer,
600        Some(0.0001),
601        None,
602        TextWrap::WordWrap,
603    )
604    .map(|t| t.line_text(text))
605    .collect::<std::vec::Vec<_>>();
606    assert_eq!(lines, ["H", "e", "", "H", "e", "o"]);
607}
608
609#[test]
610fn zero_width_char_wrap() {
611    let font = FixedTestFont;
612    let text = "He\nHe o";
613    let shape_buffer = ShapeBuffer::new(&TextLayout { font: &font, letter_spacing: None }, text);
614    let lines = TextLineBreaker::<FixedTestFont>::new(
615        text,
616        &shape_buffer,
617        Some(0.0001),
618        None,
619        TextWrap::CharWrap,
620    )
621    .map(|t| t.line_text(text))
622    .collect::<std::vec::Vec<_>>();
623    assert_eq!(lines, ["H", "e", "", "H", "e", "o"]);
624}
625
626#[test]
627fn char_wrap_sentences() {
628    let font = FixedTestFont;
629    let text = "Hello world\nHow are you?";
630    let shape_buffer = ShapeBuffer::new(&TextLayout { font: &font, letter_spacing: None }, text);
631    let lines = TextLineBreaker::<FixedTestFont>::new(
632        text,
633        &shape_buffer,
634        Some(80.),
635        None,
636        TextWrap::CharWrap,
637    )
638    .map(|t| t.line_text(text))
639    .collect::<std::vec::Vec<_>>();
640    assert_eq!(lines, ["Hello wo", "rld", "How are", "you?"]);
641}