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