1use super::FontRegistry;
10
11#[derive(Debug, Clone)]
13pub struct FontRun {
14 pub start: usize,
16 pub end: usize,
18 pub family: String,
20}
21
22pub(crate) fn resolve_family(
25 ch: char,
26 families: &str,
27 weight: u32,
28 italic: bool,
29 previous: Option<&str>,
30 registry: &FontRegistry,
31) -> String {
32 if let Some(family) = previous {
33 let shaping_control = matches!(ch, '\u{200c}' | '\u{200d}' | '\u{fe00}'..='\u{fe0f}' | '\u{e0100}'..='\u{e01ef}');
34 let combining_mark = unicode_bidi::bidi_class(ch) == unicode_bidi::BidiClass::NSM;
35 if shaping_control
36 || (combining_mark && registry.resolve(family, weight, italic).has_char(ch))
37 {
38 return family.to_string();
39 }
40 }
41 registry.resolve_for_char(families, ch, weight, italic).1
42}
43
44pub fn segment_by_font(
52 chars: &[char],
53 families: &str,
54 weight: u32,
55 italic: bool,
56 registry: &FontRegistry,
57) -> Vec<FontRun> {
58 if chars.is_empty() {
59 return vec![];
60 }
61
62 if !families.contains(',') {
68 let family = families.trim().trim_matches('"').trim_matches('\'');
69 let font = registry.resolve(family, weight, italic);
70 let all_covered = chars.iter().all(|&ch| ch.is_control() || font.has_char(ch));
71 if all_covered {
72 return vec![FontRun {
73 start: 0,
74 end: chars.len(),
75 family: family.to_string(),
76 }];
77 }
78 }
81
82 let mut runs = Vec::new();
84 let first_family = resolve_family(chars[0], families, weight, italic, None, registry);
85 let mut current_family = first_family;
86 let mut run_start = 0;
87
88 for (i, &ch) in chars.iter().enumerate().skip(1) {
89 let family = resolve_family(
90 ch,
91 families,
92 weight,
93 italic,
94 Some(¤t_family),
95 registry,
96 );
97 if family != current_family {
98 runs.push(FontRun {
99 start: run_start,
100 end: i,
101 family: current_family,
102 });
103 current_family = family;
104 run_start = i;
105 }
106 }
107
108 runs.push(FontRun {
110 start: run_start,
111 end: chars.len(),
112 family: current_family,
113 });
114
115 runs
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121
122 #[test]
123 fn test_single_font_fast_path() {
124 let registry = FontRegistry::new();
125 let chars: Vec<char> = "Hello world".chars().collect();
126 let runs = segment_by_font(&chars, "Helvetica", 400, false, ®istry);
127 assert_eq!(runs.len(), 1);
128 assert_eq!(runs[0].family, "Helvetica");
129 assert_eq!(runs[0].start, 0);
130 assert_eq!(runs[0].end, 11);
131 }
132
133 #[test]
134 fn test_empty_input() {
135 let registry = FontRegistry::new();
136 let chars: Vec<char> = vec![];
137 let runs = segment_by_font(&chars, "Helvetica, Times", 400, false, ®istry);
138 assert!(runs.is_empty());
139 }
140
141 #[test]
142 fn test_single_font_builtin_fallback() {
143 let registry = FontRegistry::new();
144 let chars: Vec<char> = "\u{041F}\u{0440}\u{0438}\u{0432}\u{0435}\u{0442}"
146 .chars()
147 .collect();
148 let runs = segment_by_font(&chars, "Helvetica", 400, false, ®istry);
149 assert!(!runs.is_empty(), "Should produce at least one run");
150 assert_eq!(runs[0].family, "Noto Sans", "Cyrillic should use Noto Sans");
152 }
153
154 #[test]
155 fn test_single_font_mixed_latin_cyrillic() {
156 let registry = FontRegistry::new();
157 let chars: Vec<char> = "Hi \u{041F}".chars().collect();
159 let runs = segment_by_font(&chars, "Helvetica", 400, false, ®istry);
160 assert!(
161 runs.len() >= 2,
162 "Should have at least 2 runs (Latin + Cyrillic), got {}",
163 runs.len()
164 );
165 }
166
167 #[test]
168 fn test_all_chars_same_font() {
169 let registry = FontRegistry::new();
170 let chars: Vec<char> = "ABC".chars().collect();
171 let runs = segment_by_font(&chars, "Helvetica, Times", 400, false, ®istry);
173 assert_eq!(runs.len(), 1);
174 assert_eq!(runs[0].family, "Helvetica");
175 }
176}