1#![allow(
15 clippy::too_many_arguments,
16 clippy::needless_range_loop,
17 clippy::ptr_arg
18)]
19
20use std::collections::HashMap;
21use std::sync::Arc;
22
23#[allow(dead_code)]
26static TEST_ENGINE: std::sync::OnceLock<Arc<RunicTextEngine>> = std::sync::OnceLock::new();
27
28pub fn test_engine() -> &'static Arc<RunicTextEngine> {
30 TEST_ENGINE.get_or_init(|| {
31 let mut engine = RunicTextEngine::new_light();
32 engine.load_font_data(include_bytes!("../Fonts/Jupiteroid.ttf").to_vec());
34 Arc::new(engine)
35 })
36}
37
38use fontdb::{Database, Family, Query, Source, Stretch, Style, Weight};
39use rustybuzz::{Direction, Feature, UnicodeBuffer};
40use swash::FontRef;
41use swash::scale::{Render, ScaleContext, Source as SwashSource};
42use unicode_bidi::BidiInfo;
43use unicode_segmentation::UnicodeSegmentation;
44
45pub const DEFAULT_FONT_SIZE: f32 = 16.0;
49
50pub const DEFAULT_LINE_HEIGHT: f32 = 1.2;
52
53const MAX_CACHE_SIZE: usize = 1024;
55
56#[derive(Debug, Clone, PartialEq)]
60pub enum ShapingError {
61 NoFontFound(String),
63 InvalidFontId,
65 EmptyShape(String),
67 InvalidFontData,
69}
70
71impl std::fmt::Display for ShapingError {
72 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73 match self {
74 ShapingError::NoFontFound(s) => write!(f, "No font found for: {}", s),
75 ShapingError::InvalidFontId => write!(f, "Invalid font ID"),
76 ShapingError::EmptyShape(s) => write!(f, "Empty shaping result for: {}", s),
77 ShapingError::InvalidFontData => write!(f, "Invalid font data"),
78 }
79 }
80}
81
82impl std::error::Error for ShapingError {}
83
84#[derive(Debug, Clone, PartialEq)]
88pub struct FontAxisInfo {
89 pub tag: u32,
91 pub tag_string: String,
93 pub min: f32,
95 pub max: f32,
97 pub default: f32,
99 pub is_standard: bool,
101}
102
103impl FontAxisInfo {
104 pub fn display_name(&self) -> &str {
106 match &self.tag_string[..] {
107 "wght" => "Weight",
108 "wdth" => "Width",
109 "ital" => "Italic",
110 "slnt" => "Slant",
111 "opsz" => "Optical Size",
112 "GRAD" => "Grade",
113 "XTRA" => "X Tra Bold",
114 "XOPQ" => "X Opacity",
115 "YOPQ" => "Y Opacity",
116 "YTLC" => "Y Tall Cap Height",
117 "YTUC" => "Y Uppercase Height",
118 "YTAS" => "Y Tall Ascender",
119 "YTDE" => "Y Tall Descender",
120 "YTFI" => "Y Tall Figure Height",
121 _ => &self.tag_string,
122 }
123 }
124}
125
126#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
130pub struct TextDecorations {
131 pub underline: bool,
133 pub strikethrough: bool,
135 pub overline: bool,
137}
138
139#[derive(Debug, Clone, Copy, PartialEq)]
141pub enum LineHeight {
142 Multiple(f32),
144 Fixed(f32),
146}
147
148impl Default for LineHeight {
149 fn default() -> Self {
150 LineHeight::Multiple(DEFAULT_LINE_HEIGHT)
151 }
152}
153
154impl LineHeight {
155 pub fn to_pixels(self, font_size: f32) -> f32 {
157 match self {
158 LineHeight::Multiple(m) => font_size * m,
159 LineHeight::Fixed(px) => px,
160 }
161 }
162}
163
164#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
166pub enum TextOverflow {
167 Clip,
169 Ellipsis,
171 Visible,
173 #[default]
175 WordWrap,
176}
177
178#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
180pub enum TextAlign {
181 #[default]
183 Start,
184 End,
186 Center,
188 Justify,
190}
191
192#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
194pub enum RenderMode {
195 Grayscale,
197 #[default]
199 Subpixel,
200 Color,
202 Msdf,
204}
205
206#[derive(Debug, Clone, Copy, PartialEq)]
208pub struct VariableAxis {
209 pub tag: u32,
211 pub value: f32,
213}
214
215impl VariableAxis {
216 pub fn new(tag_bytes: [u8; 4], value: f32) -> Self {
218 let tag = u32::from_be_bytes(tag_bytes);
219 VariableAxis { tag, value }
220 }
221
222 pub fn weight(value: f32) -> Self {
224 VariableAxis::new(*b"wght", value)
225 }
226
227 pub fn width(value: f32) -> Self {
229 VariableAxis::new(*b"wdth", value)
230 }
231
232 pub fn italic(value: f32) -> Self {
234 VariableAxis::new(*b"ital", value)
235 }
236
237 pub fn slant(value: f32) -> Self {
239 VariableAxis::new(*b"slnt", value)
240 }
241}
242
243#[derive(Debug, Clone, PartialEq)]
250pub struct TextPath {
251 pub control_points: Vec<(f32, f32)>,
253}
254
255impl TextPath {
256 pub fn new(control_points: Vec<(f32, f32)>) -> Self {
258 TextPath { control_points }
259 }
260
261 pub fn sample(&self, t: f32) -> ((f32, f32), f32) {
263 if self.control_points.is_empty() {
264 return ((0.0, 0.0), 0.0);
265 }
266 let n = self.control_points.len();
267 if n == 1 {
268 return (self.control_points[0], 0.0);
269 }
270 if n == 3 {
271 let p0 = self.control_points[0];
273 let p1 = self.control_points[1];
274 let p2 = self.control_points[2];
275 let u = 1.0 - t;
276 let tt = t * t;
277 let uu = u * u;
278 let x = uu * p0.0 + 2.0 * u * t * p1.0 + tt * p2.0;
279 let y = uu * p0.1 + 2.0 * u * t * p1.1 + tt * p2.1;
280 let tx = 2.0 * u * (p1.0 - p0.0) + 2.0 * t * (p2.0 - p1.0);
281 let ty = 2.0 * u * (p1.1 - p0.1) + 2.0 * t * (p2.1 - p1.1);
282 let angle = ty.atan2(tx);
283 ((x, y), angle)
284 } else if n == 4 {
285 let p0 = self.control_points[0];
287 let p1 = self.control_points[1];
288 let p2 = self.control_points[2];
289 let p3 = self.control_points[3];
290 let u = 1.0 - t;
291 let tt = t * t;
292 let uu = u * u;
293 let uuu = uu * u;
294 let ttt = tt * t;
295 let x = uuu * p0.0 + 3.0 * uu * t * p1.0 + 3.0 * u * tt * p2.0 + ttt * p3.0;
296 let y = uuu * p0.1 + 3.0 * uu * t * p1.1 + 3.0 * u * tt * p2.1 + ttt * p3.1;
297 let tx =
298 3.0 * uu * (p1.0 - p0.0) + 6.0 * u * t * (p2.0 - p1.0) + 3.0 * tt * (p3.0 - p2.0);
299 let ty =
300 3.0 * uu * (p1.1 - p0.1) + 6.0 * u * t * (p2.1 - p1.1) + 3.0 * tt * (p3.1 - p2.1);
301 let angle = ty.atan2(tx);
302 ((x, y), angle)
303 } else {
304 let segments = n - 1;
306 let scaled_t = t * segments as f32;
307 let idx = (scaled_t.floor() as usize).min(segments - 1);
308 let local_t = scaled_t - idx as f32;
309 let p0 = self.control_points[idx];
310 let p1 = self.control_points[idx + 1];
311 let x = p0.0 + (p1.0 - p0.0) * local_t;
312 let y = p0.1 + (p1.1 - p0.1) * local_t;
313 let tx = p1.0 - p0.0;
314 let ty = p1.1 - p0.1;
315 let angle = ty.atan2(tx);
316 ((x, y), angle)
317 }
318 }
319}
320
321#[derive(Debug, Clone, PartialEq)]
327pub enum LayoutBoundary {
328 Circle {
330 cx: f32,
332 cy: f32,
334 r: f32,
336 },
337 Polygon {
339 vertices: Vec<(f32, f32)>,
341 },
342}
343
344impl LayoutBoundary {
345 pub fn allowed_span(&self, y: f32) -> Option<(f32, f32)> {
351 match self {
352 LayoutBoundary::Circle { cx, cy, r } => {
353 let dy = y - cy;
354 if dy.abs() < *r {
355 let dx = (r * r - dy * dy).sqrt();
356 Some((cx - dx, cx + dx))
357 } else {
358 None
359 }
360 }
361 LayoutBoundary::Polygon { vertices } => {
362 if vertices.len() < 3 {
363 return None;
364 }
365 let mut intersections = Vec::new();
366 for i in 0..vertices.len() {
367 let p0 = vertices[i];
368 let p1 = vertices[(i + 1) % vertices.len()];
369 let y_min = p0.1.min(p1.1);
370 let y_max = p0.1.max(p1.1);
371 if y >= y_min && y <= y_max && (p1.1 - p0.1).abs() > 1e-5 {
372 let t = (y - p0.1) / (p1.1 - p0.1);
373 let x = p0.0 + t * (p1.0 - p0.0);
374 intersections.push(x);
375 }
376 }
377 if intersections.len() >= 2 {
378 intersections.sort_by(|a, b| a.partial_cmp(b).unwrap());
379 Some((intersections[0], intersections[intersections.len() - 1]))
380 } else {
381 None
382 }
383 }
384 }
385 }
386}
387
388#[derive(Debug, Clone, Copy, PartialEq, Eq)]
390pub struct OpenTypeFeature {
391 pub tag: u32,
393 pub value: u32,
395}
396
397impl OpenTypeFeature {
398 pub fn new(tag_bytes: [u8; 4], value: u32) -> Self {
400 let tag = u32::from_be_bytes(tag_bytes);
401 OpenTypeFeature { tag, value }
402 }
403
404 pub fn liga() -> Self {
406 OpenTypeFeature::new(*b"liga", 1)
407 }
408
409 pub fn kern() -> Self {
411 OpenTypeFeature::new(*b"kern", 1)
412 }
413
414 pub fn calt() -> Self {
416 OpenTypeFeature::new(*b"calt", 1)
417 }
418
419 pub fn dlig() -> Self {
421 OpenTypeFeature::new(*b"dlig", 1)
422 }
423}
424
425#[derive(Debug, Clone, PartialEq)]
427pub struct TextStyle {
428 pub family: String,
430 pub fallback_families: Vec<String>,
432 pub font_size: f32,
434 pub weight: Weight,
436 pub stretch: Stretch,
438 pub style: Style,
440 pub color: [u8; 4],
442 pub letter_spacing: f32,
444 pub word_spacing: f32,
446 pub line_height: LineHeight,
448 pub decorations: TextDecorations,
450 pub extra_features: Vec<OpenTypeFeature>,
452 pub variable_axes: Vec<VariableAxis>,
454 pub synthesize_styles: bool,
456 pub render_mode: RenderMode,
458 pub outline_rendering: bool,
460 pub material_effect_id: u32,
462}
463
464impl Default for TextStyle {
465 fn default() -> Self {
466 TextStyle {
467 family: "Jupiteroid".to_string(),
468 fallback_families: vec![
469 "Operation Napalm".to_string(),
470 "OSerif".to_string(),
471 "Lanix Ox".to_string(),
472 ],
473 font_size: DEFAULT_FONT_SIZE,
474 weight: Weight::NORMAL,
475 stretch: Stretch::Normal,
476 style: Style::Normal,
477 color: [255, 255, 255, 255],
478 letter_spacing: 0.0,
479 word_spacing: 0.0,
480 line_height: LineHeight::default(),
481 decorations: TextDecorations::default(),
482 extra_features: vec![],
483 variable_axes: vec![],
484 synthesize_styles: false,
485 render_mode: RenderMode::default(),
486 outline_rendering: false,
487 material_effect_id: 0,
488 }
489 }
490}
491
492impl TextStyle {
493 pub fn new(family: &str, font_size: f32) -> Self {
495 TextStyle {
496 family: family.to_string(),
497 font_size,
498 ..Default::default()
499 }
500 }
501
502 pub fn with_weight(mut self, weight: u16) -> Self {
504 self.weight = Weight(weight);
505 self
506 }
507
508 pub fn italic(mut self) -> Self {
510 self.style = Style::Italic;
511 self
512 }
513
514 pub fn with_color(mut self, r: u8, g: u8, b: u8, a: u8) -> Self {
516 self.color = [r, g, b, a];
517 self
518 }
519
520 pub fn with_letter_spacing(mut self, spacing: f32) -> Self {
522 self.letter_spacing = spacing;
523 self
524 }
525
526 pub fn with_word_spacing(mut self, spacing: f32) -> Self {
528 self.word_spacing = spacing;
529 self
530 }
531
532 pub fn with_line_height_multiple(mut self, multiple: f32) -> Self {
534 self.line_height = LineHeight::Multiple(multiple);
535 self
536 }
537
538 pub fn with_line_height_fixed(mut self, pixels: f32) -> Self {
540 self.line_height = LineHeight::Fixed(pixels);
541 self
542 }
543
544 pub fn with_feature(mut self, feature: OpenTypeFeature) -> Self {
546 self.extra_features.push(feature);
547 self
548 }
549
550 pub fn with_axis(mut self, axis: VariableAxis) -> Self {
552 self.variable_axes.push(axis);
553 self
554 }
555
556 pub fn with_underline(mut self) -> Self {
558 self.decorations.underline = true;
559 self
560 }
561
562 pub fn with_strikethrough(mut self) -> Self {
564 self.decorations.strikethrough = true;
565 self
566 }
567
568 pub fn with_outline_rendering(mut self, enabled: bool) -> Self {
570 self.outline_rendering = enabled;
571 self
572 }
573
574 pub fn with_material_effect(mut self, effect_id: u32) -> Self {
576 self.material_effect_id = effect_id;
577 self
578 }
579}
580
581#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
585pub enum PortalAlignment {
586 #[default]
588 Baseline,
589 Top,
591 Center,
593 Bottom,
595}
596
597#[derive(Debug, Clone, PartialEq, Default)]
599pub enum TextSpanKind {
600 #[default]
602 Text,
603 Portal {
605 width: f32,
607 height: f32,
609 alignment: PortalAlignment,
611 id: String,
613 },
614}
615
616#[derive(Debug, Clone, PartialEq)]
618pub struct TextSpan {
619 pub text: String,
621 pub style: TextStyle,
623 pub byte_offset: usize,
625 pub kind: TextSpanKind,
627}
628
629impl TextSpan {
630 pub fn new(text: &str, style: TextStyle) -> Self {
632 TextSpan {
633 text: text.to_string(),
634 style,
635 byte_offset: 0,
636 kind: TextSpanKind::Text,
637 }
638 }
639
640 pub fn at(text: &str, style: TextStyle, byte_offset: usize) -> Self {
642 TextSpan {
643 text: text.to_string(),
644 style,
645 byte_offset,
646 kind: TextSpanKind::Text,
647 }
648 }
649
650 pub fn portal(
652 width: f32,
653 height: f32,
654 alignment: PortalAlignment,
655 id: &str,
656 style: TextStyle,
657 ) -> Self {
658 TextSpan {
659 text: "\u{FFFC}".to_string(),
660 style,
661 byte_offset: 0,
662 kind: TextSpanKind::Portal {
663 width,
664 height,
665 alignment,
666 id: id.to_string(),
667 },
668 }
669 }
670
671 pub fn portal_at(
673 width: f32,
674 height: f32,
675 alignment: PortalAlignment,
676 id: &str,
677 style: TextStyle,
678 byte_offset: usize,
679 ) -> Self {
680 TextSpan {
681 text: "\u{FFFC}".to_string(),
682 style,
683 byte_offset,
684 kind: TextSpanKind::Portal {
685 width,
686 height,
687 alignment,
688 id: id.to_string(),
689 },
690 }
691 }
692}
693
694#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
701pub struct CacheKey {
702 pub text_hash: u64,
704 pub font_cache_key: u64,
706 pub font_size: u32,
708 pub weight: u16,
710 pub stretch: u16,
712 pub style: u8,
714 pub direction: u8,
716 pub letter_spacing: i32,
718 pub word_spacing: i32,
720}
721
722impl CacheKey {
723 pub fn new(
725 text: &str,
726 font_cache_key: u64,
727 font_size: f32,
728 weight: Weight,
729 stretch: Stretch,
730 style: Style,
731 direction: Direction,
732 letter_spacing: f32,
733 word_spacing: f32,
734 ) -> Self {
735 use std::collections::hash_map::DefaultHasher;
736 use std::hash::{Hash, Hasher};
737
738 let mut hasher = DefaultHasher::new();
739 text.hash(&mut hasher);
740 let text_hash = hasher.finish();
741
742 CacheKey {
743 text_hash,
744 font_cache_key,
745 font_size: (font_size * 2.0).round() as u32,
746 weight: weight.0,
747 stretch: stretch.to_number(),
748 style: match style {
749 Style::Normal => 0,
750 Style::Italic => 1,
751 Style::Oblique => 2,
752 },
753 direction: match direction {
754 Direction::LeftToRight => 0,
755 Direction::RightToLeft => 1,
756 _ => 0,
757 },
758 letter_spacing: (letter_spacing * 100.0).round() as i32,
759 word_spacing: (word_spacing * 100.0).round() as i32,
760 }
761 }
762}
763
764#[derive(Debug, Clone, Copy, PartialEq)]
768pub struct GlyphInstance {
769 pub glyph_id: u16,
771 pub x: f32,
773 pub y: f32,
775 pub angle: f32,
777 pub advance_width: f32,
779 pub advance_height: f32,
781 pub cluster: u32,
783 pub is_rtl: bool,
785 pub cache_key: u64,
787 pub glyph_index: usize,
789 pub time_offset: f32,
791}
792
793#[derive(Debug, Clone, Copy, PartialEq)]
798pub enum RunicPathSegment {
799 MoveTo {
801 x: f32,
803 y: f32,
805 },
806 LineTo {
808 x: f32,
810 y: f32,
812 },
813 QuadTo {
815 cx: f32,
817 cy: f32,
819 x: f32,
821 y: f32,
823 },
824 CubicTo {
826 cx1: f32,
828 cy1: f32,
830 cx2: f32,
832 cy2: f32,
834 x: f32,
836 y: f32,
838 },
839 Close,
841}
842
843#[derive(Debug, Clone, PartialEq)]
845pub struct GlyphImage {
846 pub glyph_id: u16,
848 pub width: u32,
850 pub height: u32,
852 pub data: Vec<u8>,
854 pub x_offset: f32,
856 pub y_offset: f32,
858 pub cache_key: u64,
860}
861
862#[derive(Debug, Clone, PartialEq)]
866pub struct LineInfo {
867 pub glyph_start: usize,
869 pub glyph_end: usize,
871 pub baseline_y: f32,
873 pub height: f32,
875 pub width: f32,
877 pub x_offset: f32,
879 pub byte_offset: usize,
881 pub text: String,
883}
884
885#[derive(Debug, Clone, PartialEq)]
889pub struct ShapedText {
890 pub glyphs: Vec<GlyphInstance>,
892 pub lines: Vec<LineInfo>,
894 pub width: f32,
896 pub height: f32,
898 pub text: String,
900 pub spans: Vec<TextSpan>,
902 pub has_rtl: bool,
904 pub ascent: f32,
906 pub descent: f32,
908 pub line_gap: f32,
910 pub grapheme_boundaries: Vec<usize>,
912}
913
914impl ShapedText {
915 pub fn hit_test(&self, byte_index: usize) -> (usize, u32) {
917 if self.glyphs.is_empty() {
918 return (0, 0);
919 }
920
921 let mut best_glyph = 0u32;
922 let mut best_dist = u64::MAX;
923
924 for glyph in &self.glyphs {
926 let cluster_byte = self.byte_pos_for_cluster(glyph.cluster);
927 let dist = if cluster_byte > byte_index {
928 (cluster_byte - byte_index) as u64
929 } else {
930 (byte_index - cluster_byte) as u64
931 };
932 if dist < best_dist {
933 best_dist = dist;
934 best_glyph = glyph.cluster;
935 }
936 }
937
938 for (i, glyph) in self.glyphs.iter().enumerate() {
940 if glyph.cluster == best_glyph {
941 return (i, best_glyph);
942 }
943 }
944
945 (0, 0)
946 }
947
948 pub fn cursor_position(&self, byte_index: usize) -> (f32, usize) {
950 if self.glyphs.is_empty() {
951 return (0.0, 0);
952 }
953
954 let (glyph_idx, _cluster) = self.hit_test(byte_index);
955
956 let mut line_idx = 0;
958 for (li, line) in self.lines.iter().enumerate() {
959 if glyph_idx >= line.glyph_start && glyph_idx < line.glyph_end {
960 line_idx = li;
961 break;
962 }
963 }
964
965 let glyph = &self.glyphs[glyph_idx];
967 let line = &self.lines[line_idx];
968 let x = line.x_offset + glyph.x;
969
970 (x, line_idx)
971 }
972
973 pub fn selection_rects(&self, start: usize, end: usize) -> Vec<[f32; 4]> {
975 if self.glyphs.is_empty() || start >= end {
976 return vec![];
977 }
978
979 let mut rects = Vec::new();
980 let mut current_rect: Option<[f32; 4]> = None;
981
982 for glyph in &self.glyphs {
983 let cluster_start = self.byte_pos_for_cluster(glyph.cluster);
984 let cluster_end = if glyph.cluster + 1 < self.total_clusters() {
985 self.byte_pos_for_cluster(glyph.cluster + 1)
986 } else {
987 self.text.len()
988 };
989
990 if cluster_start < end && cluster_end > start {
992 let mut line_top = 0.0f32;
994 let mut line_h = self.height;
995 for line in &self.lines {
996 if glyph.cluster >= self.glyphs[line.glyph_start].cluster
997 && (line.glyph_end == self.glyphs.len()
998 || glyph.cluster < self.glyphs[line.glyph_end].cluster)
999 {
1000 line_top = line.baseline_y - self.ascent;
1001 line_h = line.height;
1002 break;
1003 }
1004 }
1005
1006 let x = glyph.x;
1007 let w = glyph.advance_width.max(1.0);
1008
1009 if let Some(ref mut rect) = current_rect {
1010 if (rect[0] + rect[2] - x).abs() < 2.0 && (rect[1] - line_top).abs() < 1.0 {
1011 rect[2] = (x + w) - rect[0];
1013 } else {
1014 rects.push(*rect);
1016 current_rect = Some([x, line_top, w, line_h]);
1017 }
1018 } else {
1019 current_rect = Some([x, line_top, w, line_h]);
1020 }
1021 }
1022 }
1023
1024 if let Some(rect) = current_rect {
1025 rects.push(rect);
1026 }
1027
1028 rects
1029 }
1030
1031 fn byte_pos_for_cluster(&self, cluster: u32) -> usize {
1033 self.grapheme_boundaries
1034 .get(cluster as usize)
1035 .copied()
1036 .unwrap_or(self.text.len())
1037 }
1038
1039 fn total_clusters(&self) -> u32 {
1041 self.grapheme_boundaries.len() as u32
1042 }
1043}
1044
1045#[derive(Clone)]
1049struct FontData {
1050 data: std::sync::Arc<Vec<u8>>,
1051 index: u32,
1052}
1053
1054impl FontData {
1055 fn new(data: Vec<u8>, index: u32) -> Self {
1056 FontData {
1057 data: std::sync::Arc::new(data),
1058 index,
1059 }
1060 }
1061
1062 fn as_bytes(&self) -> &[u8] {
1063 &self.data
1064 }
1065
1066 fn font_ref(&self) -> Option<FontRef<'_>> {
1067 FontRef::from_index(&self.data, self.index as usize)
1068 }
1069
1070 fn face(&self) -> Option<rustybuzz::Face<'_>> {
1071 rustybuzz::Face::from_slice(&self.data, self.index)
1072 }
1073}
1074
1075struct ResolvedFont {
1079 primary: FontData,
1080 fallbacks: Vec<FontData>,
1081 cache_key: u64,
1082 units_per_em: u16,
1083 ascent: f32,
1084 descent: f32,
1085 line_gap: f32,
1086 x_height: f32,
1087 cap_height: f32,
1088 has_colr: bool,
1089}
1090
1091impl ResolvedFont {
1092 fn from_data(data: FontData) -> Option<Self> {
1093 let font_ref = data.font_ref()?;
1094 let _face_ref = font_ref; let _metrics = swash::scale::image::Image::new(); let cache_key = font_ref.key.value();
1101
1102 let ttf_face = rustybuzz::ttf_parser::Face::parse(data.as_bytes(), data.index).ok()?;
1104 let units_per_em = ttf_face.units_per_em();
1105 let ascent = ttf_face.ascender() as f32;
1106 let descent = ttf_face.descender().abs() as f32;
1107 let line_gap = ttf_face.line_gap() as f32;
1108
1109 let (os2_xh, os2_ch) = ttf_face
1110 .x_height()
1111 .and_then(|xh| ttf_face.capital_height().map(|ch| (xh as f32, ch as f32)))
1112 .unwrap_or((0.0, 0.0));
1113 let has_colr = ttf_face
1114 .raw_face()
1115 .table(rustybuzz::ttf_parser::Tag(u32::from_be_bytes(*b"COLR")))
1116 .is_some();
1117
1118 Some(ResolvedFont {
1119 primary: data,
1120 fallbacks: vec![],
1121 cache_key,
1122 units_per_em,
1123 ascent,
1124 descent,
1125 line_gap,
1126 x_height: os2_xh,
1127 cap_height: os2_ch,
1128 has_colr,
1129 })
1130 }
1131
1132 fn metrics_pixels(&self, font_size: f32) -> (f32, f32, f32) {
1133 let scale = font_size / self.units_per_em as f32;
1134 (
1135 self.ascent * scale,
1136 self.descent * scale,
1137 self.line_gap * scale,
1138 )
1139 }
1140}
1141
1142pub struct RunicTextEngine {
1146 db: Database,
1148 font_data: HashMap<fontdb::ID, FontData>,
1150 cache: HashMap<CacheKey, Vec<GlyphInstance>>,
1152 cache_order: Vec<CacheKey>,
1154 scale_context: ScaleContext,
1156}
1157
1158impl RunicTextEngine {
1159 pub fn new() -> Self {
1166 let mut db = Database::new();
1167 db.load_system_fonts();
1168
1169 let home = std::env::var("HOME").unwrap_or_default();
1171 for dir in &[
1172 format!("{}/.local/share/fonts", home),
1173 format!("{}/.fonts", home),
1174 "/usr/share/fonts".to_string(),
1175 "/usr/local/share/fonts".to_string(),
1176 ] {
1177 db.load_fonts_dir(dir);
1178 }
1179
1180 let mut engine = RunicTextEngine {
1181 db,
1182 font_data: HashMap::new(),
1183 cache: HashMap::new(),
1184 cache_order: Vec::new(),
1185 scale_context: ScaleContext::new(),
1186 };
1187
1188 engine.load_font_data(include_bytes!("../Fonts/Jupiteroid.ttf").to_vec());
1190
1191 engine
1192 }
1193
1194 pub fn new_light() -> Self {
1197 RunicTextEngine {
1198 db: Database::new(),
1199 font_data: HashMap::new(),
1200 cache: HashMap::new(),
1201 cache_order: Vec::new(),
1202 scale_context: ScaleContext::new(),
1203 }
1204 }
1205
1206 pub fn new_test() -> Self {
1209 let mut engine = Self::new_light();
1210 engine.load_font_data(include_bytes!("../Fonts/Jupiteroid.ttf").to_vec());
1211 engine
1212 }
1213
1214 pub fn load_font_data(&mut self, data: Vec<u8>) {
1216 self.db.load_font_data(data.clone());
1217 for face in self.db.faces() {
1218 let id = face.id;
1219 self.font_data.entry(id).or_insert_with(|| {
1220 let face_index = face.index;
1221 FontData::new(data.clone(), face_index)
1222 });
1223 }
1224 }
1225 fn get_font_data(&mut self, id: fontdb::ID) -> Option<FontData> {
1227 if let Some(data) = self.font_data.get(&id) {
1228 return Some(data.clone());
1229 }
1230
1231 let (source, face_index) = self.db.face_source(id)?;
1233 let data = match source {
1234 Source::Binary(arc_data) => {
1235 let bytes: Vec<u8> = arc_data.as_ref().as_ref().to_vec();
1237 bytes
1238 }
1239 Source::File(path) => std::fs::read(&path).ok()?,
1240 _ => return None,
1241 };
1242
1243 let font_data = FontData::new(data, face_index);
1244 self.font_data.insert(id, font_data.clone());
1245 Some(font_data)
1246 }
1247
1248 fn resolve_font(&mut self, style: &TextStyle) -> Result<ResolvedFont, ShapingError> {
1250 for family_name in std::iter::once(&style.family).chain(style.fallback_families.iter()) {
1252 let query = Query {
1253 families: &[Family::Name(family_name)],
1254 weight: style.weight,
1255 stretch: style.stretch,
1256 style: style.style,
1257 };
1258
1259 if let Some(id) = self.db.query(&query)
1260 && let Some(data) = self.get_font_data(id)
1261 && let Some(mut resolved) = ResolvedFont::from_data(data.clone())
1262 {
1263 let fallback_ids: Vec<fontdb::ID> = self
1265 .db
1266 .faces()
1267 .filter(|f| f.id != id)
1268 .map(|f| f.id)
1269 .collect();
1270 for fb_id in fallback_ids {
1271 if let Some(fb_data) = self.get_font_data(fb_id) {
1272 resolved.fallbacks.push(fb_data);
1273 }
1274 }
1275 return Ok(resolved);
1276 }
1277 }
1278
1279 let all_ids: Vec<fontdb::ID> = self.db.faces().map(|f| f.id).collect();
1281 for id in &all_ids {
1282 if let Some(data) = self.get_font_data(*id)
1283 && let Some(mut resolved) = ResolvedFont::from_data(data)
1284 {
1285 for fb_id in &all_ids {
1286 if *fb_id != *id
1287 && let Some(fb_data) = self.get_font_data(*fb_id)
1288 {
1289 resolved.fallbacks.push(fb_data);
1290 }
1291 }
1292 return Ok(resolved);
1293 }
1294 }
1295
1296 Err(ShapingError::NoFontFound(style.family.clone()))
1297 }
1298
1299 fn build_features(style: &TextStyle) -> Vec<Feature> {
1301 use rustybuzz::ttf_parser::Tag;
1302 let mut features = vec![
1303 Feature::new(Tag::from_bytes(b"liga"), 1, 0..usize::MAX),
1304 Feature::new(Tag::from_bytes(b"kern"), 1, 0..usize::MAX),
1305 Feature::new(Tag::from_bytes(b"calt"), 1, 0..usize::MAX),
1306 ];
1307
1308 for extra in &style.extra_features {
1309 features.push(Feature::new(
1310 Tag::from_bytes(&extra.tag.to_be_bytes()),
1311 extra.value,
1312 0..usize::MAX,
1313 ));
1314 }
1315
1316 features
1317 }
1318
1319 fn calculate_glyph_cache_key(
1326 font_cache_key: u64,
1327 font_size: f32,
1328 glyph_id: u16,
1329 style: &TextStyle,
1330 ) -> u64 {
1331 use std::collections::hash_map::DefaultHasher;
1332 use std::hash::{Hash, Hasher};
1333 let mut hasher = DefaultHasher::new();
1334 font_cache_key.hash(&mut hasher);
1335 ((font_size * 2.0).round() as u32).hash(&mut hasher);
1336 glyph_id.hash(&mut hasher);
1337 style.weight.0.hash(&mut hasher);
1338 style.stretch.to_number().hash(&mut hasher);
1339 let style_discriminant = match style.style {
1340 Style::Normal => 0u8,
1341 Style::Italic => 1u8,
1342 Style::Oblique => 2u8,
1343 };
1344 style_discriminant.hash(&mut hasher);
1345 hasher.finish()
1346 }
1347
1348 fn shape_run(
1350 &mut self,
1351 text: &str,
1352 style: &TextStyle,
1353 direction: Direction,
1354 ) -> Result<Vec<GlyphInstance>, ShapingError> {
1355 let resolved = self.resolve_font(style)?;
1356
1357 let features = Self::build_features(style);
1358
1359 let cache_key = CacheKey::new(
1361 text,
1362 resolved.cache_key,
1363 style.font_size,
1364 style.weight,
1365 style.stretch,
1366 style.style,
1367 direction,
1368 style.letter_spacing,
1369 style.word_spacing,
1370 );
1371
1372 if let Some(glyphs) = self.cache.get(&cache_key) {
1374 return Ok(glyphs.clone());
1375 }
1376
1377 let face = resolved
1379 .primary
1380 .face()
1381 .ok_or(ShapingError::InvalidFontData)?;
1382
1383 let mut buffer = UnicodeBuffer::new();
1385 buffer.push_str(text);
1386 buffer.set_direction(direction);
1387
1388 let output = rustybuzz::shape(&face, &features, buffer);
1390
1391 let glyph_infos = output.glyph_infos();
1392 let glyph_positions = output.glyph_positions();
1393
1394 let scale = style.font_size / (resolved.units_per_em as f32);
1395
1396 let mut glyphs = Vec::new();
1397 let mut x_offset = 0.0f32;
1398
1399 for (info, pos) in glyph_infos.iter().zip(glyph_positions.iter()) {
1400 let advance = (pos.x_advance as f32) * scale;
1401 let letter_space = if Self::is_space_cluster(text, info.cluster) {
1402 style.word_spacing
1403 } else {
1404 0.0
1405 };
1406
1407 let glyph_cache_key = Self::calculate_glyph_cache_key(
1408 resolved.cache_key,
1409 style.font_size,
1410 info.glyph_id as u16,
1411 style,
1412 );
1413
1414 glyphs.push(GlyphInstance {
1415 glyph_id: info.glyph_id as u16,
1416 x: x_offset + (pos.x_offset as f32) * scale,
1417 y: (pos.y_offset as f32) * scale,
1418 angle: 0.0,
1419 advance_width: advance + style.letter_spacing + letter_space,
1420 advance_height: (pos.y_advance as f32) * scale,
1421 cluster: info.cluster,
1422 is_rtl: direction == Direction::RightToLeft,
1423 cache_key: glyph_cache_key,
1424 glyph_index: 0,
1425 time_offset: 0.0,
1426 });
1427
1428 x_offset += advance + style.letter_spacing + letter_space;
1429 }
1430
1431 self.apply_fallbacks(&mut glyphs, text, style, &resolved, &features);
1433
1434 self.insert_cache(cache_key, glyphs.clone());
1436
1437 Ok(glyphs)
1438 }
1439
1440 fn is_space_cluster(text: &str, cluster: u32) -> bool {
1442 text.chars()
1443 .nth(cluster as usize)
1444 .is_some_and(|c| c.is_ascii_whitespace())
1445 }
1446
1447 fn apply_fallbacks(
1454 &mut self,
1455 glyphs: &mut [GlyphInstance],
1456 text: &str,
1457 style: &TextStyle,
1458 resolved: &ResolvedFont,
1459 features: &[Feature],
1460 ) {
1461 let len = glyphs.len();
1462 for i in 0..len {
1463 if glyphs[i].glyph_id == 0 {
1464 let glyph_cluster = glyphs[i].cluster;
1465 let glyph_is_rtl = glyphs[i].is_rtl;
1466 let glyph_x = glyphs[i].x;
1467 let c = text
1468 .chars()
1469 .nth(glyph_cluster as usize)
1470 .unwrap_or('\u{FFFD}');
1471
1472 for fallback in &resolved.fallbacks {
1474 if let Some(face) = fallback.face() {
1475 let mut buf = UnicodeBuffer::new();
1476 buf.add(c, glyph_cluster);
1477 buf.set_direction(if glyph_is_rtl {
1478 Direction::RightToLeft
1479 } else {
1480 Direction::LeftToRight
1481 });
1482
1483 let output = rustybuzz::shape(&face, features, buf);
1484 let infos = output.glyph_infos();
1485 let positions = output.glyph_positions();
1486
1487 if let (Some(info), Some(pos)) = (infos.first(), positions.first())
1488 && info.glyph_id != 0
1489 {
1490 let scale = style.font_size / (resolved.units_per_em as f32);
1491 glyphs[i].glyph_id = info.glyph_id as u16;
1492 glyphs[i].x = glyph_x + (pos.x_offset as f32) * scale;
1493 glyphs[i].y = (pos.y_offset as f32) * scale;
1494
1495 let fallback_key = fallback
1496 .font_ref()
1497 .map(|r| r.key.value())
1498 .unwrap_or(resolved.cache_key);
1499 glyphs[i].cache_key = Self::calculate_glyph_cache_key(
1500 fallback_key,
1501 style.font_size,
1502 info.glyph_id as u16,
1503 style,
1504 );
1505 break;
1506 }
1507 }
1508 }
1509 }
1510 }
1511 }
1512
1513 fn insert_cache(&mut self, key: CacheKey, value: Vec<GlyphInstance>) {
1515 if self.cache.len() >= MAX_CACHE_SIZE
1516 && let Some(oldest) = self.cache_order.first().cloned()
1517 {
1518 self.cache.remove(&oldest);
1519 self.cache_order.remove(0);
1520 }
1521
1522 self.cache.insert(key, value);
1523 self.cache_order.push(key);
1524 }
1525
1526 pub fn shape_layout(
1528 &mut self,
1529 spans: &[TextSpan],
1530 max_width: Option<f32>,
1531 align: TextAlign,
1532 overflow: TextOverflow,
1533 ) -> Result<ShapedText, ShapingError> {
1534 self.shape_layout_ex(spans, max_width, align, overflow, None, None)
1535 }
1536
1537 pub fn shape_layout_ex(
1544 &mut self,
1545 spans: &[TextSpan],
1546 max_width: Option<f32>,
1547 align: TextAlign,
1548 overflow: TextOverflow,
1549 path: Option<TextPath>,
1550 boundary: Option<LayoutBoundary>,
1551 ) -> Result<ShapedText, ShapingError> {
1552 if spans.is_empty() {
1553 return Ok(ShapedText {
1554 glyphs: vec![],
1555 lines: vec![],
1556 width: 0.0,
1557 height: 0.0,
1558 text: String::new(),
1559 spans: vec![],
1560 has_rtl: false,
1561 ascent: 0.0,
1562 descent: 0.0,
1563 line_gap: 0.0,
1564 grapheme_boundaries: vec![],
1565 });
1566 }
1567
1568 let full_text: String = spans.iter().map(|s| s.text.as_str()).collect();
1570
1571 let bidi = unicode_bidi::BidiInfo::new(&full_text, Some(unicode_bidi::Level::ltr()));
1573
1574 let mut all_glyphs: Vec<GlyphInstance> = Vec::new();
1575 let mut has_rtl = false;
1576 let mut primary_metrics = (0.0f32, 0.0f32, 0.0f32);
1577 let mut primary_line_height_px = DEFAULT_LINE_HEIGHT * DEFAULT_FONT_SIZE;
1578 let mut global_glyph_index = 0;
1579
1580 for span in spans {
1582 let direction = if let Some(para_info) = bidi.paragraphs.first() {
1584 let mut dir = Direction::LeftToRight;
1585 for bi in para_info.range.clone() {
1586 if bi < bidi.levels.len() {
1587 if bidi.levels[bi].is_rtl() {
1588 dir = Direction::RightToLeft;
1589 has_rtl = true;
1590 }
1591 break;
1592 }
1593 }
1594 dir
1595 } else {
1596 Direction::LeftToRight
1597 };
1598
1599 let mut run_glyphs = match &span.kind {
1600 TextSpanKind::Text => self.shape_run(&span.text, &span.style, direction)?,
1601 TextSpanKind::Portal { width, height, .. } => {
1602 vec![GlyphInstance {
1603 glyph_id: 0xFFFF,
1604 x: 0.0,
1605 y: 0.0,
1606 angle: 0.0,
1607 advance_width: *width,
1608 advance_height: *height,
1609 cluster: span.byte_offset as u32,
1610 is_rtl: false,
1611 cache_key: 0,
1612 glyph_index: 0,
1613 time_offset: 0.0,
1614 }]
1615 }
1616 };
1617
1618 let span_offset_x = all_glyphs
1620 .last()
1621 .map(|g| g.x + g.advance_width)
1622 .unwrap_or(0.0);
1623 for glyph in &mut run_glyphs {
1624 glyph.x += span_offset_x;
1625 }
1626
1627 if all_glyphs.is_empty() {
1629 primary_metrics = (
1630 span.style.font_size * 0.8, span.style.font_size * 0.2, span.style.font_size * 0.2, );
1634 if let Ok(resolved) = self.resolve_font(&span.style) {
1635 primary_metrics = resolved.metrics_pixels(span.style.font_size);
1636 }
1637 primary_line_height_px = span.style.line_height.to_pixels(span.style.font_size);
1638 }
1639
1640 for mut glyph in run_glyphs {
1641 glyph.glyph_index = global_glyph_index;
1642 glyph.time_offset = global_glyph_index as f32 * 0.05; global_glyph_index += 1;
1644 all_glyphs.push(glyph);
1645 }
1646 }
1647
1648 let lines = self.layout_lines(
1650 &mut all_glyphs,
1651 &full_text,
1652 &bidi,
1653 max_width,
1654 align,
1655 overflow,
1656 primary_metrics.0,
1657 primary_metrics.1,
1658 primary_metrics.2,
1659 primary_line_height_px,
1660 path.as_ref(),
1661 boundary.as_ref(),
1662 spans,
1663 );
1664
1665 let mut total_width = 0.0f32;
1667 let total_height = lines.last().map(|l| l.baseline_y + l.height).unwrap_or(0.0);
1668
1669 for line in &lines {
1670 if line.width > total_width {
1671 total_width = line.width;
1672 }
1673 }
1674
1675 let grapheme_boundaries: Vec<usize> = full_text
1676 .grapheme_indices(true)
1677 .map(|(offset, _)| offset)
1678 .collect();
1679
1680 Ok(ShapedText {
1681 glyphs: all_glyphs,
1682 lines,
1683 width: total_width,
1684 height: total_height,
1685 text: full_text,
1686 spans: spans.to_vec(),
1687 has_rtl,
1688 ascent: primary_metrics.0,
1689 descent: primary_metrics.1,
1690 line_gap: primary_metrics.2,
1691 grapheme_boundaries,
1692 })
1693 }
1694
1695 fn layout_lines(
1697 &self,
1698 glyphs: &mut Vec<GlyphInstance>,
1699 text: &str,
1700 bidi: &BidiInfo,
1701 max_width: Option<f32>,
1702 align: TextAlign,
1703 overflow: TextOverflow,
1704 ascent: f32,
1705 _descent: f32,
1706 _line_gap: f32,
1707 line_height_px: f32,
1708 path: Option<&TextPath>,
1709 boundary: Option<&LayoutBoundary>,
1710 spans: &[TextSpan],
1711 ) -> Vec<LineInfo> {
1712 let mut lines = Vec::new();
1713 let mut current_y = ascent;
1714
1715 if glyphs.is_empty() {
1716 return lines;
1717 }
1718
1719 if max_width.is_some() || boundary.is_some() {
1720 let mut line_start_glyph = 0;
1722 let mut line_start_byte = 0;
1723 let mut last_word_break_glyph = 0usize;
1724 let mut last_word_break_byte = 0usize;
1725
1726 for i in 0..glyphs.len() {
1727 let glyph = &glyphs[i];
1728 let char_at_cluster = text.chars().nth(glyph.cluster as usize).unwrap_or(' ');
1729 let is_space = char_at_cluster.is_ascii_whitespace();
1730
1731 if is_space && i > line_start_glyph {
1732 last_word_break_glyph = i + 1;
1733 let mut byte_pos = 0;
1735 let mut ci = 0u32;
1736 let text_bytes = text.as_bytes();
1737 while byte_pos < text_bytes.len() && ci <= glyph.cluster {
1738 byte_pos += Self::utf8_len(text_bytes[byte_pos]);
1739 ci += 1;
1740 }
1741 last_word_break_byte = byte_pos;
1742 }
1743
1744 let (line_x_start, line_max_w) = if let Some(b) = boundary {
1746 b.allowed_span(current_y)
1747 .unwrap_or((0.0, max_width.unwrap_or(f32::MAX)))
1748 } else {
1749 (0.0, max_width.unwrap_or(f32::MAX))
1750 };
1751
1752 let glyph_right_edge = glyph.x + glyph.advance_width;
1753 let line_left = if line_start_glyph < glyphs.len() {
1754 glyphs[line_start_glyph].x
1755 } else {
1756 0.0
1757 };
1758 let line_content_width = glyph_right_edge - line_left;
1759
1760 if line_content_width > line_max_w && i > line_start_glyph {
1761 let break_glyph = if last_word_break_glyph > line_start_glyph {
1763 last_word_break_glyph
1764 } else {
1765 i
1766 };
1767 let break_byte = if last_word_break_byte > line_start_byte {
1768 last_word_break_byte
1769 } else {
1770 let mut bp = 0;
1772 let mut ci2 = 0u32;
1773 let tb = text.as_bytes();
1774 while bp < tb.len()
1775 && ci2 < glyphs[break_glyph.min(glyphs.len() - 1)].cluster
1776 {
1777 bp += Self::utf8_len(tb[bp]);
1778 ci2 += 1;
1779 }
1780 bp
1781 };
1782
1783 let line_width: f32 = glyphs[line_start_glyph..break_glyph]
1785 .iter()
1786 .map(|g| g.advance_width)
1787 .sum();
1788
1789 let x_offset = line_x_start
1790 + Self::compute_x_offset(
1791 align,
1792 line_max_w,
1793 line_width,
1794 glyphs,
1795 line_start_glyph,
1796 break_glyph,
1797 );
1798
1799 let mut x = x_offset;
1801 for g in &mut glyphs[line_start_glyph..break_glyph] {
1802 g.x = x;
1803 if g.glyph_id == 0xFFFF {
1804 let mut portal_h = g.advance_height;
1805 let mut alignment = PortalAlignment::Baseline;
1806 for span in spans {
1807 if let TextSpanKind::Portal {
1808 height,
1809 alignment: align_mode,
1810 ..
1811 } = &span.kind
1812 && span.byte_offset as u32 == g.cluster
1813 {
1814 portal_h = *height;
1815 alignment = *align_mode;
1816 break;
1817 }
1818 }
1819 let y_offset = match alignment {
1820 PortalAlignment::Baseline => 0.0,
1821 PortalAlignment::Top => -ascent,
1822 PortalAlignment::Center => {
1823 -ascent + (line_height_px - portal_h) / 2.0
1824 }
1825 PortalAlignment::Bottom => -ascent + line_height_px - portal_h,
1826 };
1827 g.y = current_y + y_offset;
1828 } else {
1829 g.y = current_y;
1830 }
1831 x += g.advance_width;
1832 }
1833
1834 let line_text = text[line_start_byte..break_byte.min(text.len())].to_string();
1835 lines.push(LineInfo {
1836 glyph_start: line_start_glyph,
1837 glyph_end: break_glyph,
1838 baseline_y: current_y,
1839 height: line_height_px,
1840 width: line_width,
1841 x_offset,
1842 byte_offset: line_start_byte,
1843 text: line_text,
1844 });
1845
1846 current_y += line_height_px;
1847 line_start_glyph = break_glyph;
1848 line_start_byte = break_byte;
1849 }
1850 }
1851
1852 if line_start_glyph < glyphs.len() {
1854 let (line_x_start, line_max_w) = if let Some(b) = boundary {
1855 b.allowed_span(current_y)
1856 .unwrap_or((0.0, max_width.unwrap_or(f32::MAX)))
1857 } else {
1858 (0.0, max_width.unwrap_or(f32::MAX))
1859 };
1860
1861 let line_width: f32 = glyphs[line_start_glyph..]
1862 .iter()
1863 .map(|g| g.advance_width)
1864 .sum();
1865
1866 let glyph_end = glyphs.len();
1867 let x_offset = line_x_start
1868 + Self::compute_x_offset(
1869 align,
1870 line_max_w,
1871 line_width,
1872 glyphs,
1873 line_start_glyph,
1874 glyph_end,
1875 );
1876
1877 let mut x = x_offset;
1878 for g in &mut glyphs[line_start_glyph..] {
1879 g.x = x;
1880 if g.glyph_id == 0xFFFF {
1881 let mut portal_h = g.advance_height;
1883 let mut alignment = PortalAlignment::Baseline;
1884 for span in spans {
1885 if let TextSpanKind::Portal {
1886 height,
1887 alignment: align_mode,
1888 ..
1889 } = &span.kind
1890 && span.byte_offset as u32 == g.cluster
1891 {
1892 portal_h = *height;
1893 alignment = *align_mode;
1894 break;
1895 }
1896 }
1897 let y_offset = match alignment {
1899 PortalAlignment::Baseline => 0.0,
1900 PortalAlignment::Top => -ascent,
1901 PortalAlignment::Center => -ascent + (line_height_px - portal_h) / 2.0,
1902 PortalAlignment::Bottom => -ascent + line_height_px - portal_h,
1903 };
1904 g.y = current_y + y_offset;
1905 } else {
1906 g.y = current_y;
1907 }
1908 x += g.advance_width;
1909 }
1910
1911 let remaining_text = text[line_start_byte.min(text.len())..].to_string();
1912 lines.push(LineInfo {
1913 glyph_start: line_start_glyph,
1914 glyph_end: glyphs.len(),
1915 baseline_y: current_y,
1916 height: line_height_px,
1917 width: line_width,
1918 x_offset,
1919 byte_offset: line_start_byte,
1920 text: remaining_text,
1921 });
1922 }
1923 } else {
1924 let line_width: f32 = glyphs.iter().map(|g| g.advance_width).sum();
1926
1927 let mut x = 0.0;
1928 for g in glyphs.iter_mut() {
1929 g.x = x;
1930 if g.glyph_id == 0xFFFF {
1931 let mut portal_h = g.advance_height;
1933 let mut alignment = PortalAlignment::Baseline;
1934 for span in spans {
1935 if let TextSpanKind::Portal {
1936 height,
1937 alignment: align_mode,
1938 ..
1939 } = &span.kind
1940 && span.byte_offset as u32 == g.cluster
1941 {
1942 portal_h = *height;
1943 alignment = *align_mode;
1944 break;
1945 }
1946 }
1947 let y_offset = match alignment {
1949 PortalAlignment::Baseline => 0.0,
1950 PortalAlignment::Top => -ascent,
1951 PortalAlignment::Center => -ascent + (line_height_px - portal_h) / 2.0,
1952 PortalAlignment::Bottom => -ascent + line_height_px - portal_h,
1953 };
1954 g.y = current_y + y_offset;
1955 } else {
1956 g.y = current_y;
1957 }
1958 x += g.advance_width;
1959 }
1960
1961 lines.push(LineInfo {
1962 glyph_start: 0,
1963 glyph_end: glyphs.len(),
1964 baseline_y: current_y,
1965 height: line_height_px,
1966 width: line_width,
1967 x_offset: 0.0,
1968 byte_offset: 0,
1969 text: text.to_string(),
1970 });
1971 }
1972
1973 for line_idx in 0..lines.len() {
1975 let line = &lines[line_idx];
1976 if line.glyph_start < line.glyph_end && line.glyph_end <= glyphs.len() {
1977 let level = line_bidi_level(bidi, line.byte_offset);
1978 if level.is_rtl() {
1979 reorder_line_rtl(glyphs, line.glyph_start, line.glyph_end);
1980 }
1981 }
1982 }
1983
1984 if overflow == TextOverflow::Ellipsis
1986 && let Some(max_w) = max_width
1987 {
1988 for line_idx in 0..lines.len() {
1989 let line = &lines[line_idx];
1990 if line.width > max_w {
1991 let mut trunc_width = 0.0f32;
1993 let mut trunc_glyph_end = line.glyph_start;
1994 let ellipsis_w = line_height_px * 0.6 * 3.0;
1996
1997 for gi in line.glyph_start..line.glyph_end {
1998 if gi < glyphs.len() {
1999 trunc_width += glyphs[gi].advance_width;
2000 if trunc_width + ellipsis_w > max_w {
2001 break;
2002 }
2003 trunc_glyph_end = gi + 1;
2004 }
2005 }
2006
2007 lines[line_idx].glyph_end = trunc_glyph_end;
2008 lines[line_idx].width = trunc_width;
2009 }
2010 }
2011 }
2012
2013 if let Some(tp) = path
2015 && let Some(last_glyph) = glyphs.last()
2016 {
2017 let total_x_len = last_glyph.x + last_glyph.advance_width;
2018 if total_x_len > 0.0 {
2019 for glyph in glyphs.iter_mut() {
2020 let t = (glyph.x / total_x_len).clamp(0.0, 1.0);
2021 let (pos, angle) = tp.sample(t);
2022 let dy = glyph.y - ascent;
2024 let perp_x = -angle.sin() * dy;
2025 let perp_y = angle.cos() * dy;
2026
2027 glyph.x = pos.0 + perp_x;
2028 glyph.y = pos.1 + perp_y;
2029 glyph.angle = angle;
2030 }
2031 }
2032 }
2033
2034 lines
2035 }
2036
2037 fn compute_x_offset(
2039 align: TextAlign,
2040 max_w: f32,
2041 line_width: f32,
2042 glyphs: &mut [GlyphInstance],
2043 start: usize,
2044 end: usize,
2045 ) -> f32 {
2046 match align {
2047 TextAlign::Start => 0.0,
2048 TextAlign::End => (max_w - line_width).max(0.0),
2049 TextAlign::Center => ((max_w - line_width) / 2.0).max(0.0),
2050 TextAlign::Justify => {
2051 if end <= start + 1 || max_w <= line_width {
2052 return 0.0;
2053 }
2054 let extra = max_w - line_width;
2055 let space_count = glyphs[start..end]
2056 .iter()
2057 .filter(|g| g.glyph_id == 3)
2058 .count();
2059 if space_count > 0 {
2060 let add_per_space = extra / space_count as f32;
2061 let mut x = 0.0f32;
2062 for i in start..end {
2063 glyphs[i].x = x;
2064 if glyphs[i].glyph_id == 3 {
2065 x += glyphs[i].advance_width + add_per_space;
2066 } else {
2067 x += glyphs[i].advance_width;
2068 }
2069 }
2070 }
2071 0.0
2072 }
2073 }
2074 }
2075
2076 fn utf8_len(first_byte: u8) -> usize {
2078 if first_byte < 0x80 {
2079 1
2080 } else if first_byte < 0xE0 {
2081 2
2082 } else if first_byte < 0xF0 {
2083 3
2084 } else {
2085 4
2086 }
2087 }
2088
2089 pub fn rasterize_glyph(
2091 &mut self,
2092 glyph_id: u16,
2093 style: &TextStyle,
2094 ) -> Result<GlyphImage, ShapingError> {
2095 let resolved = self.resolve_font(style)?;
2096
2097 let font_ref = resolved
2098 .primary
2099 .font_ref()
2100 .ok_or(ShapingError::InvalidFontData)?;
2101
2102 let mut scaler = self
2103 .scale_context
2104 .builder(font_ref)
2105 .size(style.font_size)
2106 .build();
2107
2108 let use_color = resolved.has_colr && style.render_mode == RenderMode::Color;
2109 let use_subpixel = style.render_mode == RenderMode::Subpixel;
2110
2111 let sources: Vec<SwashSource> = if use_color {
2112 vec![SwashSource::ColorOutline(glyph_id), SwashSource::Outline]
2113 } else {
2114 vec![SwashSource::Outline]
2115 };
2116
2117 let mut render = Render::new(&sources);
2118
2119 if use_subpixel {
2120 render.format(swash::zeno::Format::Subpixel);
2121 } else {
2122 render.format(swash::zeno::Format::Alpha);
2123 }
2124
2125 if style.synthesize_styles && style.weight >= Weight(700) {
2126 render.embolden(0.04);
2127 }
2128
2129 if let Some(image) = render.render(&mut scaler, glyph_id) {
2130 log::info!("Swash rendered image for glyph {}. content: {:?}, size: {}x{}, data len: {}", glyph_id, image.content, image.placement.width, image.placement.height, image.data.len());
2131 return Ok(GlyphImage {
2132 glyph_id,
2133 width: image.placement.width,
2134 height: image.placement.height,
2135 data: image.data,
2136 x_offset: image.placement.left as f32,
2137 y_offset: image.placement.top as f32,
2138 cache_key: resolved.cache_key,
2139 });
2140 }
2141
2142 for fallback in &resolved.fallbacks {
2144 if let Some(font_ref) = fallback.font_ref() {
2145 let mut scaler = self
2146 .scale_context
2147 .builder(font_ref)
2148 .size(style.font_size)
2149 .build();
2150 if let Some(image) = render.render(&mut scaler, glyph_id) {
2151 return Ok(GlyphImage {
2152 glyph_id,
2153 width: image.placement.width,
2154 height: image.placement.height,
2155 data: image.data,
2156 x_offset: image.placement.left as f32,
2157 y_offset: image.placement.top as f32,
2158 cache_key: resolved.cache_key,
2159 });
2160 }
2161 }
2162 }
2163
2164 Err(ShapingError::EmptyShape(format!(
2165 "Could not rasterize glyph {}",
2166 glyph_id
2167 )))
2168 }
2169
2170 pub fn extract_glyph_path(
2178 &mut self,
2179 glyph_id: u16,
2180 size: f32,
2181 style: &TextStyle,
2182 ) -> Result<Vec<RunicPathSegment>, ShapingError> {
2183 let resolved = self.resolve_font(style)?;
2184 let font_ref = resolved
2185 .primary
2186 .font_ref()
2187 .ok_or(ShapingError::InvalidFontData)?;
2188
2189 let mut scaler = self.scale_context.builder(font_ref).size(size).build();
2190
2191 let map_outline_to_segments =
2193 |outline: swash::scale::outline::Outline| -> Vec<RunicPathSegment> {
2194 let mut segments = Vec::new();
2195 let mut points_iter = outline.points().iter();
2196 for verb in outline.verbs() {
2197 match verb {
2198 swash::zeno::Verb::MoveTo => {
2199 if let Some(p) = points_iter.next() {
2200 segments.push(RunicPathSegment::MoveTo { x: p.x, y: p.y });
2201 }
2202 }
2203 swash::zeno::Verb::LineTo => {
2204 if let Some(p) = points_iter.next() {
2205 segments.push(RunicPathSegment::LineTo { x: p.x, y: p.y });
2206 }
2207 }
2208 swash::zeno::Verb::QuadTo => {
2209 if let Some(cp) = points_iter.next()
2210 && let Some(p) = points_iter.next()
2211 {
2212 segments.push(RunicPathSegment::QuadTo {
2213 cx: cp.x,
2214 cy: cp.y,
2215 x: p.x,
2216 y: p.y,
2217 });
2218 }
2219 }
2220 swash::zeno::Verb::CurveTo => {
2221 if let Some(cp1) = points_iter.next()
2222 && let Some(cp2) = points_iter.next()
2223 && let Some(p) = points_iter.next()
2224 {
2225 segments.push(RunicPathSegment::CubicTo {
2226 cx1: cp1.x,
2227 cy1: cp1.y,
2228 cx2: cp2.x,
2229 cy2: cp2.y,
2230 x: p.x,
2231 y: p.y,
2232 });
2233 }
2234 }
2235 swash::zeno::Verb::Close => {
2236 segments.push(RunicPathSegment::Close);
2237 }
2238 }
2239 }
2240 segments
2241 };
2242
2243 if let Some(outline) = scaler.scale_outline(glyph_id) {
2245 return Ok(map_outline_to_segments(outline));
2246 }
2247
2248 for fallback in &resolved.fallbacks {
2250 if let Some(font_ref) = fallback.font_ref() {
2251 let mut scaler = self.scale_context.builder(font_ref).size(size).build();
2252 if let Some(outline) = scaler.scale_outline(glyph_id) {
2253 return Ok(map_outline_to_segments(outline));
2254 }
2255 }
2256 }
2257
2258 Ok(Vec::new())
2259 }
2260
2261 pub fn font_metrics(&mut self, style: &TextStyle) -> Result<FontMetrics, ShapingError> {
2263 let resolved = self.resolve_font(style)?;
2264 let (ascent, descent, line_gap) = resolved.metrics_pixels(style.font_size);
2265
2266 Ok(FontMetrics {
2267 ascent,
2268 descent,
2269 line_gap,
2270 units_per_em: resolved.units_per_em,
2271 x_height: resolved.x_height * style.font_size / resolved.units_per_em as f32,
2272 cap_height: resolved.cap_height * style.font_size / resolved.units_per_em as f32,
2273 })
2274 }
2275
2276 pub fn clear_cache(&mut self) {
2278 self.cache.clear();
2279 self.cache_order.clear();
2280 }
2281
2282 pub fn cache_stats(&self) -> (usize, usize) {
2284 (self.cache.len(), MAX_CACHE_SIZE)
2285 }
2286
2287 pub fn font_count(&self) -> usize {
2289 self.db.faces().count()
2290 }
2291
2292 pub fn query_font_axes(
2301 &mut self,
2302 family: &str,
2303 _font_size: f32,
2304 ) -> Result<Option<Vec<FontAxisInfo>>, ShapingError> {
2305 let query = Query {
2306 families: &[Family::Name(family)],
2307 weight: Weight::NORMAL,
2308 stretch: Stretch::Normal,
2309 style: Style::Normal,
2310 };
2311
2312 let id = self
2313 .db
2314 .query(&query)
2315 .ok_or_else(|| ShapingError::NoFontFound(family.to_string()))?;
2316 let data = self
2317 .get_font_data(id)
2318 .ok_or(ShapingError::InvalidFontData)?;
2319 let _font_ref = data.font_ref().ok_or(ShapingError::InvalidFontData)?;
2320
2321 let ttf_face = rustybuzz::ttf_parser::Face::parse(data.as_bytes(), data.index)
2323 .map_err(|_| ShapingError::InvalidFontData)?;
2324
2325 let fvar_data = match ttf_face
2327 .raw_face()
2328 .table(rustybuzz::ttf_parser::Tag(u32::from_be_bytes(*b"fvar")))
2329 {
2330 Some(d) => d,
2331 None => return Ok(None), };
2333
2334 if fvar_data.len() < 16 {
2337 return Ok(None);
2338 }
2339
2340 let axis_count = u16::from_be_bytes([fvar_data[8], fvar_data[9]]) as usize;
2341 let axis_size = u16::from_be_bytes([fvar_data[10], fvar_data[11]]) as usize;
2342 let data_offset = u16::from_be_bytes([fvar_data[4], fvar_data[5]]) as usize;
2343
2344 let mut axes = Vec::new();
2345 for i in 0..axis_count {
2346 let offset = data_offset + i * axis_size;
2347 if offset + axis_size > fvar_data.len() {
2348 break;
2349 }
2350
2351 let axis_data = &fvar_data[offset..offset + axis_size];
2352
2353 if axis_data.len() < 20 {
2355 break;
2356 }
2357
2358 let tag = u32::from_be_bytes([axis_data[0], axis_data[1], axis_data[2], axis_data[3]]);
2359 let min_val =
2360 f32::from_be_bytes([axis_data[4], axis_data[5], axis_data[6], axis_data[7]]);
2361 let default_val =
2362 f32::from_be_bytes([axis_data[8], axis_data[9], axis_data[10], axis_data[11]]);
2363 let max_val =
2364 f32::from_be_bytes([axis_data[12], axis_data[13], axis_data[14], axis_data[15]]);
2365 let _name_id = u16::from_be_bytes([axis_data[18], axis_data[19]]);
2366
2367 let tag_bytes = tag.to_be_bytes();
2368 let tag_string = String::from_utf8_lossy(&tag_bytes).trim().to_string();
2369
2370 let standard_tags: &[&[u8]] = &[
2372 b"wght", b"wdth", b"ital", b"slnt", b"opsz", b"GRAD", b"XTRA", b"XOPQ", b"YOPQ",
2373 b"YTLC", b"YTUC", b"YTAS", b"YTDE", b"YTFI", b"wdth",
2374 ];
2375 let is_standard = standard_tags.contains(&tag_bytes.as_slice());
2376
2377 axes.push(FontAxisInfo {
2378 tag,
2379 tag_string,
2380 min: min_val,
2381 max: max_val,
2382 default: default_val,
2383 is_standard,
2384 });
2385 }
2386
2387 if axes.is_empty() {
2388 Ok(None)
2389 } else {
2390 Ok(Some(axes))
2391 }
2392 }
2393
2394 pub fn shape(&mut self, text: &str, family: &str, size: f32) -> ShapedText {
2401 let style = TextStyle::new(family, size);
2402 let spans = vec![TextSpan::new(text, style)];
2403 self.shape_layout(&spans, None, TextAlign::Start, TextOverflow::WordWrap)
2404 .unwrap_or_else(|_| ShapedText {
2405 glyphs: Vec::new(),
2406 lines: Vec::new(),
2407 width: 0.0,
2408 height: 0.0,
2409 text: text.to_string(),
2410 spans: Vec::new(),
2411 has_rtl: false,
2412 ascent: 0.0,
2413 descent: 0.0,
2414 line_gap: 0.0,
2415 grapheme_boundaries: vec![],
2416 })
2417 }
2418
2419 pub fn rasterize(&mut self, cache_key: u64) -> Option<GlyphImage> {
2428 let mut found: Option<(CacheKey, GlyphInstance)> = None;
2429 for (ck, glyphs) in &self.cache {
2430 if let Some(g) = glyphs.iter().find(|g| g.cache_key == cache_key) {
2431 found = Some((*ck, *g));
2432 break;
2433 }
2434 }
2435 let (ck, glyph) = found?;
2436
2437 let mut family = "sans-serif".to_string();
2439 let face_ids: Vec<fontdb::ID> = self.db.faces().map(|f| f.id).collect();
2440 for id in face_ids {
2441 if let Some(font_data) = self.get_font_data(id)
2442 && let Some(font_ref) = font_data.font_ref()
2443 && font_ref.key.value() == ck.font_cache_key
2444 {
2445 if let Some(face) = self.db.face(id)
2446 && let Some((name, _)) = face.families.first()
2447 {
2448 family = name.clone();
2449 }
2450 break;
2451 }
2452 }
2453
2454 let mut style = TextStyle::new(&family, ck.font_size as f32 / 2.0);
2455 style.weight = Weight(ck.weight);
2456 style.stretch = match ck.stretch {
2457 1 => Stretch::UltraCondensed,
2458 2 => Stretch::ExtraCondensed,
2459 3 => Stretch::Condensed,
2460 4 => Stretch::SemiCondensed,
2461 5 => Stretch::Normal,
2462 6 => Stretch::SemiExpanded,
2463 7 => Stretch::Expanded,
2464 8 => Stretch::ExtraExpanded,
2465 9 => Stretch::UltraExpanded,
2466 _ => Stretch::Normal,
2467 };
2468 style.style = match ck.style {
2469 0 => Style::Normal,
2470 1 => Style::Italic,
2471 2 => Style::Oblique,
2472 _ => Style::Normal,
2473 };
2474
2475 let mut image = self.rasterize_glyph(glyph.glyph_id, &style).ok()?;
2476 image.cache_key = cache_key;
2477 Some(image)
2478 }
2479}
2480
2481fn byte_offset_level(bidi: &BidiInfo, byte_offset: usize) -> unicode_bidi::Level {
2482 if let Some(para) = bidi.paragraphs.first() {
2483 let relative = byte_offset.saturating_sub(para.range.start);
2484 if relative < bidi.levels.len() {
2485 return bidi.levels[relative];
2486 }
2487 }
2488 unicode_bidi::Level::ltr()
2489}
2490
2491fn line_bidi_level(bidi: &BidiInfo, byte_offset: usize) -> unicode_bidi::Level {
2492 byte_offset_level(bidi, byte_offset)
2493}
2494
2495fn reorder_line_rtl(glyphs: &mut [GlyphInstance], start: usize, end: usize) {
2496 if end <= start {
2497 return;
2498 }
2499 let slice = &mut glyphs[start..end];
2500 slice.reverse();
2501 let mut x = 0.0f32;
2502 for g in slice.iter_mut() {
2503 g.x = x;
2504 x += g.advance_width;
2505 }
2506}
2507
2508impl Default for RunicTextEngine {
2509 fn default() -> Self {
2510 Self::new()
2511 }
2512}
2513
2514#[derive(Debug, Clone, Copy, PartialEq)]
2518pub struct FontMetrics {
2519 pub ascent: f32,
2521 pub descent: f32,
2523 pub line_gap: f32,
2525 pub units_per_em: u16,
2527 pub x_height: f32,
2529 pub cap_height: f32,
2531}
2532
2533pub mod msdf;
2538
2539pub mod knuth_plass;
2542
2543pub mod emoji;
2546
2547pub mod subpixel;
2550
2551#[cfg(test)]
2554mod tests {
2555 use super::*;
2556
2557 #[test]
2558 fn test_basic_shaping() {
2559 let mut engine = RunicTextEngine::new_test();
2560 let style = TextStyle::new("Jupiteroid", 16.0);
2561 let glyphs = engine
2562 .shape_run("Hello", &style, Direction::LeftToRight)
2563 .unwrap();
2564 assert!(!glyphs.is_empty(), "Should produce glyphs for 'Hello'");
2565 }
2566
2567 #[test]
2568 fn test_hit_test() {
2569 let mut engine = RunicTextEngine::new_test();
2570 let style = TextStyle::new("Jupiteroid", 16.0);
2571 let spans = vec![TextSpan::new("Hello", style.clone())];
2572 let shaped = engine
2573 .shape_layout(&spans, None, TextAlign::Start, TextOverflow::WordWrap)
2574 .unwrap();
2575 let (glyph_idx, cluster) = shaped.hit_test(0);
2576 assert!(glyph_idx < shaped.glyphs.len());
2577 assert_eq!(cluster, 0);
2578 }
2579
2580 #[test]
2581 fn test_word_wrapping() {
2582 let mut engine = RunicTextEngine::new_test();
2583 let style = TextStyle::new("Jupiteroid", 16.0);
2584 let spans = vec![TextSpan::new("Hello World This Is A Test", style.clone())];
2585 let shaped = engine
2586 .shape_layout(&spans, Some(80.0), TextAlign::Start, TextOverflow::WordWrap)
2587 .unwrap();
2588 assert!(
2589 shaped.lines.len() > 1,
2590 "Should wrap into multiple lines, got {}",
2591 shaped.lines.len()
2592 );
2593 }
2594
2595 #[test]
2596 fn test_text_style_defaults() {
2597 let style = TextStyle::default();
2598 assert_eq!(style.family, "Jupiteroid");
2599 assert_eq!(style.font_size, DEFAULT_FONT_SIZE);
2600 assert_eq!(style.weight, Weight::NORMAL);
2601 assert_eq!(style.color, [255, 255, 255, 255]);
2602 assert!(!style.fallback_families.is_empty());
2603 }
2604
2605 #[test]
2606 fn test_text_style_builder() {
2607 let style = TextStyle::new("Jupiteroid", 24.0)
2608 .with_weight(700)
2609 .italic()
2610 .with_color(255, 0, 0, 255)
2611 .with_letter_spacing(1.5)
2612 .with_underline();
2613
2614 assert_eq!(style.font_size, 24.0);
2615 assert_eq!(style.weight, Weight(700));
2616 assert_eq!(style.style, Style::Italic);
2617 assert_eq!(style.color, [255, 0, 0, 255]);
2618 assert_eq!(style.letter_spacing, 1.5);
2619 assert!(style.decorations.underline);
2620 }
2621
2622 #[test]
2623 fn test_line_height() {
2624 let multiple = LineHeight::Multiple(1.5);
2625 assert_eq!(multiple.to_pixels(16.0), 24.0);
2626
2627 let fixed = LineHeight::Fixed(20.0);
2628 assert_eq!(fixed.to_pixels(16.0), 20.0);
2629 }
2630
2631 #[test]
2632 fn test_cache_key_deterministic() {
2633 let key1 = CacheKey::new(
2634 "Hello",
2635 12345,
2636 16.0,
2637 Weight::NORMAL,
2638 Stretch::Normal,
2639 Style::Normal,
2640 Direction::LeftToRight,
2641 0.0,
2642 0.0,
2643 );
2644 let key2 = CacheKey::new(
2645 "Hello",
2646 12345,
2647 16.0,
2648 Weight::NORMAL,
2649 Stretch::Normal,
2650 Style::Normal,
2651 Direction::LeftToRight,
2652 0.0,
2653 0.0,
2654 );
2655 assert_eq!(key1, key2);
2656
2657 let key3 = CacheKey::new(
2658 "World",
2659 12345,
2660 16.0,
2661 Weight::NORMAL,
2662 Stretch::Normal,
2663 Style::Normal,
2664 Direction::LeftToRight,
2665 0.0,
2666 0.0,
2667 );
2668 assert_ne!(key1, key3);
2669 }
2670
2671 #[test]
2672 fn test_cursor_position() {
2673 let mut engine = RunicTextEngine::new_test();
2674 let style = TextStyle::new("Jupiteroid", 16.0);
2675 let spans = vec![TextSpan::new("Hello", style.clone())];
2676 let shaped = engine
2677 .shape_layout(&spans, None, TextAlign::Start, TextOverflow::WordWrap)
2678 .unwrap();
2679 let (x, line) = shaped.cursor_position(0);
2680 assert_eq!(line, 0);
2681 assert!(x >= 0.0);
2682 }
2683
2684 #[test]
2685 fn test_selection_rects() {
2686 let mut engine = RunicTextEngine::new_test();
2687 let style = TextStyle::new("Jupiteroid", 16.0);
2688 let spans = vec![TextSpan::new("Hello World", style.clone())];
2689 let shaped = engine
2690 .shape_layout(&spans, None, TextAlign::Start, TextOverflow::WordWrap)
2691 .unwrap();
2692 let rects = shaped.selection_rects(0, 5);
2693 assert!(
2694 !rects.is_empty(),
2695 "Should produce selection rects for 'Hello'"
2696 );
2697 }
2698
2699 #[test]
2700 fn test_open_type_features() {
2701 let liga = OpenTypeFeature::liga();
2702 assert_eq!(liga.tag, u32::from_be_bytes(*b"liga"));
2703 assert_eq!(liga.value, 1);
2704
2705 let kern = OpenTypeFeature::kern();
2706 assert_eq!(kern.tag, u32::from_be_bytes(*b"kern"));
2707 }
2708
2709 #[test]
2710 fn test_variable_axes() {
2711 let weight = VariableAxis::weight(700.0);
2712 assert_eq!(weight.tag, u32::from_be_bytes(*b"wght"));
2713 assert_eq!(weight.value, 700.0);
2714
2715 let italic = VariableAxis::italic(1.0);
2716 assert_eq!(italic.tag, u32::from_be_bytes(*b"ital"));
2717 }
2718
2719 #[test]
2720 fn test_font_metrics() {
2721 let mut engine = RunicTextEngine::new_test();
2722 let style = TextStyle::new("Jupiteroid", 16.0);
2723 let metrics = engine.font_metrics(&style).unwrap();
2724 assert!(metrics.ascent > 0.0);
2725 assert!(metrics.descent > 0.0);
2726 assert!(metrics.units_per_em > 0);
2727 }
2728
2729 #[test]
2730 fn test_empty_input() {
2731 let mut engine = RunicTextEngine::new_test();
2732 let style = TextStyle::new("Jupiteroid", 16.0);
2733 let spans = vec![TextSpan::new("", style.clone())];
2734 let shaped = engine
2735 .shape_layout(&spans, None, TextAlign::Start, TextOverflow::WordWrap)
2736 .unwrap();
2737 assert!(shaped.glyphs.is_empty());
2738 }
2739
2740 #[test]
2741 fn test_multi_span_layout() {
2742 let mut engine = RunicTextEngine::new_test();
2743 let style1 = TextStyle::new("Jupiteroid", 16.0);
2744 let style2 = TextStyle::new("Jupiteroid", 24.0).with_color(255, 0, 0, 255);
2745 let spans = vec![
2746 TextSpan::at("Hello ", style1, 0),
2747 TextSpan::at("World", style2, 6),
2748 ];
2749 let shaped = engine
2750 .shape_layout(&spans, None, TextAlign::Start, TextOverflow::WordWrap)
2751 .unwrap();
2752 assert!(!shaped.glyphs.is_empty());
2753 assert_eq!(shaped.text, "Hello World");
2754 }
2755
2756 #[test]
2757 fn test_text_align_center() {
2758 let mut engine = RunicTextEngine::new_test();
2759 let style = TextStyle::new("Jupiteroid", 16.0);
2760 let spans = vec![TextSpan::new("Hi", style.clone())];
2761 let shaped = engine
2762 .shape_layout(
2763 &spans,
2764 Some(200.0),
2765 TextAlign::Center,
2766 TextOverflow::WordWrap,
2767 )
2768 .unwrap();
2769 assert!(!shaped.lines.is_empty());
2770 let line = &shaped.lines[0];
2771 assert!(
2772 line.x_offset > 0.0,
2773 "Center-aligned line should have positive x_offset, got {}",
2774 line.x_offset
2775 );
2776 }
2777
2778 #[test]
2779 fn test_text_overflow_ellipsis() {
2780 let mut engine = RunicTextEngine::new_test();
2781 let style = TextStyle::new("Jupiteroid", 16.0);
2782 let spans = vec![TextSpan::new("Hello World This Is Long", style.clone())];
2783 let shaped = engine
2784 .shape_layout(&spans, Some(50.0), TextAlign::Start, TextOverflow::Ellipsis)
2785 .unwrap();
2786 assert!(!shaped.lines.is_empty());
2787 }
2788
2789 #[test]
2790 fn test_decorations() {
2791 let decorations = TextDecorations {
2792 underline: true,
2793 strikethrough: true,
2794 overline: false,
2795 };
2796 assert!(decorations.underline);
2797 assert!(decorations.strikethrough);
2798 assert!(!decorations.overline);
2799 }
2800
2801 #[test]
2802 fn test_cache_eviction() {
2803 let mut engine = RunicTextEngine::new_test();
2804 let style = TextStyle::new("Jupiteroid", 16.0);
2805
2806 let _ = engine.shape_run("Test", &style, Direction::LeftToRight);
2807
2808 let (size, max) = engine.cache_stats();
2809 assert!(size > 0, "Cache should have entries after shaping");
2810 assert_eq!(max, MAX_CACHE_SIZE);
2811
2812 engine.clear_cache();
2813 let (size, _) = engine.cache_stats();
2814 assert_eq!(size, 0);
2815 }
2816
2817 #[test]
2818 fn test_font_count() {
2819 let engine = RunicTextEngine::new_test();
2820 let count = engine.font_count();
2821 assert!(count > 0, "Should find at least one font, got {}", count);
2822 }
2823
2824 #[test]
2825 fn test_jupiteroid_font_available() {
2826 let engine = RunicTextEngine::new_test();
2827 assert!(engine.font_count() > 0, "Should have fonts loaded");
2828 }
2829
2830 #[test]
2831 fn test_extract_glyph_path() {
2832 let mut engine = RunicTextEngine::new_test();
2833 let style = TextStyle::new("Jupiteroid", 16.0);
2834
2835 let glyphs = engine
2837 .shape_run("A", &style, Direction::LeftToRight)
2838 .unwrap();
2839 assert!(!glyphs.is_empty(), "Shaping 'A' should yield a glyph");
2840 let glyph_id = glyphs[0].glyph_id;
2841
2842 let path = engine.extract_glyph_path(glyph_id, 16.0, &style).unwrap();
2844
2845 assert!(!path.is_empty(), "Glyph path for 'A' should not be empty");
2847 match path[0] {
2848 RunicPathSegment::MoveTo { x, y } => {
2849 assert!(x.is_finite());
2850 assert!(y.is_finite());
2851 }
2852 _ => panic!("Expected first segment to be a MoveTo, got {:?}", path[0]),
2853 }
2854
2855 let has_close = path
2856 .iter()
2857 .any(|seg| matches!(seg, RunicPathSegment::Close));
2858 assert!(
2859 has_close,
2860 "Expected glyph path to contain at least one Close command"
2861 );
2862
2863 for segment in &path {
2865 match *segment {
2866 RunicPathSegment::MoveTo { x, y } => {
2867 assert!(x.is_finite());
2868 assert!(y.is_finite());
2869 }
2870 RunicPathSegment::LineTo { x, y } => {
2871 assert!(x.is_finite());
2872 assert!(y.is_finite());
2873 }
2874 RunicPathSegment::QuadTo { cx, cy, x, y } => {
2875 assert!(cx.is_finite());
2876 assert!(cy.is_finite());
2877 assert!(x.is_finite());
2878 assert!(y.is_finite());
2879 }
2880 RunicPathSegment::CubicTo {
2881 cx1,
2882 cy1,
2883 cx2,
2884 cy2,
2885 x,
2886 y,
2887 } => {
2888 assert!(cx1.is_finite());
2889 assert!(cy1.is_finite());
2890 assert!(cx2.is_finite());
2891 assert!(cy2.is_finite());
2892 assert!(x.is_finite());
2893 assert!(y.is_finite());
2894 }
2895 RunicPathSegment::Close => {}
2896 }
2897 }
2898 }
2899
2900 #[test]
2901 fn test_new_text_style_fields() {
2902 let style = TextStyle::new("Jupiteroid", 16.0)
2903 .with_outline_rendering(true)
2904 .with_material_effect(42);
2905
2906 assert!(style.outline_rendering);
2907 assert_eq!(style.material_effect_id, 42);
2908 }
2909
2910 #[test]
2911 fn test_text_path_sampling() {
2912 let tp = TextPath::new(vec![(0.0, 0.0), (100.0, 100.0), (200.0, 0.0)]);
2914 let ((x_start, y_start), angle_start) = tp.sample(0.0);
2915 let ((x_mid, y_mid), angle_mid) = tp.sample(0.5);
2916
2917 assert_eq!(x_start, 0.0);
2918 assert_eq!(y_start, 0.0);
2919 assert!(angle_start > 0.0);
2920
2921 assert_eq!(x_mid, 100.0);
2922 assert_eq!(y_mid, 50.0);
2923 assert!(angle_mid.abs() < 1e-4); }
2925
2926 #[test]
2927 fn test_layout_boundary_circle() {
2928 let boundary = LayoutBoundary::Circle {
2929 cx: 100.0,
2930 cy: 100.0,
2931 r: 50.0,
2932 };
2933 let span = boundary.allowed_span(100.0).unwrap();
2935 assert_eq!(span.0, 50.0);
2936 assert_eq!(span.1, 150.0);
2937
2938 let span_edge = boundary.allowed_span(150.0);
2940 assert!(span_edge.is_none() || span_edge.unwrap().0 >= 100.0);
2941 }
2942
2943 #[test]
2944 fn test_shape_layout_with_path_and_boundary() {
2945 let mut engine = RunicTextEngine::new_test();
2946 let style = TextStyle::new("Jupiteroid", 16.0);
2947 let spans = vec![TextSpan::new(
2948 "Hello World Curved Layout Test String",
2949 style,
2950 )];
2951
2952 let tp = TextPath::new(vec![(0.0, 0.0), (100.0, 50.0), (200.0, 0.0)]);
2954 let shaped_path = engine
2955 .shape_layout_ex(
2956 &spans,
2957 None,
2958 TextAlign::Start,
2959 TextOverflow::WordWrap,
2960 Some(tp),
2961 None,
2962 )
2963 .unwrap();
2964 assert!(!shaped_path.glyphs.is_empty());
2965 let has_angles = shaped_path.glyphs.iter().any(|g| g.angle != 0.0);
2967 assert!(has_angles);
2968
2969 let boundary = LayoutBoundary::Circle {
2971 cx: 100.0,
2972 cy: 100.0,
2973 r: 50.0,
2974 };
2975 let shaped_boundary = engine
2976 .shape_layout_ex(
2977 &spans,
2978 None,
2979 TextAlign::Start,
2980 TextOverflow::WordWrap,
2981 None,
2982 Some(boundary),
2983 )
2984 .unwrap();
2985 assert!(!shaped_boundary.glyphs.is_empty());
2986 }
2987
2988 #[test]
2989 fn test_portal_alignment() {
2990 let mut engine = RunicTextEngine::new_test();
2991 let style = TextStyle::new("Jupiteroid", 16.0);
2992
2993 let spans = vec![
2995 TextSpan::at("Txt ", style.clone(), 0),
2996 TextSpan::portal_at(
2997 30.0,
2998 20.0,
2999 PortalAlignment::Baseline,
3000 "p_base",
3001 style.clone(),
3002 4,
3003 ),
3004 TextSpan::portal_at(30.0, 20.0, PortalAlignment::Top, "p_top", style.clone(), 7),
3005 TextSpan::portal_at(
3006 30.0,
3007 20.0,
3008 PortalAlignment::Center,
3009 "p_center",
3010 style.clone(),
3011 10,
3012 ),
3013 TextSpan::portal_at(
3014 30.0,
3015 20.0,
3016 PortalAlignment::Bottom,
3017 "p_bottom",
3018 style.clone(),
3019 13,
3020 ),
3021 ];
3022
3023 let shaped_single = engine
3025 .shape_layout(&spans, None, TextAlign::Start, TextOverflow::WordWrap)
3026 .unwrap();
3027
3028 let portals_s: Vec<_> = shaped_single
3029 .glyphs
3030 .iter()
3031 .filter(|g| g.glyph_id == 0xFFFF)
3032 .collect();
3033 assert_eq!(portals_s.len(), 4);
3034
3035 let baseline_y = shaped_single.lines[0].baseline_y;
3036 let ascent = shaped_single.ascent;
3037 let line_height_px = shaped_single.lines[0].height;
3038
3039 assert_eq!(portals_s[0].y, baseline_y);
3041
3042 assert_eq!(portals_s[1].y, baseline_y - ascent);
3044
3045 assert_eq!(
3047 portals_s[2].y,
3048 baseline_y - ascent + (line_height_px - 20.0) / 2.0
3049 );
3050
3051 assert_eq!(portals_s[3].y, baseline_y - ascent + line_height_px - 20.0);
3053
3054 let shaped_wrapped = engine
3056 .shape_layout(&spans, Some(50.0), TextAlign::Start, TextOverflow::WordWrap)
3057 .unwrap();
3058
3059 let portals_w: Vec<_> = shaped_wrapped
3060 .glyphs
3061 .iter()
3062 .filter(|g| g.glyph_id == 0xFFFF)
3063 .collect();
3064 assert_eq!(portals_w.len(), 4);
3065 }
3066}