slint/internal/core/textlayout/fragments.rs
Simon Hausmann 38abeaec41 text handling: clean up font traits
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.
2022-05-16 08:21:14 +02:00

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,
},
]
);
}