1use crate::{
2 list::ListSettings, notification::NotificationSettings, scroll::ScrollbarShow,
3 sheet::SheetSettings,
4};
5use rgpui::{App, Global, Hsla, Pixels, SharedString, Window, WindowAppearance, px};
6use schemars::JsonSchema;
7use serde::{Deserialize, Serialize};
8use std::{
9 ops::{Deref, DerefMut},
10 rc::Rc,
11 sync::Arc,
12};
13
14mod color;
15pub mod highlight;
16mod registry;
17mod schema;
18mod theme_color;
19
20pub use color::*;
21pub use highlight::*;
22pub use registry::*;
23pub use schema::*;
24pub use theme_color::*;
25
26pub fn init(cx: &mut App) {
27 registry::init(cx);
28
29 Theme::change(ThemeMode::Light, None, cx);
31 Theme::sync_scrollbar_appearance(cx);
32}
33
34pub trait ActiveTheme {
35 fn theme(&self) -> &Theme;
36}
37
38impl ActiveTheme for App {
39 #[inline(always)]
40 fn theme(&self) -> &Theme {
41 Theme::global(self)
42 }
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
47pub struct Theme {
48 pub colors: ThemeColor,
49 pub highlight_theme: Arc<HighlightTheme>,
50 pub light_theme: Rc<ThemeConfig>,
51 pub dark_theme: Rc<ThemeConfig>,
52
53 pub mode: ThemeMode,
54 pub font_family: SharedString,
56 pub font_size: Pixels,
58 pub mono_font_family: SharedString,
66 pub mono_font_size: Pixels,
68 pub radius: Pixels,
70 pub radius_lg: Pixels,
72 pub shadow: bool,
73 pub transparent: Hsla,
74 pub scrollbar_show: ScrollbarShow,
76 #[serde(skip)]
78 pub notification: NotificationSettings,
79 pub tile_grid_size: Pixels,
81 pub tile_shadow: bool,
83 pub tile_radius: Pixels,
85 pub list: ListSettings,
87 pub sheet: SheetSettings,
89}
90
91impl Default for Theme {
92 fn default() -> Self {
93 Self::from(&ThemeColor::default())
94 }
95}
96
97impl Deref for Theme {
98 type Target = ThemeColor;
99
100 fn deref(&self) -> &Self::Target {
101 &self.colors
102 }
103}
104
105impl DerefMut for Theme {
106 fn deref_mut(&mut self) -> &mut Self::Target {
107 &mut self.colors
108 }
109}
110
111impl Global for Theme {}
112
113impl Theme {
114 #[inline(always)]
116 pub fn global(cx: &App) -> &Theme {
117 cx.global::<Theme>()
118 }
119
120 #[inline(always)]
122 pub fn global_mut(cx: &mut App) -> &mut Theme {
123 cx.global_mut::<Theme>()
124 }
125
126 #[inline(always)]
128 pub fn is_dark(&self) -> bool {
129 self.mode.is_dark()
130 }
131
132 pub fn theme_name(&self) -> &SharedString {
134 if self.is_dark() {
135 &self.dark_theme.name
136 } else {
137 &self.light_theme.name
138 }
139 }
140
141 pub fn sync_system_appearance(window: Option<&mut Window>, cx: &mut App) {
143 let appearance = window
146 .as_ref()
147 .map(|window| window.appearance())
148 .unwrap_or_else(|| cx.window_appearance());
149
150 Self::change(appearance, window, cx);
151 }
152
153 pub fn sync_scrollbar_appearance(cx: &mut App) {
155 Theme::global_mut(cx).scrollbar_show = if cx.should_auto_hide_scrollbars() {
156 ScrollbarShow::Scrolling
157 } else {
158 ScrollbarShow::Hover
159 };
160 }
161
162 pub fn change(mode: impl Into<ThemeMode>, window: Option<&mut Window>, cx: &mut App) {
164 let mode = mode.into();
165 if !cx.has_global::<Theme>() {
166 let mut theme = Theme::default();
167 theme.light_theme = ThemeRegistry::global(cx).default_light_theme().clone();
168 theme.dark_theme = ThemeRegistry::global(cx).default_dark_theme().clone();
169 cx.set_global(theme);
170 }
171
172 let theme = cx.global_mut::<Theme>();
173 theme.mode = mode;
174 if mode.is_dark() {
175 theme.apply_config(&theme.dark_theme.clone());
176 } else {
177 theme.apply_config(&theme.light_theme.clone());
178 }
179
180 if let Some(window) = window {
181 window.refresh();
182 }
183 }
184
185 #[inline]
190 pub fn input_background(&self) -> Hsla {
191 if self.is_dark() {
192 self.input.mix_oklab(self.transparent, 0.3)
193 } else {
194 self.background
195 }
196 }
197
198 #[inline]
200 pub fn editor_background(&self) -> Hsla {
201 self.highlight_theme
202 .style
203 .editor_background
204 .unwrap_or_else(|| self.input_background())
205 }
206}
207
208impl From<&ThemeColor> for Theme {
209 fn from(colors: &ThemeColor) -> Self {
210 Theme {
211 mode: ThemeMode::default(),
212 transparent: Hsla::transparent_black(),
213 font_family: ".SystemUIFont".into(),
214 font_size: px(16.),
215 mono_font_family: if cfg!(target_os = "macos") {
216 "Menlo".into()
218 } else if cfg!(target_os = "windows") {
219 "Consolas".into()
220 } else {
221 "DejaVu Sans Mono".into()
222 },
223 mono_font_size: px(13.),
224 radius: px(6.),
225 radius_lg: px(8.),
226 shadow: true,
227 scrollbar_show: ScrollbarShow::default(),
228 notification: NotificationSettings::default(),
229 tile_grid_size: px(8.),
230 tile_shadow: true,
231 tile_radius: px(0.),
232 list: ListSettings::default(),
233 colors: *colors,
234 light_theme: Rc::new(ThemeConfig::default()),
235 dark_theme: Rc::new(ThemeConfig::default()),
236 highlight_theme: HighlightTheme::default_light(),
237 sheet: SheetSettings::default(),
238 }
239 }
240}
241
242#[derive(
243 Debug,
244 Clone,
245 Copy,
246 Default,
247 PartialEq,
248 PartialOrd,
249 Eq,
250 Ord,
251 Hash,
252 Serialize,
253 Deserialize,
254 JsonSchema,
255)]
256#[serde(rename_all = "snake_case")]
257pub enum ThemeMode {
258 #[default]
259 Light,
260 Dark,
261}
262
263impl ThemeMode {
264 #[inline(always)]
265 pub fn is_dark(&self) -> bool {
266 matches!(self, Self::Dark)
267 }
268
269 pub fn name(&self) -> &'static str {
271 match self {
272 ThemeMode::Light => "light",
273 ThemeMode::Dark => "dark",
274 }
275 }
276}
277
278impl From<WindowAppearance> for ThemeMode {
279 fn from(appearance: WindowAppearance) -> Self {
280 match appearance {
281 WindowAppearance::Dark | WindowAppearance::VibrantDark => Self::Dark,
282 WindowAppearance::Light | WindowAppearance::VibrantLight => Self::Light,
283 }
284 }
285}