theme: Add support for setting light/dark icon themes (#24702)

This PR adds support for configuring both a light and dark icon theme in
`settings.json`.

In addition to accepting just an icon theme name, the `icon_theme` field
now also accepts an object in the following form:

```jsonc
{
  "icon_theme": {
    "mode": "system",
    "light": "Zed (Default)",
    "dark": "Zed (Default)"
  }
}
```

Both `light` and `dark` are required, and indicate which icon theme
should be used when the system is in light mode and dark mode,
respectively.

The `mode` field is optional and indicates which icon theme should be
used:
- `"system"` - Use the icon theme that corresponds to the system's
appearance.
- `"light"` - Use the icon theme indicated by the `light` field.
- `"dark"` - Use the icon theme indicated by the `dark` field.

Closes https://github.com/zed-industries/zed/issues/24695.

Release Notes:

- Added support for configuring both a light and dark icon theme and
switching between them based on system preference.
This commit is contained in:
Marshall Bowers 2025-02-11 18:45:37 -05:00 committed by GitHub
parent 148547ecd1
commit cc931a8fcc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 142 additions and 22 deletions

View file

@ -112,7 +112,6 @@ pub struct ThemeSettings {
/// The terminal font family can be overridden using it's own setting.
pub buffer_line_height: BufferLineHeight,
/// The current theme selection.
/// TODO: Document this further
pub theme_selection: Option<ThemeSelection>,
/// The active theme.
pub active_theme: Arc<Theme>,
@ -120,6 +119,8 @@ pub struct ThemeSettings {
///
/// Note: This setting is still experimental. See [this tracking issue](https://github.com/zed-industries/zed/issues/18078)
pub theme_overrides: Option<ThemeStyleContent>,
/// The current icon theme selection.
pub icon_theme_selection: Option<IconThemeSelection>,
/// The active icon theme.
pub active_icon_theme: Arc<IconTheme>,
/// The density of the UI.
@ -167,25 +168,28 @@ impl ThemeSettings {
/// Reloads the current icon theme.
///
/// Reads the [`ThemeSettings`] to know which icon theme should be loaded.
/// Reads the [`ThemeSettings`] to know which icon theme should be loaded,
/// taking into account the current [`SystemAppearance`].
pub fn reload_current_icon_theme(cx: &mut App) {
let mut theme_settings = ThemeSettings::get_global(cx).clone();
let system_appearance = SystemAppearance::global(cx);
let active_theme = theme_settings.active_icon_theme.clone();
let mut icon_theme_name = active_theme.name.as_ref();
if let Some(icon_theme_selection) = theme_settings.icon_theme_selection.clone() {
let mut icon_theme_name = icon_theme_selection.icon_theme(*system_appearance);
// If the selected theme doesn't exist, fall back to the default theme.
let theme_registry = ThemeRegistry::global(cx);
if theme_registry
.get_icon_theme(icon_theme_name)
.ok()
.is_none()
{
icon_theme_name = DEFAULT_ICON_THEME_NAME;
};
// If the selected icon theme doesn't exist, fall back to the default theme.
let theme_registry = ThemeRegistry::global(cx);
if theme_registry
.get_icon_theme(icon_theme_name)
.ok()
.is_none()
{
icon_theme_name = DEFAULT_ICON_THEME_NAME;
};
if let Some(_theme) = theme_settings.switch_icon_theme(icon_theme_name, cx) {
ThemeSettings::override_global(theme_settings, cx);
if let Some(_theme) = theme_settings.switch_icon_theme(icon_theme_name, cx) {
ThemeSettings::override_global(theme_settings, cx);
}
}
}
}
@ -299,6 +303,55 @@ impl ThemeSelection {
}
}
fn icon_theme_name_ref(_: &mut SchemaGenerator) -> Schema {
Schema::new_ref("#/definitions/IconThemeName".into())
}
/// Represents the selection of an icon theme, which can be either static or dynamic.
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(untagged)]
pub enum IconThemeSelection {
/// A static icon theme selection, represented by a single icon theme name.
Static(#[schemars(schema_with = "icon_theme_name_ref")] String),
/// A dynamic icon theme selection, which can change based on the [`ThemeMode`].
Dynamic {
/// The mode used to determine which theme to use.
#[serde(default)]
mode: ThemeMode,
/// The icon theme to use for light mode.
#[schemars(schema_with = "icon_theme_name_ref")]
light: String,
/// The icon theme to use for dark mode.
#[schemars(schema_with = "icon_theme_name_ref")]
dark: String,
},
}
impl IconThemeSelection {
/// Returns the icon theme name based on the given [`Appearance`].
pub fn icon_theme(&self, system_appearance: Appearance) -> &str {
match self {
Self::Static(theme) => theme,
Self::Dynamic { mode, light, dark } => match mode {
ThemeMode::Light => light,
ThemeMode::Dark => dark,
ThemeMode::System => match system_appearance {
Appearance::Light => light,
Appearance::Dark => dark,
},
},
}
}
/// Returns the [`ThemeMode`] for the [`IconThemeSelection`].
pub fn mode(&self) -> Option<ThemeMode> {
match self {
IconThemeSelection::Static(_) => None,
IconThemeSelection::Dynamic { mode, .. } => Some(*mode),
}
}
}
/// Settings for rendering text in UI and text buffers.
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
pub struct ThemeSettingsContent {
@ -344,7 +397,7 @@ pub struct ThemeSettingsContent {
pub theme: Option<ThemeSelection>,
/// The name of the icon theme to use.
#[serde(default)]
pub icon_theme: Option<String>,
pub icon_theme: Option<IconThemeSelection>,
/// UNSTABLE: Expect many elements to be broken.
///
@ -393,6 +446,27 @@ impl ThemeSettingsContent {
}
}
/// Sets the icon theme for the given appearance to the icon theme with the specified name.
pub fn set_icon_theme(&mut self, icon_theme_name: String, appearance: Appearance) {
if let Some(selection) = self.icon_theme.as_mut() {
let icon_theme_to_update = match selection {
IconThemeSelection::Static(theme) => theme,
IconThemeSelection::Dynamic { mode, light, dark } => match mode {
ThemeMode::Light => light,
ThemeMode::Dark => dark,
ThemeMode::System => match appearance {
Appearance::Light => light,
Appearance::Dark => dark,
},
},
};
*icon_theme_to_update = icon_theme_name.to_string();
} else {
self.theme = Some(ThemeSelection::Static(icon_theme_name.to_string()));
}
}
/// Sets the mode for the theme.
pub fn set_mode(&mut self, mode: ThemeMode) {
if let Some(selection) = self.theme.as_mut() {
@ -419,6 +493,27 @@ impl ThemeSettingsContent {
dark: ThemeSettings::DEFAULT_DARK_THEME.into(),
});
}
if let Some(selection) = self.icon_theme.as_mut() {
match selection {
IconThemeSelection::Static(icon_theme) => {
// If the icon theme was previously set to a single static
// theme, we don't know whether it was a light or dark
// theme, so we just use it for both.
self.icon_theme = Some(IconThemeSelection::Dynamic {
mode,
light: icon_theme.clone(),
dark: icon_theme.clone(),
});
}
IconThemeSelection::Dynamic {
mode: mode_to_update,
..
} => *mode_to_update = mode,
}
} else {
self.icon_theme = Some(IconThemeSelection::Static(DEFAULT_ICON_THEME_NAME.into()));
}
}
}
@ -588,10 +683,15 @@ impl settings::Settings for ThemeSettings {
.or(themes.get(&zed_default_dark().name))
.unwrap(),
theme_overrides: None,
icon_theme_selection: defaults.icon_theme.clone(),
active_icon_theme: defaults
.icon_theme
.as_ref()
.and_then(|name| themes.get_icon_theme(name).ok())
.and_then(|selection| {
themes
.get_icon_theme(selection.icon_theme(*system_appearance))
.ok()
})
.unwrap_or_else(|| themes.get_icon_theme(DEFAULT_ICON_THEME_NAME).unwrap()),
ui_density: defaults.ui_density.unwrap_or(UiDensity::Default),
unnecessary_code_fade: defaults.unnecessary_code_fade.unwrap_or(0.0),
@ -647,8 +747,12 @@ impl settings::Settings for ThemeSettings {
this.apply_theme_overrides();
if let Some(value) = &value.icon_theme {
if let Some(icon_theme) = themes.get_icon_theme(value).log_err() {
this.active_icon_theme = icon_theme.clone();
this.icon_theme_selection = Some(value.clone());
let icon_theme_name = value.icon_theme(*system_appearance);
if let Some(icon_theme) = themes.get_icon_theme(icon_theme_name).log_err() {
this.active_icon_theme = icon_theme;
}
}
@ -689,8 +793,21 @@ impl settings::Settings for ThemeSettings {
..Default::default()
};
let icon_theme_names = ThemeRegistry::global(cx)
.list_icon_themes()
.into_iter()
.map(|icon_theme| Value::String(icon_theme.name.to_string()))
.collect();
let icon_theme_name_schema = SchemaObject {
instance_type: Some(InstanceType::String.into()),
enum_values: Some(icon_theme_names),
..Default::default()
};
root_schema.definitions.extend([
("ThemeName".into(), theme_name_schema.into()),
("IconThemeName".into(), icon_theme_name_schema.into()),
("FontFamilies".into(), params.font_family_schema()),
("FontFallbacks".into(), params.font_fallback_schema()),
]);

View file

@ -7,7 +7,7 @@ use gpui::{
use picker::{Picker, PickerDelegate};
use settings::{update_settings_file, Settings as _, SettingsStore};
use std::sync::Arc;
use theme::{IconTheme, ThemeMeta, ThemeRegistry, ThemeSettings};
use theme::{Appearance, IconTheme, ThemeMeta, ThemeRegistry, ThemeSettings};
use ui::{prelude::*, v_flex, ListItem, ListItemSpacing};
use util::ResultExt;
use workspace::{ui::HighlightedLabel, ModalView};
@ -151,7 +151,7 @@ impl PickerDelegate for IconThemeSelectorDelegate {
fn confirm(
&mut self,
_: bool,
_window: &mut Window,
window: &mut Window,
cx: &mut Context<Picker<IconThemeSelectorDelegate>>,
) {
self.selection_completed = true;
@ -165,8 +165,10 @@ impl PickerDelegate for IconThemeSelectorDelegate {
value = theme_name
);
let appearance = Appearance::from(window.appearance());
update_settings_file::<ThemeSettings>(self.fs.clone(), cx, move |settings, _| {
settings.icon_theme = Some(theme_name.to_string());
settings.set_icon_theme(theme_name.to_string(), appearance);
});
self.selector

View file

@ -1082,6 +1082,7 @@ impl Workspace {
*SystemAppearance::global_mut(cx) = SystemAppearance(window_appearance.into());
ThemeSettings::reload_current_theme(cx);
ThemeSettings::reload_current_icon_theme(cx);
}),
cx.on_release(move |this, cx| {
this.app_state.workspace_store.update(cx, move |store, _| {