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    #[serde(default)]
323    pub icon_theme: Option<String>,
324
325    /// UNSTABLE: Expect many elements to be broken.
326    ///
327    // Controls the density of the UI.
328    #[serde(rename = "unstable.ui_density", default)]
329    pub ui_density: Option<UiDensity>,
330
331    /// How much to fade out unused code.
332    #[serde(default)]
333    pub unnecessary_code_fade: Option<f32>,
334
335    /// EXPERIMENTAL: Overrides for the current theme.
336    ///
337    /// These values will override the ones on the current theme specified in `theme`.
338    #[serde(rename = "experimental.theme_overrides", default)]
339    pub theme_overrides: Option<ThemeStyleContent>,
340}
341
342fn default_font_features() -> Option<FontFeatures> {
343    Some(FontFeatures::default())
344}
345
346fn default_font_fallbacks() -> Option<FontFallbacks> {
347    Some(FontFallbacks::default())
348}
349
350impl ThemeSettingsContent {
351    /// Sets the theme for the given appearance to the theme with the specified name.
352    pub fn set_theme(&mut self, theme_name: String, appearance: Appearance) {
353        if let Some(selection) = self.theme.as_mut() {
354            let theme_to_update = match selection {
355                ThemeSelection::Static(theme) => theme,
356                ThemeSelection::Dynamic { mode, light, dark } => match mode {
357                    ThemeMode::Light => light,
358                    ThemeMode::Dark => dark,
359                    ThemeMode::System => match appearance {
360                        Appearance::Light => light,
361                        Appearance::Dark => dark,
362                    },
363                },
364            };
365
366            *theme_to_update = theme_name.to_string();
367        } else {
368            self.theme = Some(ThemeSelection::Static(theme_name.to_string()));
369        }
370    }
371
372    /// Sets the mode for the theme.
373    pub fn set_mode(&mut self, mode: ThemeMode) {
374        if let Some(selection) = self.theme.as_mut() {
375            match selection {
376                ThemeSelection::Static(theme) => {
377                    // If the theme was previously set to a single static theme,
378                    // we don't know whether it was a light or dark theme, so we
379                    // just use it for both.
380                    self.theme = Some(ThemeSelection::Dynamic {
381                        mode,
382                        light: theme.clone(),
383                        dark: theme.clone(),
384                    });
385                }
386                ThemeSelection::Dynamic {
387                    mode: mode_to_update,
388                    ..
389                } => *mode_to_update = mode,
390            }
391        } else {
392            self.theme = Some(ThemeSelection::Dynamic {
393                mode,
394                light: ThemeSettings::DEFAULT_LIGHT_THEME.into(),
395                dark: ThemeSettings::DEFAULT_DARK_THEME.into(),
396            });
397        }
398    }
399}
400
401/// The buffer's line height.
402#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)]
403#[serde(rename_all = "snake_case")]
404pub enum BufferLineHeight {
405    /// A less dense line height.
406    #[default]
407    Comfortable,
408    /// The default line height.
409    Standard,
410    /// A custom line height.
411    ///
412    /// A line height of 1.0 is the height of the buffer's font size.
413    Custom(f32),
414}
415
416impl BufferLineHeight {
417    /// Returns the value of the line height.
418    pub fn value(&self) -> f32 {
419        match self {
420            BufferLineHeight::Comfortable => 1.618,
421            BufferLineHeight::Standard => 1.3,
422            BufferLineHeight::Custom(line_height) => *line_height,
423        }
424    }
425}
426
427impl ThemeSettings {
428    /// Returns the buffer font size.
429    pub fn buffer_font_size(&self) -> Pixels {
430        Self::clamp_font_size(self.buffer_font_size)
431    }
432
433    /// Ensures that the font size is within the valid range.
434    pub fn clamp_font_size(size: Pixels) -> Pixels {
435        size.max(MIN_FONT_SIZE)
436    }
437
438    // TODO: Rename: `line_height` -> `buffer_line_height`
439    /// Returns the buffer's line height.
440    pub fn line_height(&self) -> f32 {
441        f32::max(self.buffer_line_height.value(), MIN_LINE_HEIGHT)
442    }
443
444    /// Switches to the theme with the given name, if it exists.
445    ///
446    /// Returns a `Some` containing the new theme if it was successful.
447    /// Returns `None` otherwise.
448    pub fn switch_theme(&mut self, theme: &str, cx: &mut App) -> Option<Arc<Theme>> {
449        let themes = ThemeRegistry::default_global(cx);
450
451        let mut new_theme = None;
452
453        if let Some(theme) = themes.get(theme).log_err() {
454            self.active_theme = theme.clone();
455            new_theme = Some(theme);
456        }
457
458        self.apply_theme_overrides();
459
460        new_theme
461    }
462
463    /// Applies the theme overrides, if there are any, to the current theme.
464    pub fn apply_theme_overrides(&mut self) {
465        if let Some(theme_overrides) = &self.theme_overrides {
466            let mut base_theme = (*self.active_theme).clone();
467
468            if let Some(window_background_appearance) = theme_overrides.window_background_appearance
469            {
470                base_theme.styles.window_background_appearance =
471                    window_background_appearance.into();
472            }
473
474            base_theme
475                .styles
476                .colors
477                .refine(&theme_overrides.theme_colors_refinement());
478            base_theme
479                .styles
480                .status
481                .refine(&theme_overrides.status_colors_refinement());
482            base_theme.styles.player.merge(&theme_overrides.players);
483            base_theme.styles.accents.merge(&theme_overrides.accents);
484            base_theme.styles.syntax =
485                SyntaxTheme::merge(base_theme.styles.syntax, theme_overrides.syntax_overrides());
486
487            self.active_theme = Arc::new(base_theme);
488        }
489    }
490}
491
492// TODO: Make private, change usages to use `get_ui_font_size` instead.
493#[allow(missing_docs)]
494pub fn setup_ui_font(window: &mut Window, cx: &mut App) -> gpui::Font {
495    let (ui_font, ui_font_size) = {
496        let theme_settings = ThemeSettings::get_global(cx);
497        let font = theme_settings.ui_font.clone();
498        (font, theme_settings.ui_font_size)
499    };
500
501    window.set_rem_size(ui_font_size);
502    ui_font
503}
504
505fn clamp_font_weight(weight: f32) -> FontWeight {
506    FontWeight(weight.clamp(100., 950.))
507}
508
509impl settings::Settings for ThemeSettings {
510    const KEY: Option<&'static str> = None;
511
512    type FileContent = ThemeSettingsContent;
513
514    fn load(sources: SettingsSources<Self::FileContent>, cx: &mut App) -> Result<Self> {
515        let themes = ThemeRegistry::default_global(cx);
516        let system_appearance = SystemAppearance::default_global(cx);
517
518        let defaults = sources.default;
519        let mut this = Self {
520            ui_font_size: defaults.ui_font_size.unwrap().into(),
521            ui_font: Font {
522                family: defaults.ui_font_family.as_ref().unwrap().clone().into(),
523                features: defaults.ui_font_features.clone().unwrap(),
524                fallbacks: defaults
525                    .ui_font_fallbacks
526                    .as_ref()
527                    .map(|fallbacks| FontFallbacks::from_fonts(fallbacks.clone())),
528                weight: defaults.ui_font_weight.map(FontWeight).unwrap(),
529                style: Default::default(),
530            },
531            buffer_font: Font {
532                family: defaults.buffer_font_family.as_ref().unwrap().clone().into(),
533                features: defaults.buffer_font_features.clone().unwrap(),
534                fallbacks: defaults
535                    .buffer_font_fallbacks
536                    .as_ref()
537                    .map(|fallbacks| FontFallbacks::from_fonts(fallbacks.clone())),
538                weight: defaults.buffer_font_weight.map(FontWeight).unwrap(),
539                style: FontStyle::default(),
540            },
541            buffer_font_size: defaults.buffer_font_size.unwrap().into(),
542            buffer_line_height: defaults.buffer_line_height.unwrap(),
543            theme_selection: defaults.theme.clone(),
544            active_theme: themes
545                .get(defaults.theme.as_ref().unwrap().theme(*system_appearance))
546                .or(themes.get(&zed_default_dark().name))
547                .unwrap(),
548            theme_overrides: None,
549            active_icon_theme: defaults
550                .icon_theme
551                .as_ref()
552                .and_then(|name| themes.get_icon_theme(name).ok())
553                .unwrap_or_else(|| themes.get_icon_theme(DEFAULT_ICON_THEME_NAME).unwrap()),
554            ui_density: defaults.ui_density.unwrap_or(UiDensity::Default),
555            unnecessary_code_fade: defaults.unnecessary_code_fade.unwrap_or(0.0),
556        };
557
558        for value in sources
559            .user
560            .into_iter()
561            .chain(sources.release_channel)
562            .chain(sources.server)
563        {
564            if let Some(value) = value.ui_density {
565                this.ui_density = value;
566            }
567
568            if let Some(value) = value.buffer_font_family.clone() {
569                this.buffer_font.family = value.into();
570            }
571            if let Some(value) = value.buffer_font_features.clone() {
572                this.buffer_font.features = value;
573            }
574            if let Some(value) = value.buffer_font_fallbacks.clone() {
575                this.buffer_font.fallbacks = Some(FontFallbacks::from_fonts(value));
576            }
577            if let Some(value) = value.buffer_font_weight {
578                this.buffer_font.weight = clamp_font_weight(value);
579            }
580
581            if let Some(value) = value.ui_font_family.clone() {
582                this.ui_font.family = value.into();
583            }
584            if let Some(value) = value.ui_font_features.clone() {
585                this.ui_font.features = value;
586            }
587            if let Some(value) = value.ui_font_fallbacks.clone() {
588                this.ui_font.fallbacks = Some(FontFallbacks::from_fonts(value));
589            }
590            if let Some(value) = value.ui_font_weight {
591                this.ui_font.weight = clamp_font_weight(value);
592            }
593
594            if let Some(value) = &value.theme {
595                this.theme_selection = Some(value.clone());
596
597                let theme_name = value.theme(*system_appearance);
598
599                if let Some(theme) = themes.get(theme_name).log_err() {
600                    this.active_theme = theme;
601                }
602            }
603
604            this.theme_overrides.clone_from(&value.theme_overrides);
605            this.apply_theme_overrides();
606
607            if let Some(value) = &value.icon_theme {
608                if let Some(icon_theme) = themes.get_icon_theme(value).log_err() {
609                    this.active_icon_theme = icon_theme.clone();
610                }
611            }
612
613            merge(&mut this.ui_font_size, value.ui_font_size.map(Into::into));
614            this.ui_font_size = this.ui_font_size.clamp(px(6.), px(100.));
615
616            merge(
617                &mut this.buffer_font_size,
618                value.buffer_font_size.map(Into::into),
619            );
620            this.buffer_font_size = this.buffer_font_size.clamp(px(6.), px(100.));
621
622            merge(&mut this.buffer_line_height, value.buffer_line_height);
623
624            // Clamp the `unnecessary_code_fade` to ensure text can't disappear entirely.
625            merge(&mut this.unnecessary_code_fade, value.unnecessary_code_fade);
626            this.unnecessary_code_fade = this.unnecessary_code_fade.clamp(0.0, 0.9);
627        }
628
629        Ok(this)
630    }
631
632    fn json_schema(
633        generator: &mut SchemaGenerator,
634        params: &SettingsJsonSchemaParams,
635        cx: &App,
636    ) -> schemars::schema::RootSchema {
637        let mut root_schema = generator.root_schema_for::<ThemeSettingsContent>();
638        let theme_names = ThemeRegistry::global(cx)
639            .list_names()
640            .into_iter()
641            .map(|theme_name| Value::String(theme_name.to_string()))
642            .collect();
643
644        let theme_name_schema = SchemaObject {
645            instance_type: Some(InstanceType::String.into()),
646            enum_values: Some(theme_names),
647            ..Default::default()
648        };
649
650        root_schema.definitions.extend([
651            ("ThemeName".into(), theme_name_schema.into()),
652            ("FontFamilies".into(), params.font_family_schema()),
653            ("FontFallbacks".into(), params.font_fallback_schema()),
654        ]);
655
656        add_references_to_properties(
657            &mut root_schema,
658            &[
659                ("buffer_font_family", "#/definitions/FontFamilies"),
660                ("buffer_font_fallbacks", "#/definitions/FontFallbacks"),
661                ("ui_font_family", "#/definitions/FontFamilies"),
662                ("ui_font_fallbacks", "#/definitions/FontFallbacks"),
663            ],
664        );
665
666        root_schema
667    }
668}
669
670fn merge<T: Copy>(target: &mut T, value: Option<T>) {
671    if let Some(value) = value {
672        *target = value;
673    }
674}