WASM gallery panics: Theme::change resolves `.SystemUIFont` before any font is loaded

Author: xn3cr0nxCreated Sep 16, 2026Updated Sep 16, 2026

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::initTheme::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 main as 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

  1. Open https://gpui-kit.com/component (any story, e.g. Focus Trap / Button).
  2. Open DevTools → Console.
  3. 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, Arial

Stack (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_font

Followed 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, …):

rust
// 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:

rust
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:

rust
// 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:

rust
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:

  1. installed_font_names is a process-wide OnceLock. It is documented as depending only on system fonts that do not change. On wasm, fonts are registered later via add_fonts. If the first Theme::change runs first, the cache stays empty for the rest of the process, so even a later probe thinks nothing is installed.
  2. Bundling Inter is not enough. Inter is not on GPUI's fallback stack. A wasm app that add_fonts Inter only, then calls gpui_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 .ZedSans alias, which is why #2933 bundled it.

catch_unwind is not a fix: wasm is panic=abort.

Suggested fix

  1. system_font::resolve_default_font must not call resolve_font unless a family that probe can land on is already installed (via all_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.
  2. story-web: add_fonts before gpui_component_story::init, so IBM Plex Sans is present for both the probe and later default-style measurement.
  3. installed_font_names: do not cache an empty list (wasm-only skip, or refresh when empty), because add_fonts happens at runtime.

Happy to send a PR.

Source: longbridge/gpui-component