| 1 | // Copyright © SixtyFPS GmbH <info@slint.dev> |
| 2 | // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 |
| 3 | |
| 4 | use alloc::vec::Vec; |
| 5 | use core::ops::Range; |
| 6 | |
| 7 | use super::TextLayout; |
| 8 | |
| 9 | /// This struct describes a glyph from shaping to rendering. This includes the relative shaping |
| 10 | /// offsets, advance (in abstract lengths) and platform specific glyph data. |
| 11 | #[derive (Clone, Default, Debug)] |
| 12 | pub struct Glyph<Length> { |
| 13 | pub advance: Length, |
| 14 | pub offset_x: Length, |
| 15 | pub offset_y: Length, |
| 16 | /// Glyph IDs are font specific identifiers. In TrueType fonts zero indicates the missing glyph, which |
| 17 | /// is mapped to an Option here. |
| 18 | pub glyph_id: Option<core::num::NonZeroU16>, |
| 19 | /// The byte offset back in the original (Rust) string to the character that |
| 20 | /// "produced" this glyph. When one character produces multiple glyphs (for example |
| 21 | /// decomposed ligature), then all glyphs have the same offset. |
| 22 | pub text_byte_offset: usize, |
| 23 | } |
| 24 | |
| 25 | /// This trait defines the interface between the text layout and the platform specific |
| 26 | /// mapping of text to glyphs. An implementation of the TextShaper trait must provide |
| 27 | /// metric types (Length, LengthPrimitive), which is used for the line breaking calculation |
| 28 | /// and glyph positioning, as well as an opaque platform specific glyph data type. |
| 29 | /// |
| 30 | /// Functionality wise it provides the ability to convert a string into a set of glyphs, |
| 31 | /// each of which has basic metric fields as well as an offset back into the original string. |
| 32 | /// Typically this is implemented by using a general text shaper, which performs an M:N mapping |
| 33 | /// from unicode characters to glyphs, via glyph substitutions and script specific rules. In addition |
| 34 | /// the glyphs may be positioned for the required appearance (such as stacked diacritics). |
| 35 | /// |
| 36 | /// Finally, for convenience the TextShaper also provides a single glyph_for_char function, for example |
| 37 | /// used to lookup single glyphs (such as the elision character) as well as additional metrics |
| 38 | /// used for text paragraph layout. |
| 39 | pub trait TextShaper { |
| 40 | type LengthPrimitive: core::ops::Mul |
| 41 | + core::ops::Div |
| 42 | + core::ops::Add<Output = Self::LengthPrimitive> |
| 43 | + core::ops::AddAssign |
| 44 | + euclid::num::Zero |
| 45 | + euclid::num::One |
| 46 | + core::convert::From<i16> |
| 47 | + Copy |
| 48 | + core::fmt::Debug; |
| 49 | type Length: euclid::num::Zero |
| 50 | + core::ops::AddAssign |
| 51 | + core::ops::Add<Output = Self::Length> |
| 52 | + core::ops::Sub<Output = Self::Length> |
| 53 | + Default |
| 54 | + Clone |
| 55 | + Copy |
| 56 | + core::cmp::PartialOrd |
| 57 | + core::ops::Mul<Self::LengthPrimitive, Output = Self::Length> |
| 58 | + core::ops::Div<Self::LengthPrimitive, Output = Self::Length> |
| 59 | + core::fmt::Debug; |
| 60 | // Shapes the given string and emits the result into the given glyphs buffer. |
| 61 | fn shape_text<GlyphStorage: core::iter::Extend<Glyph<Self::Length>>>( |
| 62 | &self, |
| 63 | text: &str, |
| 64 | glyphs: &mut GlyphStorage, |
| 65 | ); |
| 66 | fn glyph_for_char(&self, ch: char) -> Option<Glyph<Self::Length>>; |
| 67 | fn max_lines(&self, max_height: Self::Length) -> usize; |
| 68 | } |
| 69 | |
| 70 | pub trait FontMetrics<Length: Copy + core::ops::Sub<Output = Length>> { |
| 71 | fn height(&self) -> Length { |
| 72 | self.ascent() - self.descent() |
| 73 | } |
| 74 | fn ascent(&self) -> Length; |
| 75 | fn descent(&self) -> Length; |
| 76 | fn x_height(&self) -> Length; |
| 77 | fn cap_height(&self) -> Length; |
| 78 | } |
| 79 | |
| 80 | pub trait AbstractFont: TextShaper + FontMetrics<<Self as TextShaper>::Length> {} |
| 81 | |
| 82 | impl<T> AbstractFont for T where T: TextShaper + FontMetrics<<Self as TextShaper>::Length> {} |
| 83 | |
| 84 | pub struct ShapeBoundaries<'a> { |
| 85 | text: &'a str, |
| 86 | #[cfg (feature = "unicode-script" )] |
| 87 | // TODO: We should do a better analysis to find boundaries for text shaping; including |
| 88 | // boundaries when the bidi level changes or an explicit separator like |
| 89 | // paragraph/lineseparator/space is encountered. |
| 90 | chars: core::str::CharIndices<'a>, |
| 91 | next_boundary_start: Option<usize>, |
| 92 | #[cfg (feature = "unicode-script" )] |
| 93 | last_script: Option<unicode_script::Script>, |
| 94 | } |
| 95 | |
| 96 | impl<'a> ShapeBoundaries<'a> { |
| 97 | pub fn new(text: &'a str) -> Self { |
| 98 | let next_boundary_start: Option = if !text.is_empty() { Some(0) } else { None }; |
| 99 | Self { |
| 100 | text, |
| 101 | #[cfg (feature = "unicode-script" )] |
| 102 | chars: text.char_indices(), |
| 103 | next_boundary_start, |
| 104 | #[cfg (feature = "unicode-script" )] |
| 105 | last_script: None, |
| 106 | } |
| 107 | } |
| 108 | } |
| 109 | |
| 110 | impl Iterator for ShapeBoundaries<'_> { |
| 111 | type Item = usize; |
| 112 | |
| 113 | #[cfg (feature = "unicode-script" )] |
| 114 | fn next(&mut self) -> Option<Self::Item> { |
| 115 | self.next_boundary_start?; |
| 116 | |
| 117 | let (next_offset, script) = loop { |
| 118 | match self.chars.next() { |
| 119 | Some((byte_offset, ch)) => { |
| 120 | use unicode_script::UnicodeScript; |
| 121 | let next_script = ch.script(); |
| 122 | let previous_script = *self.last_script.get_or_insert(next_script); |
| 123 | |
| 124 | if next_script == previous_script { |
| 125 | continue; |
| 126 | } |
| 127 | if matches!( |
| 128 | next_script, |
| 129 | unicode_script::Script::Unknown |
| 130 | | unicode_script::Script::Common |
| 131 | | unicode_script::Script::Inherited, |
| 132 | ) { |
| 133 | continue; |
| 134 | } |
| 135 | |
| 136 | break (Some(byte_offset), Some(next_script)); |
| 137 | } |
| 138 | None => { |
| 139 | break (None, None); |
| 140 | } |
| 141 | } |
| 142 | }; |
| 143 | |
| 144 | self.last_script = script; |
| 145 | self.next_boundary_start = next_offset; |
| 146 | |
| 147 | Some(self.next_boundary_start.unwrap_or(self.text.len())) |
| 148 | } |
| 149 | |
| 150 | #[cfg (not(feature = "unicode-script" ))] |
| 151 | fn next(&mut self) -> Option<Self::Item> { |
| 152 | match self.next_boundary_start { |
| 153 | Some(_) => { |
| 154 | self.next_boundary_start = None; |
| 155 | Some(self.text.len()) |
| 156 | } |
| 157 | None => None, |
| 158 | } |
| 159 | } |
| 160 | } |
| 161 | |
| 162 | #[derive (Debug)] |
| 163 | pub struct TextRun { |
| 164 | pub byte_range: Range<usize>, |
| 165 | pub glyph_range: Range<usize>, |
| 166 | // TODO: direction, etc. |
| 167 | } |
| 168 | |
| 169 | pub struct ShapeBuffer<Length> { |
| 170 | pub glyphs: Vec<Glyph<Length>>, |
| 171 | pub text_runs: Vec<TextRun>, |
| 172 | } |
| 173 | |
| 174 | impl<Length> ShapeBuffer<Length> { |
| 175 | pub fn new<Font>(layout: &TextLayout<Font>, text: &str) -> Self |
| 176 | where |
| 177 | Font: AbstractFont<Length = Length>, |
| 178 | Length: Copy + core::ops::AddAssign, |
| 179 | { |
| 180 | let mut glyphs = Vec::new(); |
| 181 | let text_runs = ShapeBoundaries::new(text) |
| 182 | .scan(0, |run_start, run_end| { |
| 183 | let glyphs_start = glyphs.len(); |
| 184 | |
| 185 | layout.font.shape_text(&text[*run_start..run_end], &mut glyphs); |
| 186 | |
| 187 | if let Some(letter_spacing) = layout.letter_spacing { |
| 188 | if glyphs.len() > glyphs_start { |
| 189 | let mut last_byte_offset = glyphs[glyphs_start].text_byte_offset; |
| 190 | for index in glyphs_start + 1..glyphs.len() { |
| 191 | let current_glyph_byte_offset = glyphs[index].text_byte_offset; |
| 192 | if current_glyph_byte_offset != last_byte_offset { |
| 193 | let previous_glyph = &mut glyphs[index - 1]; |
| 194 | previous_glyph.advance += letter_spacing; |
| 195 | } |
| 196 | last_byte_offset = current_glyph_byte_offset; |
| 197 | } |
| 198 | |
| 199 | glyphs.last_mut().unwrap().advance += letter_spacing; |
| 200 | } |
| 201 | } |
| 202 | |
| 203 | let run = TextRun { |
| 204 | byte_range: Range { start: *run_start, end: run_end }, |
| 205 | glyph_range: Range { start: glyphs_start, end: glyphs.len() }, |
| 206 | }; |
| 207 | *run_start = run_end; |
| 208 | |
| 209 | Some(run) |
| 210 | }) |
| 211 | .collect(); |
| 212 | |
| 213 | Self { glyphs, text_runs } |
| 214 | } |
| 215 | } |
| 216 | |
| 217 | #[test ] |
| 218 | fn test_shape_boundaries_simple() { |
| 219 | { |
| 220 | let simple_text = "Hello World" ; |
| 221 | let mut itemizer = ShapeBoundaries::new(simple_text); |
| 222 | assert_eq!(itemizer.next(), Some(simple_text.len())); |
| 223 | assert_eq!(itemizer.next(), None); |
| 224 | } |
| 225 | } |
| 226 | |
| 227 | #[test ] |
| 228 | fn test_shape_boundaries_empty() { |
| 229 | { |
| 230 | let mut itemizer = ShapeBoundaries::new("" ); |
| 231 | assert_eq!(itemizer.next(), None); |
| 232 | } |
| 233 | } |
| 234 | |
| 235 | #[test ] |
| 236 | #[cfg_attr ( |
| 237 | not(feature = "unicode-script" ), |
| 238 | ignore = "Not supported without the unicode-script feature" |
| 239 | )] |
| 240 | fn test_shape_boundaries_script_change() { |
| 241 | { |
| 242 | let text = "abc🍌🐒defதோசை." ; |
| 243 | let mut itemizer = ShapeBoundaries::new(text).scan(0, |start, end| { |
| 244 | let str = &text[*start..end]; |
| 245 | *start = end; |
| 246 | Some(str) |
| 247 | }); |
| 248 | assert_eq!(itemizer.next(), Some("abc🍌🐒def" )); |
| 249 | assert_eq!(itemizer.next(), Some("தோசை." )); |
| 250 | assert_eq!(itemizer.next(), None); |
| 251 | } |
| 252 | } |
| 253 | |
| 254 | #[cfg (test)] |
| 255 | impl TextShaper for &rustybuzz::Face<'_> { |
| 256 | type LengthPrimitive = f32; |
| 257 | type Length = f32; |
| 258 | fn shape_text<GlyphStorage: std::iter::Extend<Glyph<f32>>>( |
| 259 | &self, |
| 260 | text: &str, |
| 261 | glyphs: &mut GlyphStorage, |
| 262 | ) { |
| 263 | let mut buffer = rustybuzz::UnicodeBuffer::new(); |
| 264 | buffer.push_str(text); |
| 265 | let glyph_buffer = rustybuzz::shape(self, &[], buffer); |
| 266 | |
| 267 | let output_glyph_generator = |
| 268 | glyph_buffer.glyph_infos().iter().zip(glyph_buffer.glyph_positions().iter()).map( |
| 269 | |(info, position)| { |
| 270 | let mut out_glyph = Glyph::default(); |
| 271 | out_glyph.glyph_id = core::num::NonZeroU16::new(info.glyph_id as u16); |
| 272 | out_glyph.offset_x = position.x_offset as _; |
| 273 | out_glyph.offset_y = position.y_offset as _; |
| 274 | out_glyph.advance = position.x_advance as _; |
| 275 | out_glyph.text_byte_offset = info.cluster as usize; |
| 276 | out_glyph |
| 277 | }, |
| 278 | ); |
| 279 | |
| 280 | // Cannot return impl Iterator, so extend argument instead |
| 281 | glyphs.extend(output_glyph_generator); |
| 282 | } |
| 283 | |
| 284 | fn glyph_for_char(&self, _ch: char) -> Option<Glyph<f32>> { |
| 285 | todo!() |
| 286 | } |
| 287 | |
| 288 | fn max_lines(&self, max_height: f32) -> usize { |
| 289 | (max_height / self.height()).floor() as _ |
| 290 | } |
| 291 | } |
| 292 | |
| 293 | #[cfg (test)] |
| 294 | impl FontMetrics<f32> for &rustybuzz::Face<'_> { |
| 295 | fn ascent(&self) -> f32 { |
| 296 | self.ascender() as _ |
| 297 | } |
| 298 | |
| 299 | fn descent(&self) -> f32 { |
| 300 | self.descender() as _ |
| 301 | } |
| 302 | |
| 303 | fn x_height(&self) -> f32 { |
| 304 | rustybuzz::ttf_parser::Face::x_height(self).unwrap_or_default() as _ |
| 305 | } |
| 306 | |
| 307 | fn cap_height(&self) -> f32 { |
| 308 | rustybuzz::ttf_parser::Face::capital_height(self).unwrap_or_default() as _ |
| 309 | } |
| 310 | } |
| 311 | |
| 312 | #[cfg (test)] |
| 313 | fn with_dejavu_font<R>(mut callback: impl FnMut(&rustybuzz::Face<'_>) -> R) -> Option<R> { |
| 314 | let mut fontdb = fontdb::Database::new(); |
| 315 | let dejavu_path: std::path::PathBuf = |
| 316 | [env!("CARGO_MANIFEST_DIR" ), ".." , "common" , "sharedfontdb" , "DejaVuSans.ttf" ] |
| 317 | .iter() |
| 318 | .collect(); |
| 319 | fontdb.load_font_file(dejavu_path).expect("unable to load test dejavu font" ); |
| 320 | let font_id = fontdb.faces().next().unwrap().id; |
| 321 | fontdb.with_face_data(font_id, |data, font_index| { |
| 322 | let face = |
| 323 | rustybuzz::Face::from_slice(data, font_index).expect("unable to parse dejavu font" ); |
| 324 | callback(&face) |
| 325 | }) |
| 326 | } |
| 327 | |
| 328 | #[test ] |
| 329 | fn test_shaping() { |
| 330 | use std::num::NonZeroU16; |
| 331 | use TextShaper; |
| 332 | |
| 333 | with_dejavu_font(|face| { |
| 334 | { |
| 335 | let mut shaped_glyphs = Vec::new(); |
| 336 | // two glyph clusters: ā́b |
| 337 | face.shape_text("a \u{0304}\u{0301}b" , &mut shaped_glyphs); |
| 338 | |
| 339 | assert_eq!(shaped_glyphs.len(), 3); |
| 340 | assert_eq!(shaped_glyphs[0].glyph_id, NonZeroU16::new(195)); |
| 341 | assert_eq!(shaped_glyphs[0].text_byte_offset, 0); |
| 342 | |
| 343 | assert_eq!(shaped_glyphs[1].glyph_id, NonZeroU16::new(690)); |
| 344 | assert_eq!(shaped_glyphs[1].text_byte_offset, 0); |
| 345 | |
| 346 | assert_eq!(shaped_glyphs[2].glyph_id, NonZeroU16::new(69)); |
| 347 | assert_eq!(shaped_glyphs[2].text_byte_offset, 5); |
| 348 | } |
| 349 | |
| 350 | { |
| 351 | let mut shaped_glyphs = Vec::new(); |
| 352 | // two glyph clusters: ā́b |
| 353 | face.shape_text("a b" , &mut shaped_glyphs); |
| 354 | |
| 355 | assert_eq!(shaped_glyphs.len(), 3); |
| 356 | assert_eq!(shaped_glyphs[0].glyph_id, NonZeroU16::new(68)); |
| 357 | assert_eq!(shaped_glyphs[0].text_byte_offset, 0); |
| 358 | |
| 359 | assert_eq!(shaped_glyphs[1].text_byte_offset, 1); |
| 360 | |
| 361 | assert_eq!(shaped_glyphs[2].glyph_id, NonZeroU16::new(69)); |
| 362 | assert_eq!(shaped_glyphs[2].text_byte_offset, 2); |
| 363 | } |
| 364 | }); |
| 365 | } |
| 366 | |
| 367 | #[test ] |
| 368 | fn test_letter_spacing() { |
| 369 | use TextShaper; |
| 370 | |
| 371 | with_dejavu_font(|face| { |
| 372 | // two glyph clusters: ā́b |
| 373 | let text = "a \u{0304}\u{0301}b" ; |
| 374 | let advances = { |
| 375 | let mut shaped_glyphs = Vec::new(); |
| 376 | face.shape_text(text, &mut shaped_glyphs); |
| 377 | |
| 378 | assert_eq!(shaped_glyphs.len(), 3); |
| 379 | |
| 380 | shaped_glyphs.iter().map(|g| g.advance).collect::<Vec<_>>() |
| 381 | }; |
| 382 | |
| 383 | let layout = TextLayout { font: &face, letter_spacing: Some(20.) }; |
| 384 | let buffer = ShapeBuffer::new(&layout, text); |
| 385 | |
| 386 | assert_eq!(buffer.glyphs.len(), advances.len()); |
| 387 | |
| 388 | let mut expected_advances = advances; |
| 389 | expected_advances[1] += layout.letter_spacing.unwrap(); |
| 390 | *expected_advances.last_mut().unwrap() += layout.letter_spacing.unwrap(); |
| 391 | |
| 392 | assert_eq!( |
| 393 | buffer.glyphs.iter().map(|glyph| glyph.advance).collect::<Vec<_>>(), |
| 394 | expected_advances |
| 395 | ); |
| 396 | }); |
| 397 | } |
| 398 | |