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::rc::Rc;
use std::sync::Arc;
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, 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)>;
type ShapeKey = (String, Option<FaceId>, u16, 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()
}
}
struct ShapedGlyph {
face: FaceId,
glyph: u16,
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,
}
struct FaceBytes {
data: Vec<u8>,
index: u32,
}
const GLYPH_CACHE_CAP: usize = 512;
const SHAPED_CACHE_CAP: usize = 1024;
struct FontStore {
db: Database,
ui_family: Option<String>,
face_data: RefCell<HashMap<FaceId, Rc<FaceBytes>>>,
glyphs: RefCell<HashMap<GlyphKey, Option<Rc<GlyphImage>>>>,
shaper_data: RefCell<HashMap<FaceId, Rc<ShaperData>>>,
coverage: RefCell<HashMap<(FaceId, char), bool>>,
fallback_cache: RefCell<HashMap<(char, u16), Option<FaceId>>>,
face_cache: RefCell<HashMap<(FontFamily, u16), Option<FaceId>>>,
shaped: RefCell<HashMap<u64, (ShapeKey, Rc<ShapedLine>)>>,
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(HashMap::new()),
glyphs: RefCell::new(HashMap::new()),
shaper_data: RefCell::new(HashMap::new()),
coverage: RefCell::new(HashMap::new()),
fallback_cache: RefCell::new(HashMap::new()),
face_cache: RefCell::new(HashMap::new()),
shaped: RefCell::new(HashMap::new()),
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);
}
self.face_cache.borrow_mut().insert(key, result);
result
}
#[cfg(test)]
const HEAVY_WEIGHT: u16 = 600;
#[cfg(test)]
const LIGHT_WEIGHT: u16 = 500;
#[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);
self.coverage.borrow_mut().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))
};
self.fallback_cache
.borrow_mut()
.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,
) -> Rc<ShapedLine> {
let px_bits = px.to_bits();
let tracking_bits = tracking.to_bits();
let hash = shape_key_hash(text, primary, ot_weight, px_bits, tracking_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
{
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 shaper = shaper_data.shaper(&font).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,
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);
#[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.clear();
}
cache.insert(hash, (key, Rc::clone(&line)));
line
}
fn v_metrics(&self, primary: Option<FaceId>, px: f32) -> (f32, f32) {
if let Some(id) = primary {
if let Some(bytes) = self.face_bytes(id) {
if let Some(face) = FontRef::from_index(&bytes.data, bytes.index as usize) {
let m = face.metrics(&[]);
if m.units_per_em > 0 {
let sm = m.scale(px);
return (sm.ascent, -sm.descent);
}
}
}
}
(px * 0.8, -px * 0.2)
}
fn glyph_image(&self, face_id: FaceId, glyph: u16, px: f32) -> Option<Rc<GlyphImage>> {
let key = GlyphKey {
face: face_id,
glyph,
size_bits: px.to_bits(),
};
{
let cache = self.glyphs.borrow();
if let Some(cached) = cache.get(&key) {
return cached.clone();
}
}
let rendered = self.render_glyph(face_id, glyph, px);
let mut cache = self.glyphs.borrow_mut();
if cache.len() >= GLYPH_CACHE_CAP && !cache.contains_key(&key) {
cache.clear();
}
cache.insert(key, rendered.clone());
rendered
}
fn render_glyph(&self, face_id: FaceId, glyph: u16, px: 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 scaler = ctx.builder(font).size(px).hint(false).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;
}
Some(Rc::new(GlyphImage {
left: image.placement.left,
top: image.placement.top,
width: image.placement.width,
height: image.placement.height,
content: image.content,
data: image.data,
}))
}
}
fn shape_key_hash(
text: &str,
primary: Option<FaceId>,
ot_weight: u16,
px_bits: u32,
tracking_bits: u32,
) -> u64 {
use std::hash::{Hash, Hasher};
let mut h = std::collections::hash_map::DefaultHasher::new();
text.hash(&mut h);
primary.hash(&mut h);
ot_weight.hash(&mut h);
px_bits.hash(&mut h);
tracking_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 cached_system_fonts_db() -> Database {
thread_local! {
static SYSTEM_FONTS_DB: std::cell::OnceCell<Database> = const { std::cell::OnceCell::new() };
}
SYSTEM_FONTS_DB.with(|cell| {
cell.get_or_init(|| {
let mut db = Database::new();
db.load_system_fonts();
db
})
.clone()
})
}
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)
}
const DUAL_FACE_MAGIC: &[u8; 8] = b"MURIDUOF";
pub(crate) fn pack_dual_face(regular: Vec<u8>, bold: Vec<u8>) -> SystemFontSource {
let mut buf = Vec::with_capacity(DUAL_FACE_MAGIC.len() + 4 + regular.len() + bold.len());
buf.extend_from_slice(DUAL_FACE_MAGIC);
buf.extend_from_slice(&(regular.len() as u32).to_le_bytes());
buf.extend_from_slice(®ular);
buf.extend_from_slice(&bold);
SystemFontSource::Data(buf)
}
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)
.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;
let shaped = self.fonts.shape(run.text, primary, ot_weight, px, tracking);
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);
for g in &shaped.glyphs {
let Some(image) = self.fonts.glyph_image(g.face, g.glyph, px) 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 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;
match image.content {
Content::Mask | Content::SubpixelMask => {
for row in 0..ih {
for col in 0..iw {
let cov = image.data[(row * iw + col) as usize];
if cov == 0 {
continue;
}
let px = gx + col;
let py = gy + row;
if px < 0 || py < 0 || px >= pw || py >= ph {
continue;
}
let a = (cov as u16 * color.a as u16 / 255) as u8;
raster::blend_pixel(pixels, ((py * pw + px) * 4) as usize, color, a);
}
}
}
Content::Color => {
for row in 0..ih {
for col in 0..iw {
let idx = ((row * iw + col) * 4) as usize;
let (r, g, b, a) = (
image.data[idx],
image.data[idx + 1],
image.data[idx + 2],
image.data[idx + 3],
);
if a == 0 {
continue;
}
let px = gx + col;
let py = gy + row;
if px < 0 || py < 0 || px >= pw || py >= ph {
continue;
}
raster::blend_pixel(
pixels,
((py * pw + px) * 4) as usize,
Rgba::new(r, g, b, 255),
a,
);
}
}
}
}
}
#[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 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 blob = pack_dual_face(DEJAVU.to_vec(), DEJAVU_BOLD.to_vec());
let name = register_system_font(&mut db, blob);
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);
}
#[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());
}
}