// Copyright © SixtyFPS GmbH // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 #[cfg(not(feature = "std"))] 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 { 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, /// 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 + core::ops::AddAssign + euclid::num::Zero + euclid::num::One + core::convert::From + Copy + core::fmt::Debug; type Length: euclid::num::Zero + core::ops::AddAssign + core::ops::Add + core::ops::Sub + Default + Clone + Copy + core::cmp::PartialOrd + core::ops::Mul + core::ops::Div + core::fmt::Debug; // Shapes the given string and emits the result into the given glyphs buffer. fn shape_text>>( &self, text: &str, glyphs: &mut GlyphStorage, ); fn glyph_for_char(&self, ch: char) -> Option>; fn max_lines(&self, max_height: Self::Length) -> usize; } pub trait FontMetrics> { fn height(&self) -> Length { self.ascent() - self.descent() } fn ascent(&self) -> Length; fn descent(&self) -> Length; } pub trait AbstractFont: TextShaper + FontMetrics<::Length> {} impl AbstractFont for T where T: TextShaper + FontMetrics<::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, #[cfg(feature = "unicode-script")] last_script: Option, } 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.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 { 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, pub glyph_range: Range, // TODO: direction, etc. } pub struct ShapeBuffer { pub glyphs: Vec>, pub text_runs: Vec, } impl ShapeBuffer { pub fn new(layout: &TextLayout, text: &str) -> Self where Font: AbstractFont, 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>>( &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> { todo!() } fn max_lines(&self, max_height: f32) -> usize { (max_height / self.height()).floor() as _ } } #[cfg(test)] impl<'a> FontMetrics for &rustybuzz::Face<'a> { fn ascent(&self) -> f32 { self.ascender() as _ } fn descent(&self) -> f32 { self.descender() as _ } } #[cfg(test)] fn with_dejavu_font(mut callback: impl FnMut(&rustybuzz::Face<'_>) -> R) -> Option { 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::>() }; 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::>(), expected_advances ); }); }