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:
parent
148547ecd1
commit
cc931a8fcc
3 changed files with 142 additions and 22 deletions
|
@ -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()),
|
||||
]);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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, _| {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue