Skip to content

Commit 80bd968

Browse files
committed
gpui: Fix mermaid diagrams not showing text in markdown preview (#51466)
1 parent 257712e commit 80bd968

1 file changed

Lines changed: 124 additions & 8 deletions

File tree

crates/gpui/src/svg_renderer.rs

Lines changed: 124 additions & 8 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();
@@ -229,13 +247,69 @@ impl SvgRenderer {
229247
}
230248
}
231249

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

236302
const IBM_PLEX_REGULAR: &[u8] =
237303
include_bytes!("../../../assets/fonts/ibm-plex-sans/IBMPlexSans-Regular.ttf");
238-
const LILEX_REGULAR: &[u8] = include_bytes!("../../../assets/fonts/lilex/Lilex-Regular.ttf");
304+
const LILEX_REGULAR: &[u8] =
305+
include_bytes!("../../../assets/fonts/lilex/Lilex-Regular.ttf");
306+
307+
fn db_with_bundled_fonts() -> Database {
308+
let mut db = Database::new();
309+
db.load_font_data(IBM_PLEX_REGULAR.to_vec());
310+
db.load_font_data(LILEX_REGULAR.to_vec());
311+
db
312+
}
239313

240314
#[test]
241315
fn test_is_emoji_presentation() {
@@ -269,11 +343,33 @@ mod tests {
269343
}
270344

271345
#[test]
272-
fn test_select_emoji_font_skips_family_without_glyph() {
273-
let mut db = usvg::fontdb::Database::new();
346+
fn fix_generic_font_families_sets_all_families() {
347+
let mut db = db_with_bundled_fonts();
348+
fix_generic_font_families(&mut db);
349+
350+
let families = [
351+
Family::SansSerif,
352+
Family::Serif,
353+
Family::Monospace,
354+
Family::Cursive,
355+
Family::Fantasy,
356+
];
274357

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

278374
let ibm_plex_sans = db
279375
.query(&usvg::fontdb::Query {
@@ -297,4 +393,24 @@ mod tests {
297393
assert!(!font_has_char(&db, ibm_plex_sans, '│'));
298394
assert!(font_has_char(&db, selected, '│'));
299395
}
396+
397+
#[test]
398+
fn fix_generic_font_families_monospace_resolves_to_lilex() {
399+
let mut db = db_with_bundled_fonts();
400+
fix_generic_font_families(&mut db);
401+
402+
let query = Query {
403+
families: &[Family::Monospace],
404+
..Default::default()
405+
};
406+
let id = db.query(&query).expect("Monospace should resolve");
407+
let face = db.face(id).expect("Face should exist");
408+
assert!(
409+
face.families
410+
.iter()
411+
.any(|(name, _)| name.contains("Lilex")),
412+
"Monospace should map to Lilex, got {:?}",
413+
face.families
414+
);
415+
}
300416
}

0 commit comments

Comments
 (0)