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