1use 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 CheckedAdd: Copy {
30 fn checked_add(self, other: Self) -> Option<Self>;
32
33 fn saturating_add(self, other: Self) -> Self;
35}
36
37impl CheckedAdd for f32 {
38 fn checked_add(self, other: Self) -> Option<Self> {
39 Some(self + other)
40 }
41
42 fn saturating_add(self, other: Self) -> Self {
43 self + other
44 }
45}
46
47impl<U> CheckedAdd for euclid::Length<i16, U> {
48 fn checked_add(self, other: Self) -> Option<Self> {
49 self.get().checked_add(other.get()).map(euclid::Length::new)
50 }
51
52 fn saturating_add(self, other: Self) -> Self {
53 euclid::Length::new(self.get().saturating_add(other.get()))
54 }
55}
56
57pub trait TextShaper {
72 type LengthPrimitive: core::ops::Mul
73 + core::ops::Div
74 + core::ops::Add<Output = Self::LengthPrimitive>
75 + core::ops::AddAssign
76 + euclid::num::Zero
77 + euclid::num::One
78 + core::convert::From<i16>
79 + Copy
80 + core::fmt::Debug;
81 type Length: euclid::num::Zero
82 + CheckedAdd
83 + core::ops::AddAssign
84 + core::ops::Add<Output = Self::Length>
85 + core::ops::Sub<Output = Self::Length>
86 + Default
87 + Clone
88 + Copy
89 + core::cmp::PartialOrd
90 + core::ops::Mul<Self::LengthPrimitive, Output = Self::Length>
91 + core::ops::Div<Self::LengthPrimitive, Output = Self::Length>
92 + DivCount
93 + core::fmt::Debug;
94 fn shape_text<GlyphStorage: core::iter::Extend<Glyph<Self::Length>>>(
96 &self,
97 text: &str,
98 glyphs: &mut GlyphStorage,
99 );
100 fn glyph_for_char(&self, ch: char) -> Option<Glyph<Self::Length>>;
101}
102
103pub trait DivCount {
107 fn div_count(self, divisor: Self) -> usize;
110}
111
112impl DivCount for f32 {
113 fn div_count(self, divisor: Self) -> usize {
114 (self / divisor) as usize
116 }
117}
118
119impl DivCount for i16 {
120 fn div_count(self, divisor: Self) -> usize {
121 (i32::from(self) / i32::from(divisor)).max(0) as usize
123 }
124}
125
126impl<T: DivCount + Clone, U> DivCount for euclid::Length<T, U> {
127 fn div_count(self, divisor: Self) -> usize {
128 self.get().div_count(divisor.get())
129 }
130}
131
132pub trait FontMetrics<Length: Copy + core::ops::Sub<Output = Length>> {
133 fn height(&self) -> Length {
134 self.ascent() - self.descent()
135 }
136 fn ascent(&self) -> Length;
137 fn descent(&self) -> Length;
138 fn x_height(&self) -> Length;
139 fn cap_height(&self) -> Length;
140}
141
142pub trait AbstractFont: TextShaper + FontMetrics<<Self as TextShaper>::Length> {}
143
144impl<T> AbstractFont for T where T: TextShaper + FontMetrics<<Self as TextShaper>::Length> {}
145
146pub struct ShapeBoundaries<'a> {
147 text: &'a str,
148 #[cfg(feature = "unicode-script")]
149 chars: core::str::CharIndices<'a>,
153 next_boundary_start: Option<usize>,
154 #[cfg(feature = "unicode-script")]
155 last_script: Option<unicode_script::Script>,
156}
157
158impl<'a> ShapeBoundaries<'a> {
159 pub fn new(text: &'a str) -> Self {
160 let next_boundary_start = if !text.is_empty() { Some(0) } else { None };
161 Self {
162 text,
163 #[cfg(feature = "unicode-script")]
164 chars: text.char_indices(),
165 next_boundary_start,
166 #[cfg(feature = "unicode-script")]
167 last_script: None,
168 }
169 }
170}
171
172impl Iterator for ShapeBoundaries<'_> {
173 type Item = usize;
174
175 #[cfg(feature = "unicode-script")]
176 fn next(&mut self) -> Option<Self::Item> {
177 self.next_boundary_start?;
178
179 let (next_offset, script) = loop {
180 match self.chars.next() {
181 Some((byte_offset, ch)) => {
182 use unicode_script::UnicodeScript;
183 let next_script = ch.script();
184 let previous_script = *self.last_script.get_or_insert(next_script);
185
186 if next_script == previous_script {
187 continue;
188 }
189 if matches!(
190 next_script,
191 unicode_script::Script::Unknown
192 | unicode_script::Script::Common
193 | unicode_script::Script::Inherited,
194 ) {
195 continue;
196 }
197
198 break (Some(byte_offset), Some(next_script));
199 }
200 None => {
201 break (None, None);
202 }
203 }
204 };
205
206 self.last_script = script;
207 self.next_boundary_start = next_offset;
208
209 Some(self.next_boundary_start.unwrap_or(self.text.len()))
210 }
211
212 #[cfg(not(feature = "unicode-script"))]
213 fn next(&mut self) -> Option<Self::Item> {
214 match self.next_boundary_start {
215 Some(_) => {
216 self.next_boundary_start = None;
217 Some(self.text.len())
218 }
219 None => None,
220 }
221 }
222}
223
224#[derive(Debug)]
225pub struct TextRun {
226 pub byte_range: Range<usize>,
227 pub glyph_range: Range<usize>,
228 }
230
231pub struct ShapeBuffer<Length> {
232 pub glyphs: Vec<Glyph<Length>>,
233 pub text_runs: Vec<TextRun>,
234}
235
236impl<Length> ShapeBuffer<Length> {
237 pub fn new<Font>(layout: &TextLayout<Font>, text: &str) -> Self
238 where
239 Font: AbstractFont<Length = Length>,
240 Length: Copy + core::ops::AddAssign,
241 {
242 let mut glyphs = Vec::new();
243 let text_runs = ShapeBoundaries::new(text)
244 .scan(0, |run_start, run_end| {
245 let glyphs_start = glyphs.len();
246
247 layout.font.shape_text(&text[*run_start..run_end], &mut glyphs);
248
249 for glyph in &mut glyphs[glyphs_start..] {
259 glyph.text_byte_offset += *run_start;
260 }
261
262 if let Some(letter_spacing) = layout.letter_spacing
263 && glyphs.len() > glyphs_start
264 {
265 let mut last_byte_offset = glyphs[glyphs_start].text_byte_offset;
266 for index in glyphs_start + 1..glyphs.len() {
267 let current_glyph_byte_offset = glyphs[index].text_byte_offset;
268 if current_glyph_byte_offset != last_byte_offset {
269 let previous_glyph = &mut glyphs[index - 1];
270 previous_glyph.advance += letter_spacing;
271 }
272 last_byte_offset = current_glyph_byte_offset;
273 }
274
275 glyphs.last_mut().unwrap().advance += letter_spacing;
276 }
277
278 let run = TextRun {
279 byte_range: Range { start: *run_start, end: run_end },
280 glyph_range: Range { start: glyphs_start, end: glyphs.len() },
281 };
282 *run_start = run_end;
283
284 Some(run)
285 })
286 .collect();
287
288 Self { glyphs, text_runs }
289 }
290}
291
292#[test]
293fn test_div_count() {
294 assert_eq!(9.0_f32.div_count(3.0), 3);
295 assert_eq!(10.0_f32.div_count(3.0), 3);
296 assert_eq!((-10.0_f32).div_count(3.0), 0);
297 assert_eq!(3.0_f32.div_count(10.0), 0);
298 assert_eq!(f32::NAN.div_count(16.0), 0);
299
300 assert_eq!(i16::MIN.div_count(-1), 32768);
301
302 type IntLen = euclid::Length<i16, euclid::UnknownUnit>;
303 assert_eq!(IntLen::new(10).div_count(IntLen::new(3)), 3);
304 assert_eq!(IntLen::new(-10).div_count(IntLen::new(3)), 0);
305
306 type FloatLen = euclid::Length<f32, euclid::UnknownUnit>;
307 assert_eq!(FloatLen::new(10.).div_count(FloatLen::new(3.)), 3);
308}
309
310#[test]
311fn test_shape_boundaries_simple() {
312 {
313 let simple_text = "Hello World";
314 let mut itemizer = ShapeBoundaries::new(simple_text);
315 assert_eq!(itemizer.next(), Some(simple_text.len()));
316 assert_eq!(itemizer.next(), None);
317 }
318}
319
320#[test]
321fn test_shape_boundaries_empty() {
322 {
323 let mut itemizer = ShapeBoundaries::new("");
324 assert_eq!(itemizer.next(), None);
325 }
326}
327
328#[test]
329#[cfg_attr(
330 not(feature = "unicode-script"),
331 ignore = "Not supported without the unicode-script feature"
332)]
333fn test_shape_boundaries_script_change() {
334 {
335 let text = "abc🍌🐒defதோசை.";
336 let mut itemizer = ShapeBoundaries::new(text).scan(0, |start, end| {
337 let str = &text[*start..end];
338 *start = end;
339 Some(str)
340 });
341 assert_eq!(itemizer.next(), Some("abc🍌🐒def"));
342 assert_eq!(itemizer.next(), Some("தோசை."));
343 assert_eq!(itemizer.next(), None);
344 }
345}
346
347#[cfg(test)]
348impl TextShaper for &rustybuzz::Face<'_> {
349 type LengthPrimitive = f32;
350 type Length = f32;
351 fn shape_text<GlyphStorage: std::iter::Extend<Glyph<f32>>>(
352 &self,
353 text: &str,
354 glyphs: &mut GlyphStorage,
355 ) {
356 let mut buffer = rustybuzz::UnicodeBuffer::new();
357 buffer.push_str(text);
358 let glyph_buffer = rustybuzz::shape(self, &[], buffer);
359
360 let output_glyph_generator =
361 glyph_buffer.glyph_infos().iter().zip(glyph_buffer.glyph_positions().iter()).map(
362 |(info, position)| {
363 let mut out_glyph = Glyph::default();
364 out_glyph.glyph_id = core::num::NonZeroU16::new(info.glyph_id as u16);
365 out_glyph.offset_x = position.x_offset as _;
366 out_glyph.offset_y = position.y_offset as _;
367 out_glyph.advance = position.x_advance as _;
368 out_glyph.text_byte_offset = info.cluster as usize;
369 out_glyph
370 },
371 );
372
373 glyphs.extend(output_glyph_generator);
375 }
376
377 fn glyph_for_char(&self, _ch: char) -> Option<Glyph<f32>> {
378 todo!()
379 }
380}
381
382#[cfg(test)]
383impl FontMetrics<f32> for &rustybuzz::Face<'_> {
384 fn ascent(&self) -> f32 {
385 self.ascender() as _
386 }
387
388 fn descent(&self) -> f32 {
389 self.descender() as _
390 }
391
392 fn x_height(&self) -> f32 {
393 rustybuzz::ttf_parser::Face::x_height(self).unwrap_or_default() as _
394 }
395
396 fn cap_height(&self) -> f32 {
397 rustybuzz::ttf_parser::Face::capital_height(self).unwrap_or_default() as _
398 }
399}
400
401#[cfg(test)]
402fn with_default_font<R>(mut callback: impl FnMut(&rustybuzz::Face<'_>) -> R) -> R {
403 let mut collection = fontique::Collection::new(fontique::CollectionOptions {
404 system_fonts: false,
405 ..Default::default()
406 });
407 let font_path: std::path::PathBuf =
408 [env!("CARGO_MANIFEST_DIR"), "..", "common", "sharedfontique", "Inter-VariableFont.ttf"]
409 .iter()
410 .collect();
411 let registered_fonts =
412 collection.register_fonts(std::fs::read(&font_path).unwrap().into(), None);
413 let mut cache = fontique::SourceCache::default();
414 let mut query = collection.query(&mut cache);
415 query.set_families(std::iter::once(fontique::QueryFamily::from(registered_fonts[0].0)));
416 let mut font = None;
417 query.matches_with(|query_font| {
418 font = Some(query_font.clone());
419 fontique::QueryStatus::Stop
420 });
421 let font = font.unwrap();
422 let face =
423 rustybuzz::Face::from_slice(font.blob.data(), font.index).expect("unable to parse font");
424 callback(&face)
425}
426
427#[test]
428fn test_shaping() {
429 use TextShaper;
430
431 with_default_font(|face| {
432 {
433 let mut shaped_glyphs = Vec::new();
434 face.shape_text("a\u{0304}\u{0301}b", &mut shaped_glyphs);
436
437 assert_eq!(shaped_glyphs.len(), 3);
438 assert!(shaped_glyphs[0].glyph_id.is_some());
439 assert_eq!(shaped_glyphs[0].text_byte_offset, 0);
440
441 assert!(shaped_glyphs[1].glyph_id.is_some());
442 assert_eq!(shaped_glyphs[1].text_byte_offset, 0);
443
444 assert!(shaped_glyphs[2].glyph_id.is_some());
445 assert_eq!(shaped_glyphs[2].text_byte_offset, 5);
446 }
447
448 {
449 let mut shaped_glyphs = Vec::new();
450 face.shape_text("a b", &mut shaped_glyphs);
452
453 assert_eq!(shaped_glyphs.len(), 3);
454 assert!(shaped_glyphs[0].glyph_id.is_some());
455 assert_eq!(shaped_glyphs[0].text_byte_offset, 0);
456
457 assert_eq!(shaped_glyphs[1].text_byte_offset, 1);
458
459 assert!(shaped_glyphs[2].glyph_id.is_some());
460 assert_eq!(shaped_glyphs[2].text_byte_offset, 2);
461 }
462 });
463}
464
465#[test]
473#[cfg_attr(
474 not(feature = "unicode-script"),
475 ignore = "Not supported without the unicode-script feature"
476)]
477fn test_byte_offsets_are_absolute() {
478 with_default_font(|face| {
479 let text = "[01] abc";
481 let layout = TextLayout { font: &face, letter_spacing: None, line_height: None };
482 let buffer = ShapeBuffer::new(&layout, text);
483
484 assert_eq!(buffer.text_runs.len(), 2, "expected a run boundary at the script change");
485 let second_run = &buffer.text_runs[1];
486 assert_eq!(second_run.byte_range.start, 5);
487
488 for glyph in &buffer.glyphs {
491 assert!(
492 text.is_char_boundary(glyph.text_byte_offset),
493 "{} is not an index into {text:?}",
494 glyph.text_byte_offset
495 );
496 }
497 assert_eq!(buffer.glyphs[second_run.glyph_range.start].text_byte_offset, 5);
498 assert_eq!(buffer.glyphs.last().unwrap().text_byte_offset, text.len() - 1);
499 });
500}
501
502#[test]
503fn test_letter_spacing() {
504 use TextShaper;
505
506 with_default_font(|face| {
507 let text = "a\u{0304}\u{0301}b";
509 let advances = {
510 let mut shaped_glyphs = Vec::new();
511 face.shape_text(text, &mut shaped_glyphs);
512
513 assert_eq!(shaped_glyphs.len(), 3);
514
515 shaped_glyphs.iter().map(|g| g.advance).collect::<Vec<_>>()
516 };
517
518 let layout = TextLayout { font: &face, letter_spacing: Some(20.), line_height: None };
519 let buffer = ShapeBuffer::new(&layout, text);
520
521 assert_eq!(buffer.glyphs.len(), advances.len());
522
523 let mut expected_advances = advances;
524 expected_advances[1] += layout.letter_spacing.unwrap();
525 *expected_advances.last_mut().unwrap() += layout.letter_spacing.unwrap();
526
527 assert_eq!(
528 buffer.glyphs.iter().map(|glyph| glyph.advance).collect::<Vec<_>>(),
529 expected_advances
530 );
531 });
532}