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