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