Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 120 additions & 7 deletions crates/gpui/src/svg_renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,18 +105,36 @@ pub enum SvgSize {
impl SvgRenderer {
/// Creates a new SVG renderer with the provided asset source.
pub fn new(asset_source: Arc<dyn AssetSource>) -> Self {
static FONT_DB: LazyLock<Arc<usvg::fontdb::Database>> = LazyLock::new(|| {
static SYSTEM_FONT_DB: LazyLock<Arc<usvg::fontdb::Database>> = LazyLock::new(|| {
let mut db = usvg::fontdb::Database::new();
db.load_system_fonts();
Arc::new(db)
});

let fontdb = {
let mut db = (**SYSTEM_FONT_DB).clone();
load_bundled_fonts(&*asset_source, &mut db);
fix_generic_font_families(&mut db);
Arc::new(db)
};

let default_font_resolver = usvg::FontResolver::default_font_selector();
let font_resolver = Box::new(
move |font: &usvg::Font, db: &mut Arc<usvg::fontdb::Database>| {
if db.is_empty() {
*db = FONT_DB.clone();
*db = fontdb.clone();
}
if let Some(id) = default_font_resolver(font, db) {
return Some(id);
}
default_font_resolver(font, db)
// fontdb doesn't recognize CSS system font keywords like "system-ui"
// or "ui-sans-serif", so fall back to sans-serif before any face.
let sans_query = usvg::fontdb::Query {
families: &[usvg::fontdb::Family::SansSerif],
..Default::default()
};
db.query(&sans_query)
.or_else(|| db.faces().next().map(|f| f.id))
},
);
let default_fallback_selection = usvg::FontResolver::default_fallback_selector();
Expand Down Expand Up @@ -229,14 +247,69 @@ impl SvgRenderer {
}
}

fn load_bundled_fonts(asset_source: &dyn AssetSource, db: &mut usvg::fontdb::Database) {
let font_paths = [
"fonts/ibm-plex-sans/IBMPlexSans-Regular.ttf",
"fonts/lilex/Lilex-Regular.ttf",
];
for path in font_paths {
match asset_source.load(path) {
Ok(Some(data)) => db.load_font_data(data.into_owned()),
Ok(None) => log::warn!("Bundled font not found: {path}"),
Err(error) => log::warn!("Failed to load bundled font {path}: {error}"),
}
}
}

// fontdb defaults generic families to Microsoft fonts ("Arial", "Times New Roman")
// which aren't installed on most Linux systems. fontconfig normally overrides these,
// but when it fails the defaults remain and all generic family queries return None.
fn fix_generic_font_families(db: &mut usvg::fontdb::Database) {
use usvg::fontdb::{Family, Query};

let families_and_fallbacks: &[(Family<'_>, &str)] = &[
(Family::SansSerif, "IBM Plex Sans"),
// No serif font bundled; use sans-serif as best available fallback.
(Family::Serif, "IBM Plex Sans"),
(Family::Monospace, "Lilex"),
(Family::Cursive, "IBM Plex Sans"),
(Family::Fantasy, "IBM Plex Sans"),
];

for (family, fallback_name) in families_and_fallbacks {
let query = Query {
families: &[*family],
..Default::default()
};
if db.query(&query).is_none() {
match family {
Family::SansSerif => db.set_sans_serif_family(*fallback_name),
Family::Serif => db.set_serif_family(*fallback_name),
Family::Monospace => db.set_monospace_family(*fallback_name),
Family::Cursive => db.set_cursive_family(*fallback_name),
Family::Fantasy => db.set_fantasy_family(*fallback_name),
_ => {}
}
}
}
}

#[cfg(test)]
mod tests {
use super::*;
use usvg::fontdb::{Database, Family, Query};

const IBM_PLEX_REGULAR: &[u8] =
include_bytes!("../../../assets/fonts/ibm-plex-sans/IBMPlexSans-Regular.ttf");
const LILEX_REGULAR: &[u8] = include_bytes!("../../../assets/fonts/lilex/Lilex-Regular.ttf");

fn db_with_bundled_fonts() -> Database {
let mut db = Database::new();
db.load_font_data(IBM_PLEX_REGULAR.to_vec());
db.load_font_data(LILEX_REGULAR.to_vec());
db
}

#[test]
fn test_is_emoji_presentation() {
let cases = [
Expand Down Expand Up @@ -269,11 +342,33 @@ mod tests {
}

#[test]
fn test_select_emoji_font_skips_family_without_glyph() {
let mut db = usvg::fontdb::Database::new();
fn fix_generic_font_families_sets_all_families() {
let mut db = db_with_bundled_fonts();
fix_generic_font_families(&mut db);

let families = [
Family::SansSerif,
Family::Serif,
Family::Monospace,
Family::Cursive,
Family::Fantasy,
];

db.load_font_data(IBM_PLEX_REGULAR.to_vec());
db.load_font_data(LILEX_REGULAR.to_vec());
for family in families {
let query = Query {
families: &[family],
..Default::default()
};
assert!(
db.query(&query).is_some(),
"Expected generic family {family:?} to resolve after fix_generic_font_families"
);
}
}

#[test]
fn test_select_emoji_font_skips_family_without_glyph() {
let mut db = db_with_bundled_fonts();

let ibm_plex_sans = db
.query(&usvg::fontdb::Query {
Expand All @@ -297,4 +392,22 @@ mod tests {
assert!(!font_has_char(&db, ibm_plex_sans, '│'));
assert!(font_has_char(&db, selected, '│'));
}

#[test]
fn fix_generic_font_families_monospace_resolves_to_lilex() {
let mut db = db_with_bundled_fonts();
fix_generic_font_families(&mut db);

let query = Query {
families: &[Family::Monospace],
..Default::default()
};
let id = db.query(&query).expect("Monospace should resolve");
let face = db.face(id).expect("Face should exist");
assert!(
face.families.iter().any(|(name, _)| name.contains("Lilex")),
"Monospace should map to Lilex, got {:?}",
face.families
);
}
}
Loading