slint/internal/core/textlayout/shaping.rs
Olivier Goffart c98d234b9e Janitor: Always use `#![no_std] for runtime lib
And call `extern crate std` when the feature is enabled.
I've read this is the good practice on how to do it.
So that the std prelude is no longer included automatically.
There is then less difference between std and and no-std build which
should avoid surprises in the CI when we use things from the prelude.

The downside is that there is a bit of churn in the tests
2025-01-27 19:22:00 +01:00

397 lines
14 KiB
Rust

// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
use alloc::vec::Vec;
use core::ops::Range;
use super::TextLayout;
/// This struct describes a glyph from shaping to rendering. This includes the relative shaping
/// offsets, advance (in abstract lengths) and platform specific glyph data.
#[derive(Clone, Default, Debug)]
pub struct Glyph<Length> {
pub advance: Length,
pub offset_x: Length,
pub offset_y: Length,
/// Glyph IDs are font specific identifiers. In TrueType fonts zero indicates the missing glyph, which
/// is mapped to an Option here.
pub glyph_id: Option<core::num::NonZeroU16>,
/// The byte offset back in the original (Rust) string to the character that
/// "produced" this glyph. When one character produces multiple glyphs (for example
/// decomposed ligature), then all glyphs have the same offset.
pub text_byte_offset: usize,
}
/// This trait defines the interface between the text layout and the platform specific
/// mapping of text to glyphs. An implementation of the TextShaper trait must provide
/// metric types (Length, LengthPrimitive), which is used for the line breaking calculation
/// and glyph positioning, as well as an opaque platform specific glyph data type.
///
/// Functionality wise it provides the ability to convert a string into a set of glyphs,
/// each of which has basic metric fields as well as an offset back into the original string.
/// Typically this is implemented by using a general text shaper, which performs an M:N mapping
/// from unicode characters to glyphs, via glyph substitutions and script specific rules. In addition
/// the glyphs may be positioned for the required appearance (such as stacked diacritics).
///
/// Finally, for convenience the TextShaper also provides a single glyph_for_char function, for example
/// used to lookup single glyphs (such as the elision character) as well as additional metrics
/// used for text paragraph layout.
pub trait TextShaper {
type LengthPrimitive: core::ops::Mul
+ core::ops::Div
+ core::ops::Add<Output = Self::LengthPrimitive>
+ core::ops::AddAssign
+ euclid::num::Zero
+ euclid::num::One
+ core::convert::From<i16>
+ Copy
+ core::fmt::Debug;
type Length: euclid::num::Zero
+ core::ops::AddAssign
+ core::ops::Add<Output = Self::Length>
+ core::ops::Sub<Output = Self::Length>
+ Default
+ Clone
+ Copy
+ core::cmp::PartialOrd
+ core::ops::Mul<Self::LengthPrimitive, Output = Self::Length>
+ core::ops::Div<Self::LengthPrimitive, Output = Self::Length>
+ core::fmt::Debug;
// Shapes the given string and emits the result into the given glyphs buffer.
fn shape_text<GlyphStorage: core::iter::Extend<Glyph<Self::Length>>>(
&self,
text: &str,
glyphs: &mut GlyphStorage,
);
fn glyph_for_char(&self, ch: char) -> Option<Glyph<Self::Length>>;
fn max_lines(&self, max_height: Self::Length) -> usize;
}
pub trait FontMetrics<Length: Copy + core::ops::Sub<Output = Length>> {
fn height(&self) -> Length {
self.ascent() - self.descent()
}
fn ascent(&self) -> Length;
fn descent(&self) -> Length;
fn x_height(&self) -> Length;
fn cap_height(&self) -> Length;
}
pub trait AbstractFont: TextShaper + FontMetrics<<Self as TextShaper>::Length> {}
impl<T> AbstractFont for T where T: TextShaper + FontMetrics<<Self as TextShaper>::Length> {}
pub struct ShapeBoundaries<'a> {
text: &'a str,
#[cfg(feature = "unicode-script")]
// TODO: We should do a better analysis to find boundaries for text shaping; including
// boundaries when the bidi level changes or an explicit separator like
// paragraph/lineseparator/space is encountered.
chars: core::str::CharIndices<'a>,
next_boundary_start: Option<usize>,
#[cfg(feature = "unicode-script")]
last_script: Option<unicode_script::Script>,
}
impl<'a> ShapeBoundaries<'a> {
pub fn new(text: &'a str) -> Self {
let next_boundary_start = if !text.is_empty() { Some(0) } else { None };
Self {
text,
#[cfg(feature = "unicode-script")]
chars: text.char_indices(),
next_boundary_start,
#[cfg(feature = "unicode-script")]
last_script: None,
}
}
}
impl<'a> Iterator for ShapeBoundaries<'a> {
type Item = usize;
#[cfg(feature = "unicode-script")]
fn next(&mut self) -> Option<Self::Item> {
self.next_boundary_start?;
let (next_offset, script) = loop {
match self.chars.next() {
Some((byte_offset, ch)) => {
use unicode_script::UnicodeScript;
let next_script = ch.script();
let previous_script = *self.last_script.get_or_insert(next_script);
if next_script == previous_script {
continue;
}
if matches!(
next_script,
unicode_script::Script::Unknown
| unicode_script::Script::Common
| unicode_script::Script::Inherited,
) {
continue;
}
break (Some(byte_offset), Some(next_script));
}
None => {
break (None, None);
}
}
};
self.last_script = script;
self.next_boundary_start = next_offset;
Some(self.next_boundary_start.unwrap_or(self.text.len()))
}
#[cfg(not(feature = "unicode-script"))]
fn next(&mut self) -> Option<Self::Item> {
match self.next_boundary_start {
Some(_) => {
self.next_boundary_start = None;
Some(self.text.len())
}
None => None,
}
}
}
#[derive(Debug)]
pub struct TextRun {
pub byte_range: Range<usize>,
pub glyph_range: Range<usize>,
// TODO: direction, etc.
}
pub struct ShapeBuffer<Length> {
pub glyphs: Vec<Glyph<Length>>,
pub text_runs: Vec<TextRun>,
}
impl<Length> ShapeBuffer<Length> {
pub fn new<Font>(layout: &TextLayout<Font>, text: &str) -> Self
where
Font: AbstractFont<Length = Length>,
Length: Copy + core::ops::AddAssign,
{
let mut glyphs = Vec::new();
let text_runs = ShapeBoundaries::new(text)
.scan(0, |run_start, run_end| {
let glyphs_start = glyphs.len();
layout.font.shape_text(&text[*run_start..run_end], &mut glyphs);
if let Some(letter_spacing) = layout.letter_spacing {
if glyphs.len() > glyphs_start {
let mut last_byte_offset = glyphs[glyphs_start].text_byte_offset;
for index in glyphs_start + 1..glyphs.len() {
let current_glyph_byte_offset = glyphs[index].text_byte_offset;
if current_glyph_byte_offset != last_byte_offset {
let previous_glyph = &mut glyphs[index - 1];
previous_glyph.advance += letter_spacing;
}
last_byte_offset = current_glyph_byte_offset;
}
glyphs.last_mut().unwrap().advance += letter_spacing;
}
}
let run = TextRun {
byte_range: Range { start: *run_start, end: run_end },
glyph_range: Range { start: glyphs_start, end: glyphs.len() },
};
*run_start = run_end;
Some(run)
})
.collect();
Self { glyphs, text_runs }
}
}
#[test]
fn test_shape_boundaries_simple() {
{
let simple_text = "Hello World";
let mut itemizer = ShapeBoundaries::new(simple_text);
assert_eq!(itemizer.next(), Some(simple_text.len()));
assert_eq!(itemizer.next(), None);
}
}
#[test]
fn test_shape_boundaries_empty() {
{
let mut itemizer = ShapeBoundaries::new("");
assert_eq!(itemizer.next(), None);
}
}
#[test]
#[cfg_attr(
not(feature = "unicode-script"),
ignore = "Not supported without the unicode-script feature"
)]
fn test_shape_boundaries_script_change() {
{
let text = "abc🍌🐒defதோசை.";
let mut itemizer = ShapeBoundaries::new(text).scan(0, |start, end| {
let str = &text[*start..end];
*start = end;
Some(str)
});
assert_eq!(itemizer.next(), Some("abc🍌🐒def"));
assert_eq!(itemizer.next(), Some("தோசை."));
assert_eq!(itemizer.next(), None);
}
}
#[cfg(test)]
impl<'a> TextShaper for &rustybuzz::Face<'a> {
type LengthPrimitive = f32;
type Length = f32;
fn shape_text<GlyphStorage: std::iter::Extend<Glyph<f32>>>(
&self,
text: &str,
glyphs: &mut GlyphStorage,
) {
let mut buffer = rustybuzz::UnicodeBuffer::new();
buffer.push_str(text);
let glyph_buffer = rustybuzz::shape(self, &[], buffer);
let output_glyph_generator =
glyph_buffer.glyph_infos().iter().zip(glyph_buffer.glyph_positions().iter()).map(
|(info, position)| {
let mut out_glyph = Glyph::default();
out_glyph.glyph_id = core::num::NonZeroU16::new(info.glyph_id as u16);
out_glyph.offset_x = position.x_offset as _;
out_glyph.offset_y = position.y_offset as _;
out_glyph.advance = position.x_advance as _;
out_glyph.text_byte_offset = info.cluster as usize;
out_glyph
},
);
// Cannot return impl Iterator, so extend argument instead
glyphs.extend(output_glyph_generator);
}
fn glyph_for_char(&self, _ch: char) -> Option<Glyph<f32>> {
todo!()
}
fn max_lines(&self, max_height: f32) -> usize {
(max_height / self.height()).floor() as _
}
}
#[cfg(test)]
impl<'a> FontMetrics<f32> for &rustybuzz::Face<'a> {
fn ascent(&self) -> f32 {
self.ascender() as _
}
fn descent(&self) -> f32 {
self.descender() as _
}
fn x_height(&self) -> f32 {
rustybuzz::ttf_parser::Face::x_height(self).unwrap_or_default() as _
}
fn cap_height(&self) -> f32 {
rustybuzz::ttf_parser::Face::capital_height(self).unwrap_or_default() as _
}
}
#[cfg(test)]
fn with_dejavu_font<R>(mut callback: impl FnMut(&rustybuzz::Face<'_>) -> R) -> Option<R> {
let mut fontdb = fontdb::Database::new();
let dejavu_path: std::path::PathBuf =
[env!("CARGO_MANIFEST_DIR"), "..", "common", "sharedfontdb", "DejaVuSans.ttf"]
.iter()
.collect();
fontdb.load_font_file(dejavu_path).expect("unable to load test dejavu font");
let font_id = fontdb.faces().next().unwrap().id;
fontdb.with_face_data(font_id, |data, font_index| {
let face =
rustybuzz::Face::from_slice(data, font_index).expect("unable to parse dejavu font");
callback(&face)
})
}
#[test]
fn test_shaping() {
use std::num::NonZeroU16;
use TextShaper;
with_dejavu_font(|face| {
{
let mut shaped_glyphs = Vec::new();
// two glyph clusters: ā́b
face.shape_text("a\u{0304}\u{0301}b", &mut shaped_glyphs);
assert_eq!(shaped_glyphs.len(), 3);
assert_eq!(shaped_glyphs[0].glyph_id, NonZeroU16::new(195));
assert_eq!(shaped_glyphs[0].text_byte_offset, 0);
assert_eq!(shaped_glyphs[1].glyph_id, NonZeroU16::new(690));
assert_eq!(shaped_glyphs[1].text_byte_offset, 0);
assert_eq!(shaped_glyphs[2].glyph_id, NonZeroU16::new(69));
assert_eq!(shaped_glyphs[2].text_byte_offset, 5);
}
{
let mut shaped_glyphs = Vec::new();
// two glyph clusters: ā́b
face.shape_text("a b", &mut shaped_glyphs);
assert_eq!(shaped_glyphs.len(), 3);
assert_eq!(shaped_glyphs[0].glyph_id, NonZeroU16::new(68));
assert_eq!(shaped_glyphs[0].text_byte_offset, 0);
assert_eq!(shaped_glyphs[1].text_byte_offset, 1);
assert_eq!(shaped_glyphs[2].glyph_id, NonZeroU16::new(69));
assert_eq!(shaped_glyphs[2].text_byte_offset, 2);
}
});
}
#[test]
fn test_letter_spacing() {
use TextShaper;
with_dejavu_font(|face| {
// two glyph clusters: ā́b
let text = "a\u{0304}\u{0301}b";
let advances = {
let mut shaped_glyphs = Vec::new();
face.shape_text(text, &mut shaped_glyphs);
assert_eq!(shaped_glyphs.len(), 3);
shaped_glyphs.iter().map(|g| g.advance).collect::<Vec<_>>()
};
let layout = TextLayout { font: &face, letter_spacing: Some(20.) };
let buffer = ShapeBuffer::new(&layout, text);
assert_eq!(buffer.glyphs.len(), advances.len());
let mut expected_advances = advances;
expected_advances[1] += layout.letter_spacing.unwrap();
*expected_advances.last_mut().unwrap() += layout.letter_spacing.unwrap();
assert_eq!(
buffer.glyphs.iter().map(|glyph| glyph.advance).collect::<Vec<_>>(),
expected_advances
);
});
}