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