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: false,
173 overline: false,
174 strikeout: false,
175 is_link: false,
176 })
177}
178
179pub fn shape_text_with_buffer(
181 registry: &FontRegistry,
182 resolved: &ResolvedFont,
183 text: &str,
184 text_offset: usize,
185 buffer: UnicodeBuffer,
186) -> Option<(ShapedRun, UnicodeBuffer)> {
187 let entry = registry.get(resolved.font_face_id)?;
188 let face = Face::from_slice(&entry.data, entry.face_index)?;
189
190 let units_per_em = face.units_per_em() as f32;
191 if units_per_em == 0.0 {
192 return None;
193 }
194 let scale = resolved.size_px / units_per_em;
195
196 let mut buffer = buffer;
197 buffer.push_str(text);
198
199 let glyph_buffer = rustybuzz::shape(&face, &[], buffer);
200
201 let infos = glyph_buffer.glyph_infos();
202 let positions = glyph_buffer.glyph_positions();
203
204 let mut glyphs = Vec::with_capacity(infos.len());
205 let mut total_advance = 0.0f32;
206
207 for (info, pos) in infos.iter().zip(positions.iter()) {
208 let x_advance = pos.x_advance as f32 * scale;
209 let y_advance = pos.y_advance as f32 * scale;
210 let x_offset = pos.x_offset as f32 * scale;
211 let y_offset = pos.y_offset as f32 * scale;
212
213 glyphs.push(ShapedGlyph {
214 glyph_id: info.glyph_id as u16,
215 cluster: info.cluster,
216 x_advance,
217 y_advance,
218 x_offset,
219 y_offset,
220 font_face_id: resolved.font_face_id,
221 });
222
223 total_advance += x_advance;
224 }
225
226 let run = ShapedRun {
227 font_face_id: resolved.font_face_id,
228 size_px: resolved.size_px,
229 glyphs,
230 advance_width: total_advance,
231 text_range: text_offset..text_offset + text.len(),
232 underline: false,
233 overline: false,
234 strikeout: false,
235 is_link: false,
236 };
237
238 let recycled = glyph_buffer.clear();
240 Some((run, recycled))
241}
242
243pub fn font_metrics_px(registry: &FontRegistry, resolved: &ResolvedFont) -> Option<FontMetricsPx> {
245 let entry = registry.get(resolved.font_face_id)?;
246 let font_ref = swash::FontRef::from_index(&entry.data, entry.face_index as usize)?;
247 let metrics = font_ref.metrics(&[]).scale(resolved.size_px);
248
249 Some(FontMetricsPx {
250 ascent: metrics.ascent,
251 descent: metrics.descent,
252 leading: metrics.leading,
253 underline_offset: metrics.underline_offset,
254 strikeout_offset: metrics.strikeout_offset,
255 stroke_size: metrics.stroke_size,
256 })
257}
258
259pub struct BidiRun {
261 pub byte_range: std::ops::Range<usize>,
262 pub direction: TextDirection,
263 pub visual_order: usize,
265}
266
267pub fn bidi_runs(text: &str) -> Vec<BidiRun> {
270 use unicode_bidi::BidiInfo;
271
272 if text.is_empty() {
273 return vec![BidiRun {
274 byte_range: 0..0,
275 direction: TextDirection::LeftToRight,
276 visual_order: 0,
277 }];
278 }
279
280 let bidi_info = BidiInfo::new(text, None);
281
282 let mut runs = Vec::new();
283
284 for para in &bidi_info.paragraphs {
285 let para_text = &text[para.range.clone()];
286 let para_offset = para.range.start;
287
288 let levels = &bidi_info.levels[para.range.clone()];
290
291 if levels.is_empty() {
293 continue;
294 }
295
296 let mut run_start = 0usize;
297 let mut current_level = levels[0];
298
299 for (i, &level) in levels.iter().enumerate() {
300 if level != current_level {
301 let dir = if current_level.is_rtl() {
303 TextDirection::RightToLeft
304 } else {
305 TextDirection::LeftToRight
306 };
307 let start = snap_to_char_boundary(para_text, run_start);
309 let end = snap_to_char_boundary(para_text, i);
310 if start < end {
311 runs.push(BidiRun {
312 byte_range: (para_offset + start)..(para_offset + end),
313 direction: dir,
314 visual_order: runs.len(),
315 });
316 }
317 run_start = i;
318 current_level = level;
319 }
320 }
321
322 let dir = if current_level.is_rtl() {
324 TextDirection::RightToLeft
325 } else {
326 TextDirection::LeftToRight
327 };
328 let start = snap_to_char_boundary(para_text, run_start);
329 let end = para_text.len();
330 if start < end {
331 runs.push(BidiRun {
332 byte_range: (para_offset + start)..(para_offset + end),
333 direction: dir,
334 visual_order: runs.len(),
335 });
336 }
337 }
338
339 if runs.is_empty() {
340 runs.push(BidiRun {
341 byte_range: 0..text.len(),
342 direction: TextDirection::LeftToRight,
343 visual_order: 0,
344 });
345 }
346
347 runs
348}
349
350fn snap_to_char_boundary(text: &str, byte_pos: usize) -> usize {
351 if byte_pos >= text.len() {
352 return text.len();
353 }
354 let mut pos = byte_pos;
356 while pos < text.len() && !text.is_char_boundary(pos) {
357 pos += 1;
358 }
359 pos
360}
361
362pub struct FontMetricsPx {
363 pub ascent: f32,
364 pub descent: f32,
365 pub leading: f32,
366 pub underline_offset: f32,
367 pub strikeout_offset: f32,
368 pub stroke_size: f32,
369}