mirror of
https://github.com/slint-ui/slint.git
synced 2025-10-01 22:31:14 +00:00

Separate the text shaping functionality from font metrics by having a FontMetrics trait next to the TextShaper. AbstractFont is the combining super trait. This allows eliminating the font height member from TextParagraphLayout and improving the overall naming of fields and types. Finally, this prepares the API for composability of TextShaper for font fallback handling.
297 lines
10 KiB
Rust
297 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, PlatformGlyph> {
|
|
line_breaks: LineBreakIterator<'a>,
|
|
glyph_clusters: GlyphClusterIterator<'a, Length, PlatformGlyph>,
|
|
text_len: usize,
|
|
pub break_anywhere: bool,
|
|
}
|
|
|
|
impl<'a, Length, PlatformGlyph> TextFragmentIterator<'a, Length, PlatformGlyph> {
|
|
pub fn new(text: &'a str, shape_buffer: &'a ShapeBuffer<Length, PlatformGlyph>) -> 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, PlatformGlyph> Iterator
|
|
for TextFragmentIterator<'a, Length, PlatformGlyph>
|
|
{
|
|
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_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: 2 },
|
|
width: 10.,
|
|
trailing_whitespace_width: 10.,
|
|
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: 2 },
|
|
width: 10.,
|
|
trailing_whitespace_width: 10.,
|
|
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: 3 },
|
|
width: 20.,
|
|
trailing_whitespace_width: 10.,
|
|
trailing_mandatory_break: true,
|
|
})
|
|
);
|
|
assert_eq!(
|
|
fragments.next(),
|
|
Some(TextFragment {
|
|
byte_range: Range { start: 3, end: 5 },
|
|
glyph_range: Range { start: 3, end: 6 },
|
|
width: 20.,
|
|
trailing_whitespace_width: 10.,
|
|
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,
|
|
},
|
|
]
|
|
);
|
|
}
|