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