#![expect(clippy::mem_forget)]
use ecolor::Color32;
use emath::{GuiRounding as _, OrderedFloat, Vec2, vec2};
use self_cell::self_cell;
use skrifa::{GlyphId, MetadataProvider as _};
use std::collections::BTreeMap;
use vello_cpu::{color, kurbo};
use crate::{
TextOptions, TextureAtlas,
text::{
FontTweak, VariationCoords,
fonts::{Blob, CachedFamily, FontFaceKey},
},
};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct UvRect {
pub offset: Vec2,
pub size: Vec2,
pub min: [u16; 2],
pub max: [u16; 2],
}
impl UvRect {
pub fn is_nothing(&self) -> bool {
self.min == self.max
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct GlyphInfo {
pub(crate) id: Option<GlyphId>,
pub advance_width_unscaled: OrderedFloat<f32>,
}
impl GlyphInfo {
pub const INVISIBLE: Self = Self {
id: None,
advance_width_unscaled: OrderedFloat(0.0),
};
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub(super) enum GlyphIdResolution {
Glyph(GlyphId),
Invisible,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub(crate) struct LocationHash(u64);
impl nohash_hasher::IsEnabled for LocationHash {}
impl LocationHash {
#[inline]
pub fn new(location: &skrifa::instance::Location) -> Self {
if location.coords().is_empty() {
Self(0)
} else {
Self(crate::util::hash(location))
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub(super) enum SubpixelBin {
#[default]
Zero,
One,
Two,
Three,
}
impl SubpixelBin {
fn new(pos: f32) -> (i32, Self) {
let trunc = pos as i32;
let fract = pos - trunc as f32;
#[expect(clippy::collapsible_else_if)]
if pos.is_sign_negative() {
if fract > -0.125 {
(trunc, Self::Zero)
} else if fract > -0.375 {
(trunc - 1, Self::Three)
} else if fract > -0.625 {
(trunc - 1, Self::Two)
} else if fract > -0.875 {
(trunc - 1, Self::One)
} else {
(trunc - 1, Self::Zero)
}
} else {
if fract < 0.125 {
(trunc, Self::Zero)
} else if fract < 0.375 {
(trunc, Self::One)
} else if fract < 0.625 {
(trunc, Self::Two)
} else if fract < 0.875 {
(trunc, Self::Three)
} else {
(trunc + 1, Self::Zero)
}
}
}
pub fn as_float(&self) -> f32 {
match self {
Self::Zero => 0.0,
Self::One => 0.25,
Self::Two => 0.5,
Self::Three => 0.75,
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct GlyphAllocation {
pub uv_rect: UvRect,
}
#[derive(Hash, PartialEq, Eq)]
struct GlyphCacheKey(u64);
impl nohash_hasher::IsEnabled for GlyphCacheKey {}
impl GlyphCacheKey {
#[inline]
fn new(glyph_id: GlyphId, metrics: &StyledMetrics, bin: SubpixelBin) -> Self {
let StyledMetrics {
pixels_per_point,
px_scale_factor,
location_hash,
..
} = *metrics;
debug_assert!(
0.0 < pixels_per_point && pixels_per_point.is_finite(),
"Bad pixels_per_point {pixels_per_point}"
);
debug_assert!(
0.0 < px_scale_factor && px_scale_factor.is_finite(),
"Bad px_scale_factor: {px_scale_factor}"
);
Self(crate::util::hash((
glyph_id,
pixels_per_point.to_bits(),
px_scale_factor.to_bits(),
bin,
location_hash,
)))
}
}
struct DependentFontData<'a> {
skrifa: skrifa::FontRef<'a>,
charmap: skrifa::charmap::Charmap<'a>,
outline_glyphs: skrifa::outline::OutlineGlyphCollection<'a>,
metrics: skrifa::metrics::Metrics,
hinting_instance: Option<skrifa::outline::HintingInstance>,
}
self_cell! {
struct FontCell {
owner: Blob,
#[covariant]
dependent: DependentFontData,
}
}
impl FontCell {
fn px_scale_factor(&self, scale: f32) -> f32 {
let units_per_em = self.borrow_dependent().metrics.units_per_em as f32;
scale / units_per_em
}
fn allocate_glyph_uncached(
&mut self,
atlas: &mut TextureAtlas,
metrics: &StyledMetrics,
glyph_id: GlyphId,
bin: SubpixelBin,
location: skrifa::instance::LocationRef<'_>,
hinting_target: skrifa::outline::Target,
) -> Option<GlyphAllocation> {
debug_assert!(
glyph_id != skrifa::GlyphId::NOTDEF,
"Can't allocate glyph for id 0"
);
let mut path = kurbo::BezPath::new();
let mut pen = VelloPen {
path: &mut path,
x_offset: bin.as_float() as f64,
};
self.with_dependent_mut(|_, font_data| {
let outline = font_data.outline_glyphs.get(glyph_id)?;
if let Some(hinting_instance) = &mut font_data.hinting_instance {
let size = skrifa::instance::Size::new(metrics.scale);
if hinting_instance.size() != size
|| hinting_instance.location().coords() != location.coords()
|| hinting_instance.target() != hinting_target
{
hinting_instance
.reconfigure(&font_data.outline_glyphs, size, location, hinting_target)
.ok()?;
}
let draw_settings = skrifa::outline::DrawSettings::hinted(hinting_instance, false);
outline.draw(draw_settings, &mut pen).ok()?;
} else {
let draw_settings = skrifa::outline::DrawSettings::unhinted(
skrifa::instance::Size::new(metrics.scale),
location,
);
outline.draw(draw_settings, &mut pen).ok()?;
}
Some(())
})?;
let bounds = path.control_box().expand();
let width = bounds.width() as u16;
let height = bounds.height() as u16;
let uv_rect = if width == 0 || height == 0 {
UvRect::default()
} else {
let mut ctx = vello_cpu::RenderContext::new(width, height);
ctx.set_transform(kurbo::Affine::translate((-bounds.x0, -bounds.y0)));
ctx.set_paint(color::OpaqueColor::<color::Srgb>::WHITE);
ctx.fill_path(&path);
let mut dest = vello_cpu::Pixmap::new(width, height);
let mut resources = vello_cpu::Resources::new();
ctx.render_to_pixmap(&mut resources, &mut dest);
let glyph_pos = {
let color_transfer_function = atlas.options().color_transfer_function;
let (glyph_pos, image) = atlas.allocate((width as usize, height as usize));
let pixels = dest.data_as_u8_slice();
for y in 0..height as usize {
for x in 0..width as usize {
let pixel_offset = 4 * ((y * width as usize) + x);
image[(x + glyph_pos.0, y + glyph_pos.1)] = color_transfer_function
.to_atlas_color(Color32::from_rgba_premultiplied(
pixels[pixel_offset],
pixels[pixel_offset + 1],
pixels[pixel_offset + 2],
pixels[pixel_offset + 3],
));
}
}
glyph_pos
};
let offset_in_pixels = vec2(bounds.x0 as f32, bounds.y0 as f32);
let offset =
offset_in_pixels / metrics.pixels_per_point + metrics.y_offset_in_points * Vec2::Y;
UvRect {
offset,
size: vec2(width as f32, height as f32) / metrics.pixels_per_point,
min: [glyph_pos.0 as u16, glyph_pos.1 as u16],
max: [
(glyph_pos.0 + width as usize) as u16,
(glyph_pos.1 + height as usize) as u16,
],
}
};
Some(GlyphAllocation { uv_rect })
}
}
struct VelloPen<'a> {
path: &'a mut kurbo::BezPath,
x_offset: f64,
}
impl skrifa::outline::OutlinePen for VelloPen<'_> {
fn move_to(&mut self, x: f32, y: f32) {
self.path.move_to((x as f64 + self.x_offset, -y as f64));
}
fn line_to(&mut self, x: f32, y: f32) {
self.path.line_to((x as f64 + self.x_offset, -y as f64));
}
fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) {
self.path.quad_to(
(cx0 as f64 + self.x_offset, -cy0 as f64),
(x as f64 + self.x_offset, -y as f64),
);
}
fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) {
self.path.curve_to(
(cx0 as f64 + self.x_offset, -cy0 as f64),
(cx1 as f64 + self.x_offset, -cy1 as f64),
(x as f64 + self.x_offset, -y as f64),
);
}
fn close(&mut self) {
self.path.close_path();
}
}
pub struct FontFace {
name: String,
font: FontCell,
tweak: FontTweak,
subpixel_binning: bool,
shaper_data: harfrust::ShaperData,
glyph_id_cache: ahash::HashMap<char, GlyphIdResolution>,
advance_width_cache: ahash::HashMap<(char, LocationHash), OrderedFloat<f32>>,
glyph_alloc_cache: ahash::HashMap<GlyphCacheKey, GlyphAllocation>,
}
impl FontFace {
pub fn new(
options: TextOptions,
name: String,
font_data: Blob,
index: u32,
tweak: FontTweak,
) -> Result<Self, Box<dyn std::error::Error>> {
let font = FontCell::try_new(font_data, |font_data| {
let skrifa_font =
skrifa::FontRef::from_index(AsRef::<[u8]>::as_ref(font_data.as_ref()), index)?;
let charmap = skrifa_font.charmap();
let glyphs = skrifa_font.outline_glyphs();
let metrics = skrifa_font.metrics(
skrifa::instance::Size::unscaled(),
skrifa::instance::LocationRef::default(),
);
let hinting_enabled = tweak.hinting.unwrap_or(options.font_hinting);
let hinting_instance = hinting_enabled
.then(|| {
skrifa::outline::HintingInstance::new(
&glyphs,
skrifa::instance::Size::unscaled(),
skrifa::instance::LocationRef::default(),
skrifa::outline::Target::default(),
)
.ok()
})
.flatten();
Ok::<DependentFontData<'_>, Box<dyn std::error::Error>>(DependentFontData {
skrifa: skrifa_font,
charmap,
outline_glyphs: glyphs,
metrics,
hinting_instance,
})
})?;
let shaper_data = harfrust::ShaperData::new(&font.borrow_dependent().skrifa);
let subpixel_binning = tweak.subpixel_binning.unwrap_or(options.subpixel_binning);
Ok(Self {
name,
font,
tweak,
subpixel_binning,
shaper_data,
glyph_id_cache: Default::default(),
advance_width_cache: Default::default(),
glyph_alloc_cache: Default::default(),
})
}
fn ignore_character(&self, chr: char) -> bool {
use crate::text::FontDefinitions;
if !FontDefinitions::builtin_font_names().contains(&self.name.as_str()) {
return false;
}
matches!(
chr,
'\u{534d}' | '\u{5350}' |
'\u{E0FF}' | '\u{EFFD}' | '\u{F0FF}' | '\u{F200}'
)
}
fn characters(&self) -> impl Iterator<Item = char> + '_ {
self.font
.borrow_dependent()
.charmap
.mappings()
.filter_map(|(chr, _)| char::from_u32(chr).filter(|c| !self.ignore_character(*c)))
}
pub(super) fn glyph_id_resolution(&mut self, c: char) -> Option<GlyphIdResolution> {
if let Some(resolution) = self.glyph_id_cache.get(&c) {
return Some(*resolution);
}
if self.ignore_character(c) {
return None; }
let resolution = if c == '\t' || c == '\u{2009}' || c == '\u{202F}' {
self.glyph_id_resolution(' ')?
} else if invisible_char(c) {
GlyphIdResolution::Invisible
} else {
let glyph_id = self
.font
.borrow_dependent()
.charmap
.map(c)
.filter(|id| *id != GlyphId::NOTDEF)?;
GlyphIdResolution::Glyph(glyph_id)
};
self.glyph_id_cache.insert(c, resolution);
Some(resolution)
}
fn advance_width_unscaled(&mut self, c: char, metrics: &StyledMetrics) -> f32 {
let cache_key = (c, metrics.location_hash);
if let Some(advance) = self.advance_width_cache.get(&cache_key) {
return advance.0;
}
let advance = match c {
'\t' => self.tweak.tab_size * self.advance_width_unscaled(' ', metrics),
'\u{2009}' | '\u{202F}' => {
self.tweak.thin_space_width * self.advance_width_unscaled(' ', metrics)
}
_ => {
let Some(GlyphIdResolution::Glyph(glyph_id)) = self.glyph_id_resolution(c) else {
return 0.0;
};
let font_data = self.font.borrow_dependent();
let glyph_metrics = font_data
.skrifa
.glyph_metrics(skrifa::instance::Size::unscaled(), &metrics.location);
glyph_metrics.advance_width(glyph_id).unwrap_or_default()
}
};
self.advance_width_cache.insert(cache_key, advance.into());
advance
}
pub(super) fn glyph_info(&mut self, c: char, metrics: &StyledMetrics) -> Option<GlyphInfo> {
let resolution = self.glyph_id_resolution(c)?;
let glyph_info = match resolution {
GlyphIdResolution::Invisible => GlyphInfo::INVISIBLE,
GlyphIdResolution::Glyph(glyph_id) => GlyphInfo {
id: Some(glyph_id),
advance_width_unscaled: self.advance_width_unscaled(c, metrics).into(),
},
};
Some(glyph_info)
}
#[inline(always)]
pub fn styled_metrics(
&self,
pixels_per_point: f32,
font_size: f32,
coords: &VariationCoords,
) -> StyledMetrics {
let pt_scale_factor = self.font.px_scale_factor(font_size * self.tweak.scale);
let font_data = self.font.borrow_dependent();
let ascent = (font_data.metrics.ascent * pt_scale_factor).round_ui();
let descent = (font_data.metrics.descent * pt_scale_factor).round_ui();
let line_gap = (font_data.metrics.leading * pt_scale_factor).round_ui();
let scale = font_size * self.tweak.scale * pixels_per_point;
let px_scale_factor = self.font.px_scale_factor(scale);
let y_offset_in_points = ((font_size * self.tweak.scale * self.tweak.y_offset_factor)
+ self.tweak.y_offset)
.round_ui();
let axes = font_data.skrifa.axes();
let settings = std::iter::chain(self.tweak.coords.as_ref(), coords.as_ref());
let location = axes.location(settings);
let location_hash = LocationHash::new(&location);
StyledMetrics {
pixels_per_point,
px_scale_factor,
scale,
y_offset_in_points,
ascent,
row_height: ascent - descent + line_gap,
location,
location_hash,
}
}
pub(crate) fn skrifa_font_ref(&self) -> &skrifa::FontRef<'_> {
&self.font.borrow_dependent().skrifa
}
pub(crate) fn tweak(&self) -> &FontTweak {
&self.tweak
}
pub(crate) fn shaper_data(&self) -> &harfrust::ShaperData {
&self.shaper_data
}
pub fn allocate_glyph(
&mut self,
atlas: &mut TextureAtlas,
metrics: &StyledMetrics,
shaped: &ShapedGlyph,
) -> (GlyphAllocation, i32) {
let ShapedGlyph {
glyph_id,
h_pos,
is_cjk,
} = *shaped;
if glyph_id == GlyphId::NOTDEF {
return (GlyphAllocation::default(), h_pos.round() as i32);
}
let (h_pos_round, bin) = if self.subpixel_binning && !is_cjk {
SubpixelBin::new(h_pos)
} else {
(h_pos.round() as i32, SubpixelBin::Zero)
};
let cache_key = GlyphCacheKey::new(glyph_id, metrics, bin);
let hinting_target = self.tweak.hinting_target.into();
let alloc = *self.glyph_alloc_cache.entry(cache_key).or_insert_with(|| {
self.font
.allocate_glyph_uncached(
atlas,
metrics,
glyph_id,
bin,
(&metrics.location).into(),
hinting_target,
)
.unwrap_or_default()
});
(alloc, h_pos_round)
}
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct ShapedGlyph {
pub glyph_id: GlyphId,
pub h_pos: f32,
pub is_cjk: bool,
}
pub struct Font<'a> {
pub(super) fonts_by_id: &'a mut nohash_hasher::IntMap<FontFaceKey, FontFace>,
pub(super) cached_family: &'a mut CachedFamily,
pub(super) atlas: &'a mut TextureAtlas,
}
impl Font<'_> {
pub fn preload_characters(&mut self, s: &str) {
for c in s.chars() {
self.resolve_face(c);
}
}
pub fn characters(&mut self) -> &BTreeMap<char, Vec<String>> {
self.cached_family.characters.get_or_insert_with(|| {
let mut characters: BTreeMap<char, Vec<String>> = Default::default();
for font_id in &self.cached_family.fonts {
let font = self.fonts_by_id.get(font_id).expect("Nonexistent font ID");
for chr in font.characters() {
characters.entry(chr).or_default().push(font.name.clone());
}
}
characters
})
}
pub fn styled_metrics(
&self,
pixels_per_point: f32,
font_size: f32,
coords: &VariationCoords,
) -> StyledMetrics {
self.cached_family
.fonts
.first()
.and_then(|key| self.fonts_by_id.get(key))
.map(|font_face| font_face.styled_metrics(pixels_per_point, font_size, coords))
.unwrap_or_default()
}
pub fn glyph_width(&mut self, c: char, font_size: f32) -> f32 {
let face_key = self.resolve_face(c);
let Some(font_face) = self.fonts_by_id.get_mut(&face_key) else {
return 0.0;
};
let metrics = font_face.styled_metrics(1.0, font_size, &VariationCoords::default());
let Some(glyph_info) = font_face.glyph_info(c, &metrics) else {
return 0.0;
};
glyph_info.advance_width_unscaled.0 * font_face.font.px_scale_factor(font_size)
}
pub fn has_glyph(&mut self, c: char) -> bool {
self.resolve_face(c) != self.cached_family.replacement_face_key
}
pub fn has_glyphs(&mut self, s: &str) -> bool {
s.chars().all(|c| self.has_glyph(c))
}
#[inline]
pub(crate) fn resolve_face(&mut self, c: char) -> FontFaceKey {
if let Some(font_key) = self.cached_family.face_cache.get(&c) {
return *font_key;
}
self.resolve_face_slow(c)
}
#[cold]
fn resolve_face_slow(&mut self, c: char) -> FontFaceKey {
let font_key = self
.cached_family
.find_face_for_char(c, self.fonts_by_id)
.unwrap_or(self.cached_family.replacement_face_key);
self.cached_family.face_cache.insert(c, font_key);
font_key
}
pub(crate) fn glyph_info(
&mut self,
c: char,
metrics: &StyledMetrics,
) -> (FontFaceKey, GlyphInfo) {
let face_key = self.resolve_face(c);
let Some(face) = self.fonts_by_id.get_mut(&face_key) else {
return (face_key, GlyphInfo::INVISIBLE);
};
let glyph_info = face.glyph_info(c, metrics).unwrap_or_else(|| {
face.glyph_info(self.cached_family.replacement_char, metrics)
.unwrap_or(GlyphInfo::INVISIBLE)
});
(face_key, glyph_info)
}
}
#[derive(Clone, Debug, PartialEq, Default)]
pub struct StyledMetrics {
pub pixels_per_point: f32,
pub px_scale_factor: f32,
pub scale: f32,
pub y_offset_in_points: f32,
pub ascent: f32,
pub row_height: f32,
pub location: skrifa::instance::Location,
pub(crate) location_hash: LocationHash,
}
#[inline]
fn invisible_char(c: char) -> bool {
if c == '\r' {
return true;
}
matches!(
c,
'\u{200B}' | '\u{200C}' | '\u{200D}' | '\u{200E}' | '\u{200F}' | '\u{202A}' | '\u{202B}' | '\u{202C}' | '\u{202D}' | '\u{202E}' | '\u{2060}' | '\u{2061}' | '\u{2062}' | '\u{2063}' | '\u{2064}' | '\u{2066}' | '\u{2067}' | '\u{2068}' | '\u{2069}' | '\u{206A}' | '\u{206B}' | '\u{206C}' | '\u{206D}' | '\u{206E}' | '\u{206F}' | '\u{FEFF}' )
}
#[inline]
pub(super) fn is_cjk_ideograph(c: char) -> bool {
('\u{4E00}' <= c && c <= '\u{9FFF}')
|| ('\u{3400}' <= c && c <= '\u{4DBF}')
|| ('\u{2B740}' <= c && c <= '\u{2B81F}')
}
#[inline]
pub(super) fn is_kana(c: char) -> bool {
('\u{3040}' <= c && c <= '\u{309F}') || ('\u{30A0}' <= c && c <= '\u{30FF}') }
#[inline]
pub(super) fn is_cjk(c: char) -> bool {
is_cjk_ideograph(c) || is_kana(c)
}
#[inline]
pub(super) fn is_cjk_break_allowed(c: char) -> bool {
!")]}〕〉》」』】〙〗〟'\"⦆»ヽヾーァィゥェォッャュョヮヵヶぁぃぅぇぉっゃゅょゎゕゖㇰㇱㇲㇳㇴㇵㇶㇷㇸㇹㇺㇻㇼㇽㇾㇿ々〻‐゠–〜?!‼⁇⁈⁉・、:;,。.".contains(c)
}