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 shape_text_with_fallback(registry, resolved, text, text_offset, TextDirection::Auto)
35}
36
37pub fn shape_text_with_fallback(
43 registry: &FontRegistry,
44 resolved: &ResolvedFont,
45 text: &str,
46 text_offset: usize,
47 direction: TextDirection,
48) -> Option<ShapedRun> {
49 let mut run = shape_text_directed(registry, resolved, text, text_offset, direction)?;
50
51 if run.glyphs.iter().any(|g| g.glyph_id == 0) && !text.is_empty() {
53 apply_glyph_fallback(registry, resolved, text, text_offset, &mut run);
54 }
55
56 Some(run)
57}
58
59fn apply_glyph_fallback(
66 registry: &FontRegistry,
67 primary: &ResolvedFont,
68 text: &str,
69 text_offset: usize,
70 run: &mut ShapedRun,
71) {
72 use crate::font::resolve::find_fallback_font;
73
74 for glyph in &mut run.glyphs {
75 if glyph.glyph_id != 0 {
76 continue;
77 }
78
79 let byte_offset = glyph.cluster as usize;
81 let ch = match text.get(byte_offset..).and_then(|s| s.chars().next()) {
82 Some(c) => c,
83 None => continue,
84 };
85
86 let fallback_id = match find_fallback_font(registry, ch, primary.font_face_id) {
88 Some(id) => id,
89 None => continue, };
91
92 let fallback_entry = match registry.get(fallback_id) {
93 Some(e) => e,
94 None => continue,
95 };
96
97 let fallback_resolved = ResolvedFont {
99 font_face_id: fallback_id,
100 size_px: primary.size_px,
101 face_index: fallback_entry.face_index,
102 swash_cache_key: fallback_entry.swash_cache_key,
103 scale_factor: primary.scale_factor,
104 weight: primary.weight,
105 };
106
107 let char_str = &text[byte_offset..byte_offset + ch.len_utf8()];
108 if let Some(fallback_run) = shape_text_directed(
109 registry,
110 &fallback_resolved,
111 char_str,
112 text_offset + byte_offset,
113 TextDirection::Auto,
114 ) {
115 if let Some(fb_glyph) = fallback_run.glyphs.first() {
117 glyph.glyph_id = fb_glyph.glyph_id;
118 glyph.x_advance = fb_glyph.x_advance;
119 glyph.y_advance = fb_glyph.y_advance;
120 glyph.x_offset = fb_glyph.x_offset;
121 glyph.y_offset = fb_glyph.y_offset;
122 glyph.font_face_id = fallback_id;
123 }
124 }
125 }
126
127 run.advance_width = run.glyphs.iter().map(|g| g.x_advance).sum();
129}
130
131pub fn shape_text_directed(
133 registry: &FontRegistry,
134 resolved: &ResolvedFont,
135 text: &str,
136 text_offset: usize,
137 direction: TextDirection,
138) -> Option<ShapedRun> {
139 let entry = registry.get(resolved.font_face_id)?;
140 let face = Face::from_slice(&entry.data, entry.face_index)?;
141
142 let units_per_em = face.units_per_em() as f32;
143 if units_per_em == 0.0 {
144 return None;
145 }
146 let sf = resolved.scale_factor.max(f32::MIN_POSITIVE);
149 let physical_size = resolved.size_px * sf;
150 let physical_scale = physical_size / units_per_em;
151 let inv_sf = 1.0 / sf;
152
153 let mut buffer = UnicodeBuffer::new();
154 buffer.push_str(text);
155 match direction {
156 TextDirection::LeftToRight => buffer.set_direction(Direction::LeftToRight),
157 TextDirection::RightToLeft => buffer.set_direction(Direction::RightToLeft),
158 TextDirection::Auto => {} }
160
161 let glyph_buffer = rustybuzz::shape(&face, &[], buffer);
162
163 let infos = glyph_buffer.glyph_infos();
164 let positions = glyph_buffer.glyph_positions();
165
166 let mut glyphs = Vec::with_capacity(infos.len());
167 let mut total_advance = 0.0f32;
168
169 for (info, pos) in infos.iter().zip(positions.iter()) {
170 let x_advance = pos.x_advance as f32 * physical_scale * inv_sf;
171 let y_advance = pos.y_advance as f32 * physical_scale * inv_sf;
172 let x_offset = pos.x_offset as f32 * physical_scale * inv_sf;
173 let y_offset = pos.y_offset as f32 * physical_scale * inv_sf;
174
175 glyphs.push(ShapedGlyph {
176 glyph_id: info.glyph_id as u16,
177 cluster: info.cluster,
178 x_advance,
179 y_advance,
180 x_offset,
181 y_offset,
182 font_face_id: resolved.font_face_id,
183 });
184
185 total_advance += x_advance;
186 }
187
188 Some(ShapedRun {
189 font_face_id: resolved.font_face_id,
190 size_px: resolved.size_px,
191 weight: resolved.weight,
192 glyphs,
193 advance_width: total_advance,
194 text_range: text_offset..text_offset + text.len(),
195 underline_style: crate::types::UnderlineStyle::None,
196 overline: false,
197 strikeout: false,
198 is_link: false,
199 foreground_color: None,
200 underline_color: None,
201 background_color: None,
202 anchor_href: None,
203 tooltip: None,
204 vertical_alignment: crate::types::VerticalAlignment::Normal,
205 image_name: None,
206 image_height: 0.0,
207 })
208}
209
210pub fn shape_text_with_buffer(
212 registry: &FontRegistry,
213 resolved: &ResolvedFont,
214 text: &str,
215 text_offset: usize,
216 buffer: UnicodeBuffer,
217) -> Option<(ShapedRun, UnicodeBuffer)> {
218 let entry = registry.get(resolved.font_face_id)?;
219 let face = Face::from_slice(&entry.data, entry.face_index)?;
220
221 let units_per_em = face.units_per_em() as f32;
222 if units_per_em == 0.0 {
223 return None;
224 }
225 let sf = resolved.scale_factor.max(f32::MIN_POSITIVE);
226 let physical_size = resolved.size_px * sf;
227 let physical_scale = physical_size / units_per_em;
228 let inv_sf = 1.0 / sf;
229
230 let mut buffer = buffer;
231 buffer.push_str(text);
232
233 let glyph_buffer = rustybuzz::shape(&face, &[], buffer);
234
235 let infos = glyph_buffer.glyph_infos();
236 let positions = glyph_buffer.glyph_positions();
237
238 let mut glyphs = Vec::with_capacity(infos.len());
239 let mut total_advance = 0.0f32;
240
241 for (info, pos) in infos.iter().zip(positions.iter()) {
242 let x_advance = pos.x_advance as f32 * physical_scale * inv_sf;
243 let y_advance = pos.y_advance as f32 * physical_scale * inv_sf;
244 let x_offset = pos.x_offset as f32 * physical_scale * inv_sf;
245 let y_offset = pos.y_offset as f32 * physical_scale * inv_sf;
246
247 glyphs.push(ShapedGlyph {
248 glyph_id: info.glyph_id as u16,
249 cluster: info.cluster,
250 x_advance,
251 y_advance,
252 x_offset,
253 y_offset,
254 font_face_id: resolved.font_face_id,
255 });
256
257 total_advance += x_advance;
258 }
259
260 let run = ShapedRun {
261 font_face_id: resolved.font_face_id,
262 size_px: resolved.size_px,
263 weight: resolved.weight,
264 glyphs,
265 advance_width: total_advance,
266 text_range: text_offset..text_offset + text.len(),
267 underline_style: crate::types::UnderlineStyle::None,
268 overline: false,
269 strikeout: false,
270 is_link: false,
271 foreground_color: None,
272 underline_color: None,
273 background_color: None,
274 anchor_href: None,
275 tooltip: None,
276 vertical_alignment: crate::types::VerticalAlignment::Normal,
277 image_name: None,
278 image_height: 0.0,
279 };
280
281 let recycled = glyph_buffer.clear();
283 Some((run, recycled))
284}
285
286pub fn font_metrics_px(registry: &FontRegistry, resolved: &ResolvedFont) -> Option<FontMetricsPx> {
291 let entry = registry.get(resolved.font_face_id)?;
292 let font_ref = swash::FontRef::from_index(&entry.data, entry.face_index as usize)?;
293 let sf = resolved.scale_factor.max(f32::MIN_POSITIVE);
294 let physical_size = resolved.size_px * sf;
295 let metrics = font_ref.metrics(&[]).scale(physical_size);
296 let inv_sf = 1.0 / sf;
297
298 Some(FontMetricsPx {
299 ascent: metrics.ascent * inv_sf,
300 descent: metrics.descent * inv_sf,
301 leading: metrics.leading * inv_sf,
302 underline_offset: metrics.underline_offset * inv_sf,
303 strikeout_offset: metrics.strikeout_offset * inv_sf,
304 stroke_size: metrics.stroke_size * inv_sf,
305 })
306}
307
308pub struct BidiRun {
310 pub byte_range: std::ops::Range<usize>,
311 pub direction: TextDirection,
312 pub visual_order: usize,
314}
315
316pub fn bidi_runs(text: &str) -> Vec<BidiRun> {
324 use unicode_bidi::BidiInfo;
325
326 if text.is_empty() {
327 return Vec::new();
328 }
329
330 let bidi_info = BidiInfo::new(text, None);
331 let mut runs = Vec::new();
332
333 for para in &bidi_info.paragraphs {
334 let (levels, level_runs) = bidi_info.visual_runs(para, para.range.clone());
335 for level_run in level_runs {
336 if level_run.is_empty() {
337 continue;
338 }
339 let level = levels[level_run.start];
340 let direction = if level.is_rtl() {
341 TextDirection::RightToLeft
342 } else {
343 TextDirection::LeftToRight
344 };
345 let visual_order = runs.len();
346 runs.push(BidiRun {
347 byte_range: level_run,
348 direction,
349 visual_order,
350 });
351 }
352 }
353
354 if runs.is_empty() {
355 runs.push(BidiRun {
356 byte_range: 0..text.len(),
357 direction: TextDirection::LeftToRight,
358 visual_order: 0,
359 });
360 }
361
362 runs
363}
364
365pub struct FontMetricsPx {
366 pub ascent: f32,
367 pub descent: f32,
368 pub leading: f32,
369 pub underline_offset: f32,
370 pub strikeout_offset: f32,
371 pub stroke_size: f32,
372}