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