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 };
105
106 let char_str = &text[byte_offset..byte_offset + ch.len_utf8()];
107 if let Some(fallback_run) = shape_text_directed(
108 registry,
109 &fallback_resolved,
110 char_str,
111 text_offset + byte_offset,
112 TextDirection::Auto,
113 ) {
114 if let Some(fb_glyph) = fallback_run.glyphs.first() {
116 glyph.glyph_id = fb_glyph.glyph_id;
117 glyph.x_advance = fb_glyph.x_advance;
118 glyph.y_advance = fb_glyph.y_advance;
119 glyph.x_offset = fb_glyph.x_offset;
120 glyph.y_offset = fb_glyph.y_offset;
121 glyph.font_face_id = fallback_id;
122 }
123 }
124 }
125
126 run.advance_width = run.glyphs.iter().map(|g| g.x_advance).sum();
128}
129
130pub fn shape_text_directed(
132 registry: &FontRegistry,
133 resolved: &ResolvedFont,
134 text: &str,
135 text_offset: usize,
136 direction: TextDirection,
137) -> Option<ShapedRun> {
138 let entry = registry.get(resolved.font_face_id)?;
139 let face = Face::from_slice(&entry.data, entry.face_index)?;
140
141 let units_per_em = face.units_per_em() as f32;
142 if units_per_em == 0.0 {
143 return None;
144 }
145 let sf = resolved.scale_factor.max(f32::MIN_POSITIVE);
148 let physical_size = resolved.size_px * sf;
149 let physical_scale = physical_size / units_per_em;
150 let inv_sf = 1.0 / sf;
151
152 let mut buffer = UnicodeBuffer::new();
153 buffer.push_str(text);
154 match direction {
155 TextDirection::LeftToRight => buffer.set_direction(Direction::LeftToRight),
156 TextDirection::RightToLeft => buffer.set_direction(Direction::RightToLeft),
157 TextDirection::Auto => {} }
159
160 let glyph_buffer = rustybuzz::shape(&face, &[], buffer);
161
162 let infos = glyph_buffer.glyph_infos();
163 let positions = glyph_buffer.glyph_positions();
164
165 let mut glyphs = Vec::with_capacity(infos.len());
166 let mut total_advance = 0.0f32;
167
168 for (info, pos) in infos.iter().zip(positions.iter()) {
169 let x_advance = pos.x_advance as f32 * physical_scale * inv_sf;
170 let y_advance = pos.y_advance as f32 * physical_scale * inv_sf;
171 let x_offset = pos.x_offset as f32 * physical_scale * inv_sf;
172 let y_offset = pos.y_offset as f32 * physical_scale * inv_sf;
173
174 glyphs.push(ShapedGlyph {
175 glyph_id: info.glyph_id as u16,
176 cluster: info.cluster,
177 x_advance,
178 y_advance,
179 x_offset,
180 y_offset,
181 font_face_id: resolved.font_face_id,
182 });
183
184 total_advance += x_advance;
185 }
186
187 Some(ShapedRun {
188 font_face_id: resolved.font_face_id,
189 size_px: resolved.size_px,
190 glyphs,
191 advance_width: total_advance,
192 text_range: text_offset..text_offset + text.len(),
193 underline_style: crate::types::UnderlineStyle::None,
194 overline: false,
195 strikeout: false,
196 is_link: false,
197 foreground_color: None,
198 underline_color: None,
199 background_color: None,
200 anchor_href: None,
201 tooltip: None,
202 vertical_alignment: crate::types::VerticalAlignment::Normal,
203 image_name: None,
204 image_height: 0.0,
205 })
206}
207
208pub fn shape_text_with_buffer(
210 registry: &FontRegistry,
211 resolved: &ResolvedFont,
212 text: &str,
213 text_offset: usize,
214 buffer: UnicodeBuffer,
215) -> Option<(ShapedRun, UnicodeBuffer)> {
216 let entry = registry.get(resolved.font_face_id)?;
217 let face = Face::from_slice(&entry.data, entry.face_index)?;
218
219 let units_per_em = face.units_per_em() as f32;
220 if units_per_em == 0.0 {
221 return None;
222 }
223 let sf = resolved.scale_factor.max(f32::MIN_POSITIVE);
224 let physical_size = resolved.size_px * sf;
225 let physical_scale = physical_size / units_per_em;
226 let inv_sf = 1.0 / sf;
227
228 let mut buffer = buffer;
229 buffer.push_str(text);
230
231 let glyph_buffer = rustybuzz::shape(&face, &[], buffer);
232
233 let infos = glyph_buffer.glyph_infos();
234 let positions = glyph_buffer.glyph_positions();
235
236 let mut glyphs = Vec::with_capacity(infos.len());
237 let mut total_advance = 0.0f32;
238
239 for (info, pos) in infos.iter().zip(positions.iter()) {
240 let x_advance = pos.x_advance as f32 * physical_scale * inv_sf;
241 let y_advance = pos.y_advance as f32 * physical_scale * inv_sf;
242 let x_offset = pos.x_offset as f32 * physical_scale * inv_sf;
243 let y_offset = pos.y_offset as f32 * physical_scale * inv_sf;
244
245 glyphs.push(ShapedGlyph {
246 glyph_id: info.glyph_id as u16,
247 cluster: info.cluster,
248 x_advance,
249 y_advance,
250 x_offset,
251 y_offset,
252 font_face_id: resolved.font_face_id,
253 });
254
255 total_advance += x_advance;
256 }
257
258 let run = ShapedRun {
259 font_face_id: resolved.font_face_id,
260 size_px: resolved.size_px,
261 glyphs,
262 advance_width: total_advance,
263 text_range: text_offset..text_offset + text.len(),
264 underline_style: crate::types::UnderlineStyle::None,
265 overline: false,
266 strikeout: false,
267 is_link: false,
268 foreground_color: None,
269 underline_color: None,
270 background_color: None,
271 anchor_href: None,
272 tooltip: None,
273 vertical_alignment: crate::types::VerticalAlignment::Normal,
274 image_name: None,
275 image_height: 0.0,
276 };
277
278 let recycled = glyph_buffer.clear();
280 Some((run, recycled))
281}
282
283pub fn font_metrics_px(registry: &FontRegistry, resolved: &ResolvedFont) -> Option<FontMetricsPx> {
288 let entry = registry.get(resolved.font_face_id)?;
289 let font_ref = swash::FontRef::from_index(&entry.data, entry.face_index as usize)?;
290 let sf = resolved.scale_factor.max(f32::MIN_POSITIVE);
291 let physical_size = resolved.size_px * sf;
292 let metrics = font_ref.metrics(&[]).scale(physical_size);
293 let inv_sf = 1.0 / sf;
294
295 Some(FontMetricsPx {
296 ascent: metrics.ascent * inv_sf,
297 descent: metrics.descent * inv_sf,
298 leading: metrics.leading * inv_sf,
299 underline_offset: metrics.underline_offset * inv_sf,
300 strikeout_offset: metrics.strikeout_offset * inv_sf,
301 stroke_size: metrics.stroke_size * inv_sf,
302 })
303}
304
305pub struct BidiRun {
307 pub byte_range: std::ops::Range<usize>,
308 pub direction: TextDirection,
309 pub visual_order: usize,
311}
312
313pub fn bidi_runs(text: &str) -> Vec<BidiRun> {
321 use unicode_bidi::BidiInfo;
322
323 if text.is_empty() {
324 return Vec::new();
325 }
326
327 let bidi_info = BidiInfo::new(text, None);
328 let mut runs = Vec::new();
329
330 for para in &bidi_info.paragraphs {
331 let (levels, level_runs) = bidi_info.visual_runs(para, para.range.clone());
332 for level_run in level_runs {
333 if level_run.is_empty() {
334 continue;
335 }
336 let level = levels[level_run.start];
337 let direction = if level.is_rtl() {
338 TextDirection::RightToLeft
339 } else {
340 TextDirection::LeftToRight
341 };
342 let visual_order = runs.len();
343 runs.push(BidiRun {
344 byte_range: level_run,
345 direction,
346 visual_order,
347 });
348 }
349 }
350
351 if runs.is_empty() {
352 runs.push(BidiRun {
353 byte_range: 0..text.len(),
354 direction: TextDirection::LeftToRight,
355 visual_order: 0,
356 });
357 }
358
359 runs
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}