i_slint_core/textlayout/
shaping.rs1use alloc::vec::Vec;
6use core::ops::Range;
7
8use super::TextLayout;
9
10#[derive(Clone, Default, Debug)]
13pub struct Glyph<Length> {
14 pub advance: Length,
15 pub offset_x: Length,
16 pub offset_y: Length,
17 pub glyph_id: Option<core::num::NonZeroU16>,
20 pub text_byte_offset: usize,
24}
25
26pub trait TextShaper {
41 type LengthPrimitive: core::ops::Mul
42 + core::ops::Div
43 + core::ops::Add<Output = Self::LengthPrimitive>
44 + core::ops::AddAssign
45 + euclid::num::Zero
46 + euclid::num::One
47 + core::convert::From<i16>
48 + Copy
49 + core::fmt::Debug;
50 type Length: euclid::num::Zero
51 + core::ops::AddAssign
52 + core::ops::Add<Output = Self::Length>
53 + core::ops::Sub<Output = Self::Length>
54 + Default
55 + Clone
56 + Copy
57 + core::cmp::PartialOrd
58 + core::ops::Mul<Self::LengthPrimitive, Output = Self::Length>
59 + core::ops::Div<Self::LengthPrimitive, Output = Self::Length>
60 + core::fmt::Debug;
61 fn shape_text<GlyphStorage: core::iter::Extend<Glyph<Self::Length>>>(
63 &self,
64 text: &str,
65 glyphs: &mut GlyphStorage,
66 );
67 fn glyph_for_char(&self, ch: char) -> Option<Glyph<Self::Length>>;
68 fn max_lines(&self, max_height: Self::Length) -> usize;
69}
70
71pub trait FontMetrics<Length: Copy + core::ops::Sub<Output = Length>> {
72 fn height(&self) -> Length {
73 self.ascent() - self.descent()
74 }
75 fn ascent(&self) -> Length;
76 fn descent(&self) -> Length;
77 fn x_height(&self) -> Length;
78 fn cap_height(&self) -> Length;
79}
80
81pub trait AbstractFont: TextShaper + FontMetrics<<Self as TextShaper>::Length> {}
82
83impl<T> AbstractFont for T where T: TextShaper + FontMetrics<<Self as TextShaper>::Length> {}
84
85pub struct ShapeBoundaries<'a> {
86 text: &'a str,
87 #[cfg(feature = "unicode-script")]
88 chars: core::str::CharIndices<'a>,
92 next_boundary_start: Option<usize>,
93 #[cfg(feature = "unicode-script")]
94 last_script: Option<unicode_script::Script>,
95}
96
97impl<'a> ShapeBoundaries<'a> {
98 pub fn new(text: &'a str) -> Self {
99 let next_boundary_start = if !text.is_empty() { Some(0) } else { None };
100 Self {
101 text,
102 #[cfg(feature = "unicode-script")]
103 chars: text.char_indices(),
104 next_boundary_start,
105 #[cfg(feature = "unicode-script")]
106 last_script: None,
107 }
108 }
109}
110
111impl Iterator for ShapeBoundaries<'_> {
112 type Item = usize;
113
114 #[cfg(feature = "unicode-script")]
115 fn next(&mut self) -> Option<Self::Item> {
116 self.next_boundary_start?;
117
118 let (next_offset, script) = loop {
119 match self.chars.next() {
120 Some((byte_offset, ch)) => {
121 use unicode_script::UnicodeScript;
122 let next_script = ch.script();
123 let previous_script = *self.last_script.get_or_insert(next_script);
124
125 if next_script == previous_script {
126 continue;
127 }
128 if matches!(
129 next_script,
130 unicode_script::Script::Unknown
131 | unicode_script::Script::Common
132 | unicode_script::Script::Inherited,
133 ) {
134 continue;
135 }
136
137 break (Some(byte_offset), Some(next_script));
138 }
139 None => {
140 break (None, None);
141 }
142 }
143 };
144
145 self.last_script = script;
146 self.next_boundary_start = next_offset;
147
148 Some(self.next_boundary_start.unwrap_or(self.text.len()))
149 }
150
151 #[cfg(not(feature = "unicode-script"))]
152 fn next(&mut self) -> Option<Self::Item> {
153 match self.next_boundary_start {
154 Some(_) => {
155 self.next_boundary_start = None;
156 Some(self.text.len())
157 }
158 None => None,
159 }
160 }
161}
162
163#[derive(Debug)]
164pub struct TextRun {
165 pub byte_range: Range<usize>,
166 pub glyph_range: Range<usize>,
167 }
169
170pub struct ShapeBuffer<Length> {
171 pub glyphs: Vec<Glyph<Length>>,
172 pub text_runs: Vec<TextRun>,
173}
174
175impl<Length> ShapeBuffer<Length> {
176 pub fn new<Font>(layout: &TextLayout<Font>, text: &str) -> Self
177 where
178 Font: AbstractFont<Length = Length>,
179 Length: Copy + core::ops::AddAssign,
180 {
181 let mut glyphs = Vec::new();
182 let text_runs = ShapeBoundaries::new(text)
183 .scan(0, |run_start, run_end| {
184 let glyphs_start = glyphs.len();
185
186 layout.font.shape_text(&text[*run_start..run_end], &mut glyphs);
187
188 if let Some(letter_spacing) = layout.letter_spacing
189 && glyphs.len() > glyphs_start
190 {
191 let mut last_byte_offset = glyphs[glyphs_start].text_byte_offset;
192 for index in glyphs_start + 1..glyphs.len() {
193 let current_glyph_byte_offset = glyphs[index].text_byte_offset;
194 if current_glyph_byte_offset != last_byte_offset {
195 let previous_glyph = &mut glyphs[index - 1];
196 previous_glyph.advance += letter_spacing;
197 }
198 last_byte_offset = current_glyph_byte_offset;
199 }
200
201 glyphs.last_mut().unwrap().advance += letter_spacing;
202 }
203
204 let run = TextRun {
205 byte_range: Range { start: *run_start, end: run_end },
206 glyph_range: Range { start: glyphs_start, end: glyphs.len() },
207 };
208 *run_start = run_end;
209
210 Some(run)
211 })
212 .collect();
213
214 Self { glyphs, text_runs }
215 }
216}
217
218#[test]
219fn test_shape_boundaries_simple() {
220 {
221 let simple_text = "Hello World";
222 let mut itemizer = ShapeBoundaries::new(simple_text);
223 assert_eq!(itemizer.next(), Some(simple_text.len()));
224 assert_eq!(itemizer.next(), None);
225 }
226}
227
228#[test]
229fn test_shape_boundaries_empty() {
230 {
231 let mut itemizer = ShapeBoundaries::new("");
232 assert_eq!(itemizer.next(), None);
233 }
234}
235
236#[test]
237#[cfg_attr(
238 not(feature = "unicode-script"),
239 ignore = "Not supported without the unicode-script feature"
240)]
241fn test_shape_boundaries_script_change() {
242 {
243 let text = "abc🍌🐒defதோசை.";
244 let mut itemizer = ShapeBoundaries::new(text).scan(0, |start, end| {
245 let str = &text[*start..end];
246 *start = end;
247 Some(str)
248 });
249 assert_eq!(itemizer.next(), Some("abc🍌🐒def"));
250 assert_eq!(itemizer.next(), Some("தோசை."));
251 assert_eq!(itemizer.next(), None);
252 }
253}
254
255#[cfg(test)]
256impl TextShaper for &rustybuzz::Face<'_> {
257 type LengthPrimitive = f32;
258 type Length = f32;
259 fn shape_text<GlyphStorage: std::iter::Extend<Glyph<f32>>>(
260 &self,
261 text: &str,
262 glyphs: &mut GlyphStorage,
263 ) {
264 let mut buffer = rustybuzz::UnicodeBuffer::new();
265 buffer.push_str(text);
266 let glyph_buffer = rustybuzz::shape(self, &[], buffer);
267
268 let output_glyph_generator =
269 glyph_buffer.glyph_infos().iter().zip(glyph_buffer.glyph_positions().iter()).map(
270 |(info, position)| {
271 let mut out_glyph = Glyph::default();
272 out_glyph.glyph_id = core::num::NonZeroU16::new(info.glyph_id as u16);
273 out_glyph.offset_x = position.x_offset as _;
274 out_glyph.offset_y = position.y_offset as _;
275 out_glyph.advance = position.x_advance as _;
276 out_glyph.text_byte_offset = info.cluster as usize;
277 out_glyph
278 },
279 );
280
281 glyphs.extend(output_glyph_generator);
283 }
284
285 fn glyph_for_char(&self, _ch: char) -> Option<Glyph<f32>> {
286 todo!()
287 }
288
289 fn max_lines(&self, max_height: f32) -> usize {
290 (max_height / self.height()).floor() as _
291 }
292}
293
294#[cfg(test)]
295impl FontMetrics<f32> for &rustybuzz::Face<'_> {
296 fn ascent(&self) -> f32 {
297 self.ascender() as _
298 }
299
300 fn descent(&self) -> f32 {
301 self.descender() as _
302 }
303
304 fn x_height(&self) -> f32 {
305 rustybuzz::ttf_parser::Face::x_height(self).unwrap_or_default() as _
306 }
307
308 fn cap_height(&self) -> f32 {
309 rustybuzz::ttf_parser::Face::capital_height(self).unwrap_or_default() as _
310 }
311}
312
313#[cfg(test)]
314fn with_default_font<R>(mut callback: impl FnMut(&rustybuzz::Face<'_>) -> R) -> R {
315 let mut collection = fontique::Collection::new(fontique::CollectionOptions {
316 system_fonts: false,
317 ..Default::default()
318 });
319 let font_path: std::path::PathBuf =
320 [env!("CARGO_MANIFEST_DIR"), "..", "common", "sharedfontique", "Inter-VariableFont.ttf"]
321 .iter()
322 .collect();
323 let registered_fonts =
324 collection.register_fonts(std::fs::read(&font_path).unwrap().into(), None);
325 let mut cache = fontique::SourceCache::default();
326 let mut query = collection.query(&mut cache);
327 query.set_families(std::iter::once(fontique::QueryFamily::from(registered_fonts[0].0)));
328 let mut font = None;
329 query.matches_with(|query_font| {
330 font = Some(query_font.clone());
331 fontique::QueryStatus::Stop
332 });
333 let font = font.unwrap();
334 let face =
335 rustybuzz::Face::from_slice(font.blob.data(), font.index).expect("unable to parse font");
336 callback(&face)
337}
338
339#[test]
340fn test_shaping() {
341 use TextShaper;
342
343 with_default_font(|face| {
344 {
345 let mut shaped_glyphs = Vec::new();
346 face.shape_text("a\u{0304}\u{0301}b", &mut shaped_glyphs);
348
349 assert_eq!(shaped_glyphs.len(), 3);
350 assert!(shaped_glyphs[0].glyph_id.is_some());
351 assert_eq!(shaped_glyphs[0].text_byte_offset, 0);
352
353 assert!(shaped_glyphs[1].glyph_id.is_some());
354 assert_eq!(shaped_glyphs[1].text_byte_offset, 0);
355
356 assert!(shaped_glyphs[2].glyph_id.is_some());
357 assert_eq!(shaped_glyphs[2].text_byte_offset, 5);
358 }
359
360 {
361 let mut shaped_glyphs = Vec::new();
362 face.shape_text("a b", &mut shaped_glyphs);
364
365 assert_eq!(shaped_glyphs.len(), 3);
366 assert!(shaped_glyphs[0].glyph_id.is_some());
367 assert_eq!(shaped_glyphs[0].text_byte_offset, 0);
368
369 assert_eq!(shaped_glyphs[1].text_byte_offset, 1);
370
371 assert!(shaped_glyphs[2].glyph_id.is_some());
372 assert_eq!(shaped_glyphs[2].text_byte_offset, 2);
373 }
374 });
375}
376
377#[test]
378fn test_letter_spacing() {
379 use TextShaper;
380
381 with_default_font(|face| {
382 let text = "a\u{0304}\u{0301}b";
384 let advances = {
385 let mut shaped_glyphs = Vec::new();
386 face.shape_text(text, &mut shaped_glyphs);
387
388 assert_eq!(shaped_glyphs.len(), 3);
389
390 shaped_glyphs.iter().map(|g| g.advance).collect::<Vec<_>>()
391 };
392
393 let layout = TextLayout { font: &face, letter_spacing: Some(20.) };
394 let buffer = ShapeBuffer::new(&layout, text);
395
396 assert_eq!(buffer.glyphs.len(), advances.len());
397
398 let mut expected_advances = advances;
399 expected_advances[1] += layout.letter_spacing.unwrap();
400 *expected_advances.last_mut().unwrap() += layout.letter_spacing.unwrap();
401
402 assert_eq!(
403 buffer.glyphs.iter().map(|glyph| glyph.advance).collect::<Vec<_>>(),
404 expected_advances
405 );
406 });
407}