1use crate::fallback_themes::zed_default_dark;
2use crate::{
3 Appearance, DEFAULT_ICON_THEME_NAME, IconTheme, IconThemeNotFoundError, SyntaxTheme, Theme,
4 ThemeNotFoundError, ThemeRegistry, ThemeStyleContent,
5};
6use anyhow::Result;
7use collections::HashMap;
8use derive_more::{Deref, DerefMut};
9use gpui::{
10 App, Context, Font, FontFallbacks, FontFeatures, FontStyle, FontWeight, Global, Pixels,
11 SharedString, Subscription, Window, px,
12};
13use refineable::Refineable;
14use schemars::{JsonSchema, json_schema};
15use serde::{Deserialize, Serialize};
16use settings::{ParameterizedJsonSchema, Settings, SettingsSources};
17use std::sync::Arc;
18use util::ResultExt as _;
19use util::schemars::replace_subschema;
20
21const MIN_FONT_SIZE: Pixels = px(6.0);
22const MIN_LINE_HEIGHT: f32 = 1.0;
23
24#[derive(
25 Debug,
26 Default,
27 PartialEq,
28 Eq,
29 PartialOrd,
30 Ord,
31 Hash,
32 Clone,
33 Copy,
34 Serialize,
35 Deserialize,
36 JsonSchema,
37)]
38
39/// Specifies the density of the UI.
40/// Note: This setting is still experimental. See [this tracking issue](https://github.com/zed-industries/zed/issues/18078)
41#[serde(rename_all = "snake_case")]
42pub enum UiDensity {
43 /// A denser UI with tighter spacing and smaller elements.
44 #[serde(alias = "compact")]
45 Compact,
46 #[default]
47 #[serde(alias = "default")]
48 /// The default UI density.
49 Default,
50 #[serde(alias = "comfortable")]
51 /// A looser UI with more spacing and larger elements.
52 Comfortable,
53}
54
55impl UiDensity {
56 /// The spacing ratio of a given density.
57 /// TODO: Standardize usage throughout the app or remove
58 pub fn spacing_ratio(self) -> f32 {
59 match self {
60 UiDensity::Compact => 0.75,
61 UiDensity::Default => 1.0,
62 UiDensity::Comfortable => 1.25,
63 }
64 }
65}
66
67impl From<String> for UiDensity {
68 fn from(s: String) -> Self {
69 match s.as_str() {
70 "compact" => Self::Compact,
71 "default" => Self::Default,
72 "comfortable" => Self::Comfortable,
73 _ => Self::default(),
74 }
75 }
76}
77
78impl From<UiDensity> for String {
79 fn from(val: UiDensity) -> Self {
80 match val {
81 UiDensity::Compact => "compact".to_string(),
82 UiDensity::Default => "default".to_string(),
83 UiDensity::Comfortable => "comfortable".to_string(),
84 }
85 }
86}
87
88/// Customizable settings for the UI and theme system.
89#[derive(Clone, PartialEq)]
90pub struct ThemeSettings {
91 /// The UI font size. Determines the size of text in the UI,
92 /// as well as the size of a [gpui::Rems] unit.
93 ///
94 /// Changing this will impact the size of all UI elements.
95 ui_font_size: Pixels,
96 /// The font used for UI elements.
97 pub ui_font: Font,
98 /// The font size used for buffers, and the terminal.
99 ///
100 /// The terminal font size can be overridden using it's own setting.
101 buffer_font_size: Pixels,
102 /// The font used for buffers, and the terminal.
103 ///
104 /// The terminal font family can be overridden using it's own setting.
105 pub buffer_font: Font,
106 /// The agent font size. Determines the size of text in the agent panel.
107 agent_font_size: Pixels,
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 pub theme_selection: Option<ThemeSelection>,
116 /// The active theme.
117 pub active_theme: Arc<Theme>,
118 /// Manual overrides for the active theme.
119 ///
120 /// Note: This setting is still experimental. See [this tracking issue](https://github.com/zed-industries/zed/issues/18078)
121 pub experimental_theme_overrides: Option<ThemeStyleContent>,
122 /// Manual overrides per theme
123 pub theme_overrides: HashMap<String, ThemeStyleContent>,
124 /// The current icon theme selection.
125 pub icon_theme_selection: Option<IconThemeSelection>,
126 /// The active icon theme.
127 pub active_icon_theme: Arc<IconTheme>,
128 /// The density of the UI.
129 /// Note: This setting is still experimental. See [this tracking issue](
130 pub ui_density: UiDensity,
131 /// The amount of fading applied to unnecessary code.
132 pub unnecessary_code_fade: f32,
133}
134
135impl ThemeSettings {
136 const DEFAULT_LIGHT_THEME: &'static str = "One Light";
137 const DEFAULT_DARK_THEME: &'static str = "One Dark";
138
139 /// Returns the name of the default theme for the given [`Appearance`].
140 pub fn default_theme(appearance: Appearance) -> &'static str {
141 match appearance {
142 Appearance::Light => Self::DEFAULT_LIGHT_THEME,
143 Appearance::Dark => Self::DEFAULT_DARK_THEME,
144 }
145 }
146
147 /// Reloads the current theme.
148 ///
149 /// Reads the [`ThemeSettings`] to know which theme should be loaded,
150 /// taking into account the current [`SystemAppearance`].
151 pub fn reload_current_theme(cx: &mut App) {
152 let mut theme_settings = ThemeSettings::get_global(cx).clone();
153 let system_appearance = SystemAppearance::global(cx);
154
155 if let Some(theme_selection) = theme_settings.theme_selection.clone() {
156 let mut theme_name = theme_selection.theme(*system_appearance);
157
158 // If the selected theme doesn't exist, fall back to a default theme
159 // based on the system appearance.
160 let theme_registry = ThemeRegistry::global(cx);
161 if let Err(err @ ThemeNotFoundError(_)) = theme_registry.get(theme_name) {
162 if theme_registry.extensions_loaded() {
163 log::error!("{err}");
164 }
165
166 theme_name = Self::default_theme(*system_appearance);
167 };
168
169 if let Some(_theme) = theme_settings.switch_theme(theme_name, cx) {
170 ThemeSettings::override_global(theme_settings, cx);
171 }
172 }
173 }
174
175 /// Reloads the current icon theme.
176 ///
177 /// Reads the [`ThemeSettings`] to know which icon theme should be loaded,
178 /// taking into account the current [`SystemAppearance`].
179 pub fn reload_current_icon_theme(cx: &mut App) {
180 let mut theme_settings = ThemeSettings::get_global(cx).clone();
181 let system_appearance = SystemAppearance::global(cx);
182
183 if let Some(icon_theme_selection) = theme_settings.icon_theme_selection.clone() {
184 let mut icon_theme_name = icon_theme_selection.icon_theme(*system_appearance);
185
186 // If the selected icon theme doesn't exist, fall back to the default theme.
187 let theme_registry = ThemeRegistry::global(cx);
188 if let Err(err @ IconThemeNotFoundError(_)) =
189 theme_registry.get_icon_theme(icon_theme_name)
190 {
191 if theme_registry.extensions_loaded() {
192 log::error!("{err}");
193 }
194
195 icon_theme_name = DEFAULT_ICON_THEME_NAME;
196 };
197
198 if let Some(_theme) = theme_settings.switch_icon_theme(icon_theme_name, cx) {
199 ThemeSettings::override_global(theme_settings, cx);
200 }
201 }
202 }
203}
204
205/// The appearance of the system.
206#[derive(Debug, Clone, Copy, Deref)]
207pub struct SystemAppearance(pub Appearance);
208
209impl Default for SystemAppearance {
210 fn default() -> Self {
211 Self(Appearance::Dark)
212 }
213}
214
215#[derive(Deref, DerefMut, Default)]
216struct GlobalSystemAppearance(SystemAppearance);
217
218impl Global for GlobalSystemAppearance {}
219
220impl SystemAppearance {
221 /// Initializes the [`SystemAppearance`] for the application.
222 pub fn init(cx: &mut App) {
223 *cx.default_global::<GlobalSystemAppearance>() =
224 GlobalSystemAppearance(SystemAppearance(cx.window_appearance().into()));
225 }
226
227 /// Returns the global [`SystemAppearance`].
228 ///
229 /// Inserts a default [`SystemAppearance`] if one does not yet exist.
230 pub(crate) fn default_global(cx: &mut App) -> Self {
231 cx.default_global::<GlobalSystemAppearance>().0
232 }
233
234 /// Returns the global [`SystemAppearance`].
235 pub fn global(cx: &App) -> Self {
236 cx.global::<GlobalSystemAppearance>().0
237 }
238
239 /// Returns a mutable reference to the global [`SystemAppearance`].
240 pub fn global_mut(cx: &mut App) -> &mut Self {
241 cx.global_mut::<GlobalSystemAppearance>()
242 }
243}
244
245#[derive(Default)]
246struct BufferFontSize(Pixels);
247
248impl Global for BufferFontSize {}
249
250#[derive(Default)]
251pub(crate) struct UiFontSize(Pixels);
252
253impl Global for UiFontSize {}
254
255#[derive(Default)]
256pub(crate) struct AgentFontSize(Pixels);
257
258impl Global for AgentFontSize {}
259
260/// Represents the selection of a theme, which can be either static or dynamic.
261#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
262#[serde(untagged)]
263pub enum ThemeSelection {
264 /// A static theme selection, represented by a single theme name.
265 Static(ThemeName),
266 /// A dynamic theme selection, which can change based the [ThemeMode].
267 Dynamic {
268 /// The mode used to determine which theme to use.
269 #[serde(default)]
270 mode: ThemeMode,
271 /// The theme to use for light mode.
272 light: ThemeName,
273 /// The theme to use for dark mode.
274 dark: ThemeName,
275 },
276}
277
278// TODO: Rename ThemeMode -> ThemeAppearanceMode
279/// The mode use to select a theme.
280///
281/// `Light` and `Dark` will select their respective themes.
282///
283/// `System` will select the theme based on the system's appearance.
284#[derive(Debug, PartialEq, Eq, Clone, Copy, Default, Serialize, Deserialize, JsonSchema)]
285#[serde(rename_all = "snake_case")]
286pub enum ThemeMode {
287 /// Use the specified `light` theme.
288 Light,
289
290 /// Use the specified `dark` theme.
291 Dark,
292
293 /// Use the theme based on the system's appearance.
294 #[default]
295 System,
296}
297
298impl ThemeSelection {
299 /// Returns the theme name for the selected [ThemeMode].
300 pub fn theme(&self, system_appearance: Appearance) -> &str {
301 match self {
302 Self::Static(theme) => &theme.0,
303 Self::Dynamic { mode, light, dark } => match mode {
304 ThemeMode::Light => &light.0,
305 ThemeMode::Dark => &dark.0,
306 ThemeMode::System => match system_appearance {
307 Appearance::Light => &light.0,
308 Appearance::Dark => &dark.0,
309 },
310 },
311 }
312 }
313
314 /// Returns the [ThemeMode] for the [ThemeSelection].
315 pub fn mode(&self) -> Option<ThemeMode> {
316 match self {
317 ThemeSelection::Static(_) => None,
318 ThemeSelection::Dynamic { mode, .. } => Some(*mode),
319 }
320 }
321}
322
323/// Represents the selection of an icon theme, which can be either static or dynamic.
324#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
325#[serde(untagged)]
326pub enum IconThemeSelection {
327 /// A static icon theme selection, represented by a single icon theme name.
328 Static(IconThemeName),
329 /// A dynamic icon theme selection, which can change based on the [`ThemeMode`].
330 Dynamic {
331 /// The mode used to determine which theme to use.
332 #[serde(default)]
333 mode: ThemeMode,
334 /// The icon theme to use for light mode.
335 light: IconThemeName,
336 /// The icon theme to use for dark mode.
337 dark: IconThemeName,
338 },
339}
340
341impl IconThemeSelection {
342 /// Returns the icon theme name based on the given [`Appearance`].
343 pub fn icon_theme(&self, system_appearance: Appearance) -> &str {
344 match self {
345 Self::Static(theme) => &theme.0,
346 Self::Dynamic { mode, light, dark } => match mode {
347 ThemeMode::Light => &light.0,
348 ThemeMode::Dark => &dark.0,
349 ThemeMode::System => match system_appearance {
350 Appearance::Light => &light.0,
351 Appearance::Dark => &dark.0,
352 },
353 },
354 }
355 }
356
357 /// Returns the [`ThemeMode`] for the [`IconThemeSelection`].
358 pub fn mode(&self) -> Option<ThemeMode> {
359 match self {
360 IconThemeSelection::Static(_) => None,
361 IconThemeSelection::Dynamic { mode, .. } => Some(*mode),
362 }
363 }
364}
365
366/// Settings for rendering text in UI and text buffers.
367#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
368pub struct ThemeSettingsContent {
369 /// The default font size for text in the UI.
370 #[serde(default)]
371 pub ui_font_size: Option<f32>,
372 /// The name of a font to use for rendering in the UI.
373 #[serde(default)]
374 pub ui_font_family: Option<FontFamilyName>,
375 /// The font fallbacks to use for rendering in the UI.
376 #[serde(default)]
377 #[schemars(default = "default_font_fallbacks")]
378 #[schemars(extend("uniqueItems" = true))]
379 pub ui_font_fallbacks: Option<Vec<FontFamilyName>>,
380 /// The OpenType features to enable for text in the UI.
381 #[serde(default)]
382 #[schemars(default = "default_font_features")]
383 pub ui_font_features: Option<FontFeatures>,
384 /// The weight of the UI font in CSS units from 100 to 900.
385 #[serde(default)]
386 pub ui_font_weight: Option<f32>,
387 /// The name of a font to use for rendering in text buffers.
388 #[serde(default)]
389 pub buffer_font_family: Option<FontFamilyName>,
390 /// The font fallbacks to use for rendering in text buffers.
391 #[serde(default)]
392 #[schemars(extend("uniqueItems" = true))]
393 pub buffer_font_fallbacks: Option<Vec<FontFamilyName>>,
394 /// The default font size for rendering in text buffers.
395 #[serde(default)]
396 pub buffer_font_size: Option<f32>,
397 /// The weight of the editor font in CSS units from 100 to 900.
398 #[serde(default)]
399 pub buffer_font_weight: Option<f32>,
400 /// The buffer's line height.
401 #[serde(default)]
402 pub buffer_line_height: Option<BufferLineHeight>,
403 /// The OpenType features to enable for rendering in text buffers.
404 #[serde(default)]
405 #[schemars(default = "default_font_features")]
406 pub buffer_font_features: Option<FontFeatures>,
407 /// The font size for the agent panel.
408 #[serde(default)]
409 pub agent_font_size: Option<f32>,
410 /// The name of the Zed theme to use.
411 #[serde(default)]
412 pub theme: Option<ThemeSelection>,
413 /// The name of the icon theme to use.
414 #[serde(default)]
415 pub icon_theme: Option<IconThemeSelection>,
416
417 /// UNSTABLE: Expect many elements to be broken.
418 ///
419 // Controls the density of the UI.
420 #[serde(rename = "unstable.ui_density", default)]
421 pub ui_density: Option<UiDensity>,
422
423 /// How much to fade out unused code.
424 #[serde(default)]
425 pub unnecessary_code_fade: Option<f32>,
426
427 /// EXPERIMENTAL: Overrides for the current theme.
428 ///
429 /// These values will override the ones on the current theme specified in `theme`.
430 #[serde(rename = "experimental.theme_overrides", default)]
431 pub experimental_theme_overrides: Option<ThemeStyleContent>,
432
433 /// Overrides per theme
434 ///
435 /// These values will override the ones on the specified theme
436 #[serde(default)]
437 pub theme_overrides: HashMap<String, ThemeStyleContent>,
438}
439
440fn default_font_features() -> Option<FontFeatures> {
441 Some(FontFeatures::default())
442}
443
444fn default_font_fallbacks() -> Option<FontFallbacks> {
445 Some(FontFallbacks::default())
446}
447
448impl ThemeSettingsContent {
449 /// Sets the theme for the given appearance to the theme with the specified name.
450 pub fn set_theme(&mut self, theme_name: impl Into<Arc<str>>, appearance: Appearance) {
451 if let Some(selection) = self.theme.as_mut() {
452 let theme_to_update = match selection {
453 ThemeSelection::Static(theme) => theme,
454 ThemeSelection::Dynamic { mode, light, dark } => match mode {
455 ThemeMode::Light => light,
456 ThemeMode::Dark => dark,
457 ThemeMode::System => match appearance {
458 Appearance::Light => light,
459 Appearance::Dark => dark,
460 },
461 },
462 };
463
464 *theme_to_update = ThemeName(theme_name.into());
465 } else {
466 self.theme = Some(ThemeSelection::Static(ThemeName(theme_name.into())));
467 }
468 }
469
470 /// Sets the icon theme for the given appearance to the icon theme with the specified name.
471 pub fn set_icon_theme(&mut self, icon_theme_name: String, appearance: Appearance) {
472 if let Some(selection) = self.icon_theme.as_mut() {
473 let icon_theme_to_update = match selection {
474 IconThemeSelection::Static(theme) => theme,
475 IconThemeSelection::Dynamic { mode, light, dark } => match mode {
476 ThemeMode::Light => light,
477 ThemeMode::Dark => dark,
478 ThemeMode::System => match appearance {
479 Appearance::Light => light,
480 Appearance::Dark => dark,
481 },
482 },
483 };
484
485 *icon_theme_to_update = IconThemeName(icon_theme_name.into());
486 } else {
487 self.icon_theme = Some(IconThemeSelection::Static(IconThemeName(
488 icon_theme_name.into(),
489 )));
490 }
491 }
492
493 /// Sets the mode for the theme.
494 pub fn set_mode(&mut self, mode: ThemeMode) {
495 if let Some(selection) = self.theme.as_mut() {
496 match selection {
497 ThemeSelection::Static(theme) => {
498 // If the theme was previously set to a single static theme,
499 // we don't know whether it was a light or dark theme, so we
500 // just use it for both.
501 self.theme = Some(ThemeSelection::Dynamic {
502 mode,
503 light: theme.clone(),
504 dark: theme.clone(),
505 });
506 }
507 ThemeSelection::Dynamic {
508 mode: mode_to_update,
509 ..
510 } => *mode_to_update = mode,
511 }
512 } else {
513 self.theme = Some(ThemeSelection::Dynamic {
514 mode,
515 light: ThemeName(ThemeSettings::DEFAULT_LIGHT_THEME.into()),
516 dark: ThemeName(ThemeSettings::DEFAULT_DARK_THEME.into()),
517 });
518 }
519
520 if let Some(selection) = self.icon_theme.as_mut() {
521 match selection {
522 IconThemeSelection::Static(icon_theme) => {
523 // If the icon theme was previously set to a single static
524 // theme, we don't know whether it was a light or dark
525 // theme, so we just use it for both.
526 self.icon_theme = Some(IconThemeSelection::Dynamic {
527 mode,
528 light: icon_theme.clone(),
529 dark: icon_theme.clone(),
530 });
531 }
532 IconThemeSelection::Dynamic {
533 mode: mode_to_update,
534 ..
535 } => *mode_to_update = mode,
536 }
537 } else {
538 self.icon_theme = Some(IconThemeSelection::Static(IconThemeName(
539 DEFAULT_ICON_THEME_NAME.into(),
540 )));
541 }
542 }
543}
544
545/// The buffer's line height.
546#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)]
547#[serde(rename_all = "snake_case")]
548pub enum BufferLineHeight {
549 /// A less dense line height.
550 #[default]
551 Comfortable,
552 /// The default line height.
553 Standard,
554 /// A custom line height, where 1.0 is the font's height. Must be at least 1.0.
555 Custom(#[serde(deserialize_with = "deserialize_line_height")] f32),
556}
557
558fn deserialize_line_height<'de, D>(deserializer: D) -> Result<f32, D::Error>
559where
560 D: serde::Deserializer<'de>,
561{
562 let value = f32::deserialize(deserializer)?;
563 if value < 1.0 {
564 return Err(serde::de::Error::custom(
565 "buffer_line_height.custom must be at least 1.0",
566 ));
567 }
568
569 Ok(value)
570}
571
572impl BufferLineHeight {
573 /// Returns the value of the line height.
574 pub fn value(&self) -> f32 {
575 match self {
576 BufferLineHeight::Comfortable => 1.618,
577 BufferLineHeight::Standard => 1.3,
578 BufferLineHeight::Custom(line_height) => *line_height,
579 }
580 }
581}
582
583impl ThemeSettings {
584 /// Returns the buffer font size.
585 pub fn buffer_font_size(&self, cx: &App) -> Pixels {
586 let font_size = cx
587 .try_global::<BufferFontSize>()
588 .map(|size| size.0)
589 .unwrap_or(self.buffer_font_size);
590 clamp_font_size(font_size)
591 }
592
593 /// Returns the UI font size.
594 pub fn ui_font_size(&self, cx: &App) -> Pixels {
595 let font_size = cx
596 .try_global::<UiFontSize>()
597 .map(|size| size.0)
598 .unwrap_or(self.ui_font_size);
599 clamp_font_size(font_size)
600 }
601
602 /// Returns the UI font size.
603 pub fn agent_font_size(&self, cx: &App) -> Pixels {
604 let font_size = cx
605 .try_global::<AgentFontSize>()
606 .map(|size| size.0)
607 .unwrap_or(self.agent_font_size);
608 clamp_font_size(font_size)
609 }
610
611 /// Returns the buffer font size, read from the settings.
612 ///
613 /// The real buffer font size is stored in-memory, to support temporary font size changes.
614 /// Use [`Self::buffer_font_size`] to get the real font size.
615 pub fn buffer_font_size_settings(&self) -> Pixels {
616 self.buffer_font_size
617 }
618
619 /// Returns the UI font size, read from the settings.
620 ///
621 /// The real UI font size is stored in-memory, to support temporary font size changes.
622 /// Use [`Self::ui_font_size`] to get the real font size.
623 pub fn ui_font_size_settings(&self) -> Pixels {
624 self.ui_font_size
625 }
626
627 // TODO: Rename: `line_height` -> `buffer_line_height`
628 /// Returns the buffer's line height.
629 pub fn line_height(&self) -> f32 {
630 f32::max(self.buffer_line_height.value(), MIN_LINE_HEIGHT)
631 }
632
633 /// Switches to the theme with the given name, if it exists.
634 ///
635 /// Returns a `Some` containing the new theme if it was successful.
636 /// Returns `None` otherwise.
637 pub fn switch_theme(&mut self, theme: &str, cx: &mut App) -> Option<Arc<Theme>> {
638 let themes = ThemeRegistry::default_global(cx);
639
640 let mut new_theme = None;
641
642 match themes.get(theme) {
643 Ok(theme) => {
644 self.active_theme = theme.clone();
645 new_theme = Some(theme);
646 }
647 Err(err @ ThemeNotFoundError(_)) => {
648 log::error!("{err}");
649 }
650 }
651
652 self.apply_theme_overrides();
653
654 new_theme
655 }
656
657 /// Applies the theme overrides, if there are any, to the current theme.
658 pub fn apply_theme_overrides(&mut self) {
659 // Apply the old overrides setting first, so that the new setting can override those.
660 if let Some(experimental_theme_overrides) = &self.experimental_theme_overrides {
661 let mut theme = (*self.active_theme).clone();
662 ThemeSettings::modify_theme(&mut theme, experimental_theme_overrides);
663 self.active_theme = Arc::new(theme);
664 }
665
666 if let Some(theme_overrides) = self.theme_overrides.get(self.active_theme.name.as_ref()) {
667 let mut theme = (*self.active_theme).clone();
668 ThemeSettings::modify_theme(&mut theme, theme_overrides);
669 self.active_theme = Arc::new(theme);
670 }
671 }
672
673 fn modify_theme(base_theme: &mut Theme, theme_overrides: &ThemeStyleContent) {
674 if let Some(window_background_appearance) = theme_overrides.window_background_appearance {
675 base_theme.styles.window_background_appearance = window_background_appearance.into();
676 }
677
678 base_theme
679 .styles
680 .colors
681 .refine(&theme_overrides.theme_colors_refinement());
682 base_theme
683 .styles
684 .status
685 .refine(&theme_overrides.status_colors_refinement());
686 base_theme.styles.player.merge(&theme_overrides.players);
687 base_theme.styles.accents.merge(&theme_overrides.accents);
688 base_theme.styles.syntax = SyntaxTheme::merge(
689 base_theme.styles.syntax.clone(),
690 theme_overrides.syntax_overrides(),
691 );
692 }
693
694 /// Switches to the icon theme with the given name, if it exists.
695 ///
696 /// Returns a `Some` containing the new icon theme if it was successful.
697 /// Returns `None` otherwise.
698 pub fn switch_icon_theme(&mut self, icon_theme: &str, cx: &mut App) -> Option<Arc<IconTheme>> {
699 let themes = ThemeRegistry::default_global(cx);
700
701 let mut new_icon_theme = None;
702
703 if let Some(icon_theme) = themes.get_icon_theme(icon_theme).log_err() {
704 self.active_icon_theme = icon_theme.clone();
705 new_icon_theme = Some(icon_theme);
706 cx.refresh_windows();
707 }
708
709 new_icon_theme
710 }
711}
712
713/// Observe changes to the adjusted buffer font size.
714pub fn observe_buffer_font_size_adjustment<V: 'static>(
715 cx: &mut Context<V>,
716 f: impl 'static + Fn(&mut V, &mut Context<V>),
717) -> Subscription {
718 cx.observe_global::<BufferFontSize>(f)
719}
720
721/// Gets the font size, adjusted by the difference between the current buffer font size and the one set in the settings.
722pub fn adjusted_font_size(size: Pixels, cx: &App) -> Pixels {
723 let adjusted_font_size =
724 if let Some(BufferFontSize(adjusted_size)) = cx.try_global::<BufferFontSize>() {
725 let buffer_font_size = ThemeSettings::get_global(cx).buffer_font_size;
726 let delta = *adjusted_size - buffer_font_size;
727 size + delta
728 } else {
729 size
730 };
731 clamp_font_size(adjusted_font_size)
732}
733
734/// Adjusts the buffer font size.
735pub fn adjust_buffer_font_size(cx: &mut App, mut f: impl FnMut(&mut Pixels)) {
736 let buffer_font_size = ThemeSettings::get_global(cx).buffer_font_size;
737 let mut adjusted_size = cx
738 .try_global::<BufferFontSize>()
739 .map_or(buffer_font_size, |adjusted_size| adjusted_size.0);
740
741 f(&mut adjusted_size);
742 cx.set_global(BufferFontSize(clamp_font_size(adjusted_size)));
743 cx.refresh_windows();
744}
745
746/// Resets the buffer font size to the default value.
747pub fn reset_buffer_font_size(cx: &mut App) {
748 if cx.has_global::<BufferFontSize>() {
749 cx.remove_global::<BufferFontSize>();
750 cx.refresh_windows();
751 }
752}
753
754// TODO: Make private, change usages to use `get_ui_font_size` instead.
755#[allow(missing_docs)]
756pub fn setup_ui_font(window: &mut Window, cx: &mut App) -> gpui::Font {
757 let (ui_font, ui_font_size) = {
758 let theme_settings = ThemeSettings::get_global(cx);
759 let font = theme_settings.ui_font.clone();
760 (font, theme_settings.ui_font_size(cx))
761 };
762
763 window.set_rem_size(ui_font_size);
764 ui_font
765}
766
767/// Sets the adjusted UI font size.
768pub fn adjust_ui_font_size(cx: &mut App, mut f: impl FnMut(&mut Pixels)) {
769 let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx);
770 let mut adjusted_size = cx
771 .try_global::<UiFontSize>()
772 .map_or(ui_font_size, |adjusted_size| adjusted_size.0);
773
774 f(&mut adjusted_size);
775 cx.set_global(UiFontSize(clamp_font_size(adjusted_size)));
776 cx.refresh_windows();
777}
778
779/// Resets the UI font size to the default value.
780pub fn reset_ui_font_size(cx: &mut App) {
781 if cx.has_global::<UiFontSize>() {
782 cx.remove_global::<UiFontSize>();
783 cx.refresh_windows();
784 }
785}
786
787/// Sets the adjusted UI font size.
788pub fn adjust_agent_font_size(cx: &mut App, mut f: impl FnMut(&mut Pixels)) {
789 let agent_font_size = ThemeSettings::get_global(cx).agent_font_size(cx);
790 let mut adjusted_size = cx
791 .try_global::<AgentFontSize>()
792 .map_or(agent_font_size, |adjusted_size| adjusted_size.0);
793
794 f(&mut adjusted_size);
795 cx.set_global(AgentFontSize(clamp_font_size(adjusted_size)));
796 cx.refresh_windows();
797}
798
799/// Resets the UI font size to the default value.
800pub fn reset_agent_font_size(cx: &mut App) {
801 if cx.has_global::<AgentFontSize>() {
802 cx.remove_global::<AgentFontSize>();
803 cx.refresh_windows();
804 }
805}
806
807/// Ensures font size is within the valid range.
808pub fn clamp_font_size(size: Pixels) -> Pixels {
809 size.max(MIN_FONT_SIZE)
810}
811
812fn clamp_font_weight(weight: f32) -> FontWeight {
813 FontWeight(weight.clamp(100., 950.))
814}
815
816impl settings::Settings for ThemeSettings {
817 const KEY: Option<&'static str> = None;
818
819 type FileContent = ThemeSettingsContent;
820
821 fn load(sources: SettingsSources<Self::FileContent>, cx: &mut App) -> Result<Self> {
822 let themes = ThemeRegistry::default_global(cx);
823 let system_appearance = SystemAppearance::default_global(cx);
824
825 fn font_fallbacks_from_settings(
826 fallbacks: Option<Vec<FontFamilyName>>,
827 ) -> Option<FontFallbacks> {
828 fallbacks.map(|fallbacks| {
829 FontFallbacks::from_fonts(
830 fallbacks
831 .into_iter()
832 .map(|font_family| font_family.0.to_string())
833 .collect(),
834 )
835 })
836 }
837
838 let defaults = sources.default;
839 let mut this = Self {
840 ui_font_size: defaults.ui_font_size.unwrap().into(),
841 ui_font: Font {
842 family: defaults.ui_font_family.as_ref().unwrap().0.clone().into(),
843 features: defaults.ui_font_features.clone().unwrap(),
844 fallbacks: font_fallbacks_from_settings(defaults.ui_font_fallbacks.clone()),
845 weight: defaults.ui_font_weight.map(FontWeight).unwrap(),
846 style: Default::default(),
847 },
848 buffer_font: Font {
849 family: defaults
850 .buffer_font_family
851 .as_ref()
852 .unwrap()
853 .0
854 .clone()
855 .into(),
856 features: defaults.buffer_font_features.clone().unwrap(),
857 fallbacks: font_fallbacks_from_settings(defaults.buffer_font_fallbacks.clone()),
858 weight: defaults.buffer_font_weight.map(FontWeight).unwrap(),
859 style: FontStyle::default(),
860 },
861 buffer_font_size: defaults.buffer_font_size.unwrap().into(),
862 buffer_line_height: defaults.buffer_line_height.unwrap(),
863 agent_font_size: defaults.agent_font_size.unwrap().into(),
864 theme_selection: defaults.theme.clone(),
865 active_theme: themes
866 .get(defaults.theme.as_ref().unwrap().theme(*system_appearance))
867 .or(themes.get(&zed_default_dark().name))
868 .unwrap(),
869 experimental_theme_overrides: None,
870 theme_overrides: HashMap::default(),
871 icon_theme_selection: defaults.icon_theme.clone(),
872 active_icon_theme: defaults
873 .icon_theme
874 .as_ref()
875 .and_then(|selection| {
876 themes
877 .get_icon_theme(selection.icon_theme(*system_appearance))
878 .ok()
879 })
880 .unwrap_or_else(|| themes.get_icon_theme(DEFAULT_ICON_THEME_NAME).unwrap()),
881 ui_density: defaults.ui_density.unwrap_or(UiDensity::Default),
882 unnecessary_code_fade: defaults.unnecessary_code_fade.unwrap_or(0.0),
883 };
884
885 for value in sources
886 .user
887 .into_iter()
888 .chain(sources.release_channel)
889 .chain(sources.operating_system)
890 .chain(sources.profile)
891 .chain(sources.server)
892 {
893 if let Some(value) = value.ui_density {
894 this.ui_density = value;
895 }
896
897 if let Some(value) = value.buffer_font_family.clone() {
898 this.buffer_font.family = value.0.into();
899 }
900 if let Some(value) = value.buffer_font_features.clone() {
901 this.buffer_font.features = value;
902 }
903 if let Some(value) = value.buffer_font_fallbacks.clone() {
904 this.buffer_font.fallbacks = font_fallbacks_from_settings(Some(value));
905 }
906 if let Some(value) = value.buffer_font_weight {
907 this.buffer_font.weight = clamp_font_weight(value);
908 }
909
910 if let Some(value) = value.ui_font_family.clone() {
911 this.ui_font.family = value.0.into();
912 }
913 if let Some(value) = value.ui_font_features.clone() {
914 this.ui_font.features = value;
915 }
916 if let Some(value) = value.ui_font_fallbacks.clone() {
917 this.ui_font.fallbacks = font_fallbacks_from_settings(Some(value));
918 }
919 if let Some(value) = value.ui_font_weight {
920 this.ui_font.weight = clamp_font_weight(value);
921 }
922
923 if let Some(value) = &value.theme {
924 this.theme_selection = Some(value.clone());
925
926 let theme_name = value.theme(*system_appearance);
927
928 match themes.get(theme_name) {
929 Ok(theme) => {
930 this.active_theme = theme;
931 }
932 Err(err @ ThemeNotFoundError(_)) => {
933 if themes.extensions_loaded() {
934 log::error!("{err}");
935 }
936 }
937 }
938 }
939
940 this.experimental_theme_overrides
941 .clone_from(&value.experimental_theme_overrides);
942 this.theme_overrides.clone_from(&value.theme_overrides);
943 this.apply_theme_overrides();
944
945 if let Some(value) = &value.icon_theme {
946 this.icon_theme_selection = Some(value.clone());
947
948 let icon_theme_name = value.icon_theme(*system_appearance);
949
950 match themes.get_icon_theme(icon_theme_name) {
951 Ok(icon_theme) => {
952 this.active_icon_theme = icon_theme;
953 }
954 Err(err @ IconThemeNotFoundError(_)) => {
955 if themes.extensions_loaded() {
956 log::error!("{err}");
957 }
958 }
959 }
960 }
961
962 merge(&mut this.ui_font_size, value.ui_font_size.map(Into::into));
963 this.ui_font_size = this.ui_font_size.clamp(px(6.), px(100.));
964
965 merge(
966 &mut this.buffer_font_size,
967 value.buffer_font_size.map(Into::into),
968 );
969 this.buffer_font_size = this.buffer_font_size.clamp(px(6.), px(100.));
970
971 merge(
972 &mut this.agent_font_size,
973 value.agent_font_size.map(Into::into),
974 );
975 this.agent_font_size = this.agent_font_size.clamp(px(6.), px(100.));
976
977 merge(&mut this.buffer_line_height, value.buffer_line_height);
978
979 // Clamp the `unnecessary_code_fade` to ensure text can't disappear entirely.
980 merge(&mut this.unnecessary_code_fade, value.unnecessary_code_fade);
981 this.unnecessary_code_fade = this.unnecessary_code_fade.clamp(0.0, 0.9);
982 }
983
984 Ok(this)
985 }
986
987 fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
988 vscode.f32_setting("editor.fontWeight", &mut current.buffer_font_weight);
989 vscode.f32_setting("editor.fontSize", &mut current.buffer_font_size);
990 if let Some(font) = vscode.read_string("editor.font") {
991 current.buffer_font_family = Some(FontFamilyName(font.into()));
992 }
993 // TODO: possibly map editor.fontLigatures to buffer_font_features?
994 }
995}
996
997/// Newtype for a theme name. Its `ParameterizedJsonSchema` lists the theme names known at runtime.
998#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
999#[serde(transparent)]
1000pub struct ThemeName(pub Arc<str>);
1001
1002inventory::submit! {
1003 ParameterizedJsonSchema {
1004 add_and_get_ref: |generator, _params, cx| {
1005 replace_subschema::<ThemeName>(generator, || json_schema!({
1006 "type": "string",
1007 "enum": ThemeRegistry::global(cx).list_names(),
1008 }))
1009 }
1010 }
1011}
1012
1013/// Newtype for a icon theme name. Its `ParameterizedJsonSchema` lists the icon theme names known at
1014/// runtime.
1015#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1016#[serde(transparent)]
1017pub struct IconThemeName(pub Arc<str>);
1018
1019inventory::submit! {
1020 ParameterizedJsonSchema {
1021 add_and_get_ref: |generator, _params, cx| {
1022 replace_subschema::<IconThemeName>(generator, || json_schema!({
1023 "type": "string",
1024 "enum": ThemeRegistry::global(cx)
1025 .list_icon_themes()
1026 .into_iter()
1027 .map(|icon_theme| icon_theme.name)
1028 .collect::<Vec<SharedString>>(),
1029 }))
1030 }
1031 }
1032}
1033
1034/// Newtype for font family name. Its `ParameterizedJsonSchema` lists the font families known at
1035/// runtime.
1036#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1037#[serde(transparent)]
1038pub struct FontFamilyName(pub Arc<str>);
1039
1040inventory::submit! {
1041 ParameterizedJsonSchema {
1042 add_and_get_ref: |generator, params, _cx| {
1043 replace_subschema::<FontFamilyName>(generator, || {
1044 json_schema!({
1045 "type": "string",
1046 "enum": params.font_names,
1047 })
1048 })
1049 }
1050 }
1051}
1052
1053fn merge<T: Copy>(target: &mut T, value: Option<T>) {
1054 if let Some(value) = value {
1055 *target = value;
1056 }
1057}
1058
1059#[cfg(test)]
1060mod tests {
1061 use super::*;
1062 use serde_json::json;
1063
1064 #[test]
1065 fn test_buffer_line_height_deserialize_valid() {
1066 assert_eq!(
1067 serde_json::from_value::<BufferLineHeight>(json!("comfortable")).unwrap(),
1068 BufferLineHeight::Comfortable
1069 );
1070 assert_eq!(
1071 serde_json::from_value::<BufferLineHeight>(json!("standard")).unwrap(),
1072 BufferLineHeight::Standard
1073 );
1074 assert_eq!(
1075 serde_json::from_value::<BufferLineHeight>(json!({"custom": 1.0})).unwrap(),
1076 BufferLineHeight::Custom(1.0)
1077 );
1078 assert_eq!(
1079 serde_json::from_value::<BufferLineHeight>(json!({"custom": 1.5})).unwrap(),
1080 BufferLineHeight::Custom(1.5)
1081 );
1082 }
1083
1084 #[test]
1085 fn test_buffer_line_height_deserialize_invalid() {
1086 assert!(
1087 serde_json::from_value::<BufferLineHeight>(json!({"custom": 0.99}))
1088 .err()
1089 .unwrap()
1090 .to_string()
1091 .contains("buffer_line_height.custom must be at least 1.0")
1092 );
1093 assert!(
1094 serde_json::from_value::<BufferLineHeight>(json!({"custom": 0.0}))
1095 .err()
1096 .unwrap()
1097 .to_string()
1098 .contains("buffer_line_height.custom must be at least 1.0")
1099 );
1100 assert!(
1101 serde_json::from_value::<BufferLineHeight>(json!({"custom": -1.0}))
1102 .err()
1103 .unwrap()
1104 .to_string()
1105 .contains("buffer_line_height.custom must be at least 1.0")
1106 );
1107 }
1108}