1use lru::LruCache;
31use rustybuzz::{Face, Feature, GlyphBuffer, Language, Script, UnicodeBuffer};
32use std::num::NonZeroUsize;
33use std::str::FromStr;
34use std::sync::Arc;
35use unicode_segmentation::UnicodeSegmentation;
36
37#[derive(Debug, Clone, Copy)]
39pub struct ShapedGlyph {
40 #[allow(dead_code)]
42 pub glyph_id: u32,
43
44 #[allow(dead_code)]
46 pub cluster: u32,
47
48 pub x_advance: f32,
50
51 #[allow(dead_code)]
53 pub y_advance: f32,
54
55 #[allow(dead_code)]
57 pub x_offset: f32,
58
59 #[allow(dead_code)]
61 pub y_offset: f32,
62}
63
64#[derive(Debug, Clone)]
66pub struct ShapingOptions {
67 pub enable_ligatures: bool,
69
70 pub enable_kerning: bool,
72
73 pub enable_contextual_alternates: bool,
75
76 pub script: Option<String>,
78
79 pub language: Option<String>,
81
82 pub rtl: bool,
84}
85
86impl Default for ShapingOptions {
87 fn default() -> Self {
88 Self {
89 enable_ligatures: true,
90 enable_kerning: true,
91 enable_contextual_alternates: true,
92 script: None,
93 language: None,
94 rtl: false,
95 }
96 }
97}
98
99#[derive(Debug, Clone)]
101pub struct ShapedRun {
102 #[allow(dead_code)]
104 pub text: String,
105
106 #[allow(dead_code)]
108 pub glyphs: Vec<ShapedGlyph>,
109
110 #[allow(dead_code)]
112 pub total_advance: f32,
113
114 #[allow(dead_code)]
116 pub cluster_boundaries: Vec<usize>,
117}
118
119#[derive(Debug, Clone, Hash, Eq, PartialEq)]
121struct ShapeCacheKey {
122 text: String,
123 font_index: usize,
124 enable_ligatures: bool,
125 enable_kerning: bool,
126 script: Option<String>,
127 language: Option<String>,
128 rtl: bool,
129}
130
131pub struct TextShaper {
133 shape_cache: LruCache<ShapeCacheKey, Arc<ShapedRun>>,
135}
136
137impl TextShaper {
138 pub fn new() -> Self {
140 Self::with_cache_size(1000)
141 }
142
143 pub fn with_cache_size(max_cache_size: usize) -> Self {
145 Self {
146 shape_cache: LruCache::new(
147 NonZeroUsize::new(max_cache_size).unwrap_or(NonZeroUsize::new(1000).unwrap()),
148 ),
149 }
150 }
151
152 pub fn detect_grapheme_clusters<'a>(&self, text: &'a str) -> Vec<(usize, &'a str)> {
160 text.grapheme_indices(true).collect()
161 }
162
163 #[allow(dead_code)]
168 pub fn is_regional_indicator_pair(&self, grapheme: &str) -> bool {
169 let chars: Vec<char> = grapheme.chars().collect();
170 if chars.len() == 2 {
171 let is_ri = |c: char| {
172 let code = c as u32;
173 (0x1F1E6..=0x1F1FF).contains(&code)
174 };
175 is_ri(chars[0]) && is_ri(chars[1])
176 } else {
177 false
178 }
179 }
180
181 #[allow(dead_code)]
185 pub fn contains_zwj(&self, grapheme: &str) -> bool {
186 grapheme.contains('\u{200D}')
187 }
188
189 pub fn shape_text(
203 &mut self,
204 text: &str,
205 font_data: &[u8],
206 font_index: usize,
207 options: ShapingOptions,
208 ) -> Arc<ShapedRun> {
209 let cache_key = ShapeCacheKey {
211 text: text.to_string(),
212 font_index,
213 enable_ligatures: options.enable_ligatures,
214 enable_kerning: options.enable_kerning,
215 script: options.script.clone(),
216 language: options.language.clone(),
217 rtl: options.rtl,
218 };
219
220 if let Some(cached) = self.shape_cache.get(&cache_key) {
221 return Arc::clone(cached);
222 }
223
224 let clusters = self.detect_grapheme_clusters(text);
226 let cluster_boundaries: Vec<usize> = clusters.iter().map(|(idx, _)| *idx).collect();
227
228 let face = match Face::from_slice(font_data, 0) {
230 Some(face) => face,
231 None => {
232 let run = Arc::new(ShapedRun {
234 text: text.to_string(),
235 glyphs: vec![],
236 total_advance: 0.0,
237 cluster_boundaries,
238 });
239 return run;
240 }
241 };
242
243 let mut unicode_buffer = UnicodeBuffer::new();
245 unicode_buffer.push_str(text);
246
247 unicode_buffer.set_direction(if options.rtl {
249 rustybuzz::Direction::RightToLeft
250 } else {
251 rustybuzz::Direction::LeftToRight
252 });
253
254 if let Some(ref script_str) = options.script {
256 if let Ok(script) = Script::from_str(script_str) {
258 unicode_buffer.set_script(script);
259 }
260 }
261
262 if let Some(ref lang_str) = options.language {
264 if let Ok(lang) = Language::from_str(lang_str) {
266 unicode_buffer.set_language(lang);
267 }
268 }
269
270 let mut features = Vec::new();
273
274 if options.enable_ligatures {
276 if let Ok(feat) = Feature::from_str("liga") {
277 features.push(feat);
278 }
279 if let Ok(feat) = Feature::from_str("clig") {
281 features.push(feat);
282 }
283 if let Ok(feat) = Feature::from_str("dlig") {
285 features.push(feat);
286 }
287 }
288
289 if options.enable_kerning
291 && let Ok(feat) = Feature::from_str("kern")
292 {
293 features.push(feat);
294 }
295
296 if options.enable_contextual_alternates
298 && let Ok(feat) = Feature::from_str("calt")
299 {
300 features.push(feat);
301 }
302
303 if let Ok(feat) = Feature::from_str("ccmp") {
305 features.push(feat);
306 }
307
308 if let Ok(feat) = Feature::from_str("locl") {
310 features.push(feat);
311 }
312
313 let glyph_buffer = rustybuzz::shape(&face, &features, unicode_buffer);
315
316 let glyphs = self.extract_shaped_glyphs(&glyph_buffer);
318
319 let total_advance = glyphs.iter().map(|g| g.x_advance).sum();
321
322 let shaped_run = Arc::new(ShapedRun {
324 text: text.to_string(),
325 glyphs,
326 total_advance,
327 cluster_boundaries,
328 });
329
330 self.shape_cache.put(cache_key, Arc::clone(&shaped_run));
332
333 shaped_run
334 }
335
336 fn extract_shaped_glyphs(&self, buffer: &GlyphBuffer) -> Vec<ShapedGlyph> {
338 let glyph_infos = buffer.glyph_infos();
339 let glyph_positions = buffer.glyph_positions();
340
341 glyph_infos
342 .iter()
343 .zip(glyph_positions.iter())
344 .map(|(info, pos)| ShapedGlyph {
345 glyph_id: info.glyph_id,
346 cluster: info.cluster,
347 x_advance: pos.x_advance as f32,
348 y_advance: pos.y_advance as f32,
349 x_offset: pos.x_offset as f32,
350 y_offset: pos.y_offset as f32,
351 })
352 .collect()
353 }
354
355 #[allow(dead_code)]
357 pub fn clear_cache(&mut self) {
358 self.shape_cache.clear();
359 }
360
361 #[allow(dead_code)]
363 pub fn cache_size(&self) -> usize {
364 self.shape_cache.len()
365 }
366}
367
368impl Default for TextShaper {
369 fn default() -> Self {
370 Self::new()
371 }
372}
373
374#[cfg(test)]
375mod tests {
376 use super::*;
377
378 #[test]
379 fn test_grapheme_cluster_detection() {
380 let shaper = TextShaper::new();
381
382 let clusters = shaper.detect_grapheme_clusters("hello");
384 assert_eq!(clusters.len(), 5);
385
386 let clusters = shaper.detect_grapheme_clusters("ππ½");
388 assert_eq!(clusters.len(), 1); let clusters = shaper.detect_grapheme_clusters("πΊπΈ");
392 assert_eq!(clusters.len(), 1); }
394
395 #[test]
396 fn test_regional_indicator_detection() {
397 let shaper = TextShaper::new();
398
399 assert!(shaper.is_regional_indicator_pair("πΊπΈ"));
401
402 assert!(!shaper.is_regional_indicator_pair("US"));
404
405 assert!(!shaper.is_regional_indicator_pair("A"));
407 }
408
409 #[test]
410 fn test_zwj_detection() {
411 let shaper = TextShaper::new();
412
413 assert!(shaper.contains_zwj("π¨βπ©βπ§βπ¦"));
415
416 assert!(!shaper.contains_zwj("π"));
418
419 assert!(!shaper.contains_zwj("hello"));
421 }
422}