use std::cell::RefCell;
use std::collections::HashMap;
use std::sync::Arc;
use swash::scale::{Render, ScaleContext, Source};
use swash::{CacheKey, FontRef, GlyphId};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum FontWeight {
Light,
#[default]
Regular,
Medium,
SemiBold,
Bold,
}
impl FontWeight {
#[inline]
fn wants_bold(self) -> bool {
matches!(self, FontWeight::SemiBold | FontWeight::Bold)
}
}
type FaceKey = u8;
const FACE_REGULAR: FaceKey = 0;
const FACE_BOLD: FaceKey = 1;
const FACE_ICON: FaceKey = 2;
const FACE_FALLBACK_BASE: FaceKey = 128;
type GlyphCacheKey = (FaceKey, char, u32); type ColorGlyphKey = (char, u32); type KernKey = (FaceKey, char, char);
type KernEntry = Option<(i16, u16)>;
#[derive(Debug, Clone, Copy, Default)]
pub struct GlyphMetrics {
pub xmin: i32,
pub ymin: i32,
pub width: usize,
pub height: usize,
pub advance_width: f32,
}
pub type CachedGlyph = Arc<(GlyphMetrics, Vec<u8>)>;
pub struct OwnedFace {
data: Arc<Vec<u8>>,
offset: u32,
key: CacheKey,
variable_weight: Option<f32>,
}
impl OwnedFace {
pub fn from_bytes(bytes: &[u8]) -> Option<Self> {
Self::new(bytes.to_vec(), 0, 400.0)
}
fn new(bytes: Vec<u8>, index: u32, weight: f32) -> Option<Self> {
let data = Arc::new(bytes);
let (offset, key, is_variable) = {
let font = FontRef::from_index(&data, index as usize)?;
(font.offset, font.key, font.variations().len() > 0)
};
Some(Self {
data,
offset,
key,
variable_weight: is_variable.then_some(weight),
})
}
fn font_ref(&self) -> FontRef<'_> {
FontRef { data: &self.data, offset: self.offset, key: self.key }
}
}
enum Fallback {
Untried(&'static str),
Missing,
Loaded(OwnedFace),
}
enum EmojiFallback {
Untried,
Missing,
Loaded(Arc<Vec<u8>>),
}
pub struct ColorGlyph {
pub advance: f32,
pub width: u32,
pub height: u32,
pub rgba: Arc<Vec<u8>>,
}
const EMOJI_FALLBACK_PATHS: &[&str] = &[
"/System/Library/Fonts/Apple Color Emoji.ttc",
];
fn is_emoji_codepoint(c: char) -> bool {
matches!(c as u32,
0x1F300..=0x1FAFF | 0x2600..=0x27BF | 0x2190..=0x21FF | 0x2B00..=0x2BFF | 0x1F1E6..=0x1F1FF )
}
pub struct FontCache {
font: OwnedFace,
bold: Option<OwnedFace>,
icon: RefCell<Option<Arc<OwnedFace>>>,
fallbacks: RefCell<Vec<Fallback>>,
route_cache: RefCell<HashMap<(char, bool), FaceKey>>,
glyph_cache: RefCell<HashMap<GlyphCacheKey, CachedGlyph>>,
metrics_cache: RefCell<HashMap<GlyphCacheKey, f32>>,
emoji: RefCell<EmojiFallback>,
color_glyph_cache: RefCell<HashMap<ColorGlyphKey, Option<Arc<ColorGlyph>>>>,
kern_cache: RefCell<HashMap<KernKey, KernEntry>>,
scale_ctx: RefCell<ScaleContext>,
}
const FALLBACK_PATHS: &[&str] = &[
"/System/Library/Fonts/Supplemental/Arial Unicode.ttf",
"/System/Library/Fonts/Apple Symbols.ttf",
"/System/Library/Fonts/Supplemental/Zapf Dingbats.ttf",
"/usr/share/fonts/truetype/noto/NotoSans-Regular.ttf",
"/usr/share/fonts/truetype/noto/NotoSansSymbols-Regular.ttf",
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
"C:\\Windows\\Fonts\\seguisym.ttf",
"C:\\Windows\\Fonts\\msgothic.ttc",
];
const BOLD_PATHS: &[&str] = &[
"/System/Library/Fonts/Supplemental/Arial Bold.ttf",
"/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf",
"/usr/share/fonts/truetype/ubuntu/Ubuntu-B.ttf",
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
"C:\\Windows\\Fonts\\segoeuib.ttf",
"C:\\Windows\\Fonts\\arialbd.ttf",
"/system/fonts/Roboto-Bold.ttf",
];
impl FontCache {
fn build(font: OwnedFace, bold: Option<OwnedFace>) -> Self {
Self {
font,
bold,
icon: RefCell::new(None),
fallbacks: RefCell::new(
FALLBACK_PATHS.iter().map(|p| Fallback::Untried(p)).collect(),
),
route_cache: RefCell::new(HashMap::new()),
glyph_cache: RefCell::new(HashMap::new()),
metrics_cache: RefCell::new(HashMap::new()),
emoji: RefCell::new(EmojiFallback::Untried),
color_glyph_cache: RefCell::new(HashMap::new()),
kern_cache: RefCell::new(HashMap::new()),
scale_ctx: RefCell::new(ScaleContext::new()),
}
}
pub fn from_bytes(bytes: &[u8]) -> Self {
let font = OwnedFace::new(bytes.to_vec(), 0, 400.0)
.expect("invalid font bytes");
Self::build(font, None)
}
pub fn from_asset(name: impl rosace_core::asset::AssetRef) -> Option<Self> {
let bytes = rosace_core::asset::bytes(name)?;
let font = OwnedFace::new(bytes, 0, 400.0)?;
Some(Self::build(font, None))
}
pub fn embedded() -> Self {
const DEJAVU_SANS: &[u8] =
include_bytes!("../assets/fonts/DejaVuSans.ttf");
Self::from_bytes(DEJAVU_SANS)
}
pub fn bundled() -> Self {
const INTER_REGULAR: &[u8] =
include_bytes!("../assets/fonts/inter/Inter-Regular.ttf");
const INTER_BOLD: &[u8] =
include_bytes!("../assets/fonts/inter/Inter-Bold.ttf");
let regular = OwnedFace::new(INTER_REGULAR.to_vec(), 0, 400.0)
.expect("bundled Inter Regular is valid");
let bold = OwnedFace::new(INTER_BOLD.to_vec(), 0, 700.0)
.expect("bundled Inter Bold is valid");
Self::build(regular, Some(bold))
}
fn load_first(paths: &[&str], weight: f32) -> Option<OwnedFace> {
for path in paths {
if let Ok(bytes) = std::fs::read(path) {
if let Some(f) = OwnedFace::new(bytes, 0, weight) {
return Some(f);
}
}
}
None
}
fn weight_score(name: &str, want_bold: bool) -> Option<i32> {
let n = name.to_ascii_lowercase();
if n.contains("italic") || n.contains("oblique") {
return None;
}
if want_bold {
if n.ends_with("bold") && !n.contains("semi") && !n.contains("demi")
&& !n.contains("ultra") && !n.contains("extra")
{
return Some(3);
}
if n.contains("bold") { return Some(2); }
if n.contains("heavy") || n.contains("black") { return Some(1); }
None
} else {
if n.ends_with("regular") || n == "regular" { return Some(3); }
if !n.contains("bold") && !n.contains("black") && !n.contains("heavy")
&& !n.contains("light") && !n.contains("thin") && !n.contains("medium")
&& !n.contains("demi") && !n.contains("semi") && !n.contains("condensed")
&& !n.contains("narrow") && !n.contains("ultra") && !n.contains("extra")
{
return Some(2);
}
Some(0)
}
}
fn face_name(bytes: &[u8], index: u32) -> Option<String> {
let face = ttf_parser::Face::parse(bytes, index).ok()?;
face.names().into_iter()
.find(|n| n.name_id == 4 && n.is_unicode())
.and_then(|n| n.to_string())
}
fn best_face_index(bytes: &[u8], want_bold: bool) -> Option<u32> {
let n = ttf_parser::fonts_in_collection(bytes).unwrap_or(1);
let mut best: Option<(i32, u32)> = None;
for i in 0..n {
let Some(name) = Self::face_name(bytes, i) else { continue };
let Some(score) = Self::weight_score(&name, want_bold) else { continue };
if best.map(|(s, _)| score > s).unwrap_or(true) {
best = Some((score, i));
}
}
best.map(|(_, i)| i)
}
fn load_face(bytes: &[u8], index: u32, weight: f32) -> Option<OwnedFace> {
OwnedFace::new(bytes.to_vec(), index, weight)
}
pub fn system_ui() -> Option<Self> {
let candidates = [
"/System/Library/Fonts/SFNS.ttf",
"/System/Library/Fonts/Avenir Next.ttc",
"/System/Library/Fonts/HelveticaNeue.ttc",
"/System/Library/Fonts/Helvetica.ttc",
"/System/Library/Fonts/Supplemental/Arial.ttf",
"/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
"/usr/share/fonts/truetype/ubuntu/Ubuntu-R.ttf",
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
"C:\\Windows\\Fonts\\segoeui.ttf",
"C:\\Windows\\Fonts\\arial.ttf",
"/system/fonts/Roboto-Regular.ttf",
];
for path in candidates {
let Ok(bytes) = std::fs::read(path) else { continue };
let reg_idx = Self::best_face_index(&bytes, false).unwrap_or(0);
let Some(regular) = Self::load_face(&bytes, reg_idx, 400.0) else { continue };
let bold = Self::best_face_index(&bytes, true)
.and_then(|i| Self::load_face(&bytes, i, 700.0))
.or_else(|| Self::load_first(BOLD_PATHS, 700.0));
return Some(Self::build(regular, bold));
}
None
}
pub fn system_mono() -> Option<Self> {
let candidates = [
"/System/Library/Fonts/Menlo.ttc",
"/System/Library/Fonts/Monaco.ttf",
"/System/Library/Fonts/Supplemental/Courier New.ttf",
"/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf",
"/usr/share/fonts/truetype/ubuntu/UbuntuMono-R.ttf",
"/usr/share/fonts/truetype/liberation/LiberationMono-Regular.ttf",
"C:\\Windows\\Fonts\\consola.ttf",
"/system/fonts/DroidSansMono.ttf",
];
let regular = Self::load_first(&candidates, 400.0)?;
Some(Self::build(regular, None))
}
pub fn set_icon_face(&self, font: Arc<OwnedFace>) {
{
let mut slot = self.icon.borrow_mut();
if slot.is_some() {
return;
}
*slot = Some(font);
}
self.route_cache.borrow_mut().clear();
}
pub fn has_icon_face(&self) -> bool {
self.icon.borrow().is_some()
}
fn resolve(&self, c: char, weight: FontWeight) -> FaceKey {
let wants_bold = weight.wants_bold() && self.bold.is_some();
let key = (c, wants_bold);
if let Some(&f) = self.route_cache.borrow().get(&key) {
return f;
}
let face = if wants_bold && self.bold.as_ref().unwrap().font_ref().charmap().map(c) != 0 {
FACE_BOLD
} else if self.font.font_ref().charmap().map(c) != 0 {
FACE_REGULAR
} else if self
.icon
.borrow()
.as_ref()
.is_some_and(|f| f.font_ref().charmap().map(c) != 0)
{
FACE_ICON
} else {
let mut found = FACE_REGULAR; let mut fallbacks = self.fallbacks.borrow_mut();
for (i, slot) in fallbacks.iter_mut().enumerate() {
if let Fallback::Untried(path) = slot {
*slot = match std::fs::read(path).ok().and_then(|b| OwnedFace::new(b, 0, 400.0)) {
Some(f) => Fallback::Loaded(f),
None => Fallback::Missing,
};
}
if let Fallback::Loaded(f) = slot {
if f.font_ref().charmap().map(c) != 0 {
found = FACE_FALLBACK_BASE + i as FaceKey;
break;
}
}
}
found
};
self.route_cache.borrow_mut().insert(key, face);
face
}
fn with_face<R>(&self, face: FaceKey, f: impl FnOnce(&OwnedFace) -> R) -> R {
if face == FACE_BOLD {
if let Some(b) = &self.bold {
return f(b);
}
} else if face == FACE_ICON {
let icon = self.icon.borrow();
if let Some(i) = icon.as_ref() {
return f(i);
}
} else if face >= FACE_FALLBACK_BASE {
let fallbacks = self.fallbacks.borrow();
if let Some(Fallback::Loaded(fb)) = fallbacks.get((face - FACE_FALLBACK_BASE) as usize) {
return f(fb);
}
}
f(&self.font)
}
fn rasterize_glyph(&self, owned: &OwnedFace, c: char, px: f32) -> (GlyphMetrics, Vec<u8>) {
let font_ref = owned.font_ref();
let glyph_id: GlyphId = font_ref.charmap().map(c);
let advance = font_ref.glyph_metrics(&[]).scale(px).advance_width(glyph_id);
let mut ctx = self.scale_ctx.borrow_mut();
let mut builder = ctx.builder(font_ref).size(px).hint(true);
if let Some(w) = owned.variable_weight {
builder = builder.variations(&[("wght", w)]);
}
let mut scaler = builder.build();
let Some(image) = Render::new(&[Source::Outline]).render(&mut scaler, glyph_id) else {
return (GlyphMetrics { advance_width: advance, ..Default::default() }, Vec::new());
};
let metrics = GlyphMetrics {
xmin: image.placement.left,
ymin: image.placement.top - image.placement.height as i32,
width: image.placement.width as usize,
height: image.placement.height as usize,
advance_width: advance,
};
(metrics, image.data)
}
pub fn glyph_weighted(&self, c: char, px: f32, weight: FontWeight) -> CachedGlyph {
let face = self.resolve(c, weight);
let key = (face, c, px.to_bits());
{
let cache = self.glyph_cache.borrow();
if let Some(entry) = cache.get(&key) {
return Arc::clone(entry);
}
}
let (metrics, bytes) = self.with_face(face, |f| self.rasterize_glyph(f, c, px));
let entry = Arc::new((metrics, bytes));
self.glyph_cache.borrow_mut().insert(key, Arc::clone(&entry));
entry
}
pub fn glyph(&self, c: char, px: f32) -> CachedGlyph {
self.glyph_weighted(c, px, FontWeight::Regular)
}
pub fn rasterize(&self, c: char, px: f32) -> (GlyphMetrics, Vec<u8>) {
let glyph = self.glyph(c, px);
(glyph.0, glyph.1.clone())
}
fn emoji_bytes(&self) -> Option<Arc<Vec<u8>>> {
{
match &*self.emoji.borrow() {
EmojiFallback::Loaded(b) => return Some(Arc::clone(b)),
EmojiFallback::Missing => return None,
EmojiFallback::Untried => {}
}
}
let found = EMOJI_FALLBACK_PATHS.iter()
.find_map(|p| std::fs::read(p).ok())
.map(Arc::new);
*self.emoji.borrow_mut() = match &found {
Some(b) => EmojiFallback::Loaded(Arc::clone(b)),
None => EmojiFallback::Missing,
};
found
}
pub fn color_glyph_rgba(&self, c: char, px: f32) -> Option<Arc<ColorGlyph>> {
if !is_emoji_codepoint(c) { return None; }
let cache_key = (c, px.to_bits());
if let Some(hit) = self.color_glyph_cache.borrow().get(&cache_key) {
return hit.clone();
}
let result = (|| {
let bytes = self.emoji_bytes()?;
let face = ttf_parser::Face::parse(&bytes, 0).ok()?;
let gid = face.glyph_index(c)?;
let img = face.glyph_raster_image(gid, px.round().clamp(1.0, u16::MAX as f32) as u16)?;
if img.format != ttf_parser::RasterImageFormat::PNG { return None; }
let pixmap = tiny_skia::Pixmap::decode_png(img.data).ok()?;
let units_per_em = face.units_per_em() as f32;
let advance = face.glyph_hor_advance(gid)
.map(|a| a as f32 / units_per_em * px)
.unwrap_or(pixmap.width() as f32);
Some(Arc::new(ColorGlyph {
advance,
width: pixmap.width(),
height: pixmap.height(),
rgba: Arc::new(pixmap.data().to_vec()),
}))
})();
self.color_glyph_cache.borrow_mut().insert(cache_key, result.clone());
result
}
pub fn kern_weighted(&self, left: char, right: char, px: f32, weight: FontWeight) -> f32 {
let fl = self.resolve(left, weight);
if fl != self.resolve(right, weight) {
return 0.0;
}
let cache_key = (fl, left, right);
let cached = {
let cache = self.kern_cache.borrow();
cache.get(&cache_key).copied()
};
let entry = match cached {
Some(v) => v,
None => {
let v = self.with_face(fl, |owned| {
let Ok(face) = ttf_parser::Face::parse(&owned.data, 0) else { return None };
let (Some(l), Some(r)) = (face.glyph_index(left), face.glyph_index(right)) else { return None };
let upem = face.units_per_em();
let table = face.tables().kern?;
let raw = table.subtables.into_iter().find_map(|st| st.glyphs_kerning(l, r))?;
Some((raw, upem))
});
self.kern_cache.borrow_mut().insert(cache_key, v);
v
}
};
let Some((raw, units_per_em)) = entry else { return 0.0 };
if units_per_em == 0 { return 0.0; }
raw as f32 / units_per_em as f32 * px
}
pub fn kern(&self, left: char, right: char, px: f32) -> f32 {
self.kern_weighted(left, right, px, FontWeight::Regular)
}
pub fn advance_width_weighted(&self, c: char, px: f32, weight: FontWeight) -> f32 {
let face = self.resolve(c, weight);
let key = (face, c, px.to_bits());
{
let cache = self.metrics_cache.borrow();
if let Some(&w) = cache.get(&key) {
return w;
}
}
let w = self.with_face(face, |owned| {
let font_ref = owned.font_ref();
let glyph_id = font_ref.charmap().map(c);
font_ref.glyph_metrics(&[]).scale(px).advance_width(glyph_id)
});
self.metrics_cache.borrow_mut().insert(key, w);
w
}
pub fn advance_width(&self, c: char, px: f32) -> f32 {
self.advance_width_weighted(c, px, FontWeight::Regular)
}
pub fn measure_text_weighted(&self, text: &str, px: f32, weight: FontWeight) -> f32 {
let px = px * rosace_core::media_query::use_media_query().text_scale;
let mut width = 0.0;
let mut prev: Option<char> = None;
for c in text.chars() {
if let Some(p) = prev {
width += self.kern_weighted(p, c, px, weight);
}
width += self.advance_width_weighted(c, px, weight);
prev = Some(c);
}
width
}
pub fn measure_text(&self, text: &str, px: f32) -> f32 {
self.measure_text_weighted(text, px, FontWeight::Regular)
}
pub fn ascender(&self, px: f32) -> i32 {
let font_ref = self.font.font_ref();
let m = font_ref.metrics(&[]).scale(px);
if m.ascent > 0.0 { m.ascent.round() as i32 } else { (px * 0.78) as i32 }
}
pub fn line_height(&self, px: f32) -> f32 {
let font_ref = self.font.font_ref();
let m = font_ref.metrics(&[]).scale(px);
let total = m.ascent + m.descent + m.leading;
if total > 0.0 { total } else { px * 1.2 }
}
}
pub struct PlacedGlyph {
pub glyph: CachedGlyph,
pub x: i32,
pub y: i32,
pub key: u64,
pub color_rgba: Option<Arc<ColorGlyph>>,
}
pub fn layout_glyphs(
font: &FontCache,
text: &str,
origin_x: f32,
origin_y: f32,
px: f32,
weight: FontWeight,
) -> Vec<PlacedGlyph> {
let base_y = origin_y.round() as i32 + font.ascender(px);
let mut cursor_x = origin_x;
let mut prev: Option<char> = None;
let mut out = Vec::with_capacity(text.len());
let bold = weight.wants_bold() as u64;
for ch in text.chars() {
if matches!(ch as u32, 0xFE00..=0xFE0F) {
continue; }
if let Some(p) = prev {
cursor_x += font.kern_weighted(p, ch, px, weight);
}
prev = Some(ch);
if let Some(cg) = font.color_glyph_rgba(ch, px) {
let gx = cursor_x.round() as i32;
let gy = base_y - cg.height as i32; let key = ((px.to_bits() as u64) << 32) | ((ch as u64) << 1) | bold | (1 << 63);
let placeholder: CachedGlyph = Arc::new((GlyphMetrics::default(), Vec::new()));
let advance = cg.advance;
out.push(PlacedGlyph { glyph: placeholder, x: gx, y: gy, key, color_rgba: Some(cg) });
cursor_x += advance;
continue;
}
let glyph = font.glyph_weighted(ch, px, weight);
let advance = glyph.0.advance_width;
if glyph.0.width != 0 && glyph.0.height != 0 {
let gx = cursor_x.round() as i32 + glyph.0.xmin;
let gy = base_y - glyph.0.ymin - glyph.0.height as i32;
let key = ((px.to_bits() as u64) << 32) | ((ch as u64) << 1) | bold;
out.push(PlacedGlyph { glyph, x: gx, y: gy, key, color_rgba: None });
}
cursor_x += advance;
}
out
}
#[cfg(test)]
mod color_glyph_tests {
use super::*;
#[test]
fn is_emoji_codepoint_covers_common_emoji_but_not_plain_text() {
assert!(is_emoji_codepoint('😀')); assert!(is_emoji_codepoint('🎉')); assert!(is_emoji_codepoint('☀')); assert!(!is_emoji_codepoint('a'));
assert!(!is_emoji_codepoint('#'));
assert!(!is_emoji_codepoint(' '));
}
#[test]
fn color_glyph_rgba_decodes_a_real_emoji_on_this_machine() {
if EMOJI_FALLBACK_PATHS.iter().all(|p| !std::path::Path::new(p).exists()) {
eprintln!("no color-emoji font file on this machine — skipping (not a failure)");
return;
}
let font = FontCache::embedded();
let cg = font.color_glyph_rgba('😀', 32.0)
.expect("font file exists but color_glyph_rgba returned None — a real bug, not an environment gap");
assert!(cg.width > 0 && cg.height > 0, "decoded bitmap must have real dimensions");
assert_eq!(cg.rgba.len(), (cg.width * cg.height * 4) as usize, "RGBA8 buffer must match width*height*4");
assert!(cg.advance > 0.0, "a real emoji must have a positive advance width");
let has_real_color = cg.rgba.chunks_exact(4).any(|p| p[3] > 0 && (p[0] > 20 || p[1] > 20 || p[2] > 20));
assert!(has_real_color, "decoded emoji must contain real non-black visible pixels");
}
#[test]
fn color_glyph_rgba_returns_none_for_plain_text() {
let font = FontCache::embedded();
assert!(font.color_glyph_rgba('a', 16.0).is_none());
}
#[test]
fn layout_glyphs_places_a_real_emoji_with_color_rgba_set() {
if EMOJI_FALLBACK_PATHS.iter().all(|p| !std::path::Path::new(p).exists()) {
eprintln!("no color-emoji font file on this machine — skipping (not a failure)");
return;
}
let font = FontCache::embedded();
let placed = layout_glyphs(&font, "hi 😀 there", 0.0, 0.0, 16.0, FontWeight::Regular);
let pg = placed.iter().find(|pg| pg.color_rgba.is_some())
.expect("font file exists but no placed glyph had color_rgba set — a real bug, not an environment gap");
let cg = pg.color_rgba.as_ref().unwrap();
assert!(cg.width > 0 && cg.height > 0);
assert!(placed.iter().any(|pg| pg.color_rgba.is_none()), "plain characters must still be placed");
}
#[test]
fn variation_selector_16_produces_no_placed_glyph() {
let font = FontCache::embedded();
let placed = layout_glyphs(&font, "\u{FE0F}", 0.0, 0.0, 16.0, FontWeight::Regular);
assert!(placed.is_empty(), "a lone variation selector must never produce a placed glyph");
let placed = layout_glyphs(&font, "\u{2600}\u{FE0F}", 0.0, 0.0, 16.0, FontWeight::Regular);
assert!(placed.len() <= 1, "the selector must not add a second placed glyph, got {}", placed.len());
}
}