mod offscreen;
pub use offscreen::{render_menu_to_png, render_menu_to_rgba};
pub mod paint;
pub(crate) mod raster;
pub(crate) mod svg;
pub use svg::rasterize_svg;
use std::cell::RefCell;
use std::collections::HashMap;
use std::hash::{BuildHasherDefault, Hasher};
use std::rc::Rc;
use std::sync::{Arc, OnceLock};
use fontdb::{
Database, Family as DbFamily, Query, Source as DbSource, Style as DbStyle, Weight as DbWeight,
ID as FaceId,
};
use harfrust::{FontRef as HbFontRef, ShapeOptions, ShaperData, ShaperInstance, UnicodeBuffer};
use swash::scale::image::Content;
use swash::scale::{Render, ScaleContext, Source, StrikeWith};
use swash::{FontRef, GlyphId};
pub use raster::{decode_png, Framebuffer};
pub(crate) use raster::encode_rgba_png;
type DecodedIcon = Rc<(Vec<u8>, u32, u32)>;
#[derive(Default)]
struct FxHasher {
hash: u64,
}
impl FxHasher {
const SEED: u64 = 0x51_7c_c1_b7_27_22_0a_95;
#[inline]
fn add(&mut self, i: u64) {
self.hash = (self.hash.rotate_left(5) ^ i).wrapping_mul(Self::SEED);
}
}
impl Hasher for FxHasher {
#[inline]
fn write(&mut self, bytes: &[u8]) {
let mut chunks = bytes.chunks_exact(8);
for c in &mut chunks {
self.add(u64::from_le_bytes(c.try_into().unwrap()));
}
let rem = chunks.remainder();
if !rem.is_empty() {
let mut buf = [0u8; 8];
buf[..rem.len()].copy_from_slice(rem);
self.add(u64::from_le_bytes(buf));
}
}
#[inline]
fn write_u8(&mut self, i: u8) {
self.add(i as u64);
}
#[inline]
fn write_u16(&mut self, i: u16) {
self.add(i as u64);
}
#[inline]
fn write_u32(&mut self, i: u32) {
self.add(i as u64);
}
#[inline]
fn write_u64(&mut self, i: u64) {
self.add(i);
}
#[inline]
fn write_usize(&mut self, i: usize) {
self.add(i as u64);
}
#[inline]
fn finish(&self) -> u64 {
self.hash
}
}
type FxHashMap<K, V> = HashMap<K, V, BuildHasherDefault<FxHasher>>;
type ShapeKey = (String, Option<FaceId>, u16, u32, u32, u32);
type IconCacheEntry = (Arc<[u8]>, DecodedIcon);
use crate::geometry::{LogicalPoint, LogicalRect, LogicalSize};
use crate::platform::{Platform, SystemFont, SystemFontSource};
use crate::style::{Font, FontFamily, Rgba, Weight};
use crate::theme::OsFamily;
#[derive(Clone, Debug)]
pub struct TextRun<'a> {
pub text: &'a str,
pub origin: LogicalPoint,
pub font: &'a Font,
pub color: Rgba,
pub weight: Weight,
}
pub trait SceneDrawer {
fn begin_frame(&mut self, size: LogicalSize);
fn fill_round_rect(&mut self, rect: LogicalRect, corner_radius: f32, color: Rgba);
fn draw_separator(&mut self, rect: LogicalRect, color: Rgba);
fn measure_text(&self, text: &str, font: &Font) -> f32;
fn line_height(&self, font: &Font) -> f32;
fn draw_text(&mut self, run: &TextRun<'_>);
fn draw_image(&mut self, rgba: &[u8], src_w: u32, src_h: u32, dest: LogicalRect);
fn draw_image_alpha(
&mut self,
rgba: &[u8],
src_w: u32,
src_h: u32,
dest: LogicalRect,
alpha: f32,
) {
let _ = alpha;
self.draw_image(rgba, src_w, src_h, dest);
}
fn decode_icon(&self, bytes: &Arc<[u8]>) -> Option<DecodedIcon> {
decode_icon_bytes(bytes).map(Rc::new)
}
}
pub(crate) fn decode_icon_bytes(bytes: &[u8]) -> Option<(Vec<u8>, u32, u32)> {
raster::decode_png(bytes).or_else(|| svg::rasterize_svg(bytes))
}
const LINE_HEIGHT_FACTOR: f32 = 1.3;
const FALLBACK_FAMILIES: &[&str] = &[
"Apple Color Emoji",
"Segoe UI Emoji",
"Noto Color Emoji",
"Apple Symbols",
"Segoe UI Symbol",
"Symbola",
"PingFang SC",
"Hiragino Sans",
"Hiragino Sans GB",
"Microsoft YaHei",
"Yu Gothic",
"Malgun Gothic",
"Noto Sans CJK SC",
"Noto Sans SC",
"Noto Sans",
"Arial Unicode MS",
];
pub struct RasterDrawer {
scale: f32,
fb: Framebuffer,
fonts: Rc<FontStore>,
icons: RefCell<HashMap<usize, IconCacheEntry>>,
}
const ICON_CACHE_CAP: usize = 64;
impl std::fmt::Debug for RasterDrawer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RasterDrawer")
.field("scale", &self.scale)
.field("width", &self.fb.width())
.field("height", &self.fb.height())
.finish_non_exhaustive()
}
}
impl RasterDrawer {
pub fn new(scale: f32) -> Self {
Self::with_system_font(scale, None)
}
pub fn new_native(scale: f32) -> Self {
let fonts = shared_font_store(FontStoreKey::Native, || {
let mut db = cached_system_fonts_db();
let ui_family = crate::platform::current()
.system_menu_font()
.and_then(|sf| register_system_font(&mut db, sf.source))
.or_else(|| resolve_ui_family(&db));
FontStore::new(db, ui_family)
});
Self::from_shared(scale, fonts)
}
pub fn with_system_font(scale: f32, system: Option<SystemFont>) -> Self {
let mut db = cached_system_fonts_db();
let ui_family = system
.and_then(|sf| register_system_font(&mut db, sf.source))
.or_else(|| resolve_ui_family(&db));
Self::from_parts(scale, db, ui_family)
}
pub fn with_forced_theme(scale: f32, family: OsFamily) -> Self {
let fonts = shared_font_store(FontStoreKey::Forced(forced_family_tag(family)), || {
#[cfg(feature = "bundled-fonts")]
let (db, ui_family) = {
let mut db = cached_system_fonts_db();
let ui_family = resolve_forced_ui_family_bundled(&mut db, family);
(db, ui_family)
};
#[cfg(not(feature = "bundled-fonts"))]
let (db, ui_family) = {
let db = cached_system_fonts_db();
let ui_family = resolve_forced_ui_family(
&db,
family.ui_font_families(),
family.fallback_font_families(),
);
(db, ui_family)
};
FontStore::new(db, ui_family)
});
Self::from_shared(scale, fonts)
}
pub fn for_menu_options(scale: f32, options: &crate::theme::MenuOptions) -> Self {
match options.theme.forced_family() {
Some(family) => Self::with_forced_theme(scale, family),
None => Self::new_native(scale),
}
}
pub fn new_headless(scale: f32) -> Self {
const DEJAVU_SANS: &[u8] = include_bytes!("../../tests/fonts/DejaVuSans.ttf");
const DEJAVU_SANS_BOLD: &[u8] = include_bytes!("../../tests/fonts/DejaVuSans-Bold.ttf");
let mut db = Database::new();
db.load_font_data(DEJAVU_SANS.to_vec());
db.load_font_data(DEJAVU_SANS_BOLD.to_vec());
let ui_family = resolve_ui_family(&db).or_else(|| Some("DejaVu Sans".to_string()));
Self::from_parts(scale, db, ui_family)
}
fn from_parts(scale: f32, db: Database, ui_family: Option<String>) -> Self {
Self::from_shared(scale, Rc::new(FontStore::new(db, ui_family)))
}
fn from_shared(scale: f32, fonts: Rc<FontStore>) -> Self {
RasterDrawer {
scale: scale.max(0.1),
fb: Framebuffer::new(1, 1),
fonts,
icons: RefCell::new(HashMap::new()),
}
}
#[cfg(test)]
pub(crate) fn font_store_ptr(&self) -> usize {
Rc::as_ptr(&self.fonts) as usize
}
pub fn scale(&self) -> f32 {
self.scale
}
pub fn framebuffer(&self) -> &Framebuffer {
&self.fb
}
pub fn device_size(&self) -> (u32, u32) {
(self.fb.width(), self.fb.height())
}
pub fn encode_png(&self) -> Vec<u8> {
self.fb.encode_png()
}
#[cfg(test)]
pub(crate) fn shape_miss_count(&self) -> usize {
self.fonts.shape_misses.get()
}
#[cfg(test)]
pub(crate) fn weight_downgrade_count(&self) -> usize {
self.fonts.weight_downgrade_count()
}
#[cfg(test)]
pub(crate) fn owned_key_build_count(&self) -> usize {
self.fonts.owned_key_builds.get()
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
enum Embolden {
None,
Variable(u16),
Synthetic,
}
pub(crate) const WGHT_AXIS_TAG: u32 =
((b'w' as u32) << 24) | ((b'g' as u32) << 16) | ((b'h' as u32) << 8) | (b't' as u32);
pub(crate) const OPSZ_AXIS_TAG: u32 =
((b'o' as u32) << 24) | ((b'p' as u32) << 16) | ((b's' as u32) << 8) | (b'z' as u32);
struct ShapedGlyph {
face: FaceId,
glyph: u16,
emb: Embolden,
opsz: Option<f32>,
x: f32,
y: f32,
}
struct ShapedLine {
glyphs: Vec<ShapedGlyph>,
width: f32,
}
struct GlyphImage {
left: i32,
top: i32,
width: u32,
height: u32,
content: Content,
data: Vec<u8>,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
struct GlyphKey {
face: FaceId,
glyph: u16,
size_bits: u32,
emb: Embolden,
opsz_bits: u32,
}
struct FaceBytes {
data: Vec<u8>,
index: u32,
}
const GLYPH_CACHE_CAP: usize = 4096;
const SHAPED_CACHE_CAP: usize = 2048;
const COVERAGE_CACHE_CAP: usize = 4096;
const FALLBACK_CACHE_CAP: usize = 1024;
const EMBOLDEN_CACHE_CAP: usize = 256;
const FACE_CACHE_CAP: usize = 256;
const SHAPER_INSTANCE_CACHE_CAP: usize = 256;
struct FontStore {
db: Database,
ui_family: Option<String>,
face_data: RefCell<FxHashMap<FaceId, Rc<FaceBytes>>>,
glyphs: RefCell<FxHashMap<GlyphKey, Option<Rc<GlyphImage>>>>,
shaper_data: RefCell<FxHashMap<FaceId, Rc<ShaperData>>>,
shaper_instances: RefCell<FxHashMap<(FaceId, Embolden, u32), Rc<ShaperInstance>>>,
coverage: RefCell<FxHashMap<(FaceId, char), bool>>,
fallback_cache: RefCell<FxHashMap<(char, u16), Option<FaceId>>>,
embolden_cache: RefCell<FxHashMap<(FaceId, u16), Embolden>>,
face_cache: RefCell<FxHashMap<(FontFamily, u16), Option<FaceId>>>,
shaped: RefCell<FxHashMap<u64, (ShapeKey, Rc<ShapedLine>)>>,
v_metrics_cache: RefCell<FxHashMap<(FaceId, u32), (f32, f32)>>,
seg_scratch: RefCell<Vec<(FaceId, String)>>,
#[cfg(test)]
shape_misses: std::cell::Cell<usize>,
#[cfg(test)]
owned_key_builds: std::cell::Cell<usize>,
#[cfg(test)]
weight_downgrades: std::cell::Cell<usize>,
scale_ctx: RefCell<ScaleContext>,
fallback_order: Vec<FaceId>,
}
impl FontStore {
fn new(db: Database, ui_family: Option<String>) -> Self {
let fallback_order: Vec<FaceId> = db.faces().map(|f| f.id).collect();
FontStore {
db,
ui_family,
face_data: RefCell::new(FxHashMap::default()),
glyphs: RefCell::new(FxHashMap::default()),
shaper_data: RefCell::new(FxHashMap::default()),
shaper_instances: RefCell::new(FxHashMap::default()),
coverage: RefCell::new(FxHashMap::default()),
fallback_cache: RefCell::new(FxHashMap::default()),
embolden_cache: RefCell::new(FxHashMap::default()),
face_cache: RefCell::new(FxHashMap::default()),
shaped: RefCell::new(FxHashMap::default()),
v_metrics_cache: RefCell::new(FxHashMap::default()),
seg_scratch: RefCell::new(Vec::new()),
#[cfg(test)]
shape_misses: std::cell::Cell::new(0),
#[cfg(test)]
owned_key_builds: std::cell::Cell::new(0),
#[cfg(test)]
weight_downgrades: std::cell::Cell::new(0),
scale_ctx: RefCell::new(ScaleContext::new()),
fallback_order,
}
}
fn face_bytes(&self, id: FaceId) -> Option<Rc<FaceBytes>> {
if let Some(b) = self.face_data.borrow().get(&id) {
return Some(b.clone());
}
let bytes = self.db.with_face_data(id, |data, index| FaceBytes {
data: data.to_vec(),
index,
})?;
let rc = Rc::new(bytes);
self.face_data.borrow_mut().insert(id, rc.clone());
Some(rc)
}
fn db_family<'a>(&'a self, family: &'a FontFamily) -> DbFamily<'a> {
match family {
FontFamily::System => match &self.ui_family {
Some(name) => DbFamily::Name(name),
None => DbFamily::SansSerif,
},
FontFamily::SystemMono => DbFamily::Monospace,
FontFamily::Named(name) => DbFamily::Name(name),
}
}
fn resolve_face(&self, family: &FontFamily, ot_weight: u16) -> Option<FaceId> {
let key = (family.clone(), ot_weight);
if let Some(&cached) = self.face_cache.borrow().get(&key) {
return cached;
}
let result = query_face(&self.db, self.db_family(family), ot_weight);
#[cfg(test)]
if let Some(id) = result {
self.record_weight_downgrade(ot_weight, id);
}
let mut cache = self.face_cache.borrow_mut();
if cache.len() >= FACE_CACHE_CAP && !cache.contains_key(&key) {
cache.clear();
}
cache.insert(key, result);
result
}
const HEAVY_WEIGHT: u16 = 600;
const LIGHT_WEIGHT: u16 = 500;
const VARIABLE_BOLD_WEIGHT: u16 = 700;
fn face_embolden(&self, id: FaceId, ot_weight: u16) -> Embolden {
if ot_weight < Self::HEAVY_WEIGHT {
return Embolden::None;
}
let key = (id, ot_weight);
if let Some(&cached) = self.embolden_cache.borrow().get(&key) {
return cached;
}
let emb = match self.face_wght_axis_max(id) {
Some(max) => Embolden::Variable(Self::variable_bold_wght(ot_weight, max as u16)),
None => {
let actual_weight = self.db.face(id).map_or(ot_weight, |f| f.weight.0);
if actual_weight >= Self::LIGHT_WEIGHT {
Embolden::None
} else {
Embolden::Synthetic
}
}
};
let mut cache = self.embolden_cache.borrow_mut();
if cache.len() >= EMBOLDEN_CACHE_CAP && !cache.contains_key(&key) {
cache.clear();
}
cache.insert(key, emb);
emb
}
fn variable_bold_wght(ot_weight: u16, axis_max: u16) -> u16 {
ot_weight
.clamp(Self::HEAVY_WEIGHT, Self::VARIABLE_BOLD_WEIGHT)
.min(axis_max)
}
fn face_wght_axis_max(&self, id: FaceId) -> Option<f32> {
let bytes = self.face_bytes(id)?;
let font = FontRef::from_index(&bytes.data, bytes.index as usize)?;
font.variations()
.find_by_tag(WGHT_AXIS_TAG)
.map(|axis| axis.max_value())
}
fn face_opsz(&self, id: FaceId, points: f32) -> Option<f32> {
let bytes = self.face_bytes(id)?;
let font = FontRef::from_index(&bytes.data, bytes.index as usize)?;
font.variations()
.find_by_tag(OPSZ_AXIS_TAG)
.map(|axis| points.clamp(axis.min_value(), axis.max_value()))
}
fn face_debug(&self, id: FaceId) -> String {
let (families, db_weight, db_index) = self
.db
.face(id)
.map(|f| {
let fams = f
.families
.iter()
.map(|(n, _)| n.clone())
.collect::<Vec<_>>()
.join("/");
(fams, f.weight.0, f.index)
})
.unwrap_or_else(|| ("<none>".into(), 0, 0));
let axis = self
.face_bytes(id)
.and_then(|b| {
let font = FontRef::from_index(&b.data, b.index as usize)?;
let wght = font.variations().find_by_tag(WGHT_AXIS_TAG);
let n_instances = font.instances().count();
Some(match wght {
Some(a) => format!(
"wght[min={} def={} max={}] named_instances={n_instances}",
a.min_value(),
a.default_value(),
a.max_value(),
),
None => format!("wght[none] named_instances={n_instances}"),
})
})
.unwrap_or_else(|| "wght[unparsed]".into());
format!("families={families:?} db_index={db_index} db_weight={db_weight} {axis}")
}
#[cfg(test)]
fn record_weight_downgrade(&self, ot_weight: u16, resolved: FaceId) {
if ot_weight < Self::HEAVY_WEIGHT {
return;
}
let actual_weight = self.db.face(resolved).map_or(ot_weight, |f| f.weight.0);
if actual_weight < Self::LIGHT_WEIGHT {
self.weight_downgrades.set(self.weight_downgrades.get() + 1);
}
}
#[cfg(test)]
fn weight_downgrade_count(&self) -> usize {
self.weight_downgrades.get()
}
fn face_has_glyph(&self, id: FaceId, ch: char) -> bool {
if let Some(&hit) = self.coverage.borrow().get(&(id, ch)) {
return hit;
}
let hit = self
.face_bytes(id)
.and_then(|b| {
FontRef::from_index(&b.data, b.index as usize).map(|f| f.charmap().map(ch) != 0)
})
.unwrap_or(false);
let mut cache = self.coverage.borrow_mut();
if cache.len() >= COVERAGE_CACHE_CAP && !cache.contains_key(&(id, ch)) {
cache.clear();
}
cache.insert((id, ch), hit);
hit
}
fn fallback_face_for(&self, ch: char, ot_weight: u16) -> Option<FaceId> {
if let Some(&cached) = self.fallback_cache.borrow().get(&(ch, ot_weight)) {
return cached;
}
let result = 'find: {
for fam in FALLBACK_FAMILIES {
if let Some(id) = query_face(&self.db, DbFamily::Name(fam), ot_weight) {
if self.face_has_glyph(id, ch) {
break 'find Some(id);
}
}
}
self.fallback_order
.iter()
.copied()
.find(|&id| self.face_has_glyph(id, ch))
};
let mut cache = self.fallback_cache.borrow_mut();
if cache.len() >= FALLBACK_CACHE_CAP && !cache.contains_key(&(ch, ot_weight)) {
cache.clear();
}
cache.insert((ch, ot_weight), result);
result
}
#[cfg(test)]
fn segment_faces(
&self,
text: &str,
primary: Option<FaceId>,
ot_weight: u16,
) -> Vec<(FaceId, String)> {
let mut runs: Vec<(FaceId, String)> = Vec::new();
let used = self.segment_faces_into(text, primary, ot_weight, &mut runs);
runs.truncate(used);
runs
}
fn segment_faces_into(
&self,
text: &str,
primary: Option<FaceId>,
ot_weight: u16,
runs: &mut Vec<(FaceId, String)>,
) -> usize {
let mut used = 0usize;
for ch in text.chars() {
let face = match primary {
Some(p) if self.face_has_glyph(p, ch) => Some(p),
Some(p) => self.fallback_face_for(ch, ot_weight).or(Some(p)),
None => self.fallback_face_for(ch, ot_weight),
};
let Some(face) = face else { continue };
if used > 0 && runs[used - 1].0 == face {
runs[used - 1].1.push(ch);
continue;
}
if used < runs.len() {
let slot = &mut runs[used];
slot.0 = face;
slot.1.clear();
slot.1.push(ch);
} else {
let mut s = String::new();
s.push(ch);
runs.push((face, s));
}
used += 1;
}
used
}
fn shape(
&self,
text: &str,
primary: Option<FaceId>,
ot_weight: u16,
px: f32,
tracking: f32,
optical_size: Option<f32>,
) -> Rc<ShapedLine> {
let px_bits = px.to_bits();
let tracking_bits = tracking.to_bits();
let opsz_bits = optical_size.map(f32::to_bits).unwrap_or(0);
let hash = shape_key_hash(text, primary, ot_weight, px_bits, tracking_bits, opsz_bits);
if let Some((k, cached)) = self.shaped.borrow().get(&hash) {
if k.0 == text
&& k.1 == primary
&& k.2 == ot_weight
&& k.3 == px_bits
&& k.4 == tracking_bits
&& k.5 == opsz_bits
{
return Rc::clone(cached);
}
}
#[cfg(test)]
self.shape_misses.set(self.shape_misses.get() + 1);
let mut glyphs = Vec::new();
let mut pen = 0.0_f32;
let mut scratch = self.seg_scratch.borrow_mut();
let runs = &mut *scratch;
let used = self.segment_faces_into(text, primary, ot_weight, runs);
for (face_id, sub) in runs.iter().take(used) {
let face_id = *face_id;
let Some(bytes) = self.face_bytes(face_id) else {
continue;
};
let Ok(font) = HbFontRef::from_index(&bytes.data, bytes.index) else {
continue;
};
let shaper_data = {
let mut cache = self.shaper_data.borrow_mut();
Rc::clone(
cache
.entry(face_id)
.or_insert_with(|| Rc::new(ShaperData::new(&font))),
)
};
let emb = self.face_embolden(face_id, ot_weight);
let opsz = optical_size.and_then(|pts| self.face_opsz(face_id, pts));
let opsz_bits = opsz.map(f32::to_bits).unwrap_or(0);
let wght = if let Embolden::Variable(w) = emb {
Some(w as f32)
} else {
None
};
let instance: Option<Rc<ShaperInstance>> = if wght.is_some() || opsz.is_some() {
let key = (face_id, emb, opsz_bits);
let mut cache = self.shaper_instances.borrow_mut();
let inst = if let Some(inst) = cache.get(&key) {
Rc::clone(inst)
} else {
let mut vars: Vec<(&str, f32)> = Vec::with_capacity(2);
if let Some(w) = wght {
vars.push(("wght", w));
}
if let Some(o) = opsz {
vars.push(("opsz", o));
}
let inst = Rc::new(ShaperInstance::from_variations(&font, vars));
if cache.len() >= SHAPER_INSTANCE_CACHE_CAP {
cache.clear();
}
cache.insert(key, Rc::clone(&inst));
inst
};
Some(inst)
} else {
None
};
let shaper = shaper_data
.shaper(&font)
.instance(instance.as_deref())
.build();
let upem = shaper.units_per_em() as f32;
let s = if upem > 0.0 { px / upem } else { 0.0 };
let mut buffer = UnicodeBuffer::new();
buffer.push_str(sub);
buffer.guess_segment_properties();
let shaped = shaper.shape(buffer, ShapeOptions::default());
for (info, pos) in shaped
.glyph_infos()
.iter()
.zip(shaped.glyph_positions().iter())
{
glyphs.push(ShapedGlyph {
face: face_id,
glyph: info.glyph_id as u16,
emb,
opsz,
x: pen + pos.x_offset as f32 * s,
y: pos.y_offset as f32 * s,
});
pen += pos.x_advance as f32 * s + tracking;
}
}
drop(scratch);
let line = Rc::new(ShapedLine { glyphs, width: pen });
let key: ShapeKey = (
text.to_owned(),
primary,
ot_weight,
px_bits,
tracking_bits,
opsz_bits,
);
#[cfg(test)]
self.owned_key_builds.set(self.owned_key_builds.get() + 1);
let mut cache = self.shaped.borrow_mut();
if cache.len() >= SHAPED_CACHE_CAP && !cache.contains_key(&hash) {
if let Some(&victim) = cache.keys().next() {
cache.remove(&victim);
}
}
cache.insert(hash, (key, Rc::clone(&line)));
line
}
fn v_metrics(&self, primary: Option<FaceId>, px: f32) -> (f32, f32) {
let Some(id) = primary else {
return (px * 0.8, -px * 0.2);
};
let key = (id, px.to_bits());
if let Some(&cached) = self.v_metrics_cache.borrow().get(&key) {
return cached;
}
let metrics = self
.face_bytes(id)
.and_then(|bytes| {
let face = FontRef::from_index(&bytes.data, bytes.index as usize)?;
let m = face.metrics(&[]);
(m.units_per_em > 0).then(|| {
let sm = m.scale(px);
(sm.ascent, -sm.descent)
})
})
.unwrap_or((px * 0.8, -px * 0.2));
self.v_metrics_cache.borrow_mut().insert(key, metrics);
metrics
}
fn glyph_image(
&self,
face_id: FaceId,
glyph: u16,
px: f32,
emb: Embolden,
opsz: Option<f32>,
) -> Option<Rc<GlyphImage>> {
let key = GlyphKey {
face: face_id,
glyph,
size_bits: px.to_bits(),
emb,
opsz_bits: opsz.map(f32::to_bits).unwrap_or(0),
};
{
let cache = self.glyphs.borrow();
if let Some(cached) = cache.get(&key) {
return cached.clone();
}
}
let rendered = self.render_glyph(face_id, glyph, px, emb, opsz);
let mut cache = self.glyphs.borrow_mut();
if cache.len() >= GLYPH_CACHE_CAP && !cache.contains_key(&key) {
if let Some(&victim) = cache.keys().next() {
cache.remove(&victim);
}
}
cache.insert(key, rendered.clone());
rendered
}
fn render_glyph(
&self,
face_id: FaceId,
glyph: u16,
px: f32,
emb: Embolden,
opsz: Option<f32>,
) -> Option<Rc<GlyphImage>> {
let bytes = self.face_bytes(face_id)?;
let font = FontRef::from_index(&bytes.data, bytes.index as usize)?;
let mut ctx = self.scale_ctx.borrow_mut();
let mut builder = ctx.builder(font).size(px).hint(false);
let mut vars: Vec<(&str, f32)> = Vec::with_capacity(2);
if let Embolden::Variable(w) = emb {
vars.push(("wght", w as f32));
}
if let Some(o) = opsz {
vars.push(("opsz", o));
}
if !vars.is_empty() {
builder = builder.variations(vars);
}
let mut scaler = builder.build();
let image = Render::new(&[
Source::ColorOutline(0),
Source::ColorBitmap(StrikeWith::BestFit),
Source::Outline,
])
.render(&mut scaler, glyph as GlyphId)?;
if image.placement.width == 0 || image.placement.height == 0 {
return None;
}
let mut glyph_image = GlyphImage {
left: image.placement.left,
top: image.placement.top,
width: image.placement.width,
height: image.placement.height,
content: image.content,
data: image.data,
};
if emb == Embolden::Synthetic {
embolden_mask(&mut glyph_image);
}
Some(Rc::new(glyph_image))
}
}
fn debug_text_enabled() -> bool {
static FLAG: OnceLock<bool> = OnceLock::new();
*FLAG.get_or_init(|| std::env::var_os("MURI_DEBUG_TEXT").is_some())
}
fn shape_key_hash(
text: &str,
primary: Option<FaceId>,
ot_weight: u16,
px_bits: u32,
tracking_bits: u32,
opsz_bits: u32,
) -> u64 {
use std::hash::Hash;
let mut h = FxHasher::default();
text.hash(&mut h);
primary.hash(&mut h);
ot_weight.hash(&mut h);
px_bits.hash(&mut h);
tracking_bits.hash(&mut h);
opsz_bits.hash(&mut h);
h.finish()
}
fn query_face(db: &Database, family: DbFamily, ot_weight: u16) -> Option<FaceId> {
db.query(&Query {
families: &[family],
weight: DbWeight(ot_weight),
stretch: fontdb::Stretch::Normal,
style: DbStyle::Normal,
})
}
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
enum FontStoreKey {
Native,
Forced(u8),
}
fn forced_family_tag(family: OsFamily) -> u8 {
match family {
OsFamily::MacOs => 0,
OsFamily::Windows => 1,
OsFamily::Gnome => 2,
}
}
fn shared_font_store(key: FontStoreKey, build: impl FnOnce() -> FontStore) -> Rc<FontStore> {
thread_local! {
static SHARED_FONT_STORES: RefCell<HashMap<FontStoreKey, Rc<FontStore>>> =
RefCell::new(HashMap::new());
}
SHARED_FONT_STORES.with(|cell| {
if let Some(fs) = cell.borrow().get(&key) {
return Rc::clone(fs);
}
let fs = Rc::new(build());
cell.borrow_mut().insert(key, Rc::clone(&fs));
fs
})
}
fn system_fonts_db() -> &'static Database {
static SYSTEM_FONTS_DB: OnceLock<Database> = OnceLock::new();
SYSTEM_FONTS_DB.get_or_init(|| {
let mut db = Database::new();
db.load_system_fonts();
db
})
}
fn cached_system_fonts_db() -> Database {
system_fonts_db().clone()
}
pub(crate) fn prewarm_system_fonts() {
use std::sync::atomic::{AtomicBool, Ordering};
static STARTED: AtomicBool = AtomicBool::new(false);
if STARTED.swap(true, Ordering::Relaxed) {
return;
}
std::thread::spawn(|| {
let _ = system_fonts_db();
});
}
fn register_system_font(db: &mut Database, source: SystemFontSource) -> Option<String> {
let loaded: Option<FaceId> = match source {
SystemFontSource::Family(name) => {
return query_face(db, DbFamily::Name(&name), 400).map(|_| name);
}
SystemFontSource::Path(path) => db.load_font_source(DbSource::File(path)).first().copied(),
SystemFontSource::Data(data) => {
if let Some((regular, bold)) = unpack_dual_face(&data) {
return register_dual_face(db, regular, bold);
}
db.load_font_source(DbSource::Binary(Arc::new(data)))
.first()
.copied()
}
};
let name = db
.face(loaded?)
.and_then(|f| f.families.first().map(|(n, _)| n.clone()))?;
query_face(db, DbFamily::Name(&name), 400).map(|_| name)
}
pub(crate) const DUAL_FACE_MAGIC: &[u8; 8] = b"MURIDUOF";
fn unpack_dual_face(data: &[u8]) -> Option<(&[u8], &[u8])> {
let rest = data.strip_prefix(DUAL_FACE_MAGIC.as_slice())?;
let (len_bytes, rest) = rest.split_first_chunk::<4>()?;
let regular_len = u32::from_le_bytes(*len_bytes) as usize;
if regular_len > rest.len() {
return None;
}
let (regular, bold) = rest.split_at(regular_len);
if bold.is_empty() {
return None;
}
Some((regular, bold))
}
fn register_dual_face(db: &mut Database, regular: &[u8], bold: &[u8]) -> Option<String> {
let regular_id = db
.load_font_source(DbSource::Binary(Arc::new(regular.to_vec())))
.first()
.copied()?;
let name = db
.face(regular_id)
.and_then(|f| f.families.first().map(|(n, _)| n.clone()))?;
query_face(db, DbFamily::Name(&name), 400)?;
db.load_font_source(DbSource::Binary(Arc::new(bold.to_vec())));
Some(name)
}
#[cfg_attr(feature = "bundled-fonts", allow(dead_code))]
pub(crate) fn resolve_forced_ui_family(
db: &Database,
target_families: &[&str],
fallback_families: &[&str],
) -> Option<String> {
first_installed_family(db, target_families)
.or_else(|| first_installed_family(db, fallback_families))
}
fn first_installed_family(db: &Database, families: &[&str]) -> Option<String> {
families
.iter()
.find(|name| query_face(db, DbFamily::Name(name), 400).is_some())
.map(|name| name.to_string())
}
#[cfg(feature = "bundled-fonts")]
pub(crate) fn resolve_forced_ui_family_bundled(
db: &mut Database,
family: OsFamily,
) -> Option<String> {
first_installed_family(db, family.ui_font_families())
.or_else(|| bundled_fonts::register_substitute(db, family))
.or_else(|| first_installed_family(db, family.fallback_font_families()))
}
#[cfg(feature = "bundled-fonts")]
pub(crate) mod bundled_fonts {
use super::{query_face, Arc, Database, DbFamily, DbSource};
use crate::theme::OsFamily;
const INTER_REGULAR: &[u8] = include_bytes!("../../assets/fonts/inter/Inter-Regular.ttf");
const INTER_SEMIBOLD: &[u8] = include_bytes!("../../assets/fonts/inter/Inter-SemiBold.ttf");
const INTER_BOLD: &[u8] = include_bytes!("../../assets/fonts/inter/Inter-Bold.ttf");
const SELAWIK_REGULAR: &[u8] = include_bytes!("../../assets/fonts/selawik/Selawik-Regular.ttf");
const SELAWIK_SEMIBOLD: &[u8] =
include_bytes!("../../assets/fonts/selawik/Selawik-SemiBold.ttf");
const SELAWIK_BOLD: &[u8] = include_bytes!("../../assets/fonts/selawik/Selawik-Bold.ttf");
const CANTARELL_REGULAR: &[u8] =
include_bytes!("../../assets/fonts/cantarell/Cantarell-Regular.ttf");
const CANTARELL_BOLD: &[u8] = include_bytes!("../../assets/fonts/cantarell/Cantarell-Bold.ttf");
fn substitute(family: OsFamily) -> (&'static str, &'static [&'static [u8]]) {
match family {
OsFamily::MacOs => ("Inter", &[INTER_REGULAR, INTER_SEMIBOLD, INTER_BOLD]),
OsFamily::Windows => (
"Selawik",
&[SELAWIK_REGULAR, SELAWIK_SEMIBOLD, SELAWIK_BOLD],
),
OsFamily::Gnome => ("Cantarell", &[CANTARELL_REGULAR, CANTARELL_BOLD]),
}
}
pub(crate) fn register_substitute(db: &mut Database, family: OsFamily) -> Option<String> {
let (name, faces) = substitute(family);
for face in faces {
db.load_font_source(DbSource::Binary(Arc::new(face.to_vec())));
}
query_face(db, DbFamily::Name(name), 400).map(|_| name.to_string())
}
}
fn resolve_ui_family(db: &Database) -> Option<String> {
for cand in [
".SF NS",
"SF Pro Text",
"SF Pro",
"Segoe UI",
"Helvetica Neue",
"Arial",
"DejaVu Sans",
"Liberation Sans",
] {
if family_shapes_both_weights(db, cand) {
return Some(cand.to_string());
}
}
None
}
fn family_shapes_both_weights(db: &Database, name: &str) -> bool {
let regular = query_face(db, DbFamily::Name(name), 400);
let bold = query_face(db, DbFamily::Name(name), 700);
match (regular, bold) {
(Some(r), Some(b)) => {
r != b
&& face_in_family(db, r, name)
&& face_in_family(db, b, name)
&& db
.face(b)
.map(|f| f.weight >= DbWeight(600))
.unwrap_or(false)
&& face_can_shape(db, r)
&& face_can_shape(db, b)
}
_ => false,
}
}
fn face_in_family(db: &Database, id: FaceId, name: &str) -> bool {
db.face(id)
.map(|f| f.families.iter().any(|(n, _)| n == name))
.unwrap_or(false)
}
fn face_can_shape(db: &Database, id: FaceId) -> bool {
db.with_face_data(id, |data, index| {
FontRef::from_index(data, index as usize)
.map(|f| {
let cmap = f.charmap();
"Agy0".chars().all(|c| cmap.map(c) != 0)
})
.unwrap_or(false)
})
.unwrap_or(false)
}
impl SceneDrawer for RasterDrawer {
fn begin_frame(&mut self, size: LogicalSize) {
let w = ((size.width * self.scale).round() as u32).max(1);
let h = ((size.height * self.scale).round() as u32).max(1);
self.fb.reset(w, h);
}
fn fill_round_rect(&mut self, rect: LogicalRect, corner_radius: f32, color: Rgba) {
let s = self.scale;
let (x, y, w, h) = raster::scaled(rect, s);
raster::fill_round_rect(&mut self.fb, x, y, w, h, corner_radius * s, color);
}
fn draw_separator(&mut self, rect: LogicalRect, color: Rgba) {
let s = self.scale;
let x = rect.origin.x * s;
let y = (rect.origin.y * s).round();
let w = rect.size.width * s;
let h = (rect.size.height * s).max(1.0);
raster::fill_rect(&mut self.fb, x, y, w, h, color);
}
fn measure_text(&self, text: &str, font: &Font) -> f32 {
if text.is_empty() {
return 0.0;
}
let ot_weight = font.weight.ot_weight();
let primary = self.fonts.resolve_face(&font.family, ot_weight);
self.fonts
.shape(
text,
primary,
ot_weight,
font.size,
font.letter_spacing,
font.optical_size,
)
.width
}
fn line_height(&self, font: &Font) -> f32 {
font.size * LINE_HEIGHT_FACTOR
}
fn draw_text(&mut self, run: &TextRun<'_>) {
if run.text.is_empty() {
return;
}
let scale = self.scale;
let px = run.font.size * scale;
let ox = run.origin.x * scale;
let oy = run.origin.y * scale;
let ot_weight = run.weight.ot_weight();
let primary = self.fonts.resolve_face(&run.font.family, ot_weight);
let tracking = run.font.letter_spacing * scale;
if debug_text_enabled() {
let emb = primary.map(|f| self.fonts.face_embolden(f, ot_weight));
let face_dbg = primary
.map(|f| self.fonts.face_debug(f))
.unwrap_or_default();
eprintln!(
"MURI_TEXT text={:?} weight={ot_weight} letter_spacing={} tracking={tracking} \
face={primary:?} embolden={emb:?} px={px} [{face_dbg}]",
run.text, run.font.letter_spacing,
);
}
let shaped = self.fonts.shape(
run.text,
primary,
ot_weight,
px,
tracking,
run.font.optical_size,
);
let (ascent, descent) = self.fonts.v_metrics(primary, px);
let line_height = px * LINE_HEIGHT_FACTOR;
let baseline = ascent + (line_height - (ascent - descent)) / 2.0;
let color = run.color;
let (pw, ph) = (self.fb.width() as i32, self.fb.height() as i32);
if debug_text_enabled() {
if let Some(g0) = shaped.glyphs.first() {
eprintln!(
"MURI_RASTER text={:?} glyphs={} first_face={:?} first_emb={:?} \
first_opsz={:?} px={px}",
run.text,
shaped.glyphs.len(),
g0.face,
g0.emb,
g0.opsz,
);
}
}
for g in &shaped.glyphs {
let Some(image) = self.fonts.glyph_image(g.face, g.glyph, px, g.emb, g.opsz) else {
continue;
};
let gx = (ox + g.x).round() as i32 + image.left;
let gy = (oy + baseline - g.y).round() as i32 - image.top;
let pixels = self.fb.pixels_mut();
blit_glyph(pixels, pw, ph, &image, gx, gy, color);
}
}
fn draw_image(&mut self, rgba: &[u8], src_w: u32, src_h: u32, dest: LogicalRect) {
self.draw_image_alpha(rgba, src_w, src_h, dest, 1.0);
}
fn draw_image_alpha(
&mut self,
rgba: &[u8],
src_w: u32,
src_h: u32,
dest: LogicalRect,
alpha: f32,
) {
if src_w == 0 || src_h == 0 {
return;
}
let alpha = alpha.clamp(0.0, 1.0);
let s = self.scale;
let dx = (dest.origin.x * s).round() as i32;
let dy = (dest.origin.y * s).round() as i32;
let dw = (dest.size.width * s).round().max(1.0) as i32;
let dh = (dest.size.height * s).round().max(1.0) as i32;
let (pw, ph) = (self.fb.width() as i32, self.fb.height() as i32);
let pixels = self.fb.pixels_mut();
for row in 0..dh {
for col in 0..dw {
let sx = (col * src_w as i32 / dw).clamp(0, src_w as i32 - 1);
let sy = (row * src_h as i32 / dh).clamp(0, src_h as i32 - 1);
let idx = ((sy * src_w as i32 + sx) * 4) as usize;
if idx + 3 >= rgba.len() {
continue;
}
let a = (rgba[idx + 3] as f32 * alpha).round() as u8;
if a == 0 {
continue;
}
let px = dx + col;
let py = dy + row;
if px < 0 || py < 0 || px >= pw || py >= ph {
continue;
}
raster::blend_pixel(
pixels,
((py * pw + px) * 4) as usize,
Rgba::new(rgba[idx], rgba[idx + 1], rgba[idx + 2], 255),
a,
);
}
}
}
fn decode_icon(&self, bytes: &Arc<[u8]>) -> Option<DecodedIcon> {
let key = Arc::as_ptr(bytes) as *const u8 as usize;
if let Some(hit) = icon_cache_hit(self.icons.borrow().get(&key), bytes) {
return Some(hit);
}
let decoded = Rc::new(decode_icon_bytes(bytes)?);
let mut cache = self.icons.borrow_mut();
if cache.len() >= ICON_CACHE_CAP && !cache.contains_key(&key) {
cache.clear();
}
cache.insert(key, (bytes.clone(), decoded.clone()));
Some(decoded)
}
}
fn icon_cache_hit(entry: Option<&IconCacheEntry>, bytes: &Arc<[u8]>) -> Option<DecodedIcon> {
match entry {
Some((cached_bytes, hit)) if Arc::ptr_eq(cached_bytes, bytes) => Some(hit.clone()),
_ => None,
}
}
fn embolden_mask(image: &mut GlyphImage) {
if !matches!(image.content, Content::Mask | Content::SubpixelMask) {
return;
}
let w = image.width as usize;
let h = image.height as usize;
if w == 0 || h == 0 {
return;
}
let new_w = w + 1;
let mut out = vec![0u8; new_w * h];
for row in 0..h {
let src = &image.data[row * w..row * w + w];
let dst = &mut out[row * new_w..row * new_w + new_w];
for col in 0..new_w {
let here = if col < w { src[col] } else { 0 };
let left = if col > 0 { src[col - 1] } else { 0 };
dst[col] = here.max(left);
}
}
image.data = out;
image.width = new_w as u32;
}
fn blit_glyph(
pixels: &mut [u8],
pw: i32,
ph: i32,
image: &GlyphImage,
gx: i32,
gy: i32,
color: Rgba,
) {
let iw = image.width as i32;
let ih = image.height as i32;
let col0 = (-gx).max(0);
let col1 = iw.min(pw - gx);
let row0 = (-gy).max(0);
let row1 = ih.min(ph - gy);
if col0 >= col1 || row0 >= row1 {
return;
}
match image.content {
Content::Mask | Content::SubpixelMask => {
let fg_lum = raster::fg_luma(color);
for row in row0..row1 {
let src_row = (row * iw) as usize;
let mut off = (((gy + row) * pw + (gx + col0)) * 4) as usize;
for col in col0..col1 {
let cov = image.data[src_row + col as usize];
if cov != 0 {
let cov = raster::smooth_glyph_coverage_fg_lum(cov, fg_lum, pixels, off);
let a = (cov as u16 * color.a as u16 / 255) as u8;
raster::blend_pixel(pixels, off, color, a);
}
off += 4;
}
}
}
Content::Color => {
for row in row0..row1 {
let src_row = (row * iw) as usize;
let mut off = (((gy + row) * pw + (gx + col0)) * 4) as usize;
for col in col0..col1 {
let idx = (src_row + col as usize) * 4;
let a = image.data[idx + 3];
if a != 0 {
let (r, g, b) = (image.data[idx], image.data[idx + 1], image.data[idx + 2]);
raster::blend_pixel(pixels, off, Rgba::new(r, g, b, 255), a);
}
off += 4;
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::style::FontFamily;
#[test]
fn letter_spacing_widens_measured_text_proportionally() {
let drawer = RasterDrawer::new_headless(1.0);
let base = Font::system(13.0, crate::style::Weight::Regular);
let tracked = base.clone().with_letter_spacing(3.0);
let w0 = drawer.measure_text("Quit", &base);
let w1 = drawer.measure_text("Quit", &tracked);
assert!(w0 > 0.0);
let delta = w1 - w0;
assert!(
(delta - 12.0).abs() < 2.0,
"expected ~12pt wider with 3pt tracking over 4 glyphs, got {delta}"
);
let tight = drawer.measure_text("Quit", &base.clone().with_letter_spacing(-1.0));
assert!(
tight < w0,
"negative tracking must tighten: {tight} !< {w0}"
);
}
#[test]
fn variable_bold_wght_never_exceeds_a_low_axis_max() {
assert_eq!(FontStore::variable_bold_wght(700, 500), 500);
assert_eq!(FontStore::variable_bold_wght(700, 400), 400);
assert_eq!(FontStore::variable_bold_wght(700, 650), 650);
assert_eq!(FontStore::variable_bold_wght(700, 900), 700);
assert_eq!(FontStore::variable_bold_wght(800, 1000), 700);
}
#[test]
fn embolden_cache_clears_when_it_exceeds_its_cap() {
let d = RasterDrawer::new_headless(1.0);
let id = d.fonts.resolve_face(&FontFamily::System, 400).unwrap();
for w in 0..=(EMBOLDEN_CACHE_CAP as u16) {
let _ = d.fonts.face_embolden(id, FontStore::HEAVY_WEIGHT + w);
}
let len = d.fonts.embolden_cache.borrow().len();
assert!(
len <= EMBOLDEN_CACHE_CAP,
"embolden cache must clear on overflow, got {len} > {EMBOLDEN_CACHE_CAP}"
);
assert!(len >= 1, "it keeps caching after the clear");
}
#[test]
fn font_store_is_reused_across_drawers_of_the_same_config() {
let d1 = RasterDrawer::with_forced_theme(1.0, OsFamily::Windows);
let d2 = RasterDrawer::with_forced_theme(2.0, OsFamily::Windows);
assert_eq!(
d1.font_store_ptr(),
d2.font_store_ptr(),
"same-config drawers must share one built FontStore, not rebuild it per open"
);
let key = FontStoreKey::Forced(forced_family_tag(OsFamily::Windows));
let reused = shared_font_store(key, || {
panic!("FontStore must not be rebuilt for a cached key")
});
assert_eq!(Rc::as_ptr(&reused) as usize, d1.font_store_ptr());
}
#[test]
fn shape_cache_hit_does_not_build_a_new_owned_key() {
let d = RasterDrawer::new_headless(1.0);
let font = Font::system(13.0, crate::style::Weight::Regular);
assert_eq!(d.owned_key_build_count(), 0);
let w1 = d.measure_text("Reuse", &font);
assert!(w1 > 0.0);
assert_eq!(
d.owned_key_build_count(),
1,
"the first (miss) measure builds exactly one owned key"
);
let w2 = d.measure_text("Reuse", &font);
assert_eq!(w1, w2, "the cached measure must be identical");
assert_eq!(
d.owned_key_build_count(),
1,
"a cache hit must not build (allocate) a new owned key"
);
assert_eq!(d.shape_miss_count(), 1, "and it must not re-shape either");
}
#[test]
fn register_system_font_pins_present_family_data_and_falls_back() {
const DEJAVU: &[u8] = include_bytes!("../../tests/fonts/DejaVuSans.ttf");
let mut db = Database::new();
db.load_font_data(DEJAVU.to_vec());
assert_eq!(
register_system_font(&mut db, SystemFontSource::Family("DejaVu Sans".into()))
.as_deref(),
Some("DejaVu Sans")
);
assert!(register_system_font(
&mut db,
SystemFontSource::Family("No Such Family 9x".into())
)
.is_none());
assert_eq!(
register_system_font(&mut db, SystemFontSource::Data(DEJAVU.to_vec())).as_deref(),
Some("DejaVu Sans")
);
}
#[test]
fn register_system_font_unpacks_a_dual_face_blob_into_both_weights() {
const DEJAVU: &[u8] = include_bytes!("../../tests/fonts/DejaVuSans.ttf");
const DEJAVU_BOLD: &[u8] = include_bytes!("../../tests/fonts/DejaVuSans-Bold.ttf");
let mut db = Database::new();
let mut bytes = Vec::new();
bytes.extend_from_slice(DUAL_FACE_MAGIC);
bytes.extend_from_slice(&(DEJAVU.len() as u32).to_le_bytes());
bytes.extend_from_slice(DEJAVU);
bytes.extend_from_slice(DEJAVU_BOLD);
let name = register_system_font(&mut db, SystemFontSource::Data(bytes));
assert_eq!(name.as_deref(), Some("DejaVu Sans"));
let regular = query_face(&db, DbFamily::Name("DejaVu Sans"), 400);
let bold = query_face(&db, DbFamily::Name("DejaVu Sans"), 700);
assert!(regular.is_some() && bold.is_some());
assert_ne!(
regular, bold,
"a packed bold face must resolve as a distinct face from regular"
);
assert!(db.face(bold.unwrap()).unwrap().weight >= DbWeight(600));
}
#[test]
fn register_system_font_treats_a_plain_data_blob_as_single_face() {
const DEJAVU: &[u8] = include_bytes!("../../tests/fonts/DejaVuSans.ttf");
let mut db = Database::new();
assert_eq!(
register_system_font(&mut db, SystemFontSource::Data(DEJAVU.to_vec())).as_deref(),
Some("DejaVu Sans")
);
}
#[test]
fn resolve_face_picks_the_bold_face_when_one_is_registered() {
let d = RasterDrawer::new_headless(1.0);
let regular = d.fonts.resolve_face(&FontFamily::System, 400);
let bold = d.fonts.resolve_face(&FontFamily::System, 700);
assert!(regular.is_some(), "headless db must resolve a regular face");
assert!(bold.is_some(), "headless db must resolve a bold face");
assert_ne!(
regular, bold,
"bold must resolve to a face distinct from regular, not degrade to it (#56)"
);
assert_eq!(
d.weight_downgrade_count(),
0,
"a real bold face is available — this must not count as a downgrade"
);
}
#[test]
fn resolve_face_records_a_downgrade_when_no_bold_face_is_available() {
const DEJAVU: &[u8] = include_bytes!("../../tests/fonts/DejaVuSans.ttf");
let mut db = Database::new();
db.load_font_data(DEJAVU.to_vec());
let d = RasterDrawer::from_parts(1.0, db, Some("DejaVu Sans".to_string()));
let regular = d.fonts.resolve_face(&FontFamily::System, 400);
assert!(regular.is_some());
assert_eq!(
d.weight_downgrade_count(),
0,
"a regular-weight request is never a downgrade"
);
let bold_request = d.fonts.resolve_face(&FontFamily::System, 700);
assert_eq!(
bold_request, regular,
"with no bold face, fontdb's best-match falls back to the only face"
);
assert_eq!(
d.weight_downgrade_count(),
1,
"requesting bold with no bold face registered must be recorded, never silent (#56)"
);
let _ = d.fonts.resolve_face(&FontFamily::System, 700);
assert_eq!(d.weight_downgrade_count(), 1);
}
#[cfg(test)]
fn run_ink(
d: &RasterDrawer,
primary: Option<FaceId>,
text: &str,
ot_weight: u16,
px: f32,
) -> u64 {
let line = d.fonts.shape(text, primary, ot_weight, px, 0.0, None);
let mut sum = 0u64;
for g in &line.glyphs {
if let Some(img) = d.fonts.glyph_image(g.face, g.glyph, px, g.emb, g.opsz) {
if matches!(img.content, Content::Mask | Content::SubpixelMask) {
sum += img.data.iter().map(|&b| b as u64).sum::<u64>();
}
}
}
sum
}
#[test]
fn single_static_face_synthesizes_bold_when_no_wght_axis() {
const DEJAVU: &[u8] = include_bytes!("../../tests/fonts/DejaVuSans.ttf");
let mut db = Database::new();
db.load_font_data(DEJAVU.to_vec());
let d = RasterDrawer::from_parts(1.0, db, Some("DejaVu Sans".to_string()));
let regular = d.fonts.resolve_face(&FontFamily::System, 400).unwrap();
let bold = d.fonts.resolve_face(&FontFamily::System, 700).unwrap();
assert_eq!(
regular, bold,
"one static face: fontdb resolves bold back to the only face"
);
assert_eq!(
d.fonts.face_embolden(bold, 700),
Embolden::Synthetic,
"a static face with no wght axis must synthesize bold (faux-bold), #63"
);
let px = 32.0;
let reg_ink = run_ink(&d, Some(regular), "Bold", 400, px);
let bold_ink = run_ink(&d, Some(regular), "Bold", 700, px);
assert!(
bold_ink > reg_ink,
"#63: synthesized bold must ink heavier than regular (bold {bold_ink} vs regular {reg_ink})"
);
}
#[test]
fn native_sf_variable_font_renders_bold_via_wght_instancing() {
const SFNS: &str = "/System/Library/Fonts/SFNS.ttf";
if !std::path::Path::new(SFNS).exists() {
return; }
let mut db = Database::new();
let ids = db.load_font_source(DbSource::File(std::path::PathBuf::from(SFNS)));
let Some(&fid) = ids.first() else {
return;
};
let Some(family) = db
.face(fid)
.and_then(|f| f.families.first().map(|(n, _)| n.clone()))
else {
return;
};
let d = RasterDrawer::from_parts(2.0, db, Some(family));
let regular = d.fonts.resolve_face(&FontFamily::System, 400).unwrap();
let bold = d.fonts.resolve_face(&FontFamily::System, 700).unwrap();
assert_eq!(
regular, bold,
"SFNS is one face; a bold request resolves to the same variable file"
);
let emb = d.fonts.face_embolden(bold, 700);
assert!(
matches!(emb, Embolden::Variable(_)),
"SFNS exposes a wght axis, so bold must be a variable-weight instance, not synthetic or downgraded: got {emb:?}"
);
let px = 30.0;
let reg_ink = run_ink(&d, Some(regular), "Bold", 400, px);
let bold_ink = run_ink(&d, Some(regular), "Bold", 700, px);
assert!(
bold_ink > reg_ink,
"#63: SF variable bold must ink heavier than regular on the live System path \
(bold {bold_ink} vs regular {reg_ink})"
);
}
#[test]
fn variable_face_resolves_bold_via_wght_instancing_not_downgrade() {
const VAR: &[u8] = include_bytes!("../../tests/fonts/variable-wght-test.ttf");
let mut db = Database::new();
db.load_font_data(VAR.to_vec());
let d = RasterDrawer::from_parts(1.0, db, Some("Muri Var Test".to_string()));
let regular = d.fonts.resolve_face(&FontFamily::System, 400).unwrap();
let bold = d.fonts.resolve_face(&FontFamily::System, 700).unwrap();
assert_eq!(
regular, bold,
"one variable face: a bold request resolves to the same master"
);
let emb = d.fonts.face_embolden(bold, 700);
assert!(
matches!(emb, Embolden::Variable(_)),
"a variable face with a wght axis must instance the axis for bold, \
not downgrade or synthesize (#65): got {emb:?}"
);
}
#[test]
fn variable_face_with_heavy_default_still_instances_bold() {
const VAR: &[u8] = include_bytes!("../../tests/fonts/variable-wght-heavy-test.ttf");
let mut db = Database::new();
db.load_font_data(VAR.to_vec());
let d = RasterDrawer::from_parts(1.0, db, Some("Muri Var Heavy".to_string()));
let bold = d.fonts.resolve_face(&FontFamily::System, 700).unwrap();
let emb = d.fonts.face_embolden(bold, 700);
assert!(
matches!(emb, Embolden::Variable(_)),
"a variable face with a heavy default weight must still instance bold (#65): got {emb:?}"
);
}
#[test]
fn optical_size_clamps_into_the_faces_opsz_range() {
const VAR: &[u8] = include_bytes!("../../tests/fonts/variable-opsz-test.ttf");
let mut db = Database::new();
db.load_font_data(VAR.to_vec());
let d = RasterDrawer::from_parts(1.0, db, Some("Muri Var Test".to_string()));
let face = d.fonts.resolve_face(&FontFamily::System, 400).unwrap();
assert_eq!(d.fonts.face_opsz(face, 13.0), Some(17.0));
assert_eq!(d.fonts.face_opsz(face, 50.0), Some(50.0));
assert_eq!(d.fonts.face_opsz(face, 200.0), Some(96.0));
}
#[test]
fn face_without_opsz_axis_yields_no_optical_size() {
let d = RasterDrawer::new_headless(1.0);
let face = d.fonts.resolve_face(&FontFamily::System, 400).unwrap();
assert_eq!(d.fonts.face_opsz(face, 13.0), None);
}
#[test]
fn segment_faces_keeps_primary_face_when_no_fallback_face_covers_the_gap() {
let d = RasterDrawer::new_headless(1.0);
let primary = d.fonts.resolve_face(&FontFamily::System, 400);
assert!(primary.is_some(), "headless db must resolve a UI family");
let runs = d.fonts.segment_faces("a\u{1F600}b", primary, 400);
assert_eq!(runs.len(), 1, "expected one merged run, got {runs:?}");
assert_eq!(runs[0].0, primary.unwrap());
assert_eq!(runs[0].1, "a\u{1F600}b");
}
#[test]
fn segment_faces_drops_uncovered_codepoints_when_no_primary_face_is_pinned() {
let d = RasterDrawer::new_headless(1.0);
let runs = d.fonts.segment_faces("a\u{1F600}b", None, 400);
assert_eq!(runs.len(), 1, "expected one merged run, got {runs:?}");
assert_eq!(runs[0].1, "ab");
}
#[test]
fn segment_faces_keeps_one_run_when_the_primary_face_covers_everything() {
let d = RasterDrawer::new_headless(1.0);
let primary = d.fonts.resolve_face(&FontFamily::System, 400);
assert!(primary.is_some());
let runs = d.fonts.segment_faces("hello", primary, 400);
assert_eq!(runs.len(), 1);
assert_eq!(runs[0].0, primary.unwrap());
assert_eq!(runs[0].1, "hello");
}
#[test]
fn segment_faces_uses_fallback_order_when_no_primary_face_is_given() {
let d = RasterDrawer::new_headless(1.0);
let runs = d.fonts.segment_faces("hi", None, 400);
assert_eq!(runs.len(), 1, "expected one run, got {runs:?}");
assert_eq!(runs[0].1, "hi");
}
fn dejavu_only_db() -> Database {
const DEJAVU_SANS: &[u8] = include_bytes!("../../tests/fonts/DejaVuSans.ttf");
const DEJAVU_SANS_BOLD: &[u8] = include_bytes!("../../tests/fonts/DejaVuSans-Bold.ttf");
let mut db = Database::new();
db.load_font_data(DEJAVU_SANS.to_vec());
db.load_font_data(DEJAVU_SANS_BOLD.to_vec());
db
}
#[test]
fn resolve_forced_ui_family_picks_the_target_family_when_installed() {
let db = dejavu_only_db();
let resolved = resolve_forced_ui_family(&db, &["DejaVu Sans"], &["Liberation Sans"]);
assert_eq!(resolved.as_deref(), Some("DejaVu Sans"));
}
#[test]
fn resolve_forced_ui_family_falls_back_to_the_free_face_when_target_is_absent() {
let db = dejavu_only_db();
let resolved =
resolve_forced_ui_family(&db, &["Segoe UI"], &["DejaVu Sans", "Liberation Sans"]);
assert_eq!(resolved.as_deref(), Some("DejaVu Sans"));
}
#[test]
fn resolve_forced_ui_family_never_falls_back_to_the_host_ui_family() {
let db = dejavu_only_db();
assert_eq!(resolve_ui_family(&db).as_deref(), Some("DejaVu Sans"));
let resolved = resolve_forced_ui_family(&db, &["Segoe UI"], &["Nonexistent Free Face"]);
assert_eq!(resolved, None);
assert_ne!(resolved, resolve_ui_family(&db));
}
#[test]
fn os_family_target_lists_are_tried_before_their_fallbacks() {
let db = dejavu_only_db();
for family in [OsFamily::MacOs, OsFamily::Windows, OsFamily::Gnome] {
let resolved = resolve_forced_ui_family(
&db,
family.ui_font_families(),
family.fallback_font_families(),
);
assert_eq!(
resolved.as_deref(),
Some("DejaVu Sans"),
"{family:?} should land on its free fallback face on a DejaVu-only db"
);
}
}
#[cfg(feature = "bundled-fonts")]
#[test]
fn bundled_forced_family_resolves_to_the_vendored_substitute_when_target_absent() {
for (family, expected) in [
(OsFamily::MacOs, "Inter"),
(OsFamily::Windows, "Selawik"),
(OsFamily::Gnome, "Cantarell"),
] {
let mut db = Database::new();
let resolved = resolve_forced_ui_family_bundled(&mut db, family);
assert_eq!(
resolved.as_deref(),
Some(expected),
"{family:?} with no target/fallback installed must use its bundled substitute"
);
let regular = query_face(&db, DbFamily::Name(expected), 400);
let bold = query_face(&db, DbFamily::Name(expected), 700);
assert!(regular.is_some() && bold.is_some());
assert_ne!(regular, bold, "{expected} must expose a distinct bold face");
}
}
#[cfg(feature = "bundled-fonts")]
#[test]
fn bundled_substitute_beats_the_free_host_fallback() {
const DEJAVU: &[u8] = include_bytes!("../../tests/fonts/DejaVuSans.ttf");
let mut db = Database::new();
db.load_font_data(DEJAVU.to_vec()); assert!(
query_face(&db, DbFamily::Name("DejaVu Sans"), 400).is_some(),
"precondition: the free fallback face is installed"
);
let resolved = resolve_forced_ui_family_bundled(&mut db, OsFamily::Windows);
assert_eq!(
resolved.as_deref(),
Some("Selawik"),
"the bundled substitute must be preferred over the free host fallback"
);
}
#[cfg(feature = "bundled-fonts")]
#[test]
fn bundled_oem_target_present_wins_over_the_substitute() {
let mut db = cached_system_fonts_db();
if first_installed_family(&db, OsFamily::MacOs.ui_font_families()).is_none() {
return; }
let resolved = resolve_forced_ui_family_bundled(&mut db, OsFamily::MacOs);
assert!(resolved.is_some());
assert_ne!(
resolved.as_deref(),
Some("Inter"),
"an installed real target face must win over the bundled substitute"
);
}
#[test]
fn forced_resolution_without_bundling_uses_the_free_fallback() {
const DEJAVU: &[u8] = include_bytes!("../../tests/fonts/DejaVuSans.ttf");
let mut db = Database::new();
db.load_font_data(DEJAVU.to_vec());
let resolved = resolve_forced_ui_family(
&db,
OsFamily::Windows.ui_font_families(),
OsFamily::Windows.fallback_font_families(),
);
assert_eq!(resolved.as_deref(), Some("DejaVu Sans"));
}
fn solid_png(color: Rgba) -> Arc<[u8]> {
let mut fb = Framebuffer::new(2, 2);
fb.fill(color);
let bytes: Vec<u8> = fb.encode_png();
Arc::from(bytes.into_boxed_slice())
}
#[test]
fn decode_icon_hits_the_cache_for_the_same_arc_across_calls() {
let d = RasterDrawer::new_headless(1.0);
let bytes = solid_png(Rgba::opaque(10, 20, 30));
let first = d.decode_icon(&bytes).expect("valid png decodes");
let second = d.decode_icon(&bytes).expect("valid png decodes");
assert_eq!(first.0, second.0);
assert_eq!((first.1, first.2), (second.1, second.2));
assert!(
Rc::ptr_eq(&first, &second),
"expected a cache hit, got a fresh decode"
);
}
#[test]
fn decode_icon_never_conflates_two_different_arcs() {
let d = RasterDrawer::new_headless(1.0);
let red = solid_png(Rgba::opaque(255, 0, 0));
let blue = solid_png(Rgba::opaque(0, 0, 255));
let red_decoded = d.decode_icon(&red).expect("valid png decodes");
let blue_decoded = d.decode_icon(&blue).expect("valid png decodes");
assert_ne!(
red_decoded.0, blue_decoded.0,
"distinct icons must decode to distinct pixels"
);
assert_eq!(&red_decoded.0[0..4], &[255, 0, 0, 255]);
assert_eq!(&blue_decoded.0[0..4], &[0, 0, 255, 255]);
let red_again = d.decode_icon(&red).expect("valid png decodes");
let blue_again = d.decode_icon(&blue).expect("valid png decodes");
assert_eq!(red_again.0, red_decoded.0);
assert_eq!(blue_again.0, blue_decoded.0);
assert!(
Rc::ptr_eq(&red_again, &red_decoded),
"expected a cache hit for `red`"
);
assert!(
Rc::ptr_eq(&blue_again, &blue_decoded),
"expected a cache hit for `blue`"
);
}
#[test]
fn decode_icon_cache_entry_survives_the_callers_arc_being_dropped() {
let d = RasterDrawer::new_headless(1.0);
let bytes = solid_png(Rgba::opaque(4, 5, 6));
let decoded = d.decode_icon(&bytes).expect("valid png decodes");
drop(bytes);
assert_eq!(d.icons.borrow().len(), 1);
let (kept_bytes, kept_decoded) = d.icons.borrow().values().next().cloned().unwrap();
assert!(Rc::ptr_eq(&kept_decoded, &decoded));
assert!(!kept_bytes.is_empty());
}
#[test]
fn icon_cache_hit_rejects_a_key_hit_on_a_different_allocation() {
let cached: Arc<[u8]> = Arc::from(vec![1u8, 2, 3].into_boxed_slice());
let query: Arc<[u8]> = Arc::from(vec![1u8, 2, 3].into_boxed_slice());
assert!(!Arc::ptr_eq(&cached, &query));
let decoded: DecodedIcon = Rc::new((vec![9, 9, 9, 9], 1, 1));
let entry: IconCacheEntry = (cached.clone(), decoded.clone());
assert!(icon_cache_hit(Some(&entry), &query).is_none());
let hit = icon_cache_hit(Some(&entry), &cached).expect("same-Arc hit");
assert!(Rc::ptr_eq(&hit, &decoded));
assert!(icon_cache_hit(None, &cached).is_none());
}
}