1use crate::one_themes::one_dark;
2use crate::{Appearance, SyntaxTheme, Theme, ThemeRegistry, ThemeStyleContent};
3use anyhow::Result;
4use derive_more::{Deref, DerefMut};
5use gpui::{
6 px, AppContext, Font, FontFeatures, FontStyle, FontWeight, Global, Pixels, Subscription,
7 ViewContext,
8};
9use refineable::Refineable;
10use schemars::{
11 gen::SchemaGenerator,
12 schema::{InstanceType, Schema, SchemaObject},
13 JsonSchema,
14};
15use serde::{Deserialize, Serialize};
16use serde_json::Value;
17use settings::{Settings, SettingsJsonSchemaParams};
18use std::sync::Arc;
19use util::ResultExt as _;
20
21const MIN_FONT_SIZE: Pixels = px(6.0);
22const MIN_LINE_HEIGHT: f32 = 1.0;
23
24#[derive(Clone)]
25pub struct ThemeSettings {
26 pub ui_font_size: Pixels,
27 pub ui_font: Font,
28 pub buffer_font: Font,
29 pub buffer_font_size: Pixels,
30 pub buffer_line_height: BufferLineHeight,
31 pub theme_selection: Option<ThemeSelection>,
32 pub active_theme: Arc<Theme>,
33 pub theme_overrides: Option<ThemeStyleContent>,
34}
35
36impl ThemeSettings {
37 /// Reloads the current theme.
38 ///
39 /// Reads the [`ThemeSettings`] to know which theme should be loaded,
40 /// taking into account the current [`SystemAppearance`].
41 pub fn reload_current_theme(cx: &mut AppContext) {
42 let mut theme_settings = ThemeSettings::get_global(cx).clone();
43
44 if let Some(theme_selection) = theme_settings.theme_selection.clone() {
45 let theme_name = theme_selection.theme(*SystemAppearance::global(cx));
46
47 if let Some(_theme) = theme_settings.switch_theme(&theme_name, cx) {
48 ThemeSettings::override_global(theme_settings, cx);
49 }
50 }
51 }
52}
53
54/// The appearance of the system.
55#[derive(Debug, Clone, Copy, Deref)]
56pub struct SystemAppearance(pub Appearance);
57
58impl Default for SystemAppearance {
59 fn default() -> Self {
60 Self(Appearance::Dark)
61 }
62}
63
64#[derive(Deref, DerefMut, Default)]
65struct GlobalSystemAppearance(SystemAppearance);
66
67impl Global for GlobalSystemAppearance {}
68
69impl SystemAppearance {
70 /// Initializes the [`SystemAppearance`] for the application.
71 pub fn init(cx: &mut AppContext) {
72 *cx.default_global::<GlobalSystemAppearance>() =
73 GlobalSystemAppearance(SystemAppearance(cx.window_appearance().into()));
74 }
75
76 /// Returns the global [`SystemAppearance`].
77 ///
78 /// Inserts a default [`SystemAppearance`] if one does not yet exist.
79 pub(crate) fn default_global(cx: &mut AppContext) -> Self {
80 cx.default_global::<GlobalSystemAppearance>().0
81 }
82
83 /// Returns the global [`SystemAppearance`].
84 pub fn global(cx: &AppContext) -> Self {
85 cx.global::<GlobalSystemAppearance>().0
86 }
87
88 /// Returns a mutable reference to the global [`SystemAppearance`].
89 pub fn global_mut(cx: &mut AppContext) -> &mut Self {
90 cx.global_mut::<GlobalSystemAppearance>()
91 }
92}
93
94#[derive(Default)]
95pub(crate) struct AdjustedBufferFontSize(Pixels);
96
97impl Global for AdjustedBufferFontSize {}
98
99#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
100#[serde(untagged)]
101pub enum ThemeSelection {
102 Static(#[schemars(schema_with = "theme_name_ref")] String),
103 Dynamic {
104 #[serde(default)]
105 mode: ThemeMode,
106 #[schemars(schema_with = "theme_name_ref")]
107 light: String,
108 #[schemars(schema_with = "theme_name_ref")]
109 dark: String,
110 },
111}
112
113fn theme_name_ref(_: &mut SchemaGenerator) -> Schema {
114 Schema::new_ref("#/definitions/ThemeName".into())
115}
116
117#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
118#[serde(rename_all = "snake_case")]
119pub enum ThemeMode {
120 /// Use the specified `light` theme.
121 Light,
122
123 /// Use the specified `dark` theme.
124 Dark,
125
126 /// Use the theme based on the system's appearance.
127 #[default]
128 System,
129}
130
131impl ThemeSelection {
132 pub fn theme(&self, system_appearance: Appearance) -> &str {
133 match self {
134 Self::Static(theme) => theme,
135 Self::Dynamic { mode, light, dark } => match mode {
136 ThemeMode::Light => light,
137 ThemeMode::Dark => dark,
138 ThemeMode::System => match system_appearance {
139 Appearance::Light => light,
140 Appearance::Dark => dark,
141 },
142 },
143 }
144 }
145}
146
147#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
148pub struct ThemeSettingsContent {
149 #[serde(default)]
150 pub ui_font_size: Option<f32>,
151 #[serde(default)]
152 pub ui_font_family: Option<String>,
153 #[serde(default)]
154 pub ui_font_features: Option<FontFeatures>,
155 #[serde(default)]
156 pub buffer_font_family: Option<String>,
157 #[serde(default)]
158 pub buffer_font_size: Option<f32>,
159 #[serde(default)]
160 pub buffer_line_height: Option<BufferLineHeight>,
161 #[serde(default)]
162 pub buffer_font_features: Option<FontFeatures>,
163 #[serde(default)]
164 pub theme: Option<ThemeSelection>,
165
166 /// EXPERIMENTAL: Overrides for the current theme.
167 ///
168 /// These values will override the ones on the current theme specified in `theme`.
169 #[serde(rename = "experimental.theme_overrides", default)]
170 pub theme_overrides: Option<ThemeStyleContent>,
171}
172
173#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)]
174#[serde(rename_all = "snake_case")]
175pub enum BufferLineHeight {
176 #[default]
177 Comfortable,
178 Standard,
179 Custom(f32),
180}
181
182impl BufferLineHeight {
183 pub fn value(&self) -> f32 {
184 match self {
185 BufferLineHeight::Comfortable => 1.618,
186 BufferLineHeight::Standard => 1.3,
187 BufferLineHeight::Custom(line_height) => *line_height,
188 }
189 }
190}
191
192impl ThemeSettings {
193 pub fn buffer_font_size(&self, cx: &AppContext) -> Pixels {
194 cx.try_global::<AdjustedBufferFontSize>()
195 .map_or(self.buffer_font_size, |size| size.0)
196 .max(MIN_FONT_SIZE)
197 }
198
199 pub fn line_height(&self) -> f32 {
200 f32::max(self.buffer_line_height.value(), MIN_LINE_HEIGHT)
201 }
202
203 /// Switches to the theme with the given name, if it exists.
204 ///
205 /// Returns a `Some` containing the new theme if it was successful.
206 /// Returns `None` otherwise.
207 pub fn switch_theme(&mut self, theme: &str, cx: &mut AppContext) -> Option<Arc<Theme>> {
208 let themes = ThemeRegistry::default_global(cx);
209
210 let mut new_theme = None;
211
212 if let Some(theme) = themes.get(&theme).log_err() {
213 self.active_theme = theme.clone();
214 new_theme = Some(theme);
215 }
216
217 self.apply_theme_overrides();
218
219 new_theme
220 }
221
222 /// Applies the theme overrides, if there are any, to the current theme.
223 pub fn apply_theme_overrides(&mut self) {
224 if let Some(theme_overrides) = &self.theme_overrides {
225 let mut base_theme = (*self.active_theme).clone();
226
227 base_theme
228 .styles
229 .colors
230 .refine(&theme_overrides.theme_colors_refinement());
231 base_theme
232 .styles
233 .status
234 .refine(&theme_overrides.status_colors_refinement());
235 base_theme.styles.syntax = Arc::new(SyntaxTheme {
236 highlights: {
237 let mut highlights = base_theme.styles.syntax.highlights.clone();
238 // Overrides come second in the highlight list so that they take precedence
239 // over the ones in the base theme.
240 highlights.extend(theme_overrides.syntax_overrides());
241 highlights
242 },
243 });
244
245 self.active_theme = Arc::new(base_theme);
246 }
247 }
248}
249
250pub fn observe_buffer_font_size_adjustment<V: 'static>(
251 cx: &mut ViewContext<V>,
252 f: impl 'static + Fn(&mut V, &mut ViewContext<V>),
253) -> Subscription {
254 cx.observe_global::<AdjustedBufferFontSize>(f)
255}
256
257pub fn adjusted_font_size(size: Pixels, cx: &mut AppContext) -> Pixels {
258 if let Some(AdjustedBufferFontSize(adjusted_size)) = cx.try_global::<AdjustedBufferFontSize>() {
259 let buffer_font_size = ThemeSettings::get_global(cx).buffer_font_size;
260 let delta = *adjusted_size - buffer_font_size;
261 size + delta
262 } else {
263 size
264 }
265 .max(MIN_FONT_SIZE)
266}
267
268pub fn adjust_font_size(cx: &mut AppContext, f: fn(&mut Pixels)) {
269 let buffer_font_size = ThemeSettings::get_global(cx).buffer_font_size;
270 let mut adjusted_size = cx
271 .try_global::<AdjustedBufferFontSize>()
272 .map_or(buffer_font_size, |adjusted_size| adjusted_size.0);
273
274 f(&mut adjusted_size);
275 adjusted_size = adjusted_size.max(MIN_FONT_SIZE);
276 cx.set_global(AdjustedBufferFontSize(adjusted_size));
277 cx.refresh();
278}
279
280pub fn reset_font_size(cx: &mut AppContext) {
281 if cx.has_global::<AdjustedBufferFontSize>() {
282 cx.remove_global::<AdjustedBufferFontSize>();
283 cx.refresh();
284 }
285}
286
287impl settings::Settings for ThemeSettings {
288 const KEY: Option<&'static str> = None;
289
290 type FileContent = ThemeSettingsContent;
291
292 fn load(
293 defaults: &Self::FileContent,
294 user_values: &[&Self::FileContent],
295 cx: &mut AppContext,
296 ) -> Result<Self> {
297 let themes = ThemeRegistry::default_global(cx);
298 let system_appearance = SystemAppearance::default_global(cx);
299
300 let mut this = Self {
301 ui_font_size: defaults.ui_font_size.unwrap().into(),
302 ui_font: Font {
303 family: defaults.ui_font_family.clone().unwrap().into(),
304 features: defaults.ui_font_features.clone().unwrap(),
305 weight: Default::default(),
306 style: Default::default(),
307 },
308 buffer_font: Font {
309 family: defaults.buffer_font_family.clone().unwrap().into(),
310 features: defaults.buffer_font_features.clone().unwrap(),
311 weight: FontWeight::default(),
312 style: FontStyle::default(),
313 },
314 buffer_font_size: defaults.buffer_font_size.unwrap().into(),
315 buffer_line_height: defaults.buffer_line_height.unwrap(),
316 theme_selection: defaults.theme.clone(),
317 active_theme: themes
318 .get(defaults.theme.as_ref().unwrap().theme(*system_appearance))
319 .or(themes.get(&one_dark().name))
320 .unwrap(),
321 theme_overrides: None,
322 };
323
324 for value in user_values.into_iter().copied().cloned() {
325 if let Some(value) = value.buffer_font_family {
326 this.buffer_font.family = value.into();
327 }
328 if let Some(value) = value.buffer_font_features {
329 this.buffer_font.features = value;
330 }
331
332 if let Some(value) = value.ui_font_family {
333 this.ui_font.family = value.into();
334 }
335 if let Some(value) = value.ui_font_features {
336 this.ui_font.features = value;
337 }
338
339 if let Some(value) = &value.theme {
340 this.theme_selection = Some(value.clone());
341
342 let theme_name = value.theme(*system_appearance);
343
344 if let Some(theme) = themes.get(theme_name).log_err() {
345 this.active_theme = theme;
346 }
347 }
348
349 this.theme_overrides = value.theme_overrides;
350 this.apply_theme_overrides();
351
352 merge(&mut this.ui_font_size, value.ui_font_size.map(Into::into));
353 merge(
354 &mut this.buffer_font_size,
355 value.buffer_font_size.map(Into::into),
356 );
357 merge(&mut this.buffer_line_height, value.buffer_line_height);
358 }
359
360 Ok(this)
361 }
362
363 fn json_schema(
364 generator: &mut SchemaGenerator,
365 params: &SettingsJsonSchemaParams,
366 cx: &AppContext,
367 ) -> schemars::schema::RootSchema {
368 let mut root_schema = generator.root_schema_for::<ThemeSettingsContent>();
369 let theme_names = ThemeRegistry::global(cx)
370 .list_names(params.staff_mode)
371 .into_iter()
372 .map(|theme_name| Value::String(theme_name.to_string()))
373 .collect();
374
375 let theme_name_schema = SchemaObject {
376 instance_type: Some(InstanceType::String.into()),
377 enum_values: Some(theme_names),
378 ..Default::default()
379 };
380
381 let available_fonts = params
382 .font_names
383 .iter()
384 .cloned()
385 .map(Value::String)
386 .collect();
387 let fonts_schema = SchemaObject {
388 instance_type: Some(InstanceType::String.into()),
389 enum_values: Some(available_fonts),
390 ..Default::default()
391 };
392 root_schema.definitions.extend([
393 ("ThemeName".into(), theme_name_schema.into()),
394 ("FontFamilies".into(), fonts_schema.into()),
395 ]);
396
397 root_schema
398 .schema
399 .object
400 .as_mut()
401 .unwrap()
402 .properties
403 .extend([
404 (
405 "buffer_font_family".to_owned(),
406 Schema::new_ref("#/definitions/FontFamilies".into()),
407 ),
408 (
409 "ui_font_family".to_owned(),
410 Schema::new_ref("#/definitions/FontFamilies".into()),
411 ),
412 ]);
413
414 root_schema
415 }
416}
417
418fn merge<T: Copy>(target: &mut T, value: Option<T>) {
419 if let Some(value) = value {
420 *target = value;
421 }
422}