1use rustybuzz::{Direction, Face, UnicodeBuffer};
2
3use crate::font::registry::FontRegistry;
4use crate::font::resolve::ResolvedFont;
5use crate::shaping::run::{ShapedGlyph, ShapedRun};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
9pub enum TextDirection {
10 #[default]
12 Auto,
13 LeftToRight,
14 RightToLeft,
15}
16
17pub fn shape_text(
29 registry: &FontRegistry,
30 resolved: &ResolvedFont,
31 text: &str,
32 text_offset: usize,
33) -> Option<ShapedRun> {
34 let mut run = shape_text_directed(registry, resolved, text, text_offset, TextDirection::Auto)?;
35
36 if run.glyphs.iter().any(|g| g.glyph_id == 0) && !text.is_empty() {
38 apply_glyph_fallback(registry, resolved, text, text_offset, &mut run);
39 }
40
41 Some(run)
42}
43
44fn apply_glyph_fallback(
51 registry: &FontRegistry,
52 primary: &ResolvedFont,
53 text: &str,
54 text_offset: usize,
55 run: &mut ShapedRun,
56) {
57 use crate::font::resolve::find_fallback_font;
58
59 for glyph in &mut run.glyphs {
60 if glyph.glyph_id != 0 {
61 continue;
62 }
63
64 let byte_offset = glyph.cluster as usize;
66 let ch = match text.get(byte_offset..).and_then(|s| s.chars().next()) {
67 Some(c) => c,
68 None => continue,
69 };
70
71 let fallback_id = match find_fallback_font(registry, ch, primary.font_face_id) {
73 Some(id) => id,
74 None => continue, };
76
77 let fallback_entry = match registry.get(fallback_id) {
78 Some(e) => e,
79 None => continue,
80 };
81
82 let fallback_resolved = ResolvedFont {
84 font_face_id: fallback_id,
85 size_px: primary.size_px,
86 face_index: fallback_entry.face_index,
87 swash_cache_key: fallback_entry.swash_cache_key,
88 };
89
90 let char_str = &text[byte_offset..byte_offset + ch.len_utf8()];
91 if let Some(fallback_run) = shape_text_directed(
92 registry,
93 &fallback_resolved,
94 char_str,
95 text_offset + byte_offset,
96 TextDirection::Auto,
97 ) {
98 if let Some(fb_glyph) = fallback_run.glyphs.first() {
100 glyph.glyph_id = fb_glyph.glyph_id;
101 glyph.x_advance = fb_glyph.x_advance;
102 glyph.y_advance = fb_glyph.y_advance;
103 glyph.x_offset = fb_glyph.x_offset;
104 glyph.y_offset = fb_glyph.y_offset;
105 glyph.font_face_id = fallback_id;
106 }
107 }
108 }
109
110 run.advance_width = run.glyphs.iter().map(|g| g.x_advance).sum();
112}
113
114pub fn shape_text_directed(
116 registry: &FontRegistry,
117 resolved: &ResolvedFont,
118 text: &str,
119 text_offset: usize,
120 direction: TextDirection,
121) -> Option<ShapedRun> {
122 let entry = registry.get(resolved.font_face_id)?;
123 let face = Face::from_slice(&entry.data, entry.face_index)?;
124
125 let units_per_em = face.units_per_em() as f32;
126 if units_per_em == 0.0 {
127 return None;
128 }
129 let scale = resolved.size_px / units_per_em;
130
131 let mut buffer = UnicodeBuffer::new();
132 buffer.push_str(text);
133 match direction {
134 TextDirection::LeftToRight => buffer.set_direction(Direction::LeftToRight),
135 TextDirection::RightToLeft => buffer.set_direction(Direction::RightToLeft),
136 TextDirection::Auto => {} }
138
139 let glyph_buffer = rustybuzz::shape(&face, &[], buffer);
140
141 let infos = glyph_buffer.glyph_infos();
142 let positions = glyph_buffer.glyph_positions();
143
144 let mut glyphs = Vec::with_capacity(infos.len());
145 let mut total_advance = 0.0f32;
146
147 for (info, pos) in infos.iter().zip(positions.iter()) {
148 let x_advance = pos.x_advance as f32 * scale;
149 let y_advance = pos.y_advance as f32 * scale;
150 let x_offset = pos.x_offset as f32 * scale;
151 let y_offset = pos.y_offset as f32 * scale;
152
153 glyphs.push(ShapedGlyph {
154 glyph_id: info.glyph_id as u16,
155 cluster: info.cluster,
156 x_advance,
157 y_advance,
158 x_offset,
159 y_offset,
160 font_face_id: resolved.font_face_id,
161 });
162
163 total_advance += x_advance;
164 }
165
166 Some(ShapedRun {
167 font_face_id: resolved.font_face_id,
168 size_px: resolved.size_px,
169 glyphs,
170 advance_width: total_advance,
171 text_range: text_offset..text_offset + text.len(),
172 underline_style: crate::types::UnderlineStyle::None,
173 overline: false,
174 strikeout: false,
175 is_link: false,
176 foreground_color: None,
177 underline_color: None,
178 background_color: None,
179 anchor_href: None,
180 tooltip: None,
181 vertical_alignment: crate::types::VerticalAlignment::Normal,
182 image_name: None,
183 image_height: 0.0,
184 })
185}
186
187pub fn shape_text_with_buffer(
189 registry: &FontRegistry,
190 resolved: &ResolvedFont,
191 text: &str,
192 text_offset: usize,
193 buffer: UnicodeBuffer,
194) -> Option<(ShapedRun, UnicodeBuffer)> {
195 let entry = registry.get(resolved.font_face_id)?;
196 let face = Face::from_slice(&entry.data, entry.face_index)?;
197
198 let units_per_em = face.units_per_em() as f32;
199 if units_per_em == 0.0 {
200 return None;
201 }
202 let scale = resolved.size_px / units_per_em;
203
204 let mut buffer = buffer;
205 buffer.push_str(text);
206
207 let glyph_buffer = rustybuzz::shape(&face, &[], buffer);
208
209 let infos = glyph_buffer.glyph_infos();
210 let positions = glyph_buffer.glyph_positions();
211
212 let mut glyphs = Vec::with_capacity(infos.len());
213 let mut total_advance = 0.0f32;
214
215 for (info, pos) in infos.iter().zip(positions.iter()) {
216 let x_advance = pos.x_advance as f32 * scale;
217 let y_advance = pos.y_advance as f32 * scale;
218 let x_offset = pos.x_offset as f32 * scale;
219 let y_offset = pos.y_offset as f32 * scale;
220
221 glyphs.push(ShapedGlyph {
222 glyph_id: info.glyph_id as u16,
223 cluster: info.cluster,
224 x_advance,
225 y_advance,
226 x_offset,
227 y_offset,
228 font_face_id: resolved.font_face_id,
229 });
230
231 total_advance += x_advance;
232 }
233
234 let run = ShapedRun {
235 font_face_id: resolved.font_face_id,
236 size_px: resolved.size_px,
237 glyphs,
238 advance_width: total_advance,
239 text_range: text_offset..text_offset + text.len(),
240 underline_style: crate::types::UnderlineStyle::None,
241 overline: false,
242 strikeout: false,
243 is_link: false,
244 foreground_color: None,
245 underline_color: None,
246 background_color: None,
247 anchor_href: None,
248 tooltip: None,
249 vertical_alignment: crate::types::VerticalAlignment::Normal,
250 image_name: None,
251 image_height: 0.0,
252 };
253
254 let recycled = glyph_buffer.clear();
256 Some((run, recycled))
257}
258
259pub fn font_metrics_px(registry: &FontRegistry, resolved: &ResolvedFont) -> Option<FontMetricsPx> {
261 let entry = registry.get(resolved.font_face_id)?;
262 let font_ref = swash::FontRef::from_index(&entry.data, entry.face_index as usize)?;
263 let metrics = font_ref.metrics(&[]).scale(resolved.size_px);
264
265 Some(FontMetricsPx {
266 ascent: metrics.ascent,
267 descent: metrics.descent,
268 leading: metrics.leading,
269 underline_offset: metrics.underline_offset,
270 strikeout_offset: metrics.strikeout_offset,
271 stroke_size: metrics.stroke_size,
272 })
273}
274
275pub struct BidiRun {
277 pub byte_range: std::ops::Range<usize>,
278 pub direction: TextDirection,
279 pub visual_order: usize,
281}
282
283pub fn bidi_runs(text: &str) -> Vec<BidiRun> {
286 use unicode_bidi::BidiInfo;
287
288 if text.is_empty() {
289 return vec![BidiRun {
290 byte_range: 0..0,
291 direction: TextDirection::LeftToRight,
292 visual_order: 0,
293 }];
294 }
295
296 let bidi_info = BidiInfo::new(text, None);
297
298 let mut runs = Vec::new();
299
300 for para in &bidi_info.paragraphs {
301 let para_text = &text[para.range.clone()];
302 let para_offset = para.range.start;
303
304 let levels = &bidi_info.levels[para.range.clone()];
306
307 if levels.is_empty() {
309 continue;
310 }
311
312 let mut run_start = 0usize;
313 let mut current_level = levels[0];
314
315 for (i, &level) in levels.iter().enumerate() {
316 if level != current_level {
317 let dir = if current_level.is_rtl() {
319 TextDirection::RightToLeft
320 } else {
321 TextDirection::LeftToRight
322 };
323 let start = snap_to_char_boundary(para_text, run_start);
325 let end = snap_to_char_boundary(para_text, i);
326 if start < end {
327 runs.push(BidiRun {
328 byte_range: (para_offset + start)..(para_offset + end),
329 direction: dir,
330 visual_order: runs.len(),
331 });
332 }
333 run_start = i;
334 current_level = level;
335 }
336 }
337
338 let dir = if current_level.is_rtl() {
340 TextDirection::RightToLeft
341 } else {
342 TextDirection::LeftToRight
343 };
344 let start = snap_to_char_boundary(para_text, run_start);
345 let end = para_text.len();
346 if start < end {
347 runs.push(BidiRun {
348 byte_range: (para_offset + start)..(para_offset + end),
349 direction: dir,
350 visual_order: runs.len(),
351 });
352 }
353 }
354
355 if runs.is_empty() {
356 runs.push(BidiRun {
357 byte_range: 0..text.len(),
358 direction: TextDirection::LeftToRight,
359 visual_order: 0,
360 });
361 }
362
363 runs
364}
365
366fn snap_to_char_boundary(text: &str, byte_pos: usize) -> usize {
367 if byte_pos >= text.len() {
368 return text.len();
369 }
370 let mut pos = byte_pos;
372 while pos < text.len() && !text.is_char_boundary(pos) {
373 pos += 1;
374 }
375 pos
376}
377
378pub struct FontMetricsPx {
379 pub ascent: f32,
380 pub descent: f32,
381 pub leading: f32,
382 pub underline_offset: f32,
383 pub strikeout_offset: f32,
384 pub stroke_size: f32,
385}