//! The theme engine: reads the running system theme, resolves it through //! presets and overrides, and installs the tokens every component paints //! from. //! //! Pipeline (plan ยง4.5): `ThemeSource` snapshot โ†’ `resolve` overlay chain โ†’ //! `Theme` global โ†’ all windows repaint. Watchers re-run the pipeline on //! real OS change events. mod colors; mod definition; mod presets; mod ramps; mod resolve; mod source; mod system; pub use colors::ColorTokens; pub use definition::ThemeDefinition; pub use ramps::{AccentShades, Oklch}; pub use resolve::{Ramps, ResolvedTheme}; pub use system::{AccessibilityPreferences, PartialPalette, SourceKind, SystemTheme}; use gpui::{App, Global, Pixels, WindowAppearance, px}; /// Light or dark, resolved from the system appearance. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Deserialize)] #[serde(rename_all = "lowercase")] pub enum ThemeVariant { #[default] Light, Dark, } impl ThemeVariant { fn from_appearance(appearance: WindowAppearance) -> Self { match appearance { WindowAppearance::Light | WindowAppearance::VibrantLight => Self::Light, WindowAppearance::Dark | WindowAppearance::VibrantDark => Self::Dark, } } } /// The resolved theme, installed as the app-wide global. #[derive(Clone, Copy, Debug, PartialEq)] pub struct Theme { pub variant: ThemeVariant, pub colors: ColorTokens, pub ramps: Ramps, pub radius: Radius, pub accessibility: AccessibilityPreferences, } impl Global for Theme {} /// Corner radii components paint from; grows one class at a time. #[derive(Clone, Copy, Debug, PartialEq)] pub struct Radius { /// Small controls: checkbox and radio indicators. pub sm: Pixels, pub control: Pixels, /// Pill and circle shapes. pub full: Pixels, } impl Default for Radius { fn default() -> Self { Self { sm: px(4.), control: px(6.), full: px(9999.), } } } /// Reads the resolved theme global from any app context. pub trait ActiveTheme { fn theme(&self) -> &Theme; } impl ActiveTheme for App { fn theme(&self) -> &Theme { Theme::global(self) } } impl Theme { /// Reads the theme global. Panics before `init`. pub fn global(cx: &App) -> &Self { cx.global::() } fn from_resolved(resolved: ResolvedTheme) -> Self { Self { variant: resolved.variant, colors: resolved.colors, ramps: resolved.ramps, radius: resolved.radius, accessibility: resolved.accessibility, } } /// Installs the engine global, its platform watchers, and the initial resolved theme. pub fn init(cx: &mut App) { let mut engine = ThemeEngine { source: source::platform_source(), mode_override: None, preset: None, }; engine.source.install_watchers(cx.to_async()); cx.set_global(engine); reapply(cx); } /// Re-resolves when a window's appearance differs from the resolved variant. /// /// Wire to `Window::observe_window_appearance`. pub fn sync_from_window(window: &gpui::Window, cx: &mut App) { let variant = ThemeVariant::from_appearance(window.appearance()); if cx.global::().variant != variant || cx.global::().mode_override.is_none() { reapply(cx); } } } /// The engine's knobs and its platform source. struct ThemeEngine { source: Box, mode_override: Option, preset: Option<(&'static str, ThemeDefinition)>, } impl Global for ThemeEngine {} /// Reads the system, resolves through the current knobs, /// installs the theme global, and repaints every window. pub(crate) fn reapply(cx: &mut App) { let (system_appearance, preset, mode_override) = { let engine = cx.global::(); ( ThemeVariant::from_appearance(cx.window_appearance()), engine.preset.as_ref().map(|(_, def)| def.clone()), engine.mode_override, ) }; let variant = mode_override .or(preset.as_ref().and_then(|def| def.mode)) .unwrap_or(system_appearance); let system = cx.global::().source.snapshot(variant); let resolved = resolve::resolve(&system, preset.as_ref(), mode_override); cx.set_global(Theme::from_resolved(resolved)); cx.refresh_windows(); } /// Forces a variant, independent of the OS. `None` follows the system. pub fn set_mode_override(variant: Option, cx: &mut App) { cx.global_mut::().mode_override = variant; reapply(cx); } /// Installs a built-in preset, or `None` to follow the system again. pub fn set_preset(name: Option<&str>, cx: &mut App) -> anyhow::Result<()> { let preset = match name { Some(name) => { let def = presets::builtin(name).ok_or_else(|| anyhow::anyhow!("unknown preset: {name}"))?; let static_name = presets::preset_names() .iter() .copied() .find(|candidate| *candidate == name) .ok_or_else(|| anyhow::anyhow!("unknown preset: {name}"))?; Some((static_name, def)) } None => None, }; cx.global_mut::().preset = preset; reapply(cx); Ok(()) } /// The variant the mode override forces, if any. pub fn mode_override(cx: &App) -> Option { cx.global::().mode_override } /// The active preset name, if one is installed. pub fn active_preset(cx: &App) -> Option<&'static str> { cx.global::() .preset .as_ref() .map(|(name, _)| *name) } /// Clears the mode override and preset; the theme follows the system again. pub fn follow_system(cx: &mut App) { let engine = cx.global_mut::(); engine.mode_override = None; engine.preset = None; reapply(cx); } /// Which adapter produced the current system snapshot. pub fn detected_source(cx: &App) -> &'static str { cx.global::().source.label() }