Files
gpui-native-kit/crates/theme/src/lib.rs
T
2026-10-28 18:21:19 +07:00

210 lines
6.1 KiB
Rust

//! 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::<Self>()
}
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::<Theme>().variant != variant
|| cx.global::<ThemeEngine>().mode_override.is_none()
{
reapply(cx);
}
}
}
/// The engine's knobs and its platform source.
struct ThemeEngine {
source: Box<dyn source::ThemeSource>,
mode_override: Option<ThemeVariant>,
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::<ThemeEngine>();
(
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::<ThemeEngine>().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<ThemeVariant>, cx: &mut App) {
cx.global_mut::<ThemeEngine>().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::<ThemeEngine>().preset = preset;
reapply(cx);
Ok(())
}
/// The variant the mode override forces, if any.
pub fn mode_override(cx: &App) -> Option<ThemeVariant> {
cx.global::<ThemeEngine>().mode_override
}
/// The active preset name, if one is installed.
pub fn active_preset(cx: &App) -> Option<&'static str> {
cx.global::<ThemeEngine>()
.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::<ThemeEngine>();
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::<ThemeEngine>().source.label()
}