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