settings.rs

  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}