Fonts
#Default fonts
Every app starts with a UI font and a monospace font from the theme:
| Role | Family | Size |
|---|---|---|
| UI text | .SystemUIFont | 16px |
| Code / monospace | macOS: Menlo, Windows: Consolas, Linux: DejaVu Sans Mono | 13px |
The editor paints its code in mono_font_family at mono_font_size. See
Editor for details.
Both defaults are checked against the installed fonts when the theme is
applied. A missing monospace default is swapped for an installed alternative,
and when .SystemUIFont resolves to one of GPUI’s fallback families rather
than the system font itself (Linux desktops without the family GPUI maps it
to), the theme names that family directly so text lookups stay cached. A
family you set yourself is used as-is.
#System fonts
Desktop apps can use any font installed on the OS by name — no bundling, no config. GPUI resolves the family live against the system collection (CoreText on macOS, DirectWrite on Windows, fontconfig on Linux).
div().font_family("Segoe UI")
Editor::new(&editor).font_family("JetBrains Mono")
Common examples per platform:
- macOS:
SF Pro,Helvetica,Arial,Times New Roman,Menlo,Monaco - Windows:
Segoe UI,Arial,Consolas,Courier New - Linux:
Noto Sans,DejaVu Sans,Liberation Sans,DejaVu Sans Mono
If the name does not match an installed font, GPUI falls back silently — so verify the exact family name on each target platform.
#Changing fonts via Theme
Set the app-wide fonts on the Theme global, then sync to the base layer:
Theme::global_mut(cx).font_family = "Inter".into();
Theme::global_mut(cx).mono_font_family = "JetBrains Mono".into();
Theme::global_mut(cx).font_size = px(18.);
Theme::sync_base(cx);
window.refresh();
font_size doubles as the application zoom control — Root calls
window.set_rem_size(cx.theme().font_size), so rem-based spacing scales
with it. See Coding Guides for details.
#Per-element override
Any element accepts a font override without touching the theme:
div()
.font_family("JetBrains Mono")
.text_size(px(15.))
.font_weight(FontWeight::BOLD)
These are ordinary Styled
methods, so they compose with the rest of the style chain.
#Bundling custom fonts
Fonts that are not installed on the user’s system must be bundled and registered with the text system before the first frame:
cx.text_system()
.add_fonts(vec![Cow::Borrowed(
include_bytes!("../fonts/MyFont-Regular.ttf").as_slice(),
)])
.expect("Failed to load fonts");
Then reference them by family name as usual:
Theme::global_mut(cx).font_family = "MyFont".into();
Theme::sync_base(cx);
The gallery’s web build bundles Inter, JetBrains Mono, Noto Sans SC and
IBM Plex Sans this way — see crates/story-web/src/lib.rs.
#Theme JSON config
Font families and sizes can also come from a theme file:
{
"font.family": "Inter",
"font.size": 16,
"mono_font.family": "JetBrains Mono",
"mono_font.size": 13
}
Load it with ThemeRegistry:
ThemeRegistry::watch_dir(PathBuf::from("./themes"), cx, move |cx| {
if let Some(theme) = ThemeRegistry::global(cx).themes().get(&theme_name).cloned() {
Theme::global_mut(cx).apply_config(&theme);
}
});
See Theme for the full config reference.
#WebAssembly note
Browsers expose no system fonts to WASM apps. The story-web gallery
(which runs at gpui-kit.com/gallery/) must bundle every family it uses and
re-assert them after Theme::change, or the text system panics. Desktop apps
skip this entirely.
Text the bundled fonts cannot draw can still come from the browser. The web platform renders emoji through Canvas 2D with the visitor’s local fonts, so an application does not have to bundle an emoji font. The policy is chosen when the platform is constructed and cannot change afterwards:
CanvasFontFallback | Browser draws |
|---|---|
Emoji (default) | Emoji, including skin tones, flags, keycaps and ZWJ sequences |
EmojiAndCjk | Emoji plus horizontal Han, kana and modern Hangul text |
Disabled | Nothing; only bundled fonts are used |
gpui_kit::application() and gpui_kit::platform::single_threaded_web() keep
the default. To widen it, build the platform yourself:
use gpui_kit::web::{CanvasFontFallback, WebBackendPreference, WebPlatform};
let platform = Rc::new(WebPlatform::new_with_backend_and_font_fallback(
false,
WebBackendPreference::Auto,
CanvasFontFallback::EmojiAndCjk,
));
let http_client = Arc::new(platform.fetch_http_client());
let app = Application::with_platform(platform).with_http_client(http_client);
Bundled fonts stay preferred wherever they have the glyph. The fallback draws
each grapheme on its own, so CJK text rendered this way favors readability
over exact spacing and font features, and its appearance depends on the
fonts installed on the visitor’s machine. The gallery opts into
EmojiAndCjk: its bundled fonts hold only the glyphs its own stories use, and
anything a visitor types into an input would otherwise render as tofu.