// Copyright © SixtyFPS GmbH // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-commercial use core::ops::Range; use euclid::num::Zero; use super::glyphclusters::GlyphClusterIterator; use super::{BreakOpportunity, LineBreakIterator, ShapeBuffer}; #[derive(Debug, PartialEq, Eq, Default)] pub struct TextFragment { pub byte_range: Range, pub glyph_range: Range, pub width: Length, pub trailing_whitespace_width: Length, pub trailing_mandatory_break: bool, } #[derive(Clone)] pub struct TextFragmentIterator<'a, Length> { line_breaks: LineBreakIterator<'a>, glyph_clusters: GlyphClusterIterator<'a, Length>, text_len: usize, pub break_anywhere: bool, } impl<'a, Length> TextFragmentIterator<'a, Length> { pub fn new(text: &'a str, shape_buffer: &'a ShapeBuffer) -> Self { Self { line_breaks: LineBreakIterator::new(text), glyph_clusters: GlyphClusterIterator::new(text, shape_buffer), text_len: text.len(), break_anywhere: false, } } } impl<'a, Length: Clone + Default + core::ops::AddAssign + Zero + Copy> Iterator for TextFragmentIterator<'a, Length> { type Item = TextFragment; fn next(&mut self) -> Option { let first_glyph_cluster = self.glyph_clusters.next()?; let mut fragment = Self::Item::default(); let next_break_offset = if self.break_anywhere { 0 } else if let Some((next_break_offset, break_type)) = self.line_breaks.next() { if matches!(break_type, BreakOpportunity::Mandatory) { fragment.trailing_mandatory_break = true; } next_break_offset } else { self.text_len }; if first_glyph_cluster.is_whitespace { fragment.trailing_whitespace_width = first_glyph_cluster.width; } else { fragment.width = first_glyph_cluster.width; fragment.byte_range = first_glyph_cluster.byte_range.clone(); } let mut last_glyph_cluster = first_glyph_cluster.clone(); while last_glyph_cluster.byte_range.end < next_break_offset { let next_glyph_cluster = match self.glyph_clusters.next() { Some(cluster) => cluster, None => break, }; if next_glyph_cluster.is_line_or_paragraph_separator { break; } if next_glyph_cluster.is_whitespace { fragment.trailing_whitespace_width += next_glyph_cluster.width; } else { // transition from whitespace to characters by treating previous trailing whitespace // as regular characters if last_glyph_cluster.is_whitespace { fragment.width += core::mem::take(&mut fragment.trailing_whitespace_width); fragment.width += next_glyph_cluster.width; fragment.byte_range.end = next_glyph_cluster.byte_range.end; } else { fragment.width += next_glyph_cluster.width; fragment.byte_range.end = next_glyph_cluster.byte_range.end; } } last_glyph_cluster = next_glyph_cluster.clone(); } fragment.glyph_range = Range { start: first_glyph_cluster.glyph_range.start, end: last_glyph_cluster.glyph_range.end, }; Some(fragment) } } #[cfg(test)] use super::{FixedTestFont, TextLayout}; #[test] fn fragment_iterator_simple() { let font = FixedTestFont; let text = "H WX"; let shape_buffer = ShapeBuffer::new(&TextLayout { font: &font, letter_spacing: None }, text); let fragments = TextFragmentIterator::new(text, &shape_buffer).collect::>(); let expected = vec![ TextFragment { byte_range: Range { start: 0, end: 1 }, glyph_range: Range { start: 0, end: 2 }, width: 10., trailing_whitespace_width: 10., trailing_mandatory_break: false, }, TextFragment { byte_range: Range { start: 2, end: text.len() }, glyph_range: Range { start: 2, end: text.len() }, width: 20., trailing_whitespace_width: 0., trailing_mandatory_break: false, }, ]; assert_eq!(fragments, expected); } #[test] fn fragment_iterator_simple_v2() { let font = FixedTestFont; let text = "Hello World"; let shape_buffer = ShapeBuffer::new(&TextLayout { font: &font, letter_spacing: None }, text); let fragments = TextFragmentIterator::new(text, &shape_buffer).collect::>(); let expected = vec![ TextFragment { byte_range: Range { start: 0, end: 5 }, glyph_range: Range { start: 0, end: 6 }, width: 50., trailing_whitespace_width: 10., trailing_mandatory_break: false, }, TextFragment { byte_range: Range { start: 6, end: text.len() }, glyph_range: Range { start: 6, end: text.len() }, width: 10. * (text.len() - 6) as f32, trailing_whitespace_width: 0., trailing_mandatory_break: false, }, ]; assert_eq!(fragments, expected); } #[test] fn fragment_iterator_forced_break() { let font = FixedTestFont; let text = "H\nW"; let shape_buffer = ShapeBuffer::new(&TextLayout { font: &font, letter_spacing: None }, text); let fragments = TextFragmentIterator::new(text, &shape_buffer).collect::>(); assert_eq!( fragments, vec![ TextFragment { byte_range: Range { start: 0, end: 1 }, glyph_range: Range { start: 0, end: 1 }, width: 10., trailing_whitespace_width: 0., trailing_mandatory_break: true, }, TextFragment { byte_range: Range { start: 2, end: 3 }, glyph_range: Range { start: 2, end: 3 }, width: 10., trailing_whitespace_width: 0., trailing_mandatory_break: false, }, ] ); } #[test] fn fragment_iterator_forced_break_multi() { let font = FixedTestFont; let text = "H\n\n\nW"; let shape_buffer = ShapeBuffer::new(&TextLayout { font: &font, letter_spacing: None }, text); let fragments = TextFragmentIterator::new(text, &shape_buffer).collect::>(); assert_eq!( fragments, vec![ TextFragment { byte_range: Range { start: 0, end: 1 }, glyph_range: Range { start: 0, end: 1 }, width: 10., trailing_whitespace_width: 0., trailing_mandatory_break: true, }, TextFragment { byte_range: Range { start: 0, end: 0 }, glyph_range: Range { start: 2, end: 3 }, width: 0., trailing_whitespace_width: 10., trailing_mandatory_break: true, }, TextFragment { byte_range: Range { start: 0, end: 0 }, glyph_range: Range { start: 3, end: 4 }, width: 0., trailing_whitespace_width: 10., trailing_mandatory_break: true, }, TextFragment { byte_range: Range { start: 4, end: 5 }, glyph_range: Range { start: 4, end: 5 }, width: 10., trailing_whitespace_width: 0., trailing_mandatory_break: false, }, ] ); } #[test] fn fragment_iterator_nbsp() { let font = FixedTestFont; let text = "X H\u{00a0}W"; let shape_buffer = ShapeBuffer::new(&TextLayout { font: &font, letter_spacing: None }, text); let fragments = TextFragmentIterator::new(text, &shape_buffer).collect::>(); assert_eq!( fragments, vec![ TextFragment { byte_range: Range { start: 0, end: 1 }, glyph_range: Range { start: 0, end: 2 }, width: 10., trailing_whitespace_width: 10., trailing_mandatory_break: false, }, TextFragment { byte_range: Range { start: 2, end: 6 }, glyph_range: Range { start: 2, end: 5 }, width: 30., trailing_whitespace_width: 0., trailing_mandatory_break: false, } ] ); } #[test] fn fragment_iterator_break_anywhere() { let font = FixedTestFont; let text = "AB\nCD\nEF"; let shape_buffer = ShapeBuffer::new(&TextLayout { font: &font, letter_spacing: None }, text); let mut fragments = TextFragmentIterator::new(text, &shape_buffer); assert_eq!( fragments.next(), Some(TextFragment { byte_range: Range { start: 0, end: 2 }, glyph_range: Range { start: 0, end: 2 }, width: 20., trailing_whitespace_width: 0., trailing_mandatory_break: true, }) ); assert_eq!( fragments.next(), Some(TextFragment { byte_range: Range { start: 3, end: 5 }, glyph_range: Range { start: 3, end: 5 }, width: 20., trailing_whitespace_width: 0., trailing_mandatory_break: true, },) ); fragments.break_anywhere = true; let last_two = fragments.by_ref().take(2).collect::>(); assert_eq!( last_two, vec![ TextFragment { byte_range: Range { start: 6, end: 7 }, glyph_range: Range { start: 6, end: 7 }, width: 10., trailing_whitespace_width: 0., trailing_mandatory_break: false, }, TextFragment { byte_range: Range { start: 7, end: 8 }, glyph_range: Range { start: 7, end: 8 }, width: 10., trailing_whitespace_width: 0., trailing_mandatory_break: false, }, ] ); }