Skip to content

Commit eaf14d0

Browse files
authored
gpui: Fix SVG renderer not rendering text when system fonts are unavailable (#51623)
Closes #51466 Before you mark this PR as ready for review, make sure that you have: - [x] Added a solid test coverage and/or screenshots from doing manual testing - [x] Done a self-review taking into account security and performance aspects - [ ] Aligned any UI changes with the [UI checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) Release Notes: - Fixed mermaid diagrams not showing text in markdown preview by bundling fallback fonts and fixing generic font family resolution in the SVG renderer.
1 parent 1dc3bb9 commit eaf14d0

1 file changed

Lines changed: 120 additions & 7 deletions

File tree

crates/gpui/src/svg_renderer.rs

Lines changed: 120 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -105,18 +105,36 @@ pub enum SvgSize {
105105
impl SvgRenderer {
106106
/// Creates a new SVG renderer with the provided asset source.
107107
pub fn new(asset_source: Arc<dyn AssetSource>) -> Self {
108-
static FONT_DB: LazyLock<Arc<usvg::fontdb::Database>> = LazyLock::new(|| {
108+
static SYSTEM_FONT_DB: LazyLock<Arc<usvg::fontdb::Database>> = LazyLock::new(|| {
109109
let mut db = usvg::fontdb::Database::new();
110110
db.load_system_fonts();
111111
Arc::new(db)
112112
});
113+
114+
let fontdb = {
115+
let mut db = (**SYSTEM_FONT_DB).clone();
116+
load_bundled_fonts(&*asset_source, &mut db);
117+
fix_generic_font_families(&mut db);
118+
Arc::new(db)
119+
};
120+
113121
let default_font_resolver = usvg::FontResolver::default_font_selector();
114122
let font_resolver = Box::new(
115123
move |font: &usvg::Font, db: &mut Arc<usvg::fontdb::Database>| {
116124
if db.is_empty() {
117-
*db = FONT_DB.clone();
125+
*db = fontdb.clone();
126+
}
127+
if let Some(id) = default_font_resolver(font, db) {
128+
return Some(id);
118129
}
119-
default_font_resolver(font, db)
130+
// fontdb doesn't recognize CSS system font keywords like "system-ui"
131+
// or "ui-sans-serif", so fall back to sans-serif before any face.
132+
let sans_query = usvg::fontdb::Query {
133+
families: &[usvg::fontdb::Family::SansSerif],
134+
..Default::default()
135+
};
136+
db.query(&sans_query)
137+
.or_else(|| db.faces().next().map(|f| f.id))
120138
},
121139
);
122140
let default_fallback_selection = usvg::FontResolver::default_fallback_selector();
@@ -226,14 +244,69 @@ impl SvgRenderer {
226244
}
227245
}
228246

247+
fn load_bundled_fonts(asset_source: &dyn AssetSource, db: &mut usvg::fontdb::Database) {
248+
let font_paths = [
249+
"fonts/ibm-plex-sans/IBMPlexSans-Regular.ttf",
250+
"fonts/lilex/Lilex-Regular.ttf",
251+
];
252+
for path in font_paths {
253+
match asset_source.load(path) {
254+
Ok(Some(data)) => db.load_font_data(data.into_owned()),
255+
Ok(None) => log::warn!("Bundled font not found: {path}"),
256+
Err(error) => log::warn!("Failed to load bundled font {path}: {error}"),
257+
}
258+
}
259+
}
260+
261+
// fontdb defaults generic families to Microsoft fonts ("Arial", "Times New Roman")
262+
// which aren't installed on most Linux systems. fontconfig normally overrides these,
263+
// but when it fails the defaults remain and all generic family queries return None.
264+
fn fix_generic_font_families(db: &mut usvg::fontdb::Database) {
265+
use usvg::fontdb::{Family, Query};
266+
267+
let families_and_fallbacks: &[(Family<'_>, &str)] = &[
268+
(Family::SansSerif, "IBM Plex Sans"),
269+
// No serif font bundled; use sans-serif as best available fallback.
270+
(Family::Serif, "IBM Plex Sans"),
271+
(Family::Monospace, "Lilex"),
272+
(Family::Cursive, "IBM Plex Sans"),
273+
(Family::Fantasy, "IBM Plex Sans"),
274+
];
275+
276+
for (family, fallback_name) in families_and_fallbacks {
277+
let query = Query {
278+
families: &[*family],
279+
..Default::default()
280+
};
281+
if db.query(&query).is_none() {
282+
match family {
283+
Family::SansSerif => db.set_sans_serif_family(*fallback_name),
284+
Family::Serif => db.set_serif_family(*fallback_name),
285+
Family::Monospace => db.set_monospace_family(*fallback_name),
286+
Family::Cursive => db.set_cursive_family(*fallback_name),
287+
Family::Fantasy => db.set_fantasy_family(*fallback_name),
288+
_ => {}
289+
}
290+
}
291+
}
292+
}
293+
229294
#[cfg(test)]
230295
mod tests {
231296
use super::*;
297+
use usvg::fontdb::{Database, Family, Query};
232298

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

303+
fn db_with_bundled_fonts() -> Database {
304+
let mut db = Database::new();
305+
db.load_font_data(IBM_PLEX_REGULAR.to_vec());
306+
db.load_font_data(LILEX_REGULAR.to_vec());
307+
db
308+
}
309+
237310
#[test]
238311
fn test_is_emoji_presentation() {
239312
let cases = [
@@ -266,11 +339,33 @@ mod tests {
266339
}
267340

268341
#[test]
269-
fn test_select_emoji_font_skips_family_without_glyph() {
270-
let mut db = usvg::fontdb::Database::new();
342+
fn fix_generic_font_families_sets_all_families() {
343+
let mut db = db_with_bundled_fonts();
344+
fix_generic_font_families(&mut db);
345+
346+
let families = [
347+
Family::SansSerif,
348+
Family::Serif,
349+
Family::Monospace,
350+
Family::Cursive,
351+
Family::Fantasy,
352+
];
271353

272-
db.load_font_data(IBM_PLEX_REGULAR.to_vec());
273-
db.load_font_data(LILEX_REGULAR.to_vec());
354+
for family in families {
355+
let query = Query {
356+
families: &[family],
357+
..Default::default()
358+
};
359+
assert!(
360+
db.query(&query).is_some(),
361+
"Expected generic family {family:?} to resolve after fix_generic_font_families"
362+
);
363+
}
364+
}
365+
366+
#[test]
367+
fn test_select_emoji_font_skips_family_without_glyph() {
368+
let mut db = db_with_bundled_fonts();
274369

275370
let ibm_plex_sans = db
276371
.query(&usvg::fontdb::Query {
@@ -294,4 +389,22 @@ mod tests {
294389
assert!(!font_has_char(&db, ibm_plex_sans, '│'));
295390
assert!(font_has_char(&db, selected, '│'));
296391
}
392+
393+
#[test]
394+
fn fix_generic_font_families_monospace_resolves_to_lilex() {
395+
let mut db = db_with_bundled_fonts();
396+
fix_generic_font_families(&mut db);
397+
398+
let query = Query {
399+
families: &[Family::Monospace],
400+
..Default::default()
401+
};
402+
let id = db.query(&query).expect("Monospace should resolve");
403+
let face = db.face(id).expect("Face should exist");
404+
assert!(
405+
face.families.iter().any(|(name, _)| name.contains("Lilex")),
406+
"Monospace should map to Lilex, got {:?}",
407+
face.families
408+
);
409+
}
297410
}

0 commit comments

Comments
 (0)