use rustybuzz::{Direction, Face, UnicodeBuffer};
use crate::font::registry::FontRegistry;
use crate::font::resolve::ResolvedFont;
use crate::shaping::run::{ShapedGlyph, ShapedRun};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum TextDirection {
#[default]
Auto,
LeftToRight,
RightToLeft,
}
pub fn shape_text(
registry: &FontRegistry,
resolved: &ResolvedFont,
text: &str,
text_offset: usize,
) -> Option<ShapedRun> {
shape_text_with_fallback(registry, resolved, text, text_offset, TextDirection::Auto)
}
pub fn shape_text_with_fallback(
registry: &FontRegistry,
resolved: &ResolvedFont,
text: &str,
text_offset: usize,
direction: TextDirection,
) -> Option<ShapedRun> {
let mut run = shape_text_directed(registry, resolved, text, text_offset, direction)?;
if run.glyphs.iter().any(|g| g.glyph_id == 0) && !text.is_empty() {
apply_glyph_fallback(registry, resolved, text, text_offset, &mut run);
}
Some(run)
}
fn apply_glyph_fallback(
registry: &FontRegistry,
primary: &ResolvedFont,
text: &str,
text_offset: usize,
run: &mut ShapedRun,
) {
use crate::font::resolve::find_fallback_font;
for glyph in &mut run.glyphs {
if glyph.glyph_id != 0 {
continue;
}
let byte_offset = glyph.cluster as usize;
let ch = match text.get(byte_offset..).and_then(|s| s.chars().next()) {
Some(c) => c,
None => continue,
};
let fallback_id = match find_fallback_font(registry, ch, primary.font_face_id) {
Some(id) => id,
None => continue, };
let fallback_entry = match registry.get(fallback_id) {
Some(e) => e,
None => continue,
};
let fallback_resolved = ResolvedFont {
font_face_id: fallback_id,
size_px: primary.size_px,
face_index: fallback_entry.face_index,
swash_cache_key: fallback_entry.swash_cache_key,
scale_factor: primary.scale_factor,
};
let char_str = &text[byte_offset..byte_offset + ch.len_utf8()];
if let Some(fallback_run) = shape_text_directed(
registry,
&fallback_resolved,
char_str,
text_offset + byte_offset,
TextDirection::Auto,
) {
if let Some(fb_glyph) = fallback_run.glyphs.first() {
glyph.glyph_id = fb_glyph.glyph_id;
glyph.x_advance = fb_glyph.x_advance;
glyph.y_advance = fb_glyph.y_advance;
glyph.x_offset = fb_glyph.x_offset;
glyph.y_offset = fb_glyph.y_offset;
glyph.font_face_id = fallback_id;
}
}
}
run.advance_width = run.glyphs.iter().map(|g| g.x_advance).sum();
}
pub fn shape_text_directed(
registry: &FontRegistry,
resolved: &ResolvedFont,
text: &str,
text_offset: usize,
direction: TextDirection,
) -> Option<ShapedRun> {
let entry = registry.get(resolved.font_face_id)?;
let face = Face::from_slice(&entry.data, entry.face_index)?;
let units_per_em = face.units_per_em() as f32;
if units_per_em == 0.0 {
return None;
}
let sf = resolved.scale_factor.max(f32::MIN_POSITIVE);
let physical_size = resolved.size_px * sf;
let physical_scale = physical_size / units_per_em;
let inv_sf = 1.0 / sf;
let mut buffer = UnicodeBuffer::new();
buffer.push_str(text);
match direction {
TextDirection::LeftToRight => buffer.set_direction(Direction::LeftToRight),
TextDirection::RightToLeft => buffer.set_direction(Direction::RightToLeft),
TextDirection::Auto => {} }
let glyph_buffer = rustybuzz::shape(&face, &[], buffer);
let infos = glyph_buffer.glyph_infos();
let positions = glyph_buffer.glyph_positions();
let mut glyphs = Vec::with_capacity(infos.len());
let mut total_advance = 0.0f32;
for (info, pos) in infos.iter().zip(positions.iter()) {
let x_advance = pos.x_advance as f32 * physical_scale * inv_sf;
let y_advance = pos.y_advance as f32 * physical_scale * inv_sf;
let x_offset = pos.x_offset as f32 * physical_scale * inv_sf;
let y_offset = pos.y_offset as f32 * physical_scale * inv_sf;
glyphs.push(ShapedGlyph {
glyph_id: info.glyph_id as u16,
cluster: info.cluster,
x_advance,
y_advance,
x_offset,
y_offset,
font_face_id: resolved.font_face_id,
});
total_advance += x_advance;
}
Some(ShapedRun {
font_face_id: resolved.font_face_id,
size_px: resolved.size_px,
glyphs,
advance_width: total_advance,
text_range: text_offset..text_offset + text.len(),
underline_style: crate::types::UnderlineStyle::None,
overline: false,
strikeout: false,
is_link: false,
foreground_color: None,
underline_color: None,
background_color: None,
anchor_href: None,
tooltip: None,
vertical_alignment: crate::types::VerticalAlignment::Normal,
image_name: None,
image_height: 0.0,
})
}
pub fn shape_text_with_buffer(
registry: &FontRegistry,
resolved: &ResolvedFont,
text: &str,
text_offset: usize,
buffer: UnicodeBuffer,
) -> Option<(ShapedRun, UnicodeBuffer)> {
let entry = registry.get(resolved.font_face_id)?;
let face = Face::from_slice(&entry.data, entry.face_index)?;
let units_per_em = face.units_per_em() as f32;
if units_per_em == 0.0 {
return None;
}
let sf = resolved.scale_factor.max(f32::MIN_POSITIVE);
let physical_size = resolved.size_px * sf;
let physical_scale = physical_size / units_per_em;
let inv_sf = 1.0 / sf;
let mut buffer = buffer;
buffer.push_str(text);
let glyph_buffer = rustybuzz::shape(&face, &[], buffer);
let infos = glyph_buffer.glyph_infos();
let positions = glyph_buffer.glyph_positions();
let mut glyphs = Vec::with_capacity(infos.len());
let mut total_advance = 0.0f32;
for (info, pos) in infos.iter().zip(positions.iter()) {
let x_advance = pos.x_advance as f32 * physical_scale * inv_sf;
let y_advance = pos.y_advance as f32 * physical_scale * inv_sf;
let x_offset = pos.x_offset as f32 * physical_scale * inv_sf;
let y_offset = pos.y_offset as f32 * physical_scale * inv_sf;
glyphs.push(ShapedGlyph {
glyph_id: info.glyph_id as u16,
cluster: info.cluster,
x_advance,
y_advance,
x_offset,
y_offset,
font_face_id: resolved.font_face_id,
});
total_advance += x_advance;
}
let run = ShapedRun {
font_face_id: resolved.font_face_id,
size_px: resolved.size_px,
glyphs,
advance_width: total_advance,
text_range: text_offset..text_offset + text.len(),
underline_style: crate::types::UnderlineStyle::None,
overline: false,
strikeout: false,
is_link: false,
foreground_color: None,
underline_color: None,
background_color: None,
anchor_href: None,
tooltip: None,
vertical_alignment: crate::types::VerticalAlignment::Normal,
image_name: None,
image_height: 0.0,
};
let recycled = glyph_buffer.clear();
Some((run, recycled))
}
pub fn font_metrics_px(registry: &FontRegistry, resolved: &ResolvedFont) -> Option<FontMetricsPx> {
let entry = registry.get(resolved.font_face_id)?;
let font_ref = swash::FontRef::from_index(&entry.data, entry.face_index as usize)?;
let sf = resolved.scale_factor.max(f32::MIN_POSITIVE);
let physical_size = resolved.size_px * sf;
let metrics = font_ref.metrics(&[]).scale(physical_size);
let inv_sf = 1.0 / sf;
Some(FontMetricsPx {
ascent: metrics.ascent * inv_sf,
descent: metrics.descent * inv_sf,
leading: metrics.leading * inv_sf,
underline_offset: metrics.underline_offset * inv_sf,
strikeout_offset: metrics.strikeout_offset * inv_sf,
stroke_size: metrics.stroke_size * inv_sf,
})
}
pub struct BidiRun {
pub byte_range: std::ops::Range<usize>,
pub direction: TextDirection,
pub visual_order: usize,
}
pub fn bidi_runs(text: &str) -> Vec<BidiRun> {
use unicode_bidi::BidiInfo;
if text.is_empty() {
return Vec::new();
}
let bidi_info = BidiInfo::new(text, None);
let mut runs = Vec::new();
for para in &bidi_info.paragraphs {
let (levels, level_runs) = bidi_info.visual_runs(para, para.range.clone());
for level_run in level_runs {
if level_run.is_empty() {
continue;
}
let level = levels[level_run.start];
let direction = if level.is_rtl() {
TextDirection::RightToLeft
} else {
TextDirection::LeftToRight
};
let visual_order = runs.len();
runs.push(BidiRun {
byte_range: level_run,
direction,
visual_order,
});
}
}
if runs.is_empty() {
runs.push(BidiRun {
byte_range: 0..text.len(),
direction: TextDirection::LeftToRight,
visual_order: 0,
});
}
runs
}
pub struct FontMetricsPx {
pub ascent: f32,
pub descent: f32,
pub leading: f32,
pub underline_offset: f32,
pub strikeout_offset: f32,
pub stroke_size: f32,
}