1use crate::fallback_themes::zed_default_dark;
2use crate::{
3 Appearance, IconTheme, SyntaxTheme, Theme, ThemeRegistry, ThemeStyleContent,
4 DEFAULT_ICON_THEME_NAME,
5};
6use anyhow::Result;
7use derive_more::{Deref, DerefMut};
8use gpui::{
9 px, App, Font, FontFallbacks, FontFeatures, FontStyle, FontWeight, Global, Pixels, Window,
10};
11use refineable::Refineable;
12use schemars::{
13 gen::SchemaGenerator,
14 schema::{InstanceType, Schema, SchemaObject},
15 JsonSchema,
16};
17use serde::{Deserialize, Serialize};
18use serde_json::Value;
19use settings::{add_references_to_properties, Settings, SettingsJsonSchemaParams, SettingsSources};
20use std::sync::Arc;
21use util::ResultExt as _;
22
23const MIN_FONT_SIZE: Pixels = px(6.0);
24const MIN_LINE_HEIGHT: f32 = 1.0;
25
26#[derive(
27 Debug,
28 Default,
29 PartialEq,
30 Eq,
31 PartialOrd,
32 Ord,
33 Hash,
34 Clone,
35 Copy,
36 Serialize,
37 Deserialize,
38 JsonSchema,
39)]
40
41/// Specifies the density of the UI.
42/// Note: This setting is still experimental. See [this tracking issue](https://github.com/zed-industries/zed/issues/18078)
43#[serde(rename_all = "snake_case")]
44pub enum UiDensity {
45 /// A denser UI with tighter spacing and smaller elements.
46 #[serde(alias = "compact")]
47 Compact,
48 #[default]
49 #[serde(alias = "default")]
50 /// The default UI density.
51 Default,
52 #[serde(alias = "comfortable")]
53 /// A looser UI with more spacing and larger elements.
54 Comfortable,
55}
56
57impl UiDensity {
58 /// The spacing ratio of a given density.
59 /// TODO: Standardize usage throughout the app or remove
60 pub fn spacing_ratio(self) -> f32 {
61 match self {
62 UiDensity::Compact => 0.75,
63 UiDensity::Default => 1.0,
64 UiDensity::Comfortable => 1.25,
65 }
66 }
67}
68
69impl From<String> for UiDensity {
70 fn from(s: String) -> Self {
71 match s.as_str() {
72 "compact" => Self::Compact,
73 "default" => Self::Default,
74 "comfortable" => Self::Comfortable,
75 _ => Self::default(),
76 }
77 }
78}
79
80impl From<UiDensity> for String {
81 fn from(val: UiDensity) -> Self {
82 match val {
83 UiDensity::Compact => "compact".to_string(),
84 UiDensity::Default => "default".to_string(),
85 UiDensity::Comfortable => "comfortable".to_string(),
86 }
87 }
88}
89
90/// Customizable settings for the UI and theme system.
91#[derive(Clone, PartialEq)]
92pub struct ThemeSettings {
93 /// The UI font size. Determines the size of text in the UI,
94 /// as well as the size of a [gpui::Rems] unit.
95 ///
96 /// Changing this will impact the size of all UI elements.
97 pub ui_font_size: Pixels,
98 /// The font used for UI elements.
99 pub ui_font: Font,
100 /// The font size used for buffers, and the terminal.
101 ///
102 /// The terminal font size can be overridden using it's own setting.
103 pub buffer_font_size: Pixels,
104 /// The font used for buffers, and the terminal.
105 ///
106 /// The terminal font family can be overridden using it's own setting.
107 pub buffer_font: Font,
108 /// The line height for buffers, and the terminal.
109 ///
110 /// Changing this may affect the spacing of some UI elements.
111 ///
112 /// The terminal font family can be overridden using it's own setting.
113 pub buffer_line_height: BufferLineHeight,
114 /// The current theme selection.
115 /// TODO: Document this further
116 pub theme_selection: Option<ThemeSelection>,
117 /// The active theme.
118 pub active_theme: Arc<Theme>,
119 /// Manual overrides for the active theme.
120 ///
121 /// Note: This setting is still experimental. See [this tracking issue](https://github.com/zed-industries/zed/issues/18078)
122 pub theme_overrides: Option<ThemeStyleContent>,
123 /// The active icon theme.
124 pub active_icon_theme: Arc<IconTheme>,
125 /// The density of the UI.
126 /// Note: This setting is still experimental. See [this tracking issue](
127 pub ui_density: UiDensity,
128 /// The amount of fading applied to unnecessary code.
129 pub unnecessary_code_fade: f32,
130}
131
132impl ThemeSettings {
133 const DEFAULT_LIGHT_THEME: &'static str = "One Light";
134 const DEFAULT_DARK_THEME: &'static str = "One Dark";
135
136 /// Returns the name of the default theme for the given [`Appearance`].
137 pub fn default_theme(appearance: Appearance) -> &'static str {
138 match appearance {
139 Appearance::Light => Self::DEFAULT_LIGHT_THEME,
140 Appearance::Dark => Self::DEFAULT_DARK_THEME,
141 }
142 }
143
144 /// Reloads the current theme.
145 ///
146 /// Reads the [`ThemeSettings`] to know which theme should be loaded,
147 /// taking into account the current [`SystemAppearance`].
148 pub fn reload_current_theme(cx: &mut App) {
149 let mut theme_settings = ThemeSettings::get_global(cx).clone();
150 let system_appearance = SystemAppearance::global(cx);
151
152 if let Some(theme_selection) = theme_settings.theme_selection.clone() {
153 let mut theme_name = theme_selection.theme(*system_appearance);
154
155 // If the selected theme doesn't exist, fall back to a default theme
156 // based on the system appearance.
157 let theme_registry = ThemeRegistry::global(cx);
158 if theme_registry.get(theme_name).ok().is_none() {
159 theme_name = Self::default_theme(*system_appearance);
160 };
161
162 if let Some(_theme) = theme_settings.switch_theme(theme_name, cx) {
163 ThemeSettings::override_global(theme_settings, cx);
164 }
165 }
166 }
167}
168
169/// The appearance of the system.
170#[derive(Debug, Clone, Copy, Deref)]
171pub struct SystemAppearance(pub Appearance);
172
173impl Default for SystemAppearance {
174 fn default() -> Self {
175 Self(Appearance::Dark)
176 }
177}
178
179#[derive(Deref, DerefMut, Default)]
180struct GlobalSystemAppearance(SystemAppearance);
181
182impl Global for GlobalSystemAppearance {}
183
184impl SystemAppearance {
185 /// Initializes the [`SystemAppearance`] for the application.
186 pub fn init(cx: &mut App) {
187 *cx.default_global::<GlobalSystemAppearance>() =
188 GlobalSystemAppearance(SystemAppearance(cx.window_appearance().into()));
189 }
190
191 /// Returns the global [`SystemAppearance`].
192 ///
193 /// Inserts a default [`SystemAppearance`] if one does not yet exist.
194 pub(crate) fn default_global(cx: &mut App) -> Self {
195 cx.default_global::<GlobalSystemAppearance>().0
196 }
197
198 /// Returns the global [`SystemAppearance`].
199 pub fn global(cx: &App) -> Self {
200 cx.global::<GlobalSystemAppearance>().0
201 }
202
203 /// Returns a mutable reference to the global [`SystemAppearance`].
204 pub fn global_mut(cx: &mut App) -> &mut Self {
205 cx.global_mut::<GlobalSystemAppearance>()
206 }
207}
208
209/// Represents the selection of a theme, which can be either static or dynamic.
210#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
211#[serde(untagged)]
212pub enum ThemeSelection {
213 /// A static theme selection, represented by a single theme name.
214 Static(#[schemars(schema_with = "theme_name_ref")] String),
215 /// A dynamic theme selection, which can change based the [ThemeMode].
216 Dynamic {
217 /// The mode used to determine which theme to use.
218 #[serde(default)]
219 mode: ThemeMode,
220 /// The theme to use for light mode.
221 #[schemars(schema_with = "theme_name_ref")]
222 light: String,
223 /// The theme to use for dark mode.
224 #[schemars(schema_with = "theme_name_ref")]
225 dark: String,
226 },
227}
228
229fn theme_name_ref(_: &mut SchemaGenerator) -> Schema {
230 Schema::new_ref("#/definitions/ThemeName".into())
231}
232
233// TODO: Rename ThemeMode -> ThemeAppearanceMode
234/// The mode use to select a theme.
235///
236/// `Light` and `Dark` will select their respective themes.
237///
238/// `System` will select the theme based on the system's appearance.
239#[derive(Debug, PartialEq, Eq, Clone, Copy, Default, Serialize, Deserialize, JsonSchema)]
240#[serde(rename_all = "snake_case")]
241pub enum ThemeMode {
242 /// Use the specified `light` theme.
243 Light,
244
245 /// Use the specified `dark` theme.
246 Dark,
247
248 /// Use the theme based on the system's appearance.
249 #[default]
250 System,
251}
252
253impl ThemeSelection {
254 /// Returns the theme name for the selected [ThemeMode].
255 pub fn theme(&self, system_appearance: Appearance) -> &str {
256 match self {
257 Self::Static(theme) => theme,
258 Self::Dynamic { mode, light, dark } => match mode {
259 ThemeMode::Light => light,
260 ThemeMode::Dark => dark,
261 ThemeMode::System => match system_appearance {
262 Appearance::Light => light,
263 Appearance::Dark => dark,
264 },
265 },
266 }
267 }
268
269 /// Returns the [ThemeMode] for the [ThemeSelection].
270 pub fn mode(&self) -> Option<ThemeMode> {
271 match self {
272 ThemeSelection::Static(_) => None,
273 ThemeSelection::Dynamic { mode, .. } => Some(*mode),
274 }
275 }
276}
277
278/// Settings for rendering text in UI and text buffers.
279#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
280pub struct ThemeSettingsContent {
281 /// The default font size for text in the UI.
282 #[serde(default)]
283 pub ui_font_size: Option<f32>,
284 /// The name of a font to use for rendering in the UI.
285 #[serde(default)]
286 pub ui_font_family: Option<String>,
287 /// The font fallbacks to use for rendering in the UI.
288 #[serde(default)]
289 #[schemars(default = "default_font_fallbacks")]
290 pub ui_font_fallbacks: Option<Vec<String>>,
291 /// The OpenType features to enable for text in the UI.
292 #[serde(default)]
293 #[schemars(default = "default_font_features")]
294 pub ui_font_features: Option<FontFeatures>,
295 /// The weight of the UI font in CSS units from 100 to 900.
296 #[serde(default)]
297 pub ui_font_weight: Option<f32>,
298 /// The name of a font to use for rendering in text buffers.
299 #[serde(default)]
300 pub buffer_font_family: Option<String>,
301 /// The font fallbacks to use for rendering in text buffers.
302 #[serde(default)]
303 #[schemars(default = "default_font_fallbacks")]
304 pub buffer_font_fallbacks: Option<Vec<String>>,
305 /// The default font size for rendering in text buffers.
306 #[serde(default)]
307 pub buffer_font_size: Option<f32>,
308 /// The weight of the editor font in CSS units from 100 to 900.
309 #[serde(default)]
310 pub buffer_font_weight: Option<f32>,
311 /// The buffer's line height.
312 #[serde(default)]
313 pub buffer_line_height: Option<BufferLineHeight>,
314 /// The OpenType features to enable for rendering in text buffers.
315 #[serde(default)]
316 #[schemars(default = "default_font_features")]
317 pub buffer_font_features: Option<FontFeatures>,
318 /// The name of the Zed theme to use.
319 #[serde(default)]
320 pub theme: Option<ThemeSelection>,
321 /// The name of the icon theme to use.
322 ///
323 /// Currently not exposed to the user.
324 #[serde(skip)]
325 #[serde(default)]
326 pub icon_theme: Option<String>,
327
328 /// UNSTABLE: Expect many elements to be broken.
329 ///
330 // Controls the density of the UI.
331 #[serde(rename = "unstable.ui_density", default)]
332 pub ui_density: Option<UiDensity>,
333
334 /// How much to fade out unused code.
335 #[serde(default)]
336 pub unnecessary_code_fade: Option<f32>,
337
338 /// EXPERIMENTAL: Overrides for the current theme.
339 ///
340 /// These values will override the ones on the current theme specified in `theme`.
341 #[serde(rename = "experimental.theme_overrides", default)]
342 pub theme_overrides: Option<ThemeStyleContent>,
343}
344
345fn default_font_features() -> Option<FontFeatures> {
346 Some(FontFeatures::default())
347}
348
349fn default_font_fallbacks() -> Option<FontFallbacks> {
350 Some(FontFallbacks::default())
351}
352
353impl ThemeSettingsContent {
354 /// Sets the theme for the given appearance to the theme with the specified name.
355 pub fn set_theme(&mut self, theme_name: String, appearance: Appearance) {
356 if let Some(selection) = self.theme.as_mut() {
357 let theme_to_update = match selection {
358 ThemeSelection::Static(theme) => theme,
359 ThemeSelection::Dynamic { mode, light, dark } => match mode {
360 ThemeMode::Light => light,
361 ThemeMode::Dark => dark,
362 ThemeMode::System => match appearance {
363 Appearance::Light => light,
364 Appearance::Dark => dark,
365 },
366 },
367 };
368
369 *theme_to_update = theme_name.to_string();
370 } else {
371 self.theme = Some(ThemeSelection::Static(theme_name.to_string()));
372 }
373 }
374
375 /// Sets the mode for the theme.
376 pub fn set_mode(&mut self, mode: ThemeMode) {
377 if let Some(selection) = self.theme.as_mut() {
378 match selection {
379 ThemeSelection::Static(theme) => {
380 // If the theme was previously set to a single static theme,
381 // we don't know whether it was a light or dark theme, so we
382 // just use it for both.
383 self.theme = Some(ThemeSelection::Dynamic {
384 mode,
385 light: theme.clone(),
386 dark: theme.clone(),
387 });
388 }
389 ThemeSelection::Dynamic {
390 mode: mode_to_update,
391 ..
392 } => *mode_to_update = mode,
393 }
394 } else {
395 self.theme = Some(ThemeSelection::Dynamic {
396 mode,
397 light: ThemeSettings::DEFAULT_LIGHT_THEME.into(),
398 dark: ThemeSettings::DEFAULT_DARK_THEME.into(),
399 });
400 }
401 }
402}
403
404/// The buffer's line height.
405#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)]
406#[serde(rename_all = "snake_case")]
407pub enum BufferLineHeight {
408 /// A less dense line height.
409 #[default]
410 Comfortable,
411 /// The default line height.
412 Standard,
413 /// A custom line height.
414 ///
415 /// A line height of 1.0 is the height of the buffer's font size.
416 Custom(f32),
417}
418
419impl BufferLineHeight {
420 /// Returns the value of the line height.
421 pub fn value(&self) -> f32 {
422 match self {
423 BufferLineHeight::Comfortable => 1.618,
424 BufferLineHeight::Standard => 1.3,
425 BufferLineHeight::Custom(line_height) => *line_height,
426 }
427 }
428}
429
430impl ThemeSettings {
431 /// Returns the [AdjustedBufferFontSize].
432 pub fn buffer_font_size(&self) -> Pixels {
433 Self::clamp_font_size(self.buffer_font_size)
434 }
435
436 /// Ensures that the font size is within the valid range.
437 pub fn clamp_font_size(size: Pixels) -> Pixels {
438 size.max(MIN_FONT_SIZE)
439 }
440
441 // TODO: Rename: `line_height` -> `buffer_line_height`
442 /// Returns the buffer's line height.
443 pub fn line_height(&self) -> f32 {
444 f32::max(self.buffer_line_height.value(), MIN_LINE_HEIGHT)
445 }
446
447 /// Switches to the theme with the given name, if it exists.
448 ///
449 /// Returns a `Some` containing the new theme if it was successful.
450 /// Returns `None` otherwise.
451 pub fn switch_theme(&mut self, theme: &str, cx: &mut App) -> Option<Arc<Theme>> {
452 let themes = ThemeRegistry::default_global(cx);
453
454 let mut new_theme = None;
455
456 if let Some(theme) = themes.get(theme).log_err() {
457 self.active_theme = theme.clone();
458 new_theme = Some(theme);
459 }
460
461 self.apply_theme_overrides();
462
463 new_theme
464 }
465
466 /// Applies the theme overrides, if there are any, to the current theme.
467 pub fn apply_theme_overrides(&mut self) {
468 if let Some(theme_overrides) = &self.theme_overrides {
469 let mut base_theme = (*self.active_theme).clone();
470
471 if let Some(window_background_appearance) = theme_overrides.window_background_appearance
472 {
473 base_theme.styles.window_background_appearance =
474 window_background_appearance.into();
475 }
476
477 base_theme
478 .styles
479 .colors
480 .refine(&theme_overrides.theme_colors_refinement());
481 base_theme
482 .styles
483 .status
484 .refine(&theme_overrides.status_colors_refinement());
485 base_theme.styles.player.merge(&theme_overrides.players);
486 base_theme.styles.accents.merge(&theme_overrides.accents);
487 base_theme.styles.syntax =
488 SyntaxTheme::merge(base_theme.styles.syntax, theme_overrides.syntax_overrides());
489
490 self.active_theme = Arc::new(base_theme);
491 }
492 }
493}
494
495// TODO: Make private, change usages to use `get_ui_font_size` instead.
496#[allow(missing_docs)]
497pub fn setup_ui_font(window: &mut Window, cx: &mut App) -> gpui::Font {
498 let (ui_font, ui_font_size) = {
499 let theme_settings = ThemeSettings::get_global(cx);
500 let font = theme_settings.ui_font.clone();
501 (font, theme_settings.ui_font_size)
502 };
503
504 window.set_rem_size(ui_font_size);
505 ui_font
506}
507
508fn clamp_font_weight(weight: f32) -> FontWeight {
509 FontWeight(weight.clamp(100., 950.))
510}
511
512impl settings::Settings for ThemeSettings {
513 const KEY: Option<&'static str> = None;
514
515 type FileContent = ThemeSettingsContent;
516
517 fn load(sources: SettingsSources<Self::FileContent>, cx: &mut App) -> Result<Self> {
518 let themes = ThemeRegistry::default_global(cx);
519 let system_appearance = SystemAppearance::default_global(cx);
520
521 let defaults = sources.default;
522 let mut this = Self {
523 ui_font_size: defaults.ui_font_size.unwrap().into(),
524 ui_font: Font {
525 family: defaults.ui_font_family.as_ref().unwrap().clone().into(),
526 features: defaults.ui_font_features.clone().unwrap(),
527 fallbacks: defaults
528 .ui_font_fallbacks
529 .as_ref()
530 .map(|fallbacks| FontFallbacks::from_fonts(fallbacks.clone())),
531 weight: defaults.ui_font_weight.map(FontWeight).unwrap(),
532 style: Default::default(),
533 },
534 buffer_font: Font {
535 family: defaults.buffer_font_family.as_ref().unwrap().clone().into(),
536 features: defaults.buffer_font_features.clone().unwrap(),
537 fallbacks: defaults
538 .buffer_font_fallbacks
539 .as_ref()
540 .map(|fallbacks| FontFallbacks::from_fonts(fallbacks.clone())),
541 weight: defaults.buffer_font_weight.map(FontWeight).unwrap(),
542 style: FontStyle::default(),
543 },
544 buffer_font_size: defaults.buffer_font_size.unwrap().into(),
545 buffer_line_height: defaults.buffer_line_height.unwrap(),
546 theme_selection: defaults.theme.clone(),
547 active_theme: themes
548 .get(defaults.theme.as_ref().unwrap().theme(*system_appearance))
549 .or(themes.get(&zed_default_dark().name))
550 .unwrap(),
551 theme_overrides: None,
552 active_icon_theme: defaults
553 .icon_theme
554 .as_ref()
555 .and_then(|name| themes.get_icon_theme(name).ok())
556 .unwrap_or_else(|| themes.get_icon_theme(DEFAULT_ICON_THEME_NAME).unwrap()),
557 ui_density: defaults.ui_density.unwrap_or(UiDensity::Default),
558 unnecessary_code_fade: defaults.unnecessary_code_fade.unwrap_or(0.0),
559 };
560
561 for value in sources
562 .user
563 .into_iter()
564 .chain(sources.release_channel)
565 .chain(sources.server)
566 {
567 if let Some(value) = value.ui_density {
568 this.ui_density = value;
569 }
570
571 if let Some(value) = value.buffer_font_family.clone() {
572 this.buffer_font.family = value.into();
573 }
574 if let Some(value) = value.buffer_font_features.clone() {
575 this.buffer_font.features = value;
576 }
577 if let Some(value) = value.buffer_font_fallbacks.clone() {
578 this.buffer_font.fallbacks = Some(FontFallbacks::from_fonts(value));
579 }
580 if let Some(value) = value.buffer_font_weight {
581 this.buffer_font.weight = clamp_font_weight(value);
582 }
583
584 if let Some(value) = value.ui_font_family.clone() {
585 this.ui_font.family = value.into();
586 }
587 if let Some(value) = value.ui_font_features.clone() {
588 this.ui_font.features = value;
589 }
590 if let Some(value) = value.ui_font_fallbacks.clone() {
591 this.ui_font.fallbacks = Some(FontFallbacks::from_fonts(value));
592 }
593 if let Some(value) = value.ui_font_weight {
594 this.ui_font.weight = clamp_font_weight(value);
595 }
596
597 if let Some(value) = &value.theme {
598 this.theme_selection = Some(value.clone());
599
600 let theme_name = value.theme(*system_appearance);
601
602 if let Some(theme) = themes.get(theme_name).log_err() {
603 this.active_theme = theme;
604 }
605 }
606
607 this.theme_overrides.clone_from(&value.theme_overrides);
608 this.apply_theme_overrides();
609
610 if let Some(value) = &value.icon_theme {
611 if let Some(icon_theme) = themes.get_icon_theme(value).log_err() {
612 this.active_icon_theme = icon_theme.clone();
613 }
614 }
615
616 merge(&mut this.ui_font_size, value.ui_font_size.map(Into::into));
617 this.ui_font_size = this.ui_font_size.clamp(px(6.), px(100.));
618
619 merge(
620 &mut this.buffer_font_size,
621 value.buffer_font_size.map(Into::into),
622 );
623 this.buffer_font_size = this.buffer_font_size.clamp(px(6.), px(100.));
624
625 merge(&mut this.buffer_line_height, value.buffer_line_height);
626
627 // Clamp the `unnecessary_code_fade` to ensure text can't disappear entirely.
628 merge(&mut this.unnecessary_code_fade, value.unnecessary_code_fade);
629 this.unnecessary_code_fade = this.unnecessary_code_fade.clamp(0.0, 0.9);
630 }
631
632 Ok(this)
633 }
634
635 fn json_schema(
636 generator: &mut SchemaGenerator,
637 params: &SettingsJsonSchemaParams,
638 cx: &App,
639 ) -> schemars::schema::RootSchema {
640 let mut root_schema = generator.root_schema_for::<ThemeSettingsContent>();
641 let theme_names = ThemeRegistry::global(cx)
642 .list_names()
643 .into_iter()
644 .map(|theme_name| Value::String(theme_name.to_string()))
645 .collect();
646
647 let theme_name_schema = SchemaObject {
648 instance_type: Some(InstanceType::String.into()),
649 enum_values: Some(theme_names),
650 ..Default::default()
651 };
652
653 root_schema.definitions.extend([
654 ("ThemeName".into(), theme_name_schema.into()),
655 ("FontFamilies".into(), params.font_family_schema()),
656 ("FontFallbacks".into(), params.font_fallback_schema()),
657 ]);
658
659 add_references_to_properties(
660 &mut root_schema,
661 &[
662 ("buffer_font_family", "#/definitions/FontFamilies"),
663 ("buffer_font_fallbacks", "#/definitions/FontFallbacks"),
664 ("ui_font_family", "#/definitions/FontFamilies"),
665 ("ui_font_fallbacks", "#/definitions/FontFallbacks"),
666 ],
667 );
668
669 root_schema
670 }
671}
672
673fn merge<T: Copy>(target: &mut T, value: Option<T>) {
674 if let Some(value) = value {
675 *target = value;
676 }
677}