1use alloc::string::String;
5use alloc::string::ToString;
6use alloc::sync::Arc;
7use alloc::vec;
8use alloc::vec::Vec;
9use core::num::NonZeroU16;
10use hashbrown::{HashMap, HashSet};
11
12use fontdb::{Database, ID};
13use kurbo::{ParamCurve, ParamCurveArclen, ParamCurveDeriv};
14use rustybuzz::ttf_parser;
15use rustybuzz::ttf_parser::{GlyphId, Tag};
16use strict_num::NonZeroPositiveF32;
17use tiny_skia_path::{NonZeroRect, Transform};
18use unicode_script::UnicodeScript;
19
20use crate::tree::{BBox, IsValidLength};
21use crate::{
22 AlignmentBaseline, ApproxZeroUlps, BaselineShift, DominantBaseline, Fill, FillRule, Font,
23 FontResolver, LengthAdjust, PaintOrder, Path, ShapeRendering, Stroke, Text, TextAnchor,
24 TextChunk, TextDecorationStyle, TextFlow, TextPath, TextSpan, WritingMode,
25};
26
27#[derive(Clone, Debug)]
32pub struct PositionedGlyph {
33 glyph_ts: Transform,
37 cluster_ts: Transform,
39 span_ts: Transform,
41 units_per_em: u16,
43 font_size: f32,
45 pub id: GlyphId,
47 pub text: String,
49 pub font: ID,
52}
53
54impl PositionedGlyph {
55 pub fn font_size(&self) -> f32 {
57 self.font_size
58 }
59
60 pub fn transform(&self) -> Transform {
62 let sx = self.font_size / self.units_per_em as f32;
63
64 self.span_ts
65 .pre_concat(self.cluster_ts)
66 .pre_concat(Transform::from_scale(sx, sx))
67 .pre_concat(self.glyph_ts)
68 }
69
70 pub fn outline_transform(&self) -> Transform {
73 self.transform()
75 .pre_concat(Transform::from_scale(1.0, -1.0))
76 }
77
78 pub fn cbdt_transform(&self, x: f32, y: f32, pixels_per_em: f32, height: f32) -> Transform {
81 self.transform()
82 .pre_concat(Transform::from_scale(
83 self.units_per_em as f32 / pixels_per_em,
84 self.units_per_em as f32 / pixels_per_em,
85 ))
86 .pre_translate(x, -height - y)
90 }
91
92 pub fn sbix_transform(
95 &self,
96 x: f32,
97 y: f32,
98 x_min: f32,
99 y_min: f32,
100 pixels_per_em: f32,
101 height: f32,
102 ) -> Transform {
103 let bbox_x_shift = -x_min;
105
106 let bbox_y_shift = if y_min.approx_zero_ulps(4) {
107 0.128 * self.units_per_em as f32
118 } else {
119 -y_min
120 };
121
122 self.transform()
123 .pre_concat(Transform::from_translate(bbox_x_shift, bbox_y_shift))
124 .pre_concat(Transform::from_scale(
125 self.units_per_em as f32 / pixels_per_em,
126 self.units_per_em as f32 / pixels_per_em,
127 ))
128 .pre_translate(x, -height - y)
132 }
133
134 pub fn svg_transform(&self) -> Transform {
137 self.transform()
138 }
139
140 pub fn colr_transform(&self) -> Transform {
143 self.outline_transform()
144 }
145}
146
147#[derive(Clone, Debug)]
150pub struct Span {
151 pub fill: Option<Fill>,
153 pub stroke: Option<Stroke>,
155 pub paint_order: PaintOrder,
157 pub font_size: NonZeroPositiveF32,
159 pub variations: Vec<crate::FontVariation>,
161 pub font_optical_sizing: crate::FontOpticalSizing,
163 pub visible: bool,
165 pub positioned_glyphs: Vec<PositionedGlyph>,
167 pub underline: Option<Path>,
170 pub overline: Option<Path>,
173 pub line_through: Option<Path>,
176}
177
178#[derive(Clone, Debug)]
179struct GlyphCluster {
180 byte_idx: ByteIndex,
181 codepoint: char,
182 width: f32,
183 advance: f32,
184 ascent: f32,
185 descent: f32,
186 has_relative_shift: bool,
187 glyphs: Vec<PositionedGlyph>,
188 transform: Transform,
189 path_transform: Transform,
190 visible: bool,
191}
192
193impl GlyphCluster {
194 pub(crate) fn height(&self) -> f32 {
195 self.ascent - self.descent
196 }
197
198 pub(crate) fn transform(&self) -> Transform {
199 self.path_transform.post_concat(self.transform)
200 }
201}
202
203pub(crate) fn layout_text(
204 text_node: &Text,
205 resolver: &FontResolver,
206 fontdb: &mut Arc<fontdb::Database>,
207) -> Option<(Vec<Span>, NonZeroRect)> {
208 let mut fonts_cache: FontsCache = HashMap::new();
209
210 for chunk in &text_node.chunks {
211 for span in &chunk.spans {
212 if !fonts_cache.contains_key(&span.font) {
213 if let Some(font) =
214 (resolver.select_font)(&span.font, fontdb).and_then(|id| fontdb.load_font(id))
215 {
216 fonts_cache.insert(span.font.clone(), Arc::new(font));
217 }
218 }
219 }
220 }
221
222 let mut spans = vec![];
223 let mut char_offset = 0;
224 let mut last_x = 0.0;
225 let mut last_y = 0.0;
226 let mut bbox = BBox::default();
227 for chunk in &text_node.chunks {
228 let (x, y) = match chunk.text_flow {
229 TextFlow::Linear => (chunk.x.unwrap_or(last_x), chunk.y.unwrap_or(last_y)),
230 TextFlow::Path(_) => (0.0, 0.0),
231 };
232
233 let mut clusters = process_chunk(chunk, &fonts_cache, resolver, fontdb);
234 if clusters.is_empty() {
235 char_offset += chunk.text.chars().count();
236 continue;
237 }
238
239 apply_writing_mode(text_node.writing_mode, &mut clusters);
240 apply_letter_spacing(chunk, &mut clusters);
241 apply_word_spacing(chunk, &mut clusters);
242
243 apply_length_adjust(chunk, &mut clusters);
244 let mut curr_pos = resolve_clusters_positions(
245 text_node,
246 chunk,
247 char_offset,
248 text_node.writing_mode,
249 &fonts_cache,
250 &mut clusters,
251 );
252
253 let mut text_ts = Transform::default();
254 if text_node.writing_mode == WritingMode::TopToBottom {
255 if let TextFlow::Linear = chunk.text_flow {
256 text_ts = text_ts.pre_rotate_at(90.0, x, y);
257 }
258 }
259
260 for span in &chunk.spans {
261 let font = match fonts_cache.get(&span.font) {
262 Some(v) => v,
263 None => continue,
264 };
265
266 let decoration_spans = collect_decoration_spans(span, &clusters);
267
268 let mut span_ts = text_ts;
269 span_ts = span_ts.pre_translate(x, y);
270 if let TextFlow::Linear = chunk.text_flow {
271 let shift = resolve_baseline(span, font, text_node.writing_mode);
272
273 span_ts = span_ts.pre_translate(0.0, shift);
277 }
278
279 let mut underline = None;
280 let mut overline = None;
281 let mut line_through = None;
282
283 if let Some(decoration) = span.decoration.underline.clone() {
284 let offset = match text_node.writing_mode {
289 WritingMode::LeftToRight => -font.underline_position(span.font_size.get()),
290 WritingMode::TopToBottom => font.height(span.font_size.get()) / 2.0,
291 };
292
293 if let Some(path) =
294 convert_decoration(offset, span, font, decoration, &decoration_spans, span_ts)
295 {
296 bbox = bbox.expand(path.data.bounds());
297 underline = Some(path);
298 }
299 }
300
301 if let Some(decoration) = span.decoration.overline.clone() {
302 let offset = match text_node.writing_mode {
303 WritingMode::LeftToRight => -font.ascent(span.font_size.get()),
304 WritingMode::TopToBottom => -font.height(span.font_size.get()) / 2.0,
305 };
306
307 if let Some(path) =
308 convert_decoration(offset, span, font, decoration, &decoration_spans, span_ts)
309 {
310 bbox = bbox.expand(path.data.bounds());
311 overline = Some(path);
312 }
313 }
314
315 if let Some(decoration) = span.decoration.line_through.clone() {
316 let offset = match text_node.writing_mode {
317 WritingMode::LeftToRight => -font.line_through_position(span.font_size.get()),
318 WritingMode::TopToBottom => 0.0,
319 };
320
321 if let Some(path) =
322 convert_decoration(offset, span, font, decoration, &decoration_spans, span_ts)
323 {
324 bbox = bbox.expand(path.data.bounds());
325 line_through = Some(path);
326 }
327 }
328
329 let mut fill = span.fill.clone();
330 if let Some(ref mut fill) = fill {
331 fill.rule = FillRule::NonZero;
337 }
338
339 if let Some((span_fragments, span_bbox)) = convert_span(span, &clusters, span_ts) {
340 bbox = bbox.expand(span_bbox);
341
342 let positioned_glyphs = span_fragments
343 .into_iter()
344 .flat_map(|mut gc| {
345 let cluster_ts = gc.transform();
346 gc.glyphs.iter_mut().for_each(|pg| {
347 pg.cluster_ts = cluster_ts;
348 pg.span_ts = span_ts;
349 });
350 gc.glyphs
351 })
352 .collect();
353
354 spans.push(Span {
355 fill,
356 stroke: span.stroke.clone(),
357 paint_order: span.paint_order,
358 font_size: span.font_size,
359 variations: span.font.variations.clone(),
360 font_optical_sizing: span.font_optical_sizing,
361 visible: span.visible,
362 positioned_glyphs,
363 underline,
364 overline,
365 line_through,
366 });
367 }
368 }
369
370 char_offset += chunk.text.chars().count();
371
372 if text_node.writing_mode == WritingMode::TopToBottom {
373 if let TextFlow::Linear = chunk.text_flow {
374 core::mem::swap(&mut curr_pos.0, &mut curr_pos.1);
375 }
376 }
377
378 last_x = x + curr_pos.0;
379 last_y = y + curr_pos.1;
380 }
381
382 let bbox = bbox.to_non_zero_rect()?;
383
384 Some((spans, bbox))
385}
386
387fn convert_span(
388 span: &TextSpan,
389 clusters: &[GlyphCluster],
390 text_ts: Transform,
391) -> Option<(Vec<GlyphCluster>, NonZeroRect)> {
392 let mut span_clusters = vec![];
393 let mut bboxes_builder = tiny_skia_path::PathBuilder::new();
394
395 for cluster in clusters {
396 if !cluster.visible {
397 continue;
398 }
399
400 if span_contains(span, cluster.byte_idx) {
401 span_clusters.push(cluster.clone());
402 }
403
404 let mut advance = cluster.advance;
405 if advance <= 0.0 {
406 advance = 1.0;
407 }
408
409 if let Some(r) = NonZeroRect::from_xywh(0.0, -cluster.ascent, advance, cluster.height()) {
411 if let Some(r) = r.transform(cluster.transform()) {
412 bboxes_builder.push_rect(r.to_rect());
413 }
414 }
415 }
416
417 let mut bboxes = bboxes_builder.finish()?;
418 bboxes = bboxes.transform(text_ts)?;
419 let bbox = bboxes.compute_tight_bounds()?.to_non_zero_rect()?;
420
421 Some((span_clusters, bbox))
422}
423
424fn collect_decoration_spans(span: &TextSpan, clusters: &[GlyphCluster]) -> Vec<DecorationSpan> {
425 let mut spans = Vec::new();
426
427 let mut started = false;
428 let mut width = 0.0;
429 let mut transform = Transform::default();
430
431 for cluster in clusters {
432 if span_contains(span, cluster.byte_idx) {
433 if started && cluster.has_relative_shift {
434 started = false;
435 spans.push(DecorationSpan { width, transform });
436 }
437
438 if !started {
439 width = cluster.advance;
440 started = true;
441 transform = cluster.transform;
442 } else {
443 width += cluster.advance;
444 }
445 } else if started {
446 spans.push(DecorationSpan { width, transform });
447 started = false;
448 }
449 }
450
451 if started {
452 spans.push(DecorationSpan { width, transform });
453 }
454
455 spans
456}
457
458pub(crate) fn convert_decoration(
459 dy: f32,
460 span: &TextSpan,
461 font: &ResolvedFont,
462 mut decoration: TextDecorationStyle,
463 decoration_spans: &[DecorationSpan],
464 transform: Transform,
465) -> Option<Path> {
466 debug_assert!(!decoration_spans.is_empty());
467
468 let thickness = font.underline_thickness(span.font_size.get());
469
470 let mut builder = tiny_skia_path::PathBuilder::new();
471 for dec_span in decoration_spans {
472 let rect = match NonZeroRect::from_xywh(0.0, -thickness / 2.0, dec_span.width, thickness) {
473 Some(v) => v,
474 None => {
475 log::warn!("a decoration span has a malformed bbox");
476 continue;
477 }
478 };
479
480 let ts = dec_span.transform.pre_translate(0.0, dy);
481
482 let mut path = tiny_skia_path::PathBuilder::from_rect(rect.to_rect());
483 path = match path.transform(ts) {
484 Some(v) => v,
485 None => continue,
486 };
487
488 builder.push_path(&path);
489 }
490
491 let mut path_data = builder.finish()?;
492 path_data = path_data.transform(transform)?;
493
494 Path::new(
495 String::new(),
496 span.visible,
497 decoration.fill.take(),
498 decoration.stroke.take(),
499 PaintOrder::default(),
500 ShapeRendering::default(),
501 Arc::new(path_data),
502 Transform::default(),
503 )
504}
505
506#[derive(Clone, Copy)]
511pub(crate) struct DecorationSpan {
512 pub(crate) width: f32,
513 pub(crate) transform: Transform,
514}
515
516fn resolve_clusters_positions(
522 text: &Text,
523 chunk: &TextChunk,
524 char_offset: usize,
525 writing_mode: WritingMode,
526 fonts_cache: &FontsCache,
527 clusters: &mut [GlyphCluster],
528) -> (f32, f32) {
529 match chunk.text_flow {
530 TextFlow::Linear => {
531 resolve_clusters_positions_horizontal(text, chunk, char_offset, writing_mode, clusters)
532 }
533 TextFlow::Path(ref path) => resolve_clusters_positions_path(
534 text,
535 chunk,
536 char_offset,
537 path,
538 writing_mode,
539 fonts_cache,
540 clusters,
541 ),
542 }
543}
544
545fn clusters_length(clusters: &[GlyphCluster]) -> f32 {
546 clusters.iter().fold(0.0, |w, cluster| w + cluster.advance)
547}
548
549fn resolve_clusters_positions_horizontal(
550 text: &Text,
551 chunk: &TextChunk,
552 offset: usize,
553 writing_mode: WritingMode,
554 clusters: &mut [GlyphCluster],
555) -> (f32, f32) {
556 let mut x = process_anchor(chunk.anchor, clusters_length(clusters));
557 let mut y = 0.0;
558
559 for cluster in clusters {
560 let cp = offset + cluster.byte_idx.code_point_at(&chunk.text);
561 if let (Some(dx), Some(dy)) = (text.dx.get(cp), text.dy.get(cp)) {
562 if writing_mode == WritingMode::LeftToRight {
563 x += dx;
564 y += dy;
565 } else {
566 y -= dx;
567 x += dy;
568 }
569 cluster.has_relative_shift = !dx.approx_zero_ulps(4) || !dy.approx_zero_ulps(4);
570 }
571
572 cluster.transform = cluster.transform.pre_translate(x, y);
573
574 if let Some(angle) = text.rotate.get(cp).cloned() {
575 if !angle.approx_zero_ulps(4) {
576 cluster.transform = cluster.transform.pre_rotate(angle);
577 cluster.has_relative_shift = true;
578 }
579 }
580
581 x += cluster.advance;
582 }
583
584 (x, y)
585}
586
587pub(crate) fn resolve_baseline(
596 span: &TextSpan,
597 font: &ResolvedFont,
598 writing_mode: WritingMode,
599) -> f32 {
600 let mut shift = -resolve_baseline_shift(&span.baseline_shift, font, span.font_size.get());
601
602 if writing_mode == WritingMode::LeftToRight {
604 if span.alignment_baseline == AlignmentBaseline::Auto
605 || span.alignment_baseline == AlignmentBaseline::Baseline
606 {
607 shift += font.dominant_baseline_shift(span.dominant_baseline, span.font_size.get());
608 } else {
609 shift += font.alignment_baseline_shift(span.alignment_baseline, span.font_size.get());
610 }
611 }
612
613 shift
614}
615
616fn resolve_baseline_shift(baselines: &[BaselineShift], font: &ResolvedFont, font_size: f32) -> f32 {
617 let mut shift = 0.0;
618 for baseline in baselines.iter().rev() {
619 match baseline {
620 BaselineShift::Baseline => {}
621 BaselineShift::Subscript => shift -= font.subscript_offset(font_size),
622 BaselineShift::Superscript => shift += font.superscript_offset(font_size),
623 BaselineShift::Number(n) => shift += n,
624 }
625 }
626
627 shift
628}
629
630fn resolve_clusters_positions_path(
631 text: &Text,
632 chunk: &TextChunk,
633 char_offset: usize,
634 path: &TextPath,
635 writing_mode: WritingMode,
636 fonts_cache: &FontsCache,
637 clusters: &mut [GlyphCluster],
638) -> (f32, f32) {
639 let mut last_x = 0.0;
640 let mut last_y = 0.0;
641
642 let mut dy = 0.0;
643
644 let chunk_offset = match writing_mode {
647 WritingMode::LeftToRight => chunk.x.unwrap_or(0.0),
648 WritingMode::TopToBottom => chunk.y.unwrap_or(0.0),
649 };
650
651 let start_offset =
652 chunk_offset + path.start_offset + process_anchor(chunk.anchor, clusters_length(clusters));
653
654 let normals = collect_normals(text, chunk, clusters, &path.path, char_offset, start_offset);
655 for (cluster, normal) in clusters.iter_mut().zip(normals) {
656 let (x, y, angle) = match normal {
657 Some(normal) => (normal.x, normal.y, normal.angle),
658 None => {
659 cluster.visible = false;
661 continue;
662 }
663 };
664
665 cluster.has_relative_shift = true;
667
668 let orig_ts = cluster.transform;
669
670 let half_width = cluster.width / 2.0;
672 cluster.transform = Transform::default();
673 cluster.transform = cluster.transform.pre_translate(x - half_width, y);
674 cluster.transform = cluster.transform.pre_rotate_at(angle, half_width, 0.0);
675
676 let cp = char_offset + cluster.byte_idx.code_point_at(&chunk.text);
677 dy += text.dy.get(cp).cloned().unwrap_or(0.0);
678
679 let baseline_shift = chunk_span_at(chunk, cluster.byte_idx)
680 .map(|span| {
681 let font = match fonts_cache.get(&span.font) {
682 Some(v) => v,
683 None => return 0.0,
684 };
685 -resolve_baseline(span, font, writing_mode)
686 })
687 .unwrap_or(0.0);
688
689 if !dy.approx_zero_ulps(4) || !baseline_shift.approx_zero_ulps(4) {
692 let shift = kurbo::Vec2::new(0.0, (dy - baseline_shift) as f64);
693 cluster.transform = cluster
694 .transform
695 .pre_translate(shift.x as f32, shift.y as f32);
696 }
697
698 if let Some(angle) = text.rotate.get(cp).cloned() {
699 if !angle.approx_zero_ulps(4) {
700 cluster.transform = cluster.transform.pre_rotate(angle);
701 }
702 }
703
704 cluster.transform = cluster.transform.pre_concat(orig_ts);
706
707 last_x = x + cluster.advance;
708 last_y = y;
709 }
710
711 (last_x, last_y)
712}
713
714pub(crate) fn process_anchor(a: TextAnchor, text_width: f32) -> f32 {
715 match a {
716 TextAnchor::Start => 0.0, TextAnchor::Middle => -text_width / 2.0,
718 TextAnchor::End => -text_width,
719 }
720}
721
722pub(crate) struct PathNormal {
723 pub(crate) x: f32,
724 pub(crate) y: f32,
725 pub(crate) angle: f32,
726}
727
728fn collect_normals(
729 text: &Text,
730 chunk: &TextChunk,
731 clusters: &[GlyphCluster],
732 path: &tiny_skia_path::Path,
733 char_offset: usize,
734 offset: f32,
735) -> Vec<Option<PathNormal>> {
736 let mut offsets = Vec::with_capacity(clusters.len());
737 let mut normals = Vec::with_capacity(clusters.len());
738 {
739 let mut advance = offset;
740 for cluster in clusters {
741 let half_width = cluster.width / 2.0;
743
744 let cp = char_offset + cluster.byte_idx.code_point_at(&chunk.text);
746 advance += text.dx.get(cp).cloned().unwrap_or(0.0);
747
748 let offset = advance + half_width;
749
750 if offset < 0.0 {
752 normals.push(None);
753 }
754
755 offsets.push(offset as f64);
756 advance += cluster.advance;
757 }
758 }
759
760 let mut prev_mx = path.points()[0].x;
761 let mut prev_my = path.points()[0].y;
762 let mut prev_x = prev_mx;
763 let mut prev_y = prev_my;
764
765 fn create_curve_from_line(px: f32, py: f32, x: f32, y: f32) -> kurbo::CubicBez {
766 let line = kurbo::Line::new(
767 kurbo::Point::new(px as f64, py as f64),
768 kurbo::Point::new(x as f64, y as f64),
769 );
770 let p1 = line.eval(0.33);
771 let p2 = line.eval(0.66);
772 kurbo::CubicBez {
773 p0: line.p0,
774 p1,
775 p2,
776 p3: line.p1,
777 }
778 }
779
780 let mut length: f64 = 0.0;
781 for seg in path.segments() {
782 let curve = match seg {
783 tiny_skia_path::PathSegment::MoveTo(p) => {
784 prev_mx = p.x;
785 prev_my = p.y;
786 prev_x = p.x;
787 prev_y = p.y;
788 continue;
789 }
790 tiny_skia_path::PathSegment::LineTo(p) => {
791 create_curve_from_line(prev_x, prev_y, p.x, p.y)
792 }
793 tiny_skia_path::PathSegment::QuadTo(p1, p) => kurbo::QuadBez {
794 p0: kurbo::Point::new(prev_x as f64, prev_y as f64),
795 p1: kurbo::Point::new(p1.x as f64, p1.y as f64),
796 p2: kurbo::Point::new(p.x as f64, p.y as f64),
797 }
798 .raise(),
799 tiny_skia_path::PathSegment::CubicTo(p1, p2, p) => kurbo::CubicBez {
800 p0: kurbo::Point::new(prev_x as f64, prev_y as f64),
801 p1: kurbo::Point::new(p1.x as f64, p1.y as f64),
802 p2: kurbo::Point::new(p2.x as f64, p2.y as f64),
803 p3: kurbo::Point::new(p.x as f64, p.y as f64),
804 },
805 tiny_skia_path::PathSegment::Close => {
806 create_curve_from_line(prev_x, prev_y, prev_mx, prev_my)
807 }
808 };
809
810 let arclen_accuracy = {
811 let base_arclen_accuracy = 0.5;
812 let (sx, sy) = text.abs_transform.get_scale();
816 base_arclen_accuracy / (sx * sy).sqrt().max(1.0)
818 };
819
820 let curve_len = curve.arclen(arclen_accuracy as f64);
821
822 for offset in &offsets[normals.len()..] {
823 if *offset >= length && *offset <= length + curve_len {
824 let mut offset = curve.inv_arclen(offset - length, arclen_accuracy as f64);
825 debug_assert!((-1.0e-3..=1.0 + 1.0e-3).contains(&offset));
827 offset = offset.clamp(0.0, 1.0);
828
829 let pos = curve.eval(offset);
830 let d = curve.deriv().eval(offset);
831 let d = kurbo::Vec2::new(-d.y, d.x); let angle = d.atan2().to_degrees() - 90.0;
833
834 normals.push(Some(PathNormal {
835 x: pos.x as f32,
836 y: pos.y as f32,
837 angle: angle as f32,
838 }));
839
840 if normals.len() == offsets.len() {
841 break;
842 }
843 }
844 }
845
846 length += curve_len;
847 prev_x = curve.p3.x as f32;
848 prev_y = curve.p3.y as f32;
849 }
850
851 for _ in 0..(offsets.len() - normals.len()) {
853 normals.push(None);
854 }
855
856 normals
857}
858
859fn process_chunk(
864 chunk: &TextChunk,
865 fonts_cache: &FontsCache,
866 resolver: &FontResolver,
867 fontdb: &mut Arc<fontdb::Database>,
868) -> Vec<GlyphCluster> {
869 let mut positions = HashSet::new();
904
905 let mut glyphs = Vec::new();
906 for span in &chunk.spans {
907 let font = match fonts_cache.get(&span.font) {
908 Some(v) => v.clone(),
909 None => continue,
910 };
911
912 let tmp_glyphs = shape_text(
913 &chunk.text,
914 font,
915 span.small_caps,
916 span.apply_kerning,
917 &span.font.variations,
918 span.font_size.get(),
919 span.font_optical_sizing,
920 resolver,
921 fontdb,
922 );
923
924 if glyphs.is_empty() {
926 glyphs = tmp_glyphs;
927 continue;
928 }
929
930 positions.clear();
931
932 let mut iter = tmp_glyphs.into_iter();
934 while let Some(new_glyph) = iter.next() {
935 if !span_contains(span, new_glyph.byte_idx) {
936 continue;
937 }
938
939 let Some(idx) = glyphs
940 .iter()
941 .position(|g| g.byte_idx == new_glyph.byte_idx)
942 .filter(|pos| !positions.contains(pos))
943 else {
944 continue;
945 };
946
947 positions.insert(idx);
948
949 let prev_cluster_len = glyphs[idx].cluster_len;
950 if prev_cluster_len < new_glyph.cluster_len {
951 for _ in 1..new_glyph.cluster_len {
954 glyphs.remove(idx + 1);
955 }
956 } else if prev_cluster_len > new_glyph.cluster_len {
957 for j in 1..prev_cluster_len {
960 if let Some(g) = iter.next() {
961 glyphs.insert(idx + j, g);
962 }
963 }
964 }
965
966 glyphs[idx] = new_glyph;
967 }
968 }
969
970 let mut clusters = Vec::new();
972 for (range, byte_idx) in GlyphClusters::new(&glyphs) {
973 if let Some(span) = chunk_span_at(chunk, byte_idx) {
974 clusters.push(form_glyph_clusters(
975 &glyphs[range],
976 &chunk.text,
977 span.font_size.get(),
978 ));
979 }
980 }
981
982 clusters
983}
984
985fn apply_length_adjust(chunk: &TextChunk, clusters: &mut [GlyphCluster]) {
986 let is_horizontal = matches!(chunk.text_flow, TextFlow::Linear);
987
988 for span in &chunk.spans {
989 let target_width = match span.text_length {
990 Some(v) => v,
991 None => continue,
992 };
993
994 let mut width = 0.0;
995 let mut cluster_indexes = Vec::new();
996 for i in span.start..span.end {
997 if let Some(index) = clusters.iter().position(|c| c.byte_idx.value() == i) {
998 cluster_indexes.push(index);
999 }
1000 }
1001 cluster_indexes.sort();
1003 cluster_indexes.dedup();
1004
1005 for i in &cluster_indexes {
1006 width += clusters[*i].width;
1009 }
1010
1011 if cluster_indexes.is_empty() {
1012 continue;
1013 }
1014
1015 if span.length_adjust == LengthAdjust::Spacing {
1016 let factor = if cluster_indexes.len() > 1 {
1017 (target_width - width) / (cluster_indexes.len() - 1) as f32
1018 } else {
1019 0.0
1020 };
1021
1022 for i in cluster_indexes {
1023 clusters[i].advance = clusters[i].width + factor;
1024 }
1025 } else {
1026 let factor = target_width / width;
1027 if factor < 0.001 {
1029 continue;
1030 }
1031
1032 for i in cluster_indexes {
1033 clusters[i].transform = clusters[i].transform.pre_scale(factor, 1.0);
1034
1035 if !is_horizontal {
1037 clusters[i].advance *= factor;
1038 clusters[i].width *= factor;
1039 }
1040 }
1041 }
1042 }
1043}
1044
1045fn apply_writing_mode(writing_mode: WritingMode, clusters: &mut [GlyphCluster]) {
1048 if writing_mode != WritingMode::TopToBottom {
1049 return;
1050 }
1051
1052 for cluster in clusters {
1053 let orientation = unicode_vo::char_orientation(cluster.codepoint);
1054 if orientation == unicode_vo::Orientation::Upright {
1055 let mut ts = Transform::default();
1056 ts = ts.pre_translate(0.0, (cluster.ascent + cluster.descent) / 2.0);
1058 ts = ts.pre_rotate_at(
1060 -90.0,
1061 cluster.width / 2.0,
1062 -(cluster.ascent + cluster.descent) / 2.0,
1063 );
1064
1065 cluster.path_transform = ts;
1066
1067 cluster.ascent = cluster.width / 2.0;
1069 cluster.descent = -cluster.width / 2.0;
1070 } else {
1071 cluster.transform = cluster
1075 .transform
1076 .pre_translate(0.0, (cluster.ascent + cluster.descent) / 2.0);
1077 }
1078 }
1079}
1080
1081fn apply_letter_spacing(chunk: &TextChunk, clusters: &mut [GlyphCluster]) {
1085 if !chunk
1087 .spans
1088 .iter()
1089 .any(|span| !span.letter_spacing.approx_zero_ulps(4))
1090 {
1091 return;
1092 }
1093
1094 let num_clusters = clusters.len();
1095 for (i, cluster) in clusters.iter_mut().enumerate() {
1096 let script = cluster.codepoint.script();
1101 if script_supports_letter_spacing(script) {
1102 if let Some(span) = chunk_span_at(chunk, cluster.byte_idx) {
1103 if i != num_clusters - 1 {
1106 cluster.advance += span.letter_spacing;
1107 }
1108
1109 if !cluster.advance.is_valid_length() {
1112 cluster.width = 0.0;
1113 cluster.advance = 0.0;
1114 cluster.glyphs = vec![];
1115 }
1116 }
1117 }
1118 }
1119}
1120
1121fn apply_word_spacing(chunk: &TextChunk, clusters: &mut [GlyphCluster]) {
1125 if !chunk
1127 .spans
1128 .iter()
1129 .any(|span| !span.word_spacing.approx_zero_ulps(4))
1130 {
1131 return;
1132 }
1133
1134 for cluster in clusters {
1135 if is_word_separator_characters(cluster.codepoint) {
1136 if let Some(span) = chunk_span_at(chunk, cluster.byte_idx) {
1137 cluster.advance += span.word_spacing;
1141
1142 }
1144 }
1145 }
1146}
1147
1148fn form_glyph_clusters(glyphs: &[Glyph], text: &str, font_size: f32) -> GlyphCluster {
1149 debug_assert!(!glyphs.is_empty());
1150
1151 let mut width = 0.0;
1152 let mut x: f32 = 0.0;
1153
1154 let mut positioned_glyphs = vec![];
1155
1156 for glyph in glyphs {
1157 let sx = glyph.font.scale(font_size);
1158
1159 let ts = Transform::from_translate(x + glyph.dx as f32, -glyph.dy as f32);
1166
1167 positioned_glyphs.push(PositionedGlyph {
1168 glyph_ts: ts,
1169 cluster_ts: Transform::default(),
1171 span_ts: Transform::default(),
1173 units_per_em: glyph.font.units_per_em.get(),
1174 font_size,
1175 font: glyph.font.id,
1176 text: glyph.text.clone(),
1177 id: glyph.id,
1178 });
1179
1180 x += glyph.width as f32;
1181
1182 let glyph_width = glyph.width as f32 * sx;
1183 if glyph_width > width {
1184 width = glyph_width;
1185 }
1186 }
1187
1188 let byte_idx = glyphs[0].byte_idx;
1189 let font = glyphs[0].font.clone();
1190 GlyphCluster {
1191 byte_idx,
1192 codepoint: byte_idx.char_from(text),
1193 width,
1194 advance: width,
1195 ascent: font.ascent(font_size),
1196 descent: font.descent(font_size),
1197 has_relative_shift: false,
1198 transform: Transform::default(),
1199 path_transform: Transform::default(),
1200 glyphs: positioned_glyphs,
1201 visible: true,
1202 }
1203}
1204
1205pub(crate) trait DatabaseExt {
1206 fn load_font(&self, id: ID) -> Option<ResolvedFont>;
1207 fn has_char(&self, id: ID, c: char) -> bool;
1208}
1209
1210impl DatabaseExt for Database {
1211 #[inline(never)]
1212 fn load_font(&self, id: ID) -> Option<ResolvedFont> {
1213 self.with_face_data(id, |data, face_index| -> Option<ResolvedFont> {
1214 let font = ttf_parser::Face::parse(data, face_index).ok()?;
1215
1216 let units_per_em = NonZeroU16::new(font.units_per_em())?;
1217
1218 let ascent = font.ascender();
1219 let descent = font.descender();
1220
1221 let x_height = font
1222 .x_height()
1223 .and_then(|x| u16::try_from(x).ok())
1224 .and_then(NonZeroU16::new);
1225 let x_height = match x_height {
1226 Some(height) => height,
1227 None => {
1228 u16::try_from((f32::from(ascent - descent) * 0.45) as i32)
1231 .ok()
1232 .and_then(NonZeroU16::new)?
1233 }
1234 };
1235
1236 let line_through = font.strikeout_metrics();
1237 let line_through_position = match line_through {
1238 Some(metrics) => metrics.position,
1239 None => x_height.get() as i16 / 2,
1240 };
1241
1242 let (underline_position, underline_thickness) = match font.underline_metrics() {
1243 Some(metrics) => {
1244 let thickness = u16::try_from(metrics.thickness)
1245 .ok()
1246 .and_then(NonZeroU16::new)
1247 .unwrap_or_else(|| NonZeroU16::new(units_per_em.get() / 12).unwrap());
1249
1250 (metrics.position, thickness)
1251 }
1252 None => (
1253 -(units_per_em.get() as i16) / 9,
1254 NonZeroU16::new(units_per_em.get() / 12).unwrap(),
1255 ),
1256 };
1257
1258 let mut subscript_offset = libm::roundf(units_per_em.get() as f32 / 0.2) as i16;
1260 let mut superscript_offset = libm::roundf(units_per_em.get() as f32 / 0.4) as i16;
1261 if let Some(metrics) = font.subscript_metrics() {
1262 subscript_offset = metrics.y_offset;
1263 }
1264
1265 if let Some(metrics) = font.superscript_metrics() {
1266 superscript_offset = metrics.y_offset;
1267 }
1268
1269 Some(ResolvedFont {
1270 id,
1271 units_per_em,
1272 ascent,
1273 descent,
1274 x_height,
1275 underline_position,
1276 underline_thickness,
1277 line_through_position,
1278 subscript_offset,
1279 superscript_offset,
1280 })
1281 })?
1282 }
1283
1284 #[inline(never)]
1285 fn has_char(&self, id: ID, c: char) -> bool {
1286 let res = self.with_face_data(id, |font_data, face_index| -> Option<bool> {
1287 let font = ttf_parser::Face::parse(font_data, face_index).ok()?;
1288 font.glyph_index(c)?;
1289 Some(true)
1290 });
1291
1292 res == Some(Some(true))
1293 }
1294}
1295
1296pub(crate) fn shape_text(
1298 text: &str,
1299 font: Arc<ResolvedFont>,
1300 small_caps: bool,
1301 apply_kerning: bool,
1302 variations: &[crate::FontVariation],
1303 font_size: f32,
1304 font_optical_sizing: crate::FontOpticalSizing,
1305 resolver: &FontResolver,
1306 fontdb: &mut Arc<fontdb::Database>,
1307) -> Vec<Glyph> {
1308 let mut glyphs = shape_text_with_font(
1309 text,
1310 font.clone(),
1311 small_caps,
1312 apply_kerning,
1313 variations,
1314 font_size,
1315 font_optical_sizing,
1316 fontdb,
1317 )
1318 .unwrap_or_default();
1319
1320 let mut used_fonts = vec![font.id];
1322
1323 'outer: loop {
1325 let mut missing = None;
1326 for glyph in &glyphs {
1327 if glyph.is_missing() {
1328 missing = Some(glyph.byte_idx.char_from(text));
1329 break;
1330 }
1331 }
1332
1333 if let Some(c) = missing {
1334 let fallback_font = match (resolver.select_fallback)(c, &used_fonts, fontdb)
1335 .and_then(|id| fontdb.load_font(id))
1336 {
1337 Some(v) => Arc::new(v),
1338 None => break 'outer,
1339 };
1340
1341 let fallback_glyphs = shape_text_with_font(
1343 text,
1344 fallback_font.clone(),
1345 small_caps,
1346 apply_kerning,
1347 variations,
1348 font_size,
1349 font_optical_sizing,
1350 fontdb,
1351 )
1352 .unwrap_or_default();
1353
1354 let all_matched = fallback_glyphs.iter().all(|g| !g.is_missing());
1355 if all_matched {
1356 glyphs = fallback_glyphs;
1358 break 'outer;
1359 }
1360
1361 if glyphs.len() != fallback_glyphs.len() {
1364 break 'outer;
1365 }
1366
1367 for i in 0..glyphs.len() {
1371 if glyphs[i].is_missing() && !fallback_glyphs[i].is_missing() {
1372 glyphs[i] = fallback_glyphs[i].clone();
1373 }
1374 }
1375
1376 used_fonts.push(fallback_font.id);
1378 } else {
1379 break 'outer;
1380 }
1381 }
1382
1383 for glyph in &glyphs {
1385 if glyph.is_missing() {
1386 let c = glyph.byte_idx.char_from(text);
1387 log::warn!(
1389 "No fonts with a {}/U+{:X} character were found.",
1390 c,
1391 c as u32
1392 );
1393 }
1394 }
1395
1396 glyphs
1397}
1398
1399fn shape_text_with_font(
1403 text: &str,
1404 font: Arc<ResolvedFont>,
1405 small_caps: bool,
1406 apply_kerning: bool,
1407 variations: &[crate::FontVariation],
1408 font_size: f32,
1409 font_optical_sizing: crate::FontOpticalSizing,
1410 fontdb: &fontdb::Database,
1411) -> Option<Vec<Glyph>> {
1412 fontdb.with_face_data(font.id, |font_data, face_index| -> Option<Vec<Glyph>> {
1413 let mut rb_font = rustybuzz::Face::from_slice(font_data, face_index)?;
1414
1415 let mut final_variations: Vec<rustybuzz::Variation> = variations
1417 .iter()
1418 .map(|v| rustybuzz::Variation {
1419 tag: Tag::from_bytes(&v.tag),
1420 value: v.value,
1421 })
1422 .collect();
1423
1424 if font_optical_sizing == crate::FontOpticalSizing::Auto {
1428 let has_explicit_opsz = variations.iter().any(|v| v.tag == *b"opsz");
1429 if !has_explicit_opsz {
1430 if let Some(axes) = rb_font.tables().fvar {
1432 let has_opsz_axis = axes
1433 .axes
1434 .into_iter()
1435 .any(|axis| axis.tag == ttf_parser::Tag::from_bytes(b"opsz"));
1436 if has_opsz_axis {
1437 final_variations.push(rustybuzz::Variation {
1438 tag: Tag::from_bytes(b"opsz"),
1439 value: font_size,
1440 });
1441 }
1442 }
1443 }
1444 }
1445
1446 if !final_variations.is_empty() {
1448 rb_font.set_variations(&final_variations);
1449 }
1450
1451 let bidi_info = unicode_bidi::BidiInfo::new(text, Some(unicode_bidi::Level::ltr()));
1452 let paragraph = &bidi_info.paragraphs[0];
1453 let line = paragraph.range.clone();
1454
1455 let mut glyphs = Vec::new();
1456
1457 let (levels, runs) = bidi_info.visual_runs(paragraph, line);
1458 for run in runs.iter() {
1459 let sub_text = &text[run.clone()];
1460 if sub_text.is_empty() {
1461 continue;
1462 }
1463
1464 let ltr = levels[run.start].is_ltr();
1465 let hb_direction = if ltr {
1466 rustybuzz::Direction::LeftToRight
1467 } else {
1468 rustybuzz::Direction::RightToLeft
1469 };
1470
1471 let mut buffer = rustybuzz::UnicodeBuffer::new();
1472 buffer.push_str(sub_text);
1473 buffer.set_direction(hb_direction);
1474
1475 let mut features = Vec::new();
1476 if small_caps {
1477 features.push(rustybuzz::Feature::new(Tag::from_bytes(b"smcp"), 1, ..));
1478 }
1479
1480 if !apply_kerning {
1481 features.push(rustybuzz::Feature::new(Tag::from_bytes(b"kern"), 0, ..));
1482 }
1483
1484 let output = rustybuzz::shape(&rb_font, &features, buffer);
1485
1486 let positions = output.glyph_positions();
1487 let infos = output.glyph_infos();
1488
1489 for i in 0..output.len() {
1490 let pos = positions[i];
1491 let info = infos[i];
1492 let idx = run.start + info.cluster as usize;
1493
1494 let start = info.cluster as usize;
1495
1496 let end = if ltr {
1497 i.checked_add(1)
1498 } else {
1499 i.checked_sub(1)
1500 }
1501 .and_then(|last| infos.get(last))
1502 .map_or(sub_text.len(), |info| info.cluster as usize);
1503
1504 glyphs.push(Glyph {
1505 byte_idx: ByteIndex::new(idx),
1506 cluster_len: end.checked_sub(start).unwrap_or(0), text: sub_text[start..end].to_string(),
1508 id: GlyphId(info.glyph_id as u16),
1509 dx: pos.x_offset,
1510 dy: pos.y_offset,
1511 width: pos.x_advance,
1512 font: font.clone(),
1513 });
1514 }
1515 }
1516
1517 Some(glyphs)
1518 })?
1519}
1520
1521pub(crate) struct GlyphClusters<'a> {
1526 data: &'a [Glyph],
1527 idx: usize,
1528}
1529
1530impl<'a> GlyphClusters<'a> {
1531 pub(crate) fn new(data: &'a [Glyph]) -> Self {
1532 GlyphClusters { data, idx: 0 }
1533 }
1534}
1535
1536impl Iterator for GlyphClusters<'_> {
1537 type Item = (core::ops::Range<usize>, ByteIndex);
1538
1539 fn next(&mut self) -> Option<Self::Item> {
1540 if self.idx == self.data.len() {
1541 return None;
1542 }
1543
1544 let start = self.idx;
1545 let cluster = self.data[self.idx].byte_idx;
1546 for g in &self.data[self.idx..] {
1547 if g.byte_idx != cluster {
1548 break;
1549 }
1550
1551 self.idx += 1;
1552 }
1553
1554 Some((start..self.idx, cluster))
1555 }
1556}
1557
1558pub(crate) fn script_supports_letter_spacing(script: unicode_script::Script) -> bool {
1564 use unicode_script::Script;
1565
1566 !matches!(
1567 script,
1568 Script::Arabic
1569 | Script::Syriac
1570 | Script::Nko
1571 | Script::Manichaean
1572 | Script::Psalter_Pahlavi
1573 | Script::Mandaic
1574 | Script::Mongolian
1575 | Script::Phags_Pa
1576 | Script::Devanagari
1577 | Script::Bengali
1578 | Script::Gurmukhi
1579 | Script::Modi
1580 | Script::Sharada
1581 | Script::Syloti_Nagri
1582 | Script::Tirhuta
1583 | Script::Ogham
1584 )
1585}
1586
1587#[derive(Clone)]
1591pub(crate) struct Glyph {
1592 pub(crate) id: GlyphId,
1594
1595 pub(crate) byte_idx: ByteIndex,
1599
1600 pub(crate) cluster_len: usize,
1602
1603 pub(crate) text: String,
1605
1606 pub(crate) dx: i32,
1608
1609 pub(crate) dy: i32,
1611
1612 pub(crate) width: i32,
1614
1615 pub(crate) font: Arc<ResolvedFont>,
1619}
1620
1621impl Glyph {
1622 fn is_missing(&self) -> bool {
1623 self.id.0 == 0
1624 }
1625}
1626
1627#[derive(Clone, Copy, Debug)]
1628pub(crate) struct ResolvedFont {
1629 pub(crate) id: ID,
1630
1631 units_per_em: NonZeroU16,
1632
1633 ascent: i16,
1635 descent: i16,
1636 x_height: NonZeroU16,
1637
1638 underline_position: i16,
1639 underline_thickness: NonZeroU16,
1640
1641 line_through_position: i16,
1645
1646 subscript_offset: i16,
1647 superscript_offset: i16,
1648}
1649
1650pub(crate) fn chunk_span_at(chunk: &TextChunk, byte_offset: ByteIndex) -> Option<&TextSpan> {
1651 chunk
1652 .spans
1653 .iter()
1654 .find(|&span| span_contains(span, byte_offset))
1655}
1656
1657pub(crate) fn span_contains(span: &TextSpan, byte_offset: ByteIndex) -> bool {
1658 byte_offset.value() >= span.start && byte_offset.value() < span.end
1659}
1660
1661pub(crate) fn is_word_separator_characters(c: char) -> bool {
1665 matches!(
1666 c as u32,
1667 0x0020 | 0x00A0 | 0x1361 | 0x010100 | 0x010101 | 0x01039F | 0x01091F
1668 )
1669}
1670
1671impl ResolvedFont {
1672 #[inline]
1673 pub(crate) fn scale(&self, font_size: f32) -> f32 {
1674 font_size / self.units_per_em.get() as f32
1675 }
1676
1677 #[inline]
1678 pub(crate) fn ascent(&self, font_size: f32) -> f32 {
1679 self.ascent as f32 * self.scale(font_size)
1680 }
1681
1682 #[inline]
1683 pub(crate) fn descent(&self, font_size: f32) -> f32 {
1684 self.descent as f32 * self.scale(font_size)
1685 }
1686
1687 #[inline]
1688 pub(crate) fn height(&self, font_size: f32) -> f32 {
1689 self.ascent(font_size) - self.descent(font_size)
1690 }
1691
1692 #[inline]
1693 pub(crate) fn x_height(&self, font_size: f32) -> f32 {
1694 self.x_height.get() as f32 * self.scale(font_size)
1695 }
1696
1697 #[inline]
1698 pub(crate) fn underline_position(&self, font_size: f32) -> f32 {
1699 self.underline_position as f32 * self.scale(font_size)
1700 }
1701
1702 #[inline]
1703 fn underline_thickness(&self, font_size: f32) -> f32 {
1704 self.underline_thickness.get() as f32 * self.scale(font_size)
1705 }
1706
1707 #[inline]
1708 pub(crate) fn line_through_position(&self, font_size: f32) -> f32 {
1709 self.line_through_position as f32 * self.scale(font_size)
1710 }
1711
1712 #[inline]
1713 fn subscript_offset(&self, font_size: f32) -> f32 {
1714 self.subscript_offset as f32 * self.scale(font_size)
1715 }
1716
1717 #[inline]
1718 fn superscript_offset(&self, font_size: f32) -> f32 {
1719 self.superscript_offset as f32 * self.scale(font_size)
1720 }
1721
1722 fn dominant_baseline_shift(&self, baseline: DominantBaseline, font_size: f32) -> f32 {
1723 let alignment = match baseline {
1724 DominantBaseline::Auto => AlignmentBaseline::Auto,
1725 DominantBaseline::UseScript => AlignmentBaseline::Auto, DominantBaseline::NoChange => AlignmentBaseline::Auto, DominantBaseline::ResetSize => AlignmentBaseline::Auto, DominantBaseline::Ideographic => AlignmentBaseline::Ideographic,
1729 DominantBaseline::Alphabetic => AlignmentBaseline::Alphabetic,
1730 DominantBaseline::Hanging => AlignmentBaseline::Hanging,
1731 DominantBaseline::Mathematical => AlignmentBaseline::Mathematical,
1732 DominantBaseline::Central => AlignmentBaseline::Central,
1733 DominantBaseline::Middle => AlignmentBaseline::Middle,
1734 DominantBaseline::TextAfterEdge => AlignmentBaseline::TextAfterEdge,
1735 DominantBaseline::TextBeforeEdge => AlignmentBaseline::TextBeforeEdge,
1736 };
1737
1738 self.alignment_baseline_shift(alignment, font_size)
1739 }
1740
1741 fn alignment_baseline_shift(&self, alignment: AlignmentBaseline, font_size: f32) -> f32 {
1771 match alignment {
1772 AlignmentBaseline::Auto => 0.0,
1773 AlignmentBaseline::Baseline => 0.0,
1774 AlignmentBaseline::BeforeEdge | AlignmentBaseline::TextBeforeEdge => {
1775 self.ascent(font_size)
1776 }
1777 AlignmentBaseline::Middle => self.x_height(font_size) * 0.5,
1778 AlignmentBaseline::Central => self.ascent(font_size) - self.height(font_size) * 0.5,
1779 AlignmentBaseline::AfterEdge | AlignmentBaseline::TextAfterEdge => {
1780 self.descent(font_size)
1781 }
1782 AlignmentBaseline::Ideographic => self.descent(font_size),
1783 AlignmentBaseline::Alphabetic => 0.0,
1784 AlignmentBaseline::Hanging => self.ascent(font_size) * 0.8,
1785 AlignmentBaseline::Mathematical => self.ascent(font_size) * 0.5,
1786 }
1787 }
1788}
1789
1790pub(crate) type FontsCache = HashMap<Font, Arc<ResolvedFont>>;
1791
1792#[derive(Clone, Copy, PartialEq, Debug)]
1796pub(crate) struct ByteIndex(usize);
1797
1798impl ByteIndex {
1799 fn new(i: usize) -> Self {
1800 ByteIndex(i)
1801 }
1802
1803 pub(crate) fn value(&self) -> usize {
1804 self.0
1805 }
1806
1807 pub(crate) fn code_point_at(&self, text: &str) -> usize {
1809 text.char_indices()
1810 .take_while(|(i, _)| *i != self.0)
1811 .count()
1812 }
1813
1814 pub(crate) fn char_from(&self, text: &str) -> char {
1816 text[self.0..].chars().next().unwrap()
1817 }
1818}