use std::collections::HashMap;
use gpui::{AnyWindowHandle, App, Global, SharedString, Subscription, Window, WindowAppearance};
use crate::layout::LayoutTheme;
use crate::semantic::{RoleColor, ThemeColors};
use crate::theme::{Appearance, Theme};
use herogpui_core::Color;
pub struct ThemeProvider {
active: SharedString,
themes: HashMap<SharedString, Theme>,
follow_system_appearance: bool,
appearance_observers: HashMap<AnyWindowHandle, Subscription>,
}
impl Global for ThemeProvider {}
impl ThemeProvider {
pub fn init(cx: &mut App) {
Self::init_with(Theme::light(), cx);
}
pub fn init_with(theme: Theme, cx: &mut App) {
let mut themes = HashMap::new();
themes.insert("light".into(), Theme::light());
themes.insert("dark".into(), Theme::dark());
let id = theme.id.clone();
themes.insert(id.clone(), theme);
cx.set_global(Self {
active: id,
themes,
follow_system_appearance: false,
appearance_observers: HashMap::new(),
});
}
pub fn get(cx: &App) -> &Self {
cx.global::<ThemeProvider>()
}
pub fn theme(&self) -> &Theme {
self.themes
.get(&self.active)
.or_else(|| self.themes.get("light"))
.expect("the built-in light theme is registered by `init_with` and never removed")
}
pub fn contains(&self, id: &str) -> bool {
self.themes.contains_key(id)
}
pub fn active_id(&self) -> &SharedString {
&self.active
}
pub fn register(&mut self, theme: Theme) {
self.active = theme.id.clone();
self.themes.insert(theme.id.clone(), theme);
}
pub fn insert(&mut self, theme: Theme) {
self.themes.insert(theme.id.clone(), theme);
}
pub fn theme_ids(&self) -> Vec<SharedString> {
let mut ids: Vec<_> = self.themes.keys().cloned().collect();
ids.sort();
ids
}
pub fn set_active(&mut self, id: impl Into<SharedString>) -> Result<(), UnknownThemeError> {
let id = id.into();
if !self.themes.contains_key(&id) {
return Err(UnknownThemeError { id });
}
self.active = id;
Ok(())
}
pub fn follows_system_appearance(&self) -> bool {
self.follow_system_appearance
}
}
pub trait ActiveTheme {
fn theme(&self) -> &Theme;
fn colors(&self) -> &ThemeColors;
fn layout(&self) -> &LayoutTheme;
fn components(&self) -> &crate::ComponentThemes;
fn role(&self, color: Color) -> &RoleColor;
fn is_dark_theme(&self) -> bool;
fn reduce_motion(&self) -> bool;
}
impl ActiveTheme for App {
fn theme(&self) -> &Theme {
ThemeProvider::get(self).theme()
}
fn colors(&self) -> &ThemeColors {
&self.theme().colors
}
fn layout(&self) -> &LayoutTheme {
&self.theme().layout
}
fn components(&self) -> &crate::ComponentThemes {
&self.theme().components
}
fn role(&self, color: Color) -> &RoleColor {
match color {
Color::Default => &self.colors().default,
Color::Accent => &self.colors().accent,
Color::Success => &self.colors().success,
Color::Warning => &self.colors().warning,
Color::Danger => &self.colors().danger,
}
}
fn is_dark_theme(&self) -> bool {
self.theme().is_dark()
}
fn reduce_motion(&self) -> bool {
App::reduce_motion(self)
}
}
pub fn set_theme(theme: Theme, cx: &mut App) {
let provider = cx.global_mut::<ThemeProvider>();
provider.register(theme);
cx.refresh_windows();
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct UnknownThemeError {
pub id: SharedString,
}
impl std::fmt::Display for UnknownThemeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"no theme is registered under the id {:?}",
self.id.as_ref()
)
}
}
impl std::error::Error for UnknownThemeError {}
pub fn use_theme(id: impl Into<SharedString>, cx: &mut App) -> Result<(), UnknownThemeError> {
let provider = cx.global_mut::<ThemeProvider>();
provider.set_active(id)?;
cx.refresh_windows();
Ok(())
}
pub fn set_reduce_motion(v: bool, cx: &mut App) {
cx.set_reduce_motion(v);
cx.refresh_windows();
}
pub fn toggle_reduce_motion(cx: &mut App) {
let next = !App::reduce_motion(cx);
set_reduce_motion(next, cx);
}
pub fn follow_system_appearance(window: &mut Window, cx: &mut App) {
let subscription = window.observe_window_appearance(|window, cx| {
apply_system_appearance(window.appearance(), cx);
});
let handle = window.window_handle();
let appearance = window.appearance();
let provider = cx.global_mut::<ThemeProvider>();
provider.follow_system_appearance = true;
provider.appearance_observers.insert(handle, subscription);
apply_system_appearance(appearance, cx);
}
pub fn stop_following_system_appearance(cx: &mut App) {
let provider = cx.global_mut::<ThemeProvider>();
provider.follow_system_appearance = false;
provider.appearance_observers.clear();
}
fn apply_system_appearance(appearance: WindowAppearance, cx: &mut App) {
let provider = ThemeProvider::get(cx);
if !provider.follows_system_appearance() {
return;
}
let id = match Appearance::from(appearance) {
Appearance::Light => "light",
Appearance::Dark => "dark",
};
if provider.active_id().as_ref() == id {
return;
}
let _ = use_theme(id, cx);
}
pub fn toggle_light_dark(cx: &mut App) {
let dark = cx.theme().is_dark();
let next = if dark { "light" } else { "dark" };
let _ = use_theme(next, cx);
}
#[cfg(test)]
mod tests {
use super::*;
use gpui::TestAppContext;
#[gpui::test]
fn init_leaves_the_reduce_motion_preference_alone(cx: &mut TestAppContext) {
cx.update(|cx| {
ThemeProvider::init(cx);
assert!(!cx.reduce_motion());
});
cx.update(|cx| {
cx.set_reduce_motion(true);
ThemeProvider::init_with(Theme::dark(), cx);
assert!(cx.reduce_motion());
set_reduce_motion(false, cx);
assert!(!ActiveTheme::reduce_motion(cx));
});
}
}