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 line height for buffers, and the terminal.
110 ///
111 /// Changing this may affect the spacing of some UI elements.
112 ///
113 /// The terminal font family can be overridden using it's own setting.
114 pub buffer_line_height: BufferLineHeight,
115 /// The current theme selection.
116 pub theme_selection: Option<ThemeSelection>,
117 /// The active theme.
118 pub active_theme: Arc<Theme>,
119 /// Manual overrides for the active theme.
120 ///
121 /// Note: This setting is still experimental. See [this tracking issue](https://github.com/zed-industries/zed/issues/18078)
122 pub theme_overrides: Option<ThemeStyleContent>,
123 /// The current icon theme selection.
124 pub icon_theme_selection: Option<IconThemeSelection>,
125 /// The active icon theme.
126 pub active_icon_theme: Arc<IconTheme>,
127 /// The density of the UI.
128 /// Note: This setting is still experimental. See [this tracking issue](
129 pub ui_density: UiDensity,
130 /// The amount of fading applied to unnecessary code.
131 pub unnecessary_code_fade: f32,
132}
133
134impl ThemeSettings {
135 const DEFAULT_LIGHT_THEME: &'static str = "One Light";
136 const DEFAULT_DARK_THEME: &'static str = "One Dark";
137
138 /// Returns the name of the default theme for the given [`Appearance`].
139 pub fn default_theme(appearance: Appearance) -> &'static str {
140 match appearance {
141 Appearance::Light => Self::DEFAULT_LIGHT_THEME,
142 Appearance::Dark => Self::DEFAULT_DARK_THEME,
143 }
144 }
145
146 /// Reloads the current theme.
147 ///
148 /// Reads the [`ThemeSettings`] to know which theme should be loaded,
149 /// taking into account the current [`SystemAppearance`].
150 pub fn reload_current_theme(cx: &mut App) {
151 let mut theme_settings = ThemeSettings::get_global(cx).clone();
152 let system_appearance = SystemAppearance::global(cx);
153
154 if let Some(theme_selection) = theme_settings.theme_selection.clone() {
155 let mut theme_name = theme_selection.theme(*system_appearance);
156
157 // If the selected theme doesn't exist, fall back to a default theme
158 // based on the system appearance.
159 let theme_registry = ThemeRegistry::global(cx);
160 if let Err(err @ ThemeNotFoundError(_)) = theme_registry.get(theme_name) {
161 if theme_registry.extensions_loaded() {
162 log::error!("{err}");
163 }
164
165 theme_name = Self::default_theme(*system_appearance);
166 };
167
168 if let Some(_theme) = theme_settings.switch_theme(theme_name, cx) {
169 ThemeSettings::override_global(theme_settings, cx);
170 }
171 }
172 }
173
174 /// Reloads the current icon theme.
175 ///
176 /// Reads the [`ThemeSettings`] to know which icon theme should be loaded,
177 /// taking into account the current [`SystemAppearance`].
178 pub fn reload_current_icon_theme(cx: &mut App) {
179 let mut theme_settings = ThemeSettings::get_global(cx).clone();
180 let system_appearance = SystemAppearance::global(cx);
181
182 if let Some(icon_theme_selection) = theme_settings.icon_theme_selection.clone() {
183 let mut icon_theme_name = icon_theme_selection.icon_theme(*system_appearance);
184
185 // If the selected icon theme doesn't exist, fall back to the default theme.
186 let theme_registry = ThemeRegistry::global(cx);
187 if let Err(err @ IconThemeNotFoundError(_)) =
188 theme_registry.get_icon_theme(icon_theme_name)
189 {
190 if theme_registry.extensions_loaded() {
191 log::error!("{err}");
192 }
193
194 icon_theme_name = DEFAULT_ICON_THEME_NAME;
195 };
196
197 if let Some(_theme) = theme_settings.switch_icon_theme(icon_theme_name, cx) {
198 ThemeSettings::override_global(theme_settings, cx);
199 }
200 }
201 }
202}
203
204/// The appearance of the system.
205#[derive(Debug, Clone, Copy, Deref)]
206pub struct SystemAppearance(pub Appearance);
207
208impl Default for SystemAppearance {
209 fn default() -> Self {
210 Self(Appearance::Dark)
211 }
212}
213
214#[derive(Deref, DerefMut, Default)]
215struct GlobalSystemAppearance(SystemAppearance);
216
217impl Global for GlobalSystemAppearance {}
218
219impl SystemAppearance {
220 /// Initializes the [`SystemAppearance`] for the application.
221 pub fn init(cx: &mut App) {
222 *cx.default_global::<GlobalSystemAppearance>() =
223 GlobalSystemAppearance(SystemAppearance(cx.window_appearance().into()));
224 }
225
226 /// Returns the global [`SystemAppearance`].
227 ///
228 /// Inserts a default [`SystemAppearance`] if one does not yet exist.
229 pub(crate) fn default_global(cx: &mut App) -> Self {
230 cx.default_global::<GlobalSystemAppearance>().0
231 }
232
233 /// Returns the global [`SystemAppearance`].
234 pub fn global(cx: &App) -> Self {
235 cx.global::<GlobalSystemAppearance>().0
236 }
237
238 /// Returns a mutable reference to the global [`SystemAppearance`].
239 pub fn global_mut(cx: &mut App) -> &mut Self {
240 cx.global_mut::<GlobalSystemAppearance>()
241 }
242}
243
244#[derive(Default)]
245struct BufferFontSize(Pixels);
246
247impl Global for BufferFontSize {}
248
249#[derive(Default)]
250pub(crate) struct UiFontSize(Pixels);
251
252impl Global for UiFontSize {}
253
254/// Represents the selection of a theme, which can be either static or dynamic.
255#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
256#[serde(untagged)]
257pub enum ThemeSelection {
258 /// A static theme selection, represented by a single theme name.
259 Static(#[schemars(schema_with = "theme_name_ref")] String),
260 /// A dynamic theme selection, which can change based the [ThemeMode].
261 Dynamic {
262 /// The mode used to determine which theme to use.
263 #[serde(default)]
264 mode: ThemeMode,
265 /// The theme to use for light mode.
266 #[schemars(schema_with = "theme_name_ref")]
267 light: String,
268 /// The theme to use for dark mode.
269 #[schemars(schema_with = "theme_name_ref")]
270 dark: String,
271 },
272}
273
274fn theme_name_ref(_: &mut SchemaGenerator) -> Schema {
275 Schema::new_ref("#/definitions/ThemeName".into())
276}
277
278// TODO: Rename ThemeMode -> ThemeAppearanceMode
279/// The mode use to select a theme.
280///
281/// `Light` and `Dark` will select their respective themes.
282///
283/// `System` will select the theme based on the system's appearance.
284#[derive(Debug, PartialEq, Eq, Clone, Copy, Default, Serialize, Deserialize, JsonSchema)]
285#[serde(rename_all = "snake_case")]
286pub enum ThemeMode {
287 /// Use the specified `light` theme.
288 Light,
289
290 /// Use the specified `dark` theme.
291 Dark,
292
293 /// Use the theme based on the system's appearance.
294 #[default]
295 System,
296}
297
298impl ThemeSelection {
299 /// Returns the theme name for the selected [ThemeMode].
300 pub fn theme(&self, system_appearance: Appearance) -> &str {
301 match self {
302 Self::Static(theme) => theme,
303 Self::Dynamic { mode, light, dark } => match mode {
304 ThemeMode::Light => light,
305 ThemeMode::Dark => dark,
306 ThemeMode::System => match system_appearance {
307 Appearance::Light => light,
308 Appearance::Dark => dark,
309 },
310 },
311 }
312 }
313
314 /// Returns the [ThemeMode] for the [ThemeSelection].
315 pub fn mode(&self) -> Option<ThemeMode> {
316 match self {
317 ThemeSelection::Static(_) => None,
318 ThemeSelection::Dynamic { mode, .. } => Some(*mode),
319 }
320 }
321}
322
323fn icon_theme_name_ref(_: &mut SchemaGenerator) -> Schema {
324 Schema::new_ref("#/definitions/IconThemeName".into())
325}
326
327/// Represents the selection of an icon theme, which can be either static or dynamic.
328#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
329#[serde(untagged)]
330pub enum IconThemeSelection {
331 /// A static icon theme selection, represented by a single icon theme name.
332 Static(#[schemars(schema_with = "icon_theme_name_ref")] String),
333 /// A dynamic icon theme selection, which can change based on the [`ThemeMode`].
334 Dynamic {
335 /// The mode used to determine which theme to use.
336 #[serde(default)]
337 mode: ThemeMode,
338 /// The icon theme to use for light mode.
339 #[schemars(schema_with = "icon_theme_name_ref")]
340 light: String,
341 /// The icon theme to use for dark mode.
342 #[schemars(schema_with = "icon_theme_name_ref")]
343 dark: String,
344 },
345}
346
347impl IconThemeSelection {
348 /// Returns the icon theme name based on the given [`Appearance`].
349 pub fn icon_theme(&self, system_appearance: Appearance) -> &str {
350 match self {
351 Self::Static(theme) => theme,
352 Self::Dynamic { mode, light, dark } => match mode {
353 ThemeMode::Light => light,
354 ThemeMode::Dark => dark,
355 ThemeMode::System => match system_appearance {
356 Appearance::Light => light,
357 Appearance::Dark => dark,
358 },
359 },
360 }
361 }
362
363 /// Returns the [`ThemeMode`] for the [`IconThemeSelection`].
364 pub fn mode(&self) -> Option<ThemeMode> {
365 match self {
366 IconThemeSelection::Static(_) => None,
367 IconThemeSelection::Dynamic { mode, .. } => Some(*mode),
368 }
369 }
370}
371
372/// Settings for rendering text in UI and text buffers.
373#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
374pub struct ThemeSettingsContent {
375 /// The default font size for text in the UI.
376 #[serde(default)]
377 pub ui_font_size: Option<f32>,
378 /// The name of a font to use for rendering in the UI.
379 #[serde(default)]
380 pub ui_font_family: Option<String>,
381 /// The font fallbacks to use for rendering in the UI.
382 #[serde(default)]
383 #[schemars(default = "default_font_fallbacks")]
384 pub ui_font_fallbacks: Option<Vec<String>>,
385 /// The OpenType features to enable for text in the UI.
386 #[serde(default)]
387 #[schemars(default = "default_font_features")]
388 pub ui_font_features: Option<FontFeatures>,
389 /// The weight of the UI font in CSS units from 100 to 900.
390 #[serde(default)]
391 pub ui_font_weight: Option<f32>,
392 /// The name of a font to use for rendering in text buffers.
393 #[serde(default)]
394 pub buffer_font_family: Option<String>,
395 /// The font fallbacks to use for rendering in text buffers.
396 #[serde(default)]
397 #[schemars(default = "default_font_fallbacks")]
398 pub buffer_font_fallbacks: Option<Vec<String>>,
399 /// The default font size for rendering in text buffers.
400 #[serde(default)]
401 pub buffer_font_size: Option<f32>,
402 /// The weight of the editor font in CSS units from 100 to 900.
403 #[serde(default)]
404 pub buffer_font_weight: Option<f32>,
405 /// The buffer's line height.
406 #[serde(default)]
407 pub buffer_line_height: Option<BufferLineHeight>,
408 /// The OpenType features to enable for rendering in text buffers.
409 #[serde(default)]
410 #[schemars(default = "default_font_features")]
411 pub buffer_font_features: Option<FontFeatures>,
412 /// The name of the Zed theme to use.
413 #[serde(default)]
414 pub theme: Option<ThemeSelection>,
415 /// The name of the icon theme to use.
416 #[serde(default)]
417 pub icon_theme: Option<IconThemeSelection>,
418
419 /// UNSTABLE: Expect many elements to be broken.
420 ///
421 // Controls the density of the UI.
422 #[serde(rename = "unstable.ui_density", default)]
423 pub ui_density: Option<UiDensity>,
424
425 /// How much to fade out unused code.
426 #[serde(default)]
427 pub unnecessary_code_fade: Option<f32>,
428
429 /// EXPERIMENTAL: Overrides for the current theme.
430 ///
431 /// These values will override the ones on the current theme specified in `theme`.
432 #[serde(rename = "experimental.theme_overrides", default)]
433 pub theme_overrides: Option<ThemeStyleContent>,
434}
435
436fn default_font_features() -> Option<FontFeatures> {
437 Some(FontFeatures::default())
438}
439
440fn default_font_fallbacks() -> Option<FontFallbacks> {
441 Some(FontFallbacks::default())
442}
443
444impl ThemeSettingsContent {
445 /// Sets the theme for the given appearance to the theme with the specified name.
446 pub fn set_theme(&mut self, theme_name: String, appearance: Appearance) {
447 if let Some(selection) = self.theme.as_mut() {
448 let theme_to_update = match selection {
449 ThemeSelection::Static(theme) => theme,
450 ThemeSelection::Dynamic { mode, light, dark } => match mode {
451 ThemeMode::Light => light,
452 ThemeMode::Dark => dark,
453 ThemeMode::System => match appearance {
454 Appearance::Light => light,
455 Appearance::Dark => dark,
456 },
457 },
458 };
459
460 *theme_to_update = theme_name.to_string();
461 } else {
462 self.theme = Some(ThemeSelection::Static(theme_name.to_string()));
463 }
464 }
465
466 /// Sets the icon theme for the given appearance to the icon theme with the specified name.
467 pub fn set_icon_theme(&mut self, icon_theme_name: String, appearance: Appearance) {
468 if let Some(selection) = self.icon_theme.as_mut() {
469 let icon_theme_to_update = match selection {
470 IconThemeSelection::Static(theme) => theme,
471 IconThemeSelection::Dynamic { mode, light, dark } => match mode {
472 ThemeMode::Light => light,
473 ThemeMode::Dark => dark,
474 ThemeMode::System => match appearance {
475 Appearance::Light => light,
476 Appearance::Dark => dark,
477 },
478 },
479 };
480
481 *icon_theme_to_update = icon_theme_name.to_string();
482 } else {
483 self.icon_theme = Some(IconThemeSelection::Static(icon_theme_name.to_string()));
484 }
485 }
486
487 /// Sets the mode for the theme.
488 pub fn set_mode(&mut self, mode: ThemeMode) {
489 if let Some(selection) = self.theme.as_mut() {
490 match selection {
491 ThemeSelection::Static(theme) => {
492 // If the theme was previously set to a single static theme,
493 // we don't know whether it was a light or dark theme, so we
494 // just use it for both.
495 self.theme = Some(ThemeSelection::Dynamic {
496 mode,
497 light: theme.clone(),
498 dark: theme.clone(),
499 });
500 }
501 ThemeSelection::Dynamic {
502 mode: mode_to_update,
503 ..
504 } => *mode_to_update = mode,
505 }
506 } else {
507 self.theme = Some(ThemeSelection::Dynamic {
508 mode,
509 light: ThemeSettings::DEFAULT_LIGHT_THEME.into(),
510 dark: ThemeSettings::DEFAULT_DARK_THEME.into(),
511 });
512 }
513
514 if let Some(selection) = self.icon_theme.as_mut() {
515 match selection {
516 IconThemeSelection::Static(icon_theme) => {
517 // If the icon theme was previously set to a single static
518 // theme, we don't know whether it was a light or dark
519 // theme, so we just use it for both.
520 self.icon_theme = Some(IconThemeSelection::Dynamic {
521 mode,
522 light: icon_theme.clone(),
523 dark: icon_theme.clone(),
524 });
525 }
526 IconThemeSelection::Dynamic {
527 mode: mode_to_update,
528 ..
529 } => *mode_to_update = mode,
530 }
531 } else {
532 self.icon_theme = Some(IconThemeSelection::Static(DEFAULT_ICON_THEME_NAME.into()));
533 }
534 }
535}
536
537/// The buffer's line height.
538#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)]
539#[serde(rename_all = "snake_case")]
540pub enum BufferLineHeight {
541 /// A less dense line height.
542 #[default]
543 Comfortable,
544 /// The default line height.
545 Standard,
546 /// A custom line height.
547 ///
548 /// A line height of 1.0 is the height of the buffer's font size.
549 Custom(f32),
550}
551
552impl BufferLineHeight {
553 /// Returns the value of the line height.
554 pub fn value(&self) -> f32 {
555 match self {
556 BufferLineHeight::Comfortable => 1.618,
557 BufferLineHeight::Standard => 1.3,
558 BufferLineHeight::Custom(line_height) => *line_height,
559 }
560 }
561}
562
563impl ThemeSettings {
564 /// Returns the buffer font size.
565 pub fn buffer_font_size(&self, cx: &App) -> Pixels {
566 let font_size = cx
567 .try_global::<BufferFontSize>()
568 .map(|size| size.0)
569 .unwrap_or(self.buffer_font_size);
570 clamp_font_size(font_size)
571 }
572
573 /// Returns the UI font size.
574 pub fn ui_font_size(&self, cx: &App) -> Pixels {
575 let font_size = cx
576 .try_global::<UiFontSize>()
577 .map(|size| size.0)
578 .unwrap_or(self.ui_font_size);
579 clamp_font_size(font_size)
580 }
581
582 /// Returns the buffer font size, read from the settings.
583 ///
584 /// The real buffer font size is stored in-memory, to support temporary font size changes.
585 /// Use [`Self::buffer_font_size`] to get the real font size.
586 pub fn buffer_font_size_settings(&self) -> Pixels {
587 self.buffer_font_size
588 }
589
590 /// Returns the UI font size, read from the settings.
591 ///
592 /// The real UI font size is stored in-memory, to support temporary font size changes.
593 /// Use [`Self::ui_font_size`] to get the real font size.
594 pub fn ui_font_size_settings(&self) -> Pixels {
595 self.ui_font_size
596 }
597
598 // TODO: Rename: `line_height` -> `buffer_line_height`
599 /// Returns the buffer's line height.
600 pub fn line_height(&self) -> f32 {
601 f32::max(self.buffer_line_height.value(), MIN_LINE_HEIGHT)
602 }
603
604 /// Switches to the theme with the given name, if it exists.
605 ///
606 /// Returns a `Some` containing the new theme if it was successful.
607 /// Returns `None` otherwise.
608 pub fn switch_theme(&mut self, theme: &str, cx: &mut App) -> Option<Arc<Theme>> {
609 let themes = ThemeRegistry::default_global(cx);
610
611 let mut new_theme = None;
612
613 match themes.get(theme) {
614 Ok(theme) => {
615 self.active_theme = theme.clone();
616 new_theme = Some(theme);
617 }
618 Err(err @ ThemeNotFoundError(_)) => {
619 log::error!("{err}");
620 }
621 }
622
623 self.apply_theme_overrides();
624
625 new_theme
626 }
627
628 /// Applies the theme overrides, if there are any, to the current theme.
629 pub fn apply_theme_overrides(&mut self) {
630 if let Some(theme_overrides) = &self.theme_overrides {
631 let mut base_theme = (*self.active_theme).clone();
632
633 if let Some(window_background_appearance) = theme_overrides.window_background_appearance
634 {
635 base_theme.styles.window_background_appearance =
636 window_background_appearance.into();
637 }
638
639 base_theme
640 .styles
641 .colors
642 .refine(&theme_overrides.theme_colors_refinement());
643 base_theme
644 .styles
645 .status
646 .refine(&theme_overrides.status_colors_refinement());
647 base_theme.styles.player.merge(&theme_overrides.players);
648 base_theme.styles.accents.merge(&theme_overrides.accents);
649 base_theme.styles.syntax =
650 SyntaxTheme::merge(base_theme.styles.syntax, theme_overrides.syntax_overrides());
651
652 self.active_theme = Arc::new(base_theme);
653 }
654 }
655
656 /// Switches to the icon theme with the given name, if it exists.
657 ///
658 /// Returns a `Some` containing the new icon theme if it was successful.
659 /// Returns `None` otherwise.
660 pub fn switch_icon_theme(&mut self, icon_theme: &str, cx: &mut App) -> Option<Arc<IconTheme>> {
661 let themes = ThemeRegistry::default_global(cx);
662
663 let mut new_icon_theme = None;
664
665 if let Some(icon_theme) = themes.get_icon_theme(icon_theme).log_err() {
666 self.active_icon_theme = icon_theme.clone();
667 new_icon_theme = Some(icon_theme);
668 cx.refresh_windows();
669 }
670
671 new_icon_theme
672 }
673}
674
675/// Observe changes to the adjusted buffer font size.
676pub fn observe_buffer_font_size_adjustment<V: 'static>(
677 cx: &mut Context<V>,
678 f: impl 'static + Fn(&mut V, &mut Context<V>),
679) -> Subscription {
680 cx.observe_global::<BufferFontSize>(f)
681}
682
683/// Gets the font size, adjusted by the difference between the current buffer font size and the one set in the settings.
684pub fn adjusted_font_size(size: Pixels, cx: &App) -> Pixels {
685 let adjusted_font_size =
686 if let Some(BufferFontSize(adjusted_size)) = cx.try_global::<BufferFontSize>() {
687 let buffer_font_size = ThemeSettings::get_global(cx).buffer_font_size;
688 let delta = *adjusted_size - buffer_font_size;
689 size + delta
690 } else {
691 size
692 };
693 clamp_font_size(adjusted_font_size)
694}
695
696/// Adjusts the buffer font size.
697pub fn adjust_buffer_font_size(cx: &mut App, mut f: impl FnMut(&mut Pixels)) {
698 let buffer_font_size = ThemeSettings::get_global(cx).buffer_font_size;
699 let mut adjusted_size = cx
700 .try_global::<BufferFontSize>()
701 .map_or(buffer_font_size, |adjusted_size| adjusted_size.0);
702
703 f(&mut adjusted_size);
704 cx.set_global(BufferFontSize(clamp_font_size(adjusted_size)));
705 cx.refresh_windows();
706}
707
708/// Resets the buffer font size to the default value.
709pub fn reset_buffer_font_size(cx: &mut App) {
710 if cx.has_global::<BufferFontSize>() {
711 cx.remove_global::<BufferFontSize>();
712 cx.refresh_windows();
713 }
714}
715
716// TODO: Make private, change usages to use `get_ui_font_size` instead.
717#[allow(missing_docs)]
718pub fn setup_ui_font(window: &mut Window, cx: &mut App) -> gpui::Font {
719 let (ui_font, ui_font_size) = {
720 let theme_settings = ThemeSettings::get_global(cx);
721 let font = theme_settings.ui_font.clone();
722 (font, theme_settings.ui_font_size(cx))
723 };
724
725 window.set_rem_size(ui_font_size);
726 ui_font
727}
728
729/// Sets the adjusted UI font size.
730pub fn adjust_ui_font_size(cx: &mut App, mut f: impl FnMut(&mut Pixels)) {
731 let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx);
732 let mut adjusted_size = cx
733 .try_global::<UiFontSize>()
734 .map_or(ui_font_size, |adjusted_size| adjusted_size.0);
735
736 f(&mut adjusted_size);
737 cx.set_global(UiFontSize(clamp_font_size(adjusted_size)));
738 cx.refresh_windows();
739}
740
741/// Resets the UI font size to the default value.
742pub fn reset_ui_font_size(cx: &mut App) {
743 if cx.has_global::<UiFontSize>() {
744 cx.remove_global::<UiFontSize>();
745 cx.refresh_windows();
746 }
747}
748
749/// Ensures font size is within the valid range.
750pub fn clamp_font_size(size: Pixels) -> Pixels {
751 size.max(MIN_FONT_SIZE)
752}
753
754fn clamp_font_weight(weight: f32) -> FontWeight {
755 FontWeight(weight.clamp(100., 950.))
756}
757
758impl settings::Settings for ThemeSettings {
759 const KEY: Option<&'static str> = None;
760
761 type FileContent = ThemeSettingsContent;
762
763 fn load(sources: SettingsSources<Self::FileContent>, cx: &mut App) -> Result<Self> {
764 let themes = ThemeRegistry::default_global(cx);
765 let system_appearance = SystemAppearance::default_global(cx);
766
767 let defaults = sources.default;
768 let mut this = Self {
769 ui_font_size: defaults.ui_font_size.unwrap().into(),
770 ui_font: Font {
771 family: defaults.ui_font_family.as_ref().unwrap().clone().into(),
772 features: defaults.ui_font_features.clone().unwrap(),
773 fallbacks: defaults
774 .ui_font_fallbacks
775 .as_ref()
776 .map(|fallbacks| FontFallbacks::from_fonts(fallbacks.clone())),
777 weight: defaults.ui_font_weight.map(FontWeight).unwrap(),
778 style: Default::default(),
779 },
780 buffer_font: Font {
781 family: defaults.buffer_font_family.as_ref().unwrap().clone().into(),
782 features: defaults.buffer_font_features.clone().unwrap(),
783 fallbacks: defaults
784 .buffer_font_fallbacks
785 .as_ref()
786 .map(|fallbacks| FontFallbacks::from_fonts(fallbacks.clone())),
787 weight: defaults.buffer_font_weight.map(FontWeight).unwrap(),
788 style: FontStyle::default(),
789 },
790 buffer_font_size: defaults.buffer_font_size.unwrap().into(),
791 buffer_line_height: defaults.buffer_line_height.unwrap(),
792 theme_selection: defaults.theme.clone(),
793 active_theme: themes
794 .get(defaults.theme.as_ref().unwrap().theme(*system_appearance))
795 .or(themes.get(&zed_default_dark().name))
796 .unwrap(),
797 theme_overrides: None,
798 icon_theme_selection: defaults.icon_theme.clone(),
799 active_icon_theme: defaults
800 .icon_theme
801 .as_ref()
802 .and_then(|selection| {
803 themes
804 .get_icon_theme(selection.icon_theme(*system_appearance))
805 .ok()
806 })
807 .unwrap_or_else(|| themes.get_icon_theme(DEFAULT_ICON_THEME_NAME).unwrap()),
808 ui_density: defaults.ui_density.unwrap_or(UiDensity::Default),
809 unnecessary_code_fade: defaults.unnecessary_code_fade.unwrap_or(0.0),
810 };
811
812 for value in sources
813 .user
814 .into_iter()
815 .chain(sources.release_channel)
816 .chain(sources.server)
817 {
818 if let Some(value) = value.ui_density {
819 this.ui_density = value;
820 }
821
822 if let Some(value) = value.buffer_font_family.clone() {
823 this.buffer_font.family = value.into();
824 }
825 if let Some(value) = value.buffer_font_features.clone() {
826 this.buffer_font.features = value;
827 }
828 if let Some(value) = value.buffer_font_fallbacks.clone() {
829 this.buffer_font.fallbacks = Some(FontFallbacks::from_fonts(value));
830 }
831 if let Some(value) = value.buffer_font_weight {
832 this.buffer_font.weight = clamp_font_weight(value);
833 }
834
835 if let Some(value) = value.ui_font_family.clone() {
836 this.ui_font.family = value.into();
837 }
838 if let Some(value) = value.ui_font_features.clone() {
839 this.ui_font.features = value;
840 }
841 if let Some(value) = value.ui_font_fallbacks.clone() {
842 this.ui_font.fallbacks = Some(FontFallbacks::from_fonts(value));
843 }
844 if let Some(value) = value.ui_font_weight {
845 this.ui_font.weight = clamp_font_weight(value);
846 }
847
848 if let Some(value) = &value.theme {
849 this.theme_selection = Some(value.clone());
850
851 let theme_name = value.theme(*system_appearance);
852
853 match themes.get(theme_name) {
854 Ok(theme) => {
855 this.active_theme = theme;
856 }
857 Err(err @ ThemeNotFoundError(_)) => {
858 if themes.extensions_loaded() {
859 log::error!("{err}");
860 }
861 }
862 }
863 }
864
865 this.theme_overrides.clone_from(&value.theme_overrides);
866 this.apply_theme_overrides();
867
868 if let Some(value) = &value.icon_theme {
869 this.icon_theme_selection = Some(value.clone());
870
871 let icon_theme_name = value.icon_theme(*system_appearance);
872
873 match themes.get_icon_theme(icon_theme_name) {
874 Ok(icon_theme) => {
875 this.active_icon_theme = icon_theme;
876 }
877 Err(err @ IconThemeNotFoundError(_)) => {
878 if themes.extensions_loaded() {
879 log::error!("{err}");
880 }
881 }
882 }
883 }
884
885 merge(&mut this.ui_font_size, value.ui_font_size.map(Into::into));
886 this.ui_font_size = this.ui_font_size.clamp(px(6.), px(100.));
887
888 merge(
889 &mut this.buffer_font_size,
890 value.buffer_font_size.map(Into::into),
891 );
892 this.buffer_font_size = this.buffer_font_size.clamp(px(6.), px(100.));
893
894 merge(&mut this.buffer_line_height, value.buffer_line_height);
895
896 // Clamp the `unnecessary_code_fade` to ensure text can't disappear entirely.
897 merge(&mut this.unnecessary_code_fade, value.unnecessary_code_fade);
898 this.unnecessary_code_fade = this.unnecessary_code_fade.clamp(0.0, 0.9);
899 }
900
901 Ok(this)
902 }
903
904 fn json_schema(
905 generator: &mut SchemaGenerator,
906 params: &SettingsJsonSchemaParams,
907 cx: &App,
908 ) -> schemars::schema::RootSchema {
909 let mut root_schema = generator.root_schema_for::<ThemeSettingsContent>();
910 let theme_names = ThemeRegistry::global(cx)
911 .list_names()
912 .into_iter()
913 .map(|theme_name| Value::String(theme_name.to_string()))
914 .collect();
915
916 let theme_name_schema = SchemaObject {
917 instance_type: Some(InstanceType::String.into()),
918 enum_values: Some(theme_names),
919 ..Default::default()
920 };
921
922 let icon_theme_names = ThemeRegistry::global(cx)
923 .list_icon_themes()
924 .into_iter()
925 .map(|icon_theme| Value::String(icon_theme.name.to_string()))
926 .collect();
927
928 let icon_theme_name_schema = SchemaObject {
929 instance_type: Some(InstanceType::String.into()),
930 enum_values: Some(icon_theme_names),
931 ..Default::default()
932 };
933
934 root_schema.definitions.extend([
935 ("ThemeName".into(), theme_name_schema.into()),
936 ("IconThemeName".into(), icon_theme_name_schema.into()),
937 ("FontFamilies".into(), params.font_family_schema()),
938 ("FontFallbacks".into(), params.font_fallback_schema()),
939 ]);
940
941 add_references_to_properties(
942 &mut root_schema,
943 &[
944 ("buffer_font_family", "#/definitions/FontFamilies"),
945 ("buffer_font_fallbacks", "#/definitions/FontFallbacks"),
946 ("ui_font_family", "#/definitions/FontFamilies"),
947 ("ui_font_fallbacks", "#/definitions/FontFallbacks"),
948 ],
949 );
950
951 root_schema
952 }
953
954 fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
955 vscode.f32_setting("editor.fontWeight", &mut current.buffer_font_weight);
956 vscode.f32_setting("editor.fontSize", &mut current.buffer_font_size);
957 vscode.string_setting("editor.font", &mut current.buffer_font_family);
958 // TODO: possibly map editor.fontLigatures to buffer_font_features?
959 }
960}
961
962fn merge<T: Copy>(target: &mut T, value: Option<T>) {
963 if let Some(value) = value {
964 *target = value;
965 }
966}