WASM gallery panics: Theme::change resolves `.SystemUIFont` before any font is loaded
Description
Every embedded "Rust & WASM" example on https://gpui-kit.com/component (and /gallery/) comes up blank. The canvas never paints; the console shows a panic in gpui-pre 0.3.5's text system, then Uncaught RuntimeError: unreachable.
This is a regression of the failure #2933 already fixed. That PR bundled IBM Plex Sans so .SystemUIFont could resolve on web. #3085 later made Theme::change probe that alias with the panicking TextSystem::resolve_font during init. story-web still calls gpui_component_story::init (which calls gpui_kit::init → Theme::change) before add_fonts, so the probe runs against an empty font database. WASM has no system fonts (WebTextSystem::new("IBM Plex Sans", …) plus an empty cosmic-text db), and wasm is panic=abort, so the gallery dies.
Environment
- GPUI (
gpui-pre): v0.3.5 (panic path:gpui-pre-0.3.5/src/text_system.rs:154) - GPUI Component / gpui-kit: current
mainas deployed on gpui-kit.com - Platform: Web (Chrome),
https://gpui-kit.com/component— reproduced on Focus Trap; every story iframe is blank - Introduced by: #3085 (
theme: Name the family the system font resolves to, merged 2026-09-15) - Previously fixed (same symptom, different call site): #2933
Steps to Reproduce
- Open https://gpui-kit.com/component (any story, e.g. Focus Trap / Button).
- Open DevTools → Console.
- Observe the embedded example stay empty (macOS traffic-light chrome, no content).
Locally: bun run dev in website/, open /docs/components/button (or /gallery/).
Screenshots
The example frame is empty. Console:
panicked at /home/runner/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/gpui-pre-0.3.5/src/text_system.rs:154:9:
failed to resolve font '.SystemUIFont' or any of the fallbacks: .ZedMono, .ZedSans, Helvetica, Segoe UI, Ubuntu, Adwaita Sans, Cantarell, Noto Sans, DejaVu Sans, ArialStack (abbreviated, from the production bundle gpui_component_story_web-D775T_wt.js):
gpui::text_system::TextSystem::resolve_font
gpui_component::theme::system_font::resolve_default_fontFollowed by Uncaught RuntimeError: unreachable from the same wasm module (wasm abort).
Expected
The story renders inside the frame, as it did after #2933. Theme::change / gpui_kit::init must not panic when the host has no system fonts.
Actual
theme::init always calls Theme::change(ThemeMode::Light, …):
// crates/component/src/theme/mod.rs
pub fn init(cx: &mut App) {
registry::init(cx);
Theme::change(ThemeMode::Light, None, cx);
Theme::sync_scrollbar_appearance(cx);
}Theme::change now ends with:
system_font::resolve_default_font(cx);
mono_font::resolve_default_mono_font(cx);resolve_default_font calls the infallible TextSystem::resolve_font, whose contract is to panic if the family and every fallback miss:
// crates/component/src/theme/system_font.rs
let resolved = text_system
.get_font_for_id(text_system.resolve_font(&font(SYSTEM_UI_FONT)))
.map(|font| font.family);On web, .SystemUIFont maps to "IBM Plex Sans" (WebTextSystem::new("IBM Plex Sans", …)). The fallback stack is .ZedMono (Lilex), .ZedSans (IBM Plex Sans), Helvetica, Segoe UI, Ubuntu, Adwaita Sans, Cantarell, Noto Sans, DejaVu Sans, Arial. None of those exist until add_fonts runs. story-web currently does:
gpui_component_story::init(cx); // Theme::change → panic
// … only then add_fonts(Inter, Noto Sans SC, JetBrains Mono, IBM Plex Sans)
apply_theme(…);Two extra landmines in the same area:
installed_font_namesis a process-wideOnceLock. It is documented as depending only on system fonts that do not change. On wasm, fonts are registered later viaadd_fonts. If the firstTheme::changeruns first, the cache stays empty for the rest of the process, so even a later probe thinks nothing is installed.- Bundling Inter is not enough. Inter is not on GPUI's fallback stack. A wasm app that
add_fontsInter only, then callsgpui_kit::init, still panics —resolve_font(".SystemUIFont")will not land on Inter. Noto Sans is on the stack (that is the workaround used in other wasm GPUI apps); IBM Plex Sans is too, via the.ZedSansalias, which is why #2933 bundled it.
catch_unwind is not a fix: wasm is panic=abort.
Suggested fix
system_font::resolve_default_fontmust not callresolve_fontunless a family that probe can land on is already installed (viaall_font_names). If the db is empty or only holds families outside the fallback stack (e.g. Inter), skip. This is a Linux-only optimization; it must not crash wasm / embedded hosts.story-web:add_fontsbeforegpui_component_story::init, so IBM Plex Sans is present for both the probe and later default-style measurement.installed_font_names: do not cache an empty list (wasm-only skip, or refresh when empty), becauseadd_fontshappens at runtime.
Happy to send a PR.
Source: longbridge/gpui-component