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