1use rustybuzz::{Face, Feature, GlyphBuffer, Language, Script, UnicodeBuffer};
31use std::collections::HashMap;
32use std::str::FromStr;
33use std::sync::Arc;
34use unicode_segmentation::UnicodeSegmentation;
35
36#[derive(Debug, Clone, Copy)]
38pub struct ShapedGlyph {
39 #[allow(dead_code)]
41 pub glyph_id: u32,
42
43 #[allow(dead_code)]
45 pub cluster: u32,
46
47 pub x_advance: f32,
49
50 #[allow(dead_code)]
52 pub y_advance: f32,
53
54 #[allow(dead_code)]
56 pub x_offset: f32,
57
58 #[allow(dead_code)]
60 pub y_offset: f32,
61}
62
63#[derive(Debug, Clone)]
65pub struct ShapingOptions {
66 pub enable_ligatures: bool,
68
69 pub enable_kerning: bool,
71
72 pub enable_contextual_alternates: bool,
74
75 pub script: Option<String>,
77
78 pub language: Option<String>,
80
81 pub rtl: bool,
83}
84
85impl Default for ShapingOptions {
86 fn default() -> Self {
87 Self {
88 enable_ligatures: true,
89 enable_kerning: true,
90 enable_contextual_alternates: true,
91 script: None,
92 language: None,
93 rtl: false,
94 }
95 }
96}
97
98#[derive(Debug, Clone)]
100pub struct ShapedRun {
101 #[allow(dead_code)]
103 pub text: String,
104
105 #[allow(dead_code)]
107 pub glyphs: Vec<ShapedGlyph>,
108
109 #[allow(dead_code)]
111 pub total_advance: f32,
112
113 #[allow(dead_code)]
115 pub cluster_boundaries: Vec<usize>,
116}
117
118#[derive(Debug, Clone, Hash, Eq, PartialEq)]
120struct ShapeCacheKey {
121 text: String,
122 font_index: usize,
123 enable_ligatures: bool,
124 enable_kerning: bool,
125 script: Option<String>,
126 language: Option<String>,
127 rtl: bool,
128}
129
130pub struct TextShaper {
132 shape_cache: HashMap<ShapeCacheKey, Arc<ShapedRun>>,
134
135 max_cache_size: usize,
137}
138
139impl TextShaper {
140 pub fn new() -> Self {
142 Self::with_cache_size(1000)
143 }
144
145 pub fn with_cache_size(max_cache_size: usize) -> Self {
147 Self {
148 shape_cache: HashMap::new(),
149 max_cache_size,
150 }
151 }
152
153 pub fn detect_grapheme_clusters<'a>(&self, text: &'a str) -> Vec<(usize, &'a str)> {
161 text.grapheme_indices(true).collect()
162 }
163
164 #[allow(dead_code)]
169 pub fn is_regional_indicator_pair(&self, grapheme: &str) -> bool {
170 let chars: Vec<char> = grapheme.chars().collect();
171 if chars.len() == 2 {
172 let is_ri = |c: char| {
173 let code = c as u32;
174 (0x1F1E6..=0x1F1FF).contains(&code)
175 };
176 is_ri(chars[0]) && is_ri(chars[1])
177 } else {
178 false
179 }
180 }
181
182 #[allow(dead_code)]
186 pub fn contains_zwj(&self, grapheme: &str) -> bool {
187 grapheme.contains('\u{200D}')
188 }
189
190 pub fn shape_text(
204 &mut self,
205 text: &str,
206 font_data: &[u8],
207 font_index: usize,
208 options: ShapingOptions,
209 ) -> Arc<ShapedRun> {
210 let cache_key = ShapeCacheKey {
212 text: text.to_string(),
213 font_index,
214 enable_ligatures: options.enable_ligatures,
215 enable_kerning: options.enable_kerning,
216 script: options.script.clone(),
217 language: options.language.clone(),
218 rtl: options.rtl,
219 };
220
221 if let Some(cached) = self.shape_cache.get(&cache_key) {
222 return Arc::clone(cached);
223 }
224
225 let clusters = self.detect_grapheme_clusters(text);
227 let cluster_boundaries: Vec<usize> = clusters.iter().map(|(idx, _)| *idx).collect();
228
229 let face = match Face::from_slice(font_data, 0) {
231 Some(face) => face,
232 None => {
233 let run = Arc::new(ShapedRun {
235 text: text.to_string(),
236 glyphs: vec![],
237 total_advance: 0.0,
238 cluster_boundaries,
239 });
240 return run;
241 }
242 };
243
244 let mut unicode_buffer = UnicodeBuffer::new();
246 unicode_buffer.push_str(text);
247
248 unicode_buffer.set_direction(if options.rtl {
250 rustybuzz::Direction::RightToLeft
251 } else {
252 rustybuzz::Direction::LeftToRight
253 });
254
255 if let Some(ref script_str) = options.script {
257 if let Ok(script) = Script::from_str(script_str) {
259 unicode_buffer.set_script(script);
260 }
261 }
262
263 if let Some(ref lang_str) = options.language {
265 if let Ok(lang) = Language::from_str(lang_str) {
267 unicode_buffer.set_language(lang);
268 }
269 }
270
271 let mut features = Vec::new();
274
275 if options.enable_ligatures {
277 if let Ok(feat) = Feature::from_str("liga") {
278 features.push(feat);
279 }
280 if let Ok(feat) = Feature::from_str("clig") {
282 features.push(feat);
283 }
284 if let Ok(feat) = Feature::from_str("dlig") {
286 features.push(feat);
287 }
288 }
289
290 if options.enable_kerning
292 && let Ok(feat) = Feature::from_str("kern")
293 {
294 features.push(feat);
295 }
296
297 if options.enable_contextual_alternates
299 && let Ok(feat) = Feature::from_str("calt")
300 {
301 features.push(feat);
302 }
303
304 if let Ok(feat) = Feature::from_str("ccmp") {
306 features.push(feat);
307 }
308
309 if let Ok(feat) = Feature::from_str("locl") {
311 features.push(feat);
312 }
313
314 let glyph_buffer = rustybuzz::shape(&face, &features, unicode_buffer);
316
317 let glyphs = self.extract_shaped_glyphs(&glyph_buffer);
319
320 let total_advance = glyphs.iter().map(|g| g.x_advance).sum();
322
323 let shaped_run = Arc::new(ShapedRun {
325 text: text.to_string(),
326 glyphs,
327 total_advance,
328 cluster_boundaries,
329 });
330
331 if self.shape_cache.len() >= self.max_cache_size {
333 if let Some(key) = self.shape_cache.keys().next().cloned() {
336 self.shape_cache.remove(&key);
337 }
338 }
339
340 self.shape_cache.insert(cache_key, Arc::clone(&shaped_run));
341
342 shaped_run
343 }
344
345 fn extract_shaped_glyphs(&self, buffer: &GlyphBuffer) -> Vec<ShapedGlyph> {
347 let glyph_infos = buffer.glyph_infos();
348 let glyph_positions = buffer.glyph_positions();
349
350 glyph_infos
351 .iter()
352 .zip(glyph_positions.iter())
353 .map(|(info, pos)| ShapedGlyph {
354 glyph_id: info.glyph_id,
355 cluster: info.cluster,
356 x_advance: pos.x_advance as f32,
357 y_advance: pos.y_advance as f32,
358 x_offset: pos.x_offset as f32,
359 y_offset: pos.y_offset as f32,
360 })
361 .collect()
362 }
363
364 #[allow(dead_code)]
366 pub fn clear_cache(&mut self) {
367 self.shape_cache.clear();
368 }
369
370 #[allow(dead_code)]
372 pub fn cache_size(&self) -> usize {
373 self.shape_cache.len()
374 }
375}
376
377impl Default for TextShaper {
378 fn default() -> Self {
379 Self::new()
380 }
381}
382
383#[cfg(test)]
384mod tests {
385 use super::*;
386
387 #[test]
388 fn test_grapheme_cluster_detection() {
389 let shaper = TextShaper::new();
390
391 let clusters = shaper.detect_grapheme_clusters("hello");
393 assert_eq!(clusters.len(), 5);
394
395 let clusters = shaper.detect_grapheme_clusters("ππ½");
397 assert_eq!(clusters.len(), 1); let clusters = shaper.detect_grapheme_clusters("πΊπΈ");
401 assert_eq!(clusters.len(), 1); }
403
404 #[test]
405 fn test_regional_indicator_detection() {
406 let shaper = TextShaper::new();
407
408 assert!(shaper.is_regional_indicator_pair("πΊπΈ"));
410
411 assert!(!shaper.is_regional_indicator_pair("US"));
413
414 assert!(!shaper.is_regional_indicator_pair("A"));
416 }
417
418 #[test]
419 fn test_zwj_detection() {
420 let shaper = TextShaper::new();
421
422 assert!(shaper.contains_zwj("π¨βπ©βπ§βπ¦"));
424
425 assert!(!shaper.contains_zwj("π"));
427
428 assert!(!shaper.contains_zwj("hello"));
430 }
431}