use crate::{App, Global, Hsla, Pixels, SharedString, Window, WindowAppearance, px};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{
ops::{Deref, DerefMut},
rc::Rc,
sync::Arc,
};
pub mod color;
pub mod highlight;
pub mod registry;
pub mod schema;
pub mod settings;
mod theme_color;
pub use color::*;
pub use highlight::*;
pub use registry::*;
pub use schema::*;
pub use settings::*;
pub use theme_color::*;
pub fn init(cx: &mut App) {
registry::init(cx);
Theme::change(ThemeMode::Light, None, cx);
Theme::sync_scrollbar_appearance(cx);
}
pub trait ActiveTheme {
fn theme(&self) -> &Theme;
}
impl ActiveTheme for App {
#[inline(always)]
fn theme(&self) -> &Theme {
Theme::global(self)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct Theme {
pub colors: ThemeColor,
#[serde(default)]
pub tokens: ThemeTokens,
pub highlight_theme: Arc<HighlightTheme>,
pub light_theme: Rc<ThemeConfig>,
pub dark_theme: Rc<ThemeConfig>,
pub mode: ThemeMode,
pub font_family: SharedString,
pub font_size: Pixels,
pub mono_font_family: SharedString,
pub mono_font_size: Pixels,
pub radius: Pixels,
pub radius_lg: Pixels,
pub shadow: bool,
pub transparent: Hsla,
pub scrollbar_show: ScrollbarShow,
#[serde(skip)]
pub notification: NotificationSettings,
pub tile_grid_size: Pixels,
pub tile_shadow: bool,
pub tile_radius: Pixels,
pub list: crate::theme::settings::ListSettings,
pub sheet: SheetSettings,
}
impl Default for Theme {
fn default() -> Self {
let (colors, highlight_theme) = &DEFAULT_THEME_COLORS[&ThemeMode::Light];
let mut theme = Self::from(&**colors);
theme.highlight_theme = highlight_theme.clone();
theme
}
}
impl Deref for Theme {
type Target = ThemeColor;
fn deref(&self) -> &Self::Target {
&self.colors
}
}
impl DerefMut for Theme {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.colors
}
}
impl Global for Theme {}
impl Theme {
#[inline(always)]
pub fn global(cx: &App) -> &Theme {
cx.global::<Theme>()
}
#[inline(always)]
pub fn global_mut(cx: &mut App) -> &mut Theme {
cx.global_mut::<Theme>()
}
#[inline(always)]
pub fn is_dark(&self) -> bool {
self.mode.is_dark()
}
pub fn theme_name(&self) -> &SharedString {
if self.is_dark() {
&self.dark_theme.name
} else {
&self.light_theme.name
}
}
pub fn sync_system_appearance(window: Option<&mut Window>, cx: &mut App) {
let appearance = window
.as_ref()
.map(|window| window.appearance())
.unwrap_or_else(|| cx.window_appearance());
Self::change(appearance, window, cx);
}
pub fn sync_scrollbar_appearance(cx: &mut App) {
Theme::global_mut(cx).scrollbar_show = if cx.should_auto_hide_scrollbars() {
ScrollbarShow::Scrolling
} else {
ScrollbarShow::Hover
};
}
pub fn change(mode: impl Into<ThemeMode>, window: Option<&mut Window>, cx: &mut App) {
let mode = mode.into();
if !cx.has_global::<Theme>() {
let mut theme = Theme::default();
theme.light_theme = ThemeRegistry::global(cx).default_light_theme().clone();
theme.dark_theme = ThemeRegistry::global(cx).default_dark_theme().clone();
cx.set_global(theme);
}
let theme = cx.global_mut::<Theme>();
theme.mode = mode;
if mode.is_dark() {
theme.apply_config(&theme.dark_theme.clone());
} else {
theme.apply_config(&theme.light_theme.clone());
}
if let Some(window) = window {
window.refresh();
}
}
pub fn apply_named(name: &str, window: Option<&mut Window>, cx: &mut App) -> bool {
let Some(config) = ThemeRegistry::global(cx).themes().get(name).cloned() else {
return false;
};
if !cx.has_global::<Theme>() {
let mut theme = Theme::default();
theme.light_theme = ThemeRegistry::global(cx).default_light_theme().clone();
theme.dark_theme = ThemeRegistry::global(cx).default_dark_theme().clone();
cx.set_global(theme);
}
let theme = cx.global_mut::<Theme>();
theme.mode = config.mode;
theme.apply_config(&config);
if let Some(window) = window {
window.refresh();
}
true
}
#[inline]
pub fn input_background(&self) -> Hsla {
if self.is_dark() {
self.input.mix_oklab(self.transparent, 0.3)
} else {
self.background
}
}
#[inline]
pub(crate) fn editor_background(&self) -> Hsla {
self.highlight_theme
.style
.editor_background
.unwrap_or_else(|| self.input_background())
}
}
impl From<&ThemeColor> for Theme {
fn from(colors: &ThemeColor) -> Self {
Theme {
mode: ThemeMode::default(),
transparent: Hsla::transparent_black(),
font_family: ".SystemUIFont".into(),
font_size: px(16.),
mono_font_family: if cfg!(target_os = "macos") {
"Menlo".into()
} else if cfg!(target_os = "windows") {
"Consolas".into()
} else {
"DejaVu Sans Mono".into()
},
mono_font_size: px(13.),
radius: px(6.),
radius_lg: px(8.),
shadow: true,
scrollbar_show: ScrollbarShow::default(),
notification: NotificationSettings::default(),
tile_grid_size: px(8.),
tile_shadow: true,
tile_radius: px(0.),
list: crate::theme::settings::ListSettings::default(),
colors: *colors,
tokens: ThemeTokens::from(colors),
light_theme: Rc::new(ThemeConfig::default()),
dark_theme: Rc::new(ThemeConfig::default()),
highlight_theme: HighlightTheme::default_light(),
sheet: SheetSettings::default(),
}
}
}
#[derive(
Debug,
Clone,
Copy,
Default,
PartialEq,
PartialOrd,
Eq,
Ord,
Hash,
Serialize,
Deserialize,
JsonSchema,
)]
#[serde(rename_all = "snake_case")]
pub enum ThemeMode {
#[default]
Light,
Dark,
}
impl ThemeMode {
#[inline(always)]
pub fn is_dark(&self) -> bool {
matches!(self, Self::Dark)
}
pub fn name(&self) -> &'static str {
match self {
ThemeMode::Light => "light",
ThemeMode::Dark => "dark",
}
}
}
impl From<WindowAppearance> for ThemeMode {
fn from(appearance: WindowAppearance) -> Self {
match appearance {
WindowAppearance::Dark | WindowAppearance::VibrantDark => Self::Dark,
WindowAppearance::Light | WindowAppearance::VibrantLight => Self::Light,
}
}
}
#[cfg(test)]
mod tests {
use super::{Theme, ThemeRegistry};
const PACK_JSON: &str = r#"{"name":"pack","themes":[
{"name":"Pack Dark","mode":"dark"},
{"name":"Pack Light","mode":"light"}
]}"#;
#[rgpui::test]
fn apply_named_applies_registered_theme(cx: &mut crate::TestAppContext) {
cx.update(|cx| {
super::registry::init(cx);
ThemeRegistry::global_mut(cx)
.load_themes_from_str(PACK_JSON)
.expect("fixture pack should parse");
assert!(Theme::apply_named("Pack Dark", None, cx));
assert!(Theme::global(cx).is_dark());
assert_eq!(Theme::global(cx).theme_name().as_str(), "Pack Dark");
assert!(Theme::apply_named("Pack Light", None, cx));
assert!(!Theme::global(cx).is_dark());
assert_eq!(Theme::global(cx).theme_name().as_str(), "Pack Light");
assert!(!Theme::apply_named("No Such Theme", None, cx));
assert_eq!(Theme::global(cx).theme_name().as_str(), "Pack Light");
});
}
}