theme: build out the theme engine core with resolve pipeline
This commit is contained in:
+125
-17
@@ -1,17 +1,30 @@
|
||||
//! The theme engine: reads the running system theme and resolves it into the
|
||||
//! semantic tokens every component paints from.
|
||||
//! The theme engine: reads the running system theme, resolves it through
|
||||
//! presets and overrides, and installs the tokens every component paints
|
||||
//! from.
|
||||
//!
|
||||
//! Phase B replaces the bundled light/dark presets with platform adapters
|
||||
//! (`NSColor` / `UISettings` / portal / Omarchy) behind a `ThemeSource` seam.
|
||||
//! 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, WindowAppearance};
|
||||
|
||||
/// Light or dark, resolved from the system appearance.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ThemeVariant {
|
||||
#[default]
|
||||
Light,
|
||||
@@ -32,6 +45,8 @@ impl ThemeVariant {
|
||||
pub struct Theme {
|
||||
pub variant: ThemeVariant,
|
||||
pub colors: ColorTokens,
|
||||
pub ramps: Ramps,
|
||||
pub accessibility: AccessibilityPreferences,
|
||||
}
|
||||
|
||||
impl Global for Theme {}
|
||||
@@ -42,27 +57,120 @@ impl Theme {
|
||||
cx.global::<Self>()
|
||||
}
|
||||
|
||||
pub fn for_variant(variant: ThemeVariant) -> Self {
|
||||
fn from_resolved(resolved: ResolvedTheme) -> Self {
|
||||
Self {
|
||||
variant,
|
||||
colors: match variant {
|
||||
ThemeVariant::Light => ColorTokens::light(),
|
||||
ThemeVariant::Dark => ColorTokens::dark(),
|
||||
},
|
||||
variant: resolved.variant,
|
||||
colors: resolved.colors,
|
||||
ramps: resolved.ramps,
|
||||
accessibility: resolved.accessibility,
|
||||
}
|
||||
}
|
||||
|
||||
/// Installs the theme global for the current system appearance.
|
||||
/// Installs the engine global, its platform watchers, and the initial resolved theme.
|
||||
pub fn init(cx: &mut App) {
|
||||
let variant = ThemeVariant::from_appearance(cx.window_appearance());
|
||||
cx.set_global(Self::for_variant(variant));
|
||||
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-installs the global when a window's appearance differs from it.
|
||||
/// 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::<Self>().variant != variant {
|
||||
cx.set_global(Self::for_variant(variant));
|
||||
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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user