Add infrastructure for loading icon themes from extensions (#23203)

This PR adds the supporting infrastructure to support loading icon
themes defined by extensions.

Here's an example icon theme:

```json
{
  "name": "My Icon Theme",
  "author": "Me <me@example.com>",
  "themes": [
    {
      "name": "My Icon Theme",
      "appearance": "dark",
      "file_icons": {
        "gleam": { "path": "./icons/file_type_gleam.svg" },
        "toml": { "path": "./icons/file_type_toml.svg" }
      }
    }
  ]
}
```

The icon paths are resolved relative to the root of the extension
directory.

Release Notes:

- N/A
This commit is contained in:
Marshall Bowers 2025-01-15 18:33:47 -05:00 committed by GitHub
parent 3b8a5c9647
commit f53915c711
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 303 additions and 10 deletions

View file

@ -124,14 +124,14 @@ const FILE_ICONS: &[(&str, &str)] = &[
("zig", "icons/file_icons/zig.svg"),
];
/// The ID of the default icon theme.
pub(crate) const DEFAULT_ICON_THEME_ID: &str = "zed";
/// The name of the default icon theme.
pub(crate) const DEFAULT_ICON_THEME_NAME: &str = "Zed (Default)";
/// Returns the default icon theme.
pub fn default_icon_theme() -> IconTheme {
IconTheme {
id: DEFAULT_ICON_THEME_ID.into(),
name: "Zed (Default)".into(),
id: "zed".into(),
name: DEFAULT_ICON_THEME_NAME.into(),
appearance: Appearance::Dark,
directory_icons: DirectoryIcons {
collapsed: Some("icons/file_icons/folder.svg".into()),

View file

@ -0,0 +1,44 @@
#![allow(missing_docs)]
use gpui::SharedString;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use crate::AppearanceContent;
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct IconThemeFamilyContent {
pub name: String,
pub author: String,
pub themes: Vec<IconThemeContent>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct IconThemeContent {
pub name: String,
pub appearance: AppearanceContent,
#[serde(default)]
pub directory_icons: DirectoryIconsContent,
#[serde(default)]
pub chevron_icons: ChevronIconsContent,
#[serde(default)]
pub file_icons: HashMap<String, IconDefinitionContent>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
pub struct DirectoryIconsContent {
pub collapsed: Option<SharedString>,
pub expanded: Option<SharedString>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
pub struct ChevronIconsContent {
pub collapsed: Option<SharedString>,
pub expanded: Option<SharedString>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct IconDefinitionContent {
pub path: SharedString,
}

View file

@ -11,8 +11,9 @@ use parking_lot::RwLock;
use util::ResultExt;
use crate::{
read_user_theme, refine_theme_family, Appearance, IconTheme, Theme, ThemeFamily,
ThemeFamilyContent, DEFAULT_ICON_THEME_ID,
read_icon_theme, read_user_theme, refine_theme_family, Appearance, AppearanceContent,
ChevronIcons, DirectoryIcons, IconDefinition, IconTheme, Theme, ThemeFamily,
ThemeFamilyContent, DEFAULT_ICON_THEME_NAME,
};
/// The metadata for a theme.
@ -80,7 +81,7 @@ impl ThemeRegistry {
let default_icon_theme = crate::default_icon_theme();
registry.state.write().icon_themes.insert(
default_icon_theme.id.clone().into(),
default_icon_theme.name.clone(),
Arc::new(default_icon_theme),
);
@ -208,7 +209,7 @@ impl ThemeRegistry {
/// Returns the default icon theme.
pub fn default_icon_theme(&self) -> Result<Arc<IconTheme>> {
self.get_icon_theme(DEFAULT_ICON_THEME_ID)
self.get_icon_theme(DEFAULT_ICON_THEME_NAME)
}
/// Returns the icon theme with the specified name.
@ -220,6 +221,67 @@ impl ThemeRegistry {
.ok_or_else(|| anyhow!("icon theme not found: {name}"))
.cloned()
}
/// Removes the icon themes with the given names from the registry.
pub fn remove_icon_themes(&self, icon_themes_to_remove: &[SharedString]) {
self.state
.write()
.icon_themes
.retain(|name, _| !icon_themes_to_remove.contains(name))
}
/// Loads the icon theme from the specified path and adds it to the registry.
///
/// The `icons_root_dir` parameter indicates the root directory from which
/// the relative paths to icons in the theme should be resolved against.
pub async fn load_icon_theme(
&self,
icon_theme_path: &Path,
icons_root_dir: &Path,
fs: Arc<dyn Fs>,
) -> Result<()> {
let icon_theme_family = read_icon_theme(icon_theme_path, fs).await?;
let mut state = self.state.write();
for icon_theme in icon_theme_family.themes {
let icon_theme = IconTheme {
id: uuid::Uuid::new_v4().to_string(),
name: icon_theme.name.into(),
appearance: match icon_theme.appearance {
AppearanceContent::Light => Appearance::Light,
AppearanceContent::Dark => Appearance::Dark,
},
directory_icons: DirectoryIcons {
collapsed: icon_theme.directory_icons.collapsed,
expanded: icon_theme.directory_icons.expanded,
},
chevron_icons: ChevronIcons {
collapsed: icon_theme.chevron_icons.collapsed,
expanded: icon_theme.chevron_icons.expanded,
},
file_icons: icon_theme
.file_icons
.into_iter()
.map(|(key, icon)| {
let path = icons_root_dir.join(icon.path.as_ref());
(
key,
IconDefinition {
path: path.to_string_lossy().to_string().into(),
},
)
})
.collect(),
};
state
.icon_themes
.insert(icon_theme.name.clone(), Arc::new(icon_theme));
}
Ok(())
}
}
impl Default for ThemeRegistry {

View file

@ -1,7 +1,7 @@
use crate::fallback_themes::zed_default_dark;
use crate::{
Appearance, IconTheme, SyntaxTheme, Theme, ThemeRegistry, ThemeStyleContent,
DEFAULT_ICON_THEME_ID,
DEFAULT_ICON_THEME_NAME,
};
use anyhow::Result;
use derive_more::{Deref, DerefMut};
@ -647,7 +647,7 @@ impl settings::Settings for ThemeSettings {
.icon_theme
.as_ref()
.and_then(|name| themes.get_icon_theme(name).ok())
.unwrap_or_else(|| themes.get_icon_theme(DEFAULT_ICON_THEME_ID).unwrap()),
.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),
};

View file

@ -12,6 +12,7 @@ mod default_colors;
mod fallback_themes;
mod font_family_cache;
mod icon_theme;
mod icon_theme_schema;
mod registry;
mod scale;
mod schema;
@ -34,6 +35,7 @@ use uuid::Uuid;
pub use crate::default_colors::*;
pub use crate::font_family_cache::*;
pub use crate::icon_theme::*;
pub use crate::icon_theme_schema::*;
pub use crate::registry::*;
pub use crate::scale::*;
pub use crate::schema::*;
@ -364,3 +366,14 @@ pub async fn read_user_theme(theme_path: &Path, fs: Arc<dyn Fs>) -> Result<Theme
Ok(theme_family)
}
/// Asynchronously reads the icon theme from the specified path.
pub async fn read_icon_theme(
icon_theme_path: &Path,
fs: Arc<dyn Fs>,
) -> Result<IconThemeFamilyContent> {
let reader = fs.open_sync(icon_theme_path).await?;
let icon_theme_family: IconThemeFamilyContent = serde_json_lenient::from_reader(reader)?;
Ok(icon_theme_family)
}