From 88e42cc7aaba8c301904cf522bf09cc960274253 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 14 Jan 2025 17:49:36 -0500 Subject: [PATCH] Refactor file icons to use `IconTheme` (#23153) This PR adds the initial concept of an `IconTheme` and refactors `FileIcons` to use it to resolve the icons. The `IconTheme` will ultimately be used to allow users to select a different set of icons to use. Currently, however, this is just laying the foundation for that work. The association between file types and icons is now handled by the icon theme when we resolve file icons. This mapping has been moved out of `file_types.json` and into `icon_theme.rs`. Release Notes: - N/A --- Cargo.lock | 2 + assets/icons/file_icons/file_types.json | 203 ------------------------ crates/file_icons/Cargo.toml | 6 +- crates/file_icons/src/file_icons.rs | 31 ++-- crates/repl/src/kernels/mod.rs | 2 +- crates/tasks_ui/src/modal.rs | 2 +- crates/theme/src/icon_theme.rs | 127 +++++++++++++++ crates/theme/src/registry.rs | 21 ++- crates/theme/src/settings.rs | 24 ++- crates/theme/src/theme.rs | 2 + crates/zed/src/main.rs | 2 +- 11 files changed, 196 insertions(+), 226 deletions(-) create mode 100644 crates/theme/src/icon_theme.rs diff --git a/Cargo.lock b/Cargo.lock index 44165e8a44..60ff786626 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4490,6 +4490,8 @@ dependencies = [ "serde", "serde_derive", "serde_json", + "settings", + "theme", "util", ] diff --git a/assets/icons/file_icons/file_types.json b/assets/icons/file_icons/file_types.json index 1f579b0da8..463d0d9c07 100644 --- a/assets/icons/file_icons/file_types.json +++ b/assets/icons/file_icons/file_types.json @@ -210,208 +210,5 @@ "zsh_profile": "terminal", "zshenv": "terminal", "zshrc": "terminal" - }, - "types": { - "astro": { - "icon": "icons/file_icons/astro.svg" - }, - "audio": { - "icon": "icons/file_icons/audio.svg" - }, - "bun": { - "icon": "icons/file_icons/bun.svg" - }, - "c": { - "icon": "icons/file_icons/c.svg" - }, - "code": { - "icon": "icons/file_icons/code.svg" - }, - "coffeescript": { - "icon": "icons/file_icons/coffeescript.svg" - }, - "collapsed_chevron": { - "icon": "icons/file_icons/chevron_right.svg" - }, - "collapsed_folder": { - "icon": "icons/file_icons/folder.svg" - }, - "cpp": { - "icon": "icons/file_icons/cpp.svg" - }, - "css": { - "icon": "icons/file_icons/css.svg" - }, - "dart": { - "icon": "icons/file_icons/dart.svg" - }, - "default": { - "icon": "icons/file_icons/file.svg" - }, - "diff": { - "icon": "icons/file_icons/diff.svg" - }, - "docker": { - "icon": "icons/file_icons/docker.svg" - }, - "document": { - "icon": "icons/file_icons/book.svg" - }, - "elixir": { - "icon": "icons/file_icons/elixir.svg" - }, - "elm": { - "icon": "icons/file_icons/elm.svg" - }, - "erlang": { - "icon": "icons/file_icons/erlang.svg" - }, - "eslint": { - "icon": "icons/file_icons/eslint.svg" - }, - "expanded_chevron": { - "icon": "icons/file_icons/chevron_down.svg" - }, - "expanded_folder": { - "icon": "icons/file_icons/folder_open.svg" - }, - "font": { - "icon": "icons/file_icons/font.svg" - }, - "fsharp": { - "icon": "icons/file_icons/fsharp.svg" - }, - "gleam": { - "icon": "icons/file_icons/gleam.svg" - }, - "go": { - "icon": "icons/file_icons/go.svg" - }, - "graphql": { - "icon": "icons/file_icons/graphql.svg" - }, - "haskell": { - "icon": "icons/file_icons/haskell.svg" - }, - "hcl": { - "icon": "icons/file_icons/hcl.svg" - }, - "heroku": { - "icon": "icons/file_icons/heroku.svg" - }, - "image": { - "icon": "icons/file_icons/image.svg" - }, - "java": { - "icon": "icons/file_icons/java.svg" - }, - "javascript": { - "icon": "icons/file_icons/javascript.svg" - }, - "julia": { - "icon": "icons/file_icons/julia.svg" - }, - "kotlin": { - "icon": "icons/file_icons/kotlin.svg" - }, - "lock": { - "icon": "icons/file_icons/lock.svg" - }, - "log": { - "icon": "icons/file_icons/info.svg" - }, - "lua": { - "icon": "icons/file_icons/lua.svg" - }, - "metal": { - "icon": "icons/file_icons/metal.svg" - }, - "nim": { - "icon": "icons/file_icons/nim.svg" - }, - "nix": { - "icon": "icons/file_icons/nix.svg" - }, - "ocaml": { - "icon": "icons/file_icons/ocaml.svg" - }, - "phoenix": { - "icon": "icons/file_icons/phoenix.svg" - }, - "php": { - "icon": "icons/file_icons/php.svg" - }, - "prettier": { - "icon": "icons/file_icons/prettier.svg" - }, - "prisma": { - "icon": "icons/file_icons/prisma.svg" - }, - "python": { - "icon": "icons/file_icons/python.svg" - }, - "r": { - "icon": "icons/file_icons/r.svg" - }, - "react": { - "icon": "icons/file_icons/react.svg" - }, - "roc": { - "icon": "icons/file_icons/roc.svg" - }, - "ruby": { - "icon": "icons/file_icons/ruby.svg" - }, - "rust": { - "icon": "icons/file_icons/rust.svg" - }, - "sass": { - "icon": "icons/file_icons/sass.svg" - }, - "scala": { - "icon": "icons/file_icons/scala.svg" - }, - "settings": { - "icon": "icons/file_icons/settings.svg" - }, - "storage": { - "icon": "icons/file_icons/database.svg" - }, - "swift": { - "icon": "icons/file_icons/swift.svg" - }, - "tcl": { - "icon": "icons/file_icons/tcl.svg" - }, - "template": { - "icon": "icons/file_icons/html.svg" - }, - "terminal": { - "icon": "icons/file_icons/terminal.svg" - }, - "terraform": { - "icon": "icons/file_icons/terraform.svg" - }, - "toml": { - "icon": "icons/file_icons/toml.svg" - }, - "typescript": { - "icon": "icons/file_icons/typescript.svg" - }, - "v": { - "icon": "icons/file_icons/v.svg" - }, - "vcs": { - "icon": "icons/file_icons/git.svg" - }, - "video": { - "icon": "icons/file_icons/video.svg" - }, - "vue": { - "icon": "icons/file_icons/vue.svg" - }, - "zig": { - "icon": "icons/file_icons/zig.svg" - } } } diff --git a/crates/file_icons/Cargo.toml b/crates/file_icons/Cargo.toml index c3e9268997..c0847cd888 100644 --- a/crates/file_icons/Cargo.toml +++ b/crates/file_icons/Cargo.toml @@ -13,9 +13,11 @@ path = "src/file_icons.rs" doctest = false [dependencies] +collections.workspace = true gpui.workspace = true -util.workspace = true serde.workspace = true serde_derive.workspace = true serde_json.workspace = true -collections.workspace = true +settings.workspace = true +theme.workspace = true +util.workspace = true diff --git a/crates/file_icons/src/file_icons.rs b/crates/file_icons/src/file_icons.rs index a0b39fb763..62ddb98f69 100644 --- a/crates/file_icons/src/file_icons.rs +++ b/crates/file_icons/src/file_icons.rs @@ -4,18 +4,14 @@ use collections::HashMap; use gpui::{AppContext, AssetSource, Global, SharedString}; use serde_derive::Deserialize; +use settings::Settings; +use theme::ThemeSettings; use util::{maybe, paths::PathExt}; -#[derive(Deserialize, Debug)] -struct TypeConfig { - icon: SharedString, -} - #[derive(Deserialize, Debug)] pub struct FileIcons { stems: HashMap, suffixes: HashMap, - types: HashMap, } impl Global for FileIcons {} @@ -37,14 +33,13 @@ impl FileIcons { pub fn new(assets: impl AssetSource) -> Self { assets - .load("icons/file_icons/file_types.json") + .load(FILE_TYPES_ASSET) .ok() .flatten() .and_then(|file| serde_json::from_str::(str::from_utf8(&file).unwrap()).ok()) .unwrap_or_else(|| FileIcons { stems: HashMap::default(), suffixes: HashMap::default(), - types: HashMap::default(), }) } @@ -57,20 +52,24 @@ impl FileIcons { let suffix = path.icon_stem_or_suffix()?; if let Some(type_str) = this.stems.get(suffix) { - return this.get_type_icon(type_str); + return this.get_icon_for_type(type_str, cx); } this.suffixes .get(suffix) - .and_then(|type_str| this.get_type_icon(type_str)) + .and_then(|type_str| this.get_icon_for_type(type_str, cx)) }) - .or_else(|| this.get_type_icon("default")) + .or_else(|| this.get_icon_for_type("default", cx)) } - pub fn get_type_icon(&self, typ: &str) -> Option { - self.types + pub fn get_icon_for_type(&self, typ: &str, cx: &AppContext) -> Option { + let theme_settings = ThemeSettings::get_global(cx); + + theme_settings + .active_icon_theme + .file_icons .get(typ) - .map(|type_config| type_config.icon.clone()) + .map(|icon_definition| icon_definition.path.clone()) } pub fn get_folder_icon(expanded: bool, cx: &AppContext) -> Option { @@ -82,7 +81,7 @@ impl FileIcons { COLLAPSED_DIRECTORY_TYPE }; - this.get_type_icon(key) + this.get_icon_for_type(key, cx) } pub fn get_chevron_icon(expanded: bool, cx: &AppContext) -> Option { @@ -94,6 +93,6 @@ impl FileIcons { COLLAPSED_CHEVRON_TYPE }; - this.get_type_icon(key) + this.get_icon_for_type(key, cx) } } diff --git a/crates/repl/src/kernels/mod.rs b/crates/repl/src/kernels/mod.rs index e829b1946c..ddd3cebf01 100644 --- a/crates/repl/src/kernels/mod.rs +++ b/crates/repl/src/kernels/mod.rs @@ -69,7 +69,7 @@ impl KernelSpecification { }; file_icons::FileIcons::get(cx) - .get_type_icon(&lang_name.to_lowercase()) + .get_icon_for_type(&lang_name.to_lowercase(), cx) .map(Icon::from_path) .unwrap_or(Icon::new(IconName::ReplNeutral)) } diff --git a/crates/tasks_ui/src/modal.rs b/crates/tasks_ui/src/modal.rs index ead9a01396..1dc63c4852 100644 --- a/crates/tasks_ui/src/modal.rs +++ b/crates/tasks_ui/src/modal.rs @@ -350,7 +350,7 @@ impl PickerDelegate for TasksModalDelegate { TaskSourceKind::AbsPath { .. } => Some(Icon::new(IconName::Settings)), TaskSourceKind::Worktree { .. } => Some(Icon::new(IconName::FileTree)), TaskSourceKind::Language { name } => file_icons::FileIcons::get(cx) - .get_type_icon(&name.to_lowercase()) + .get_icon_for_type(&name.to_lowercase(), cx) .map(Icon::from_path), } .map(|icon| icon.color(Color::Muted).size(IconSize::Small)); diff --git a/crates/theme/src/icon_theme.rs b/crates/theme/src/icon_theme.rs new file mode 100644 index 0000000000..1a08a93f08 --- /dev/null +++ b/crates/theme/src/icon_theme.rs @@ -0,0 +1,127 @@ +use collections::HashMap; +use gpui::SharedString; + +use crate::Appearance; + +/// A family of icon themes. +pub struct IconThemeFamily { + /// The unique ID for the icon theme family. + pub id: String, + /// The name of the icon theme family. + pub name: SharedString, + /// The author of the icon theme family. + pub author: SharedString, + /// The list of icon themes in the family. + pub themes: Vec, +} + +/// An icon theme. +#[derive(Debug, PartialEq)] +pub struct IconTheme { + /// The unique ID for the icon theme. + pub id: String, + /// The name of the icon theme. + pub name: SharedString, + /// The appearance of the icon theme (e.g., light or dark). + pub appearance: Appearance, + /// The mapping of file types to icon definitions. + pub file_icons: HashMap, +} + +/// An icon definition. +#[derive(Debug, PartialEq)] +pub struct IconDefinition { + /// The path to the icon file. + pub path: SharedString, +} + +/// A mapping of a file type identifier to its corresponding icon. +const FILE_ICONS: &[(&str, &str)] = &[ + ("astro", "icons/file_icons/astro.svg"), + ("audio", "icons/file_icons/audio.svg"), + ("bun", "icons/file_icons/bun.svg"), + ("c", "icons/file_icons/c.svg"), + ("code", "icons/file_icons/code.svg"), + ("coffeescript", "icons/file_icons/coffeescript.svg"), + ("collapsed_chevron", "icons/file_icons/chevron_right.svg"), + ("collapsed_folder", "icons/file_icons/folder.svg"), + ("cpp", "icons/file_icons/cpp.svg"), + ("css", "icons/file_icons/css.svg"), + ("dart", "icons/file_icons/dart.svg"), + ("default", "icons/file_icons/file.svg"), + ("diff", "icons/file_icons/diff.svg"), + ("docker", "icons/file_icons/docker.svg"), + ("document", "icons/file_icons/book.svg"), + ("elixir", "icons/file_icons/elixir.svg"), + ("elm", "icons/file_icons/elm.svg"), + ("erlang", "icons/file_icons/erlang.svg"), + ("eslint", "icons/file_icons/eslint.svg"), + ("expanded_chevron", "icons/file_icons/chevron_down.svg"), + ("expanded_folder", "icons/file_icons/folder_open.svg"), + ("font", "icons/file_icons/font.svg"), + ("fsharp", "icons/file_icons/fsharp.svg"), + ("gleam", "icons/file_icons/gleam.svg"), + ("go", "icons/file_icons/go.svg"), + ("graphql", "icons/file_icons/graphql.svg"), + ("haskell", "icons/file_icons/haskell.svg"), + ("hcl", "icons/file_icons/hcl.svg"), + ("heroku", "icons/file_icons/heroku.svg"), + ("image", "icons/file_icons/image.svg"), + ("java", "icons/file_icons/java.svg"), + ("javascript", "icons/file_icons/javascript.svg"), + ("julia", "icons/file_icons/julia.svg"), + ("kotlin", "icons/file_icons/kotlin.svg"), + ("lock", "icons/file_icons/lock.svg"), + ("log", "icons/file_icons/info.svg"), + ("lua", "icons/file_icons/lua.svg"), + ("metal", "icons/file_icons/metal.svg"), + ("nim", "icons/file_icons/nim.svg"), + ("nix", "icons/file_icons/nix.svg"), + ("ocaml", "icons/file_icons/ocaml.svg"), + ("phoenix", "icons/file_icons/phoenix.svg"), + ("php", "icons/file_icons/php.svg"), + ("prettier", "icons/file_icons/prettier.svg"), + ("prisma", "icons/file_icons/prisma.svg"), + ("python", "icons/file_icons/python.svg"), + ("r", "icons/file_icons/r.svg"), + ("react", "icons/file_icons/react.svg"), + ("roc", "icons/file_icons/roc.svg"), + ("ruby", "icons/file_icons/ruby.svg"), + ("rust", "icons/file_icons/rust.svg"), + ("sass", "icons/file_icons/sass.svg"), + ("scala", "icons/file_icons/scala.svg"), + ("settings", "icons/file_icons/settings.svg"), + ("storage", "icons/file_icons/database.svg"), + ("swift", "icons/file_icons/swift.svg"), + ("tcl", "icons/file_icons/tcl.svg"), + ("template", "icons/file_icons/html.svg"), + ("terminal", "icons/file_icons/terminal.svg"), + ("terraform", "icons/file_icons/terraform.svg"), + ("toml", "icons/file_icons/toml.svg"), + ("typescript", "icons/file_icons/typescript.svg"), + ("v", "icons/file_icons/v.svg"), + ("vcs", "icons/file_icons/git.svg"), + ("video", "icons/file_icons/video.svg"), + ("vue", "icons/file_icons/vue.svg"), + ("zig", "icons/file_icons/zig.svg"), +]; + +/// The ID of the default icon theme. +pub(crate) const DEFAULT_ICON_THEME_ID: &str = "zed"; + +/// Returns the default icon theme. +pub fn default_icon_theme() -> IconTheme { + IconTheme { + id: DEFAULT_ICON_THEME_ID.into(), + name: "Zed (Default)".into(), + appearance: Appearance::Dark, + file_icons: HashMap::from_iter(FILE_ICONS.into_iter().map(|(ty, path)| { + ( + ty.to_string(), + IconDefinition { + path: (*path).into(), + }, + ) + })), + } +} diff --git a/crates/theme/src/registry.rs b/crates/theme/src/registry.rs index a511fb9da3..ad5ee9c86e 100644 --- a/crates/theme/src/registry.rs +++ b/crates/theme/src/registry.rs @@ -11,7 +11,8 @@ use parking_lot::RwLock; use util::ResultExt; use crate::{ - read_user_theme, refine_theme_family, Appearance, Theme, ThemeFamily, ThemeFamilyContent, + read_user_theme, refine_theme_family, Appearance, IconTheme, Theme, ThemeFamily, + ThemeFamilyContent, }; /// The metadata for a theme. @@ -36,6 +37,7 @@ impl Global for GlobalThemeRegistry {} struct ThemeRegistryState { themes: HashMap>, + icon_themes: HashMap>, } /// The registry for themes. @@ -67,6 +69,7 @@ impl ThemeRegistry { let registry = Self { state: RwLock::new(ThemeRegistryState { themes: HashMap::default(), + icon_themes: HashMap::default(), }), assets, }; @@ -75,6 +78,12 @@ impl ThemeRegistry { // for tests. registry.insert_theme_families([crate::fallback_themes::zed_default_themes()]); + let default_icon_theme = crate::default_icon_theme(); + registry.state.write().icon_themes.insert( + default_icon_theme.id.clone().into(), + Arc::new(default_icon_theme), + ); + registry } @@ -196,6 +205,16 @@ impl ThemeRegistry { Ok(()) } + + /// Returns the icon theme with the specified name. + pub fn get_icon_theme(&self, name: &str) -> Result> { + self.state + .read() + .icon_themes + .get(name) + .ok_or_else(|| anyhow!("icon theme not found: {name}")) + .cloned() + } } impl Default for ThemeRegistry { diff --git a/crates/theme/src/settings.rs b/crates/theme/src/settings.rs index cebfea84bc..618e3f0268 100644 --- a/crates/theme/src/settings.rs +++ b/crates/theme/src/settings.rs @@ -1,5 +1,8 @@ use crate::fallback_themes::zed_default_dark; -use crate::{Appearance, SyntaxTheme, Theme, ThemeRegistry, ThemeStyleContent}; +use crate::{ + Appearance, IconTheme, SyntaxTheme, Theme, ThemeRegistry, ThemeStyleContent, + DEFAULT_ICON_THEME_ID, +}; use anyhow::Result; use derive_more::{Deref, DerefMut}; use gpui::{ @@ -118,6 +121,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, + /// The active icon theme. + pub active_icon_theme: Arc, /// The density of the UI. /// Note: This setting is still experimental. See [this tracking issue]( pub ui_density: UiDensity, @@ -324,6 +329,12 @@ pub struct ThemeSettingsContent { /// The name of the Zed theme to use. #[serde(default)] pub theme: Option, + /// The name of the icon theme to use. + /// + /// Currently not exposed to the user. + #[serde(skip)] + #[serde(default)] + pub icon_theme: Option, /// UNSTABLE: Expect many elements to be broken. /// @@ -632,6 +643,11 @@ impl settings::Settings for ThemeSettings { .or(themes.get(&zed_default_dark().name)) .unwrap(), theme_overrides: None, + active_icon_theme: defaults + .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()), ui_density: defaults.ui_density.unwrap_or(UiDensity::Default), unnecessary_code_fade: defaults.unnecessary_code_fade.unwrap_or(0.0), }; @@ -685,6 +701,12 @@ impl settings::Settings for ThemeSettings { this.theme_overrides.clone_from(&value.theme_overrides); 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(); + } + } + merge(&mut this.ui_font_size, value.ui_font_size.map(Into::into)); this.ui_font_size = this.ui_font_size.clamp(px(6.), px(100.)); diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 2a4802b4eb..4f395696aa 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -11,6 +11,7 @@ mod default_colors; mod fallback_themes; mod font_family_cache; +mod icon_theme; mod registry; mod scale; mod schema; @@ -32,6 +33,7 @@ use uuid::Uuid; pub use crate::default_colors::*; pub use crate::font_family_cache::*; +pub use crate::icon_theme::*; pub use crate::registry::*; pub use crate::scale::*; pub use crate::schema::*; diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index cc4e98f38c..4283c8de04 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -1217,7 +1217,7 @@ fn watch_file_types(fs: Arc, cx: &mut AppContext) { use gpui::UpdateGlobal; let path = { - let p = Path::new("assets/icons/file_icons/file_types.json"); + let p = Path::new("assets").join(file_icons::FILE_TYPES_ASSET); let Ok(full_path) = p.canonicalize() else { return; };