1use harfrust::{Direction, Feature, FontRef, ShapeOptions, Tag, UnicodeBuffer};
2
3use crate::font::registry::FontRegistry;
4use crate::font::resolve::ResolvedFont;
5use crate::shaping::run::{ShapedGlyph, ShapedRun};
6use crate::types::FontFeature;
7
8pub fn to_harfrust_features(features: &[FontFeature]) -> Vec<Feature> {
12 features
13 .iter()
14 .map(|f| Feature::new(Tag::new(&f.tag), f.value, ..))
15 .collect()
16}
17
18fn units_per_em(bytes: &[u8], face_index: u32) -> Option<u16> {
26 let font_ref = swash::FontRef::from_index(bytes, face_index as usize)?;
27 let upem = font_ref.metrics(&[]).units_per_em;
28 if upem == 0 { None } else { Some(upem) }
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
33pub enum TextDirection {
34 #[default]
36 Auto,
37 LeftToRight,
38 RightToLeft,
39}
40
41pub fn shape_text(
53 registry: &FontRegistry,
54 resolved: &ResolvedFont,
55 text: &str,
56 text_offset: usize,
57) -> Option<ShapedRun> {
58 shape_text_with_fallback(
59 registry,
60 resolved,
61 text,
62 text_offset,
63 TextDirection::Auto,
64 &[],
65 )
66}
67
68pub fn shape_text_with_fallback(
74 registry: &FontRegistry,
75 resolved: &ResolvedFont,
76 text: &str,
77 text_offset: usize,
78 direction: TextDirection,
79 features: &[Feature],
80) -> Option<ShapedRun> {
81 let mut run = shape_text_directed(registry, resolved, text, text_offset, direction, features)?;
82
83 if run.glyphs.iter().any(|g| g.glyph_id == 0) && !text.is_empty() {
85 apply_glyph_fallback(registry, resolved, text, text_offset, features, &mut run);
86 }
87
88 Some(run)
89}
90
91fn apply_glyph_fallback(
98 registry: &FontRegistry,
99 primary: &ResolvedFont,
100 text: &str,
101 text_offset: usize,
102 features: &[Feature],
103 run: &mut ShapedRun,
104) {
105 use crate::font::resolve::find_fallback_font;
106
107 for glyph in &mut run.glyphs {
108 if glyph.glyph_id != 0 {
109 continue;
110 }
111
112 let byte_offset = glyph.cluster as usize;
114 let ch = match text.get(byte_offset..).and_then(|s| s.chars().next()) {
115 Some(c) => c,
116 None => continue,
117 };
118
119 let fallback_id = match find_fallback_font(registry, ch, primary.font_face_id) {
121 Some(id) => id,
122 None => continue, };
124
125 let fallback_entry = match registry.get(fallback_id) {
126 Some(e) => e,
127 None => continue,
128 };
129
130 let fallback_resolved = ResolvedFont {
132 font_face_id: fallback_id,
133 size_px: primary.size_px,
134 face_index: fallback_entry.face_index,
135 swash_cache_key: fallback_entry.swash_cache_key,
136 scale_factor: primary.scale_factor,
137 weight: primary.weight,
138 };
139
140 let char_str = &text[byte_offset..byte_offset + ch.len_utf8()];
141 if let Some(fallback_run) = shape_text_directed(
142 registry,
143 &fallback_resolved,
144 char_str,
145 text_offset + byte_offset,
146 TextDirection::Auto,
147 features,
148 ) {
149 if let Some(fb_glyph) = fallback_run.glyphs.first() {
151 glyph.glyph_id = fb_glyph.glyph_id;
152 glyph.x_advance = fb_glyph.x_advance;
153 glyph.y_advance = fb_glyph.y_advance;
154 glyph.x_offset = fb_glyph.x_offset;
155 glyph.y_offset = fb_glyph.y_offset;
156 glyph.font_face_id = fallback_id;
157 }
158 }
159 }
160
161 run.advance_width = run.glyphs.iter().map(|g| g.x_advance).sum();
163}
164
165pub fn shape_text_directed(
167 registry: &FontRegistry,
168 resolved: &ResolvedFont,
169 text: &str,
170 text_offset: usize,
171 direction: TextDirection,
172 features: &[Feature],
173) -> Option<ShapedRun> {
174 let entry = registry.get(resolved.font_face_id)?;
175 let font = FontRef::from_index(entry.bytes(), entry.face_index).ok()?;
176
177 let upem = units_per_em(entry.bytes(), entry.face_index).unwrap_or(0) as f32;
178 if upem == 0.0 {
179 return None;
180 }
181 let sf = resolved.scale_factor.max(f32::MIN_POSITIVE);
184 let physical_size = resolved.size_px * sf;
185 let physical_scale = physical_size / upem;
186 let inv_sf = 1.0 / sf;
187
188 let mut buffer = UnicodeBuffer::new();
189 buffer.push_str(text);
190 match direction {
191 TextDirection::LeftToRight => buffer.set_direction(Direction::LeftToRight),
192 TextDirection::RightToLeft => buffer.set_direction(Direction::RightToLeft),
193 TextDirection::Auto => buffer.guess_segment_properties(),
196 }
197
198 let resolved_direction = if buffer.direction() == Direction::RightToLeft {
201 TextDirection::RightToLeft
202 } else {
203 TextDirection::LeftToRight
204 };
205
206 let shaper_data = entry.shaper_data(&font);
210 let shaper = shaper_data.shaper(&font).build();
211 let glyph_buffer = shaper.shape(buffer, ShapeOptions::new().features(features));
212
213 let infos = glyph_buffer.glyph_infos();
214 let positions = glyph_buffer.glyph_positions();
215
216 let mut glyphs = Vec::with_capacity(infos.len());
217 let mut total_advance = 0.0f32;
218
219 for (info, pos) in infos.iter().zip(positions.iter()) {
220 let x_advance = pos.x_advance as f32 * physical_scale * inv_sf;
221 let y_advance = pos.y_advance as f32 * physical_scale * inv_sf;
222 let x_offset = pos.x_offset as f32 * physical_scale * inv_sf;
223 let y_offset = pos.y_offset as f32 * physical_scale * inv_sf;
224
225 glyphs.push(ShapedGlyph {
226 glyph_id: info.glyph_id as u16,
227 cluster: info.cluster,
228 x_advance,
229 y_advance,
230 x_offset,
231 y_offset,
232 font_face_id: resolved.font_face_id,
233 });
234
235 total_advance += x_advance;
236 }
237
238 Some(ShapedRun {
239 font_face_id: resolved.font_face_id,
240 size_px: resolved.size_px,
241 weight: resolved.weight,
242 glyphs,
243 advance_width: total_advance,
244 text_range: text_offset..text_offset + text.len(),
245 direction: resolved_direction,
246 underline_style: crate::types::UnderlineStyle::None,
247 overline: false,
248 strikeout: false,
249 is_link: false,
250 foreground_color: None,
251 underline_color: None,
252 background_color: None,
253 anchor_href: None,
254 tooltip: None,
255 vertical_alignment: crate::types::VerticalAlignment::Normal,
256 image_name: None,
257 image_height: 0.0,
258 })
259}
260
261pub fn shape_text_with_buffer(
263 registry: &FontRegistry,
264 resolved: &ResolvedFont,
265 text: &str,
266 text_offset: usize,
267 buffer: UnicodeBuffer,
268 features: &[Feature],
269) -> Option<(ShapedRun, UnicodeBuffer)> {
270 let entry = registry.get(resolved.font_face_id)?;
271 let font = FontRef::from_index(entry.bytes(), entry.face_index).ok()?;
272
273 let upem = units_per_em(entry.bytes(), entry.face_index).unwrap_or(0) as f32;
274 if upem == 0.0 {
275 return None;
276 }
277 let sf = resolved.scale_factor.max(f32::MIN_POSITIVE);
278 let physical_size = resolved.size_px * sf;
279 let physical_scale = physical_size / upem;
280 let inv_sf = 1.0 / sf;
281
282 let mut buffer = buffer;
283 buffer.push_str(text);
284 buffer.guess_segment_properties();
287
288 let resolved_direction = if buffer.direction() == Direction::RightToLeft {
289 TextDirection::RightToLeft
290 } else {
291 TextDirection::LeftToRight
292 };
293
294 let shaper_data = entry.shaper_data(&font);
295 let shaper = shaper_data.shaper(&font).build();
296 let glyph_buffer = shaper.shape(buffer, ShapeOptions::new().features(features));
297
298 let infos = glyph_buffer.glyph_infos();
299 let positions = glyph_buffer.glyph_positions();
300
301 let mut glyphs = Vec::with_capacity(infos.len());
302 let mut total_advance = 0.0f32;
303
304 for (info, pos) in infos.iter().zip(positions.iter()) {
305 let x_advance = pos.x_advance as f32 * physical_scale * inv_sf;
306 let y_advance = pos.y_advance as f32 * physical_scale * inv_sf;
307 let x_offset = pos.x_offset as f32 * physical_scale * inv_sf;
308 let y_offset = pos.y_offset as f32 * physical_scale * inv_sf;
309
310 glyphs.push(ShapedGlyph {
311 glyph_id: info.glyph_id as u16,
312 cluster: info.cluster,
313 x_advance,
314 y_advance,
315 x_offset,
316 y_offset,
317 font_face_id: resolved.font_face_id,
318 });
319
320 total_advance += x_advance;
321 }
322
323 let run = ShapedRun {
324 font_face_id: resolved.font_face_id,
325 size_px: resolved.size_px,
326 weight: resolved.weight,
327 glyphs,
328 advance_width: total_advance,
329 text_range: text_offset..text_offset + text.len(),
330 direction: resolved_direction,
331 underline_style: crate::types::UnderlineStyle::None,
332 overline: false,
333 strikeout: false,
334 is_link: false,
335 foreground_color: None,
336 underline_color: None,
337 background_color: None,
338 anchor_href: None,
339 tooltip: None,
340 vertical_alignment: crate::types::VerticalAlignment::Normal,
341 image_name: None,
342 image_height: 0.0,
343 };
344
345 let recycled = glyph_buffer.clear();
347 Some((run, recycled))
348}
349
350pub fn font_metrics_px(registry: &FontRegistry, resolved: &ResolvedFont) -> Option<FontMetricsPx> {
355 let entry = registry.get(resolved.font_face_id)?;
356 let font_ref = swash::FontRef::from_index(entry.bytes(), entry.face_index as usize)?;
357 let sf = resolved.scale_factor.max(f32::MIN_POSITIVE);
358 let physical_size = resolved.size_px * sf;
359 let metrics = font_ref.metrics(&[]).scale(physical_size);
360 let inv_sf = 1.0 / sf;
361
362 Some(FontMetricsPx {
363 ascent: metrics.ascent * inv_sf,
364 descent: metrics.descent * inv_sf,
365 leading: metrics.leading * inv_sf,
366 underline_offset: metrics.underline_offset * inv_sf,
367 strikeout_offset: metrics.strikeout_offset * inv_sf,
368 stroke_size: metrics.stroke_size * inv_sf,
369 })
370}
371
372pub struct BidiRun {
374 pub byte_range: std::ops::Range<usize>,
375 pub direction: TextDirection,
376 pub visual_order: usize,
378}
379
380pub fn bidi_runs(text: &str) -> Vec<BidiRun> {
388 use unicode_bidi::BidiInfo;
389
390 if text.is_empty() {
391 return Vec::new();
392 }
393
394 let bidi_info = BidiInfo::new(text, None);
395 let mut runs = Vec::new();
396
397 for para in &bidi_info.paragraphs {
398 let (levels, level_runs) = bidi_info.visual_runs(para, para.range.clone());
399 for level_run in level_runs {
400 if level_run.is_empty() {
401 continue;
402 }
403 let level = levels[level_run.start];
404 let direction = if level.is_rtl() {
405 TextDirection::RightToLeft
406 } else {
407 TextDirection::LeftToRight
408 };
409 let visual_order = runs.len();
410 runs.push(BidiRun {
411 byte_range: level_run,
412 direction,
413 visual_order,
414 });
415 }
416 }
417
418 if runs.is_empty() {
419 runs.push(BidiRun {
420 byte_range: 0..text.len(),
421 direction: TextDirection::LeftToRight,
422 visual_order: 0,
423 });
424 }
425
426 runs
427}
428
429pub struct FontMetricsPx {
430 pub ascent: f32,
431 pub descent: f32,
432 pub leading: f32,
433 pub underline_offset: f32,
434 pub strikeout_offset: f32,
435 pub stroke_size: f32,
436}