mirror of
https://github.com/slint-ui/slint.git
synced 2025-09-04 09:30:34 +00:00

Remove platform glyph abstraction, just use non-zero u16 as glyph index like everyone else. As a bonus, this reduces the memory consumption of the glyph buffers per glyph from a size of a pointer to the size of the u16.
301 lines
10 KiB
Rust
301 lines
10 KiB
Rust
// Copyright © SixtyFPS GmbH <info@slint-ui.com>
|
|
// 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<Length> {
|
|
pub byte_range: Range<usize>,
|
|
pub glyph_range: Range<usize>,
|
|
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<Length>) -> 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<Length>;
|
|
|
|
fn next(&mut self) -> Option<Self::Item> {
|
|
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::<Vec<_>>();
|
|
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::<Vec<_>>();
|
|
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::<Vec<_>>();
|
|
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::<Vec<_>>();
|
|
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::<Vec<_>>();
|
|
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::<Vec<_>>();
|
|
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,
|
|
},
|
|
]
|
|
);
|
|
}
|