Add support for named folder icons (#36351)
Adds a `named_directory_icons` field to the icon theme that can be used
to specify a collection of icons for collapsed and expanded folders
based on the folder name.
The `named_directory_icons` is a map from the folder name to a
`DirectoryIcons` object containing the paths to the expanded and
collapsed icons for that folder:
```json
{
"named_directory_icons": {
".angular": {
"collapsed": "./icons/folder_angular.svg",
"expanded": "./icons/folder_angular_open.svg"
}
}
}
```
Closes #20295
Also referenced
https://github.com/zed-industries/zed/pull/23987#issuecomment-2638869213
Example using https://github.com/jacobtread/zed-vscode-icons/ which I've
ported over from a VSCode theme,
<img width="609" height="1307" alt="image"
src="https://github.com/user-attachments/assets/2d3c120a-b2f0-43fd-889d-641ad4bb9cee"
/>
Release Notes:
- Added support for icon themes to change the folder icon based on the
directory name.
---------
Co-authored-by: Marshall Bowers <git@maxdeviant.com>
This commit is contained in:
@@ -162,7 +162,7 @@ impl MentionUri {
|
||||
FileIcons::get_icon(abs_path, cx).unwrap_or_else(|| IconName::File.path().into())
|
||||
}
|
||||
MentionUri::PastedImage => IconName::Image.path().into(),
|
||||
MentionUri::Directory { .. } => FileIcons::get_folder_icon(false, cx)
|
||||
MentionUri::Directory { abs_path } => FileIcons::get_folder_icon(false, abs_path, cx)
|
||||
.unwrap_or_else(|| IconName::Folder.path().into()),
|
||||
MentionUri::Symbol { .. } => IconName::Code.path().into(),
|
||||
MentionUri::Thread { .. } => IconName::Thread.path().into(),
|
||||
|
||||
@@ -596,11 +596,12 @@ impl ContextPickerCompletionProvider {
|
||||
file_name.to_string()
|
||||
};
|
||||
|
||||
let path = Path::new(&full_path);
|
||||
let crease_icon_path = if is_directory {
|
||||
FileIcons::get_folder_icon(false, cx).unwrap_or_else(|| IconName::Folder.path().into())
|
||||
FileIcons::get_folder_icon(false, path, cx)
|
||||
.unwrap_or_else(|| IconName::Folder.path().into())
|
||||
} else {
|
||||
FileIcons::get_icon(Path::new(&full_path), cx)
|
||||
.unwrap_or_else(|| IconName::File.path().into())
|
||||
FileIcons::get_icon(path, cx).unwrap_or_else(|| IconName::File.path().into())
|
||||
};
|
||||
let completion_icon_path = if is_recent {
|
||||
IconName::HistoryRerun.path().into()
|
||||
|
||||
@@ -330,7 +330,7 @@ pub fn render_file_context_entry(
|
||||
});
|
||||
|
||||
let file_icon = if is_directory {
|
||||
FileIcons::get_folder_icon(false, cx)
|
||||
FileIcons::get_folder_icon(false, path, cx)
|
||||
} else {
|
||||
FileIcons::get_icon(path, cx)
|
||||
}
|
||||
|
||||
@@ -695,14 +695,15 @@ impl PickerDelegate for OpenPathDelegate {
|
||||
if !settings.file_icons {
|
||||
return None;
|
||||
}
|
||||
|
||||
let path = path::Path::new(&candidate.path.string);
|
||||
let icon = if candidate.is_dir {
|
||||
if is_current_dir_candidate {
|
||||
return Some(Icon::new(IconName::ReplyArrowRight).color(Color::Muted));
|
||||
} else {
|
||||
FileIcons::get_folder_icon(false, cx)?
|
||||
FileIcons::get_folder_icon(false, path, cx)?
|
||||
}
|
||||
} else {
|
||||
let path = path::Path::new(&candidate.path.string);
|
||||
FileIcons::get_icon(path, cx)?
|
||||
};
|
||||
Some(Icon::from_path(icon).color(Color::Muted))
|
||||
|
||||
@@ -93,8 +93,47 @@ impl FileIcons {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_folder_icon(expanded: bool, cx: &App) -> Option<SharedString> {
|
||||
fn get_folder_icon(icon_theme: &Arc<IconTheme>, expanded: bool) -> Option<SharedString> {
|
||||
pub fn get_folder_icon(expanded: bool, path: &Path, cx: &App) -> Option<SharedString> {
|
||||
fn get_folder_icon(
|
||||
icon_theme: &Arc<IconTheme>,
|
||||
path: &Path,
|
||||
expanded: bool,
|
||||
) -> Option<SharedString> {
|
||||
let name = path.file_name()?.to_str()?.trim();
|
||||
if name.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let directory_icons = icon_theme.named_directory_icons.get(name)?;
|
||||
|
||||
if expanded {
|
||||
directory_icons.expanded.clone()
|
||||
} else {
|
||||
directory_icons.collapsed.clone()
|
||||
}
|
||||
}
|
||||
|
||||
get_folder_icon(
|
||||
&ThemeSettings::get_global(cx).active_icon_theme,
|
||||
path,
|
||||
expanded,
|
||||
)
|
||||
.or_else(|| {
|
||||
Self::default_icon_theme(cx)
|
||||
.and_then(|icon_theme| get_folder_icon(&icon_theme, path, expanded))
|
||||
})
|
||||
.or_else(|| {
|
||||
// If we can't find a specific folder icon for the folder at the given path, fall back to the generic folder
|
||||
// icon.
|
||||
Self::get_generic_folder_icon(expanded, cx)
|
||||
})
|
||||
}
|
||||
|
||||
fn get_generic_folder_icon(expanded: bool, cx: &App) -> Option<SharedString> {
|
||||
fn get_generic_folder_icon(
|
||||
icon_theme: &Arc<IconTheme>,
|
||||
expanded: bool,
|
||||
) -> Option<SharedString> {
|
||||
if expanded {
|
||||
icon_theme.directory_icons.expanded.clone()
|
||||
} else {
|
||||
@@ -102,10 +141,12 @@ impl FileIcons {
|
||||
}
|
||||
}
|
||||
|
||||
get_folder_icon(&ThemeSettings::get_global(cx).active_icon_theme, expanded).or_else(|| {
|
||||
Self::default_icon_theme(cx)
|
||||
.and_then(|icon_theme| get_folder_icon(&icon_theme, expanded))
|
||||
})
|
||||
get_generic_folder_icon(&ThemeSettings::get_global(cx).active_icon_theme, expanded).or_else(
|
||||
|| {
|
||||
Self::default_icon_theme(cx)
|
||||
.and_then(|icon_theme| get_generic_folder_icon(&icon_theme, expanded))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub fn get_chevron_icon(expanded: bool, cx: &App) -> Option<SharedString> {
|
||||
|
||||
@@ -2319,7 +2319,7 @@ impl OutlinePanel {
|
||||
is_active,
|
||||
);
|
||||
let icon = if settings.folder_icons {
|
||||
FileIcons::get_folder_icon(is_expanded, cx)
|
||||
FileIcons::get_folder_icon(is_expanded, &directory.entry.path, cx)
|
||||
} else {
|
||||
FileIcons::get_chevron_icon(is_expanded, cx)
|
||||
}
|
||||
@@ -2416,7 +2416,7 @@ impl OutlinePanel {
|
||||
.unwrap_or_default();
|
||||
let color = entry_git_aware_label_color(git_status, is_ignored, is_active);
|
||||
let icon = if settings.folder_icons {
|
||||
FileIcons::get_folder_icon(is_expanded, cx)
|
||||
FileIcons::get_folder_icon(is_expanded, &Path::new(&name), cx)
|
||||
} else {
|
||||
FileIcons::get_chevron_icon(is_expanded, cx)
|
||||
}
|
||||
|
||||
@@ -4778,7 +4778,7 @@ impl ProjectPanel {
|
||||
}
|
||||
_ => {
|
||||
if show_folder_icons {
|
||||
FileIcons::get_folder_icon(is_expanded, cx)
|
||||
FileIcons::get_folder_icon(is_expanded, &entry.path, cx)
|
||||
} else {
|
||||
FileIcons::get_chevron_icon(is_expanded, cx)
|
||||
}
|
||||
|
||||
@@ -28,6 +28,8 @@ pub struct IconTheme {
|
||||
pub appearance: Appearance,
|
||||
/// The icons used for directories.
|
||||
pub directory_icons: DirectoryIcons,
|
||||
/// The icons used for named directories.
|
||||
pub named_directory_icons: HashMap<String, DirectoryIcons>,
|
||||
/// The icons used for chevrons.
|
||||
pub chevron_icons: ChevronIcons,
|
||||
/// The mapping of file stems to their associated icon keys.
|
||||
@@ -39,7 +41,7 @@ pub struct IconTheme {
|
||||
}
|
||||
|
||||
/// The icons used for directories.
|
||||
#[derive(Debug, PartialEq)]
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct DirectoryIcons {
|
||||
/// The path to the icon to use for a collapsed directory.
|
||||
pub collapsed: Option<SharedString>,
|
||||
@@ -392,6 +394,7 @@ static DEFAULT_ICON_THEME: LazyLock<Arc<IconTheme>> = LazyLock::new(|| {
|
||||
collapsed: Some("icons/file_icons/folder.svg".into()),
|
||||
expanded: Some("icons/file_icons/folder_open.svg".into()),
|
||||
},
|
||||
named_directory_icons: HashMap::default(),
|
||||
chevron_icons: ChevronIcons {
|
||||
collapsed: Some("icons/file_icons/chevron_right.svg".into()),
|
||||
expanded: Some("icons/file_icons/chevron_down.svg".into()),
|
||||
|
||||
@@ -21,6 +21,8 @@ pub struct IconThemeContent {
|
||||
#[serde(default)]
|
||||
pub directory_icons: DirectoryIconsContent,
|
||||
#[serde(default)]
|
||||
pub named_directory_icons: HashMap<String, DirectoryIconsContent>,
|
||||
#[serde(default)]
|
||||
pub chevron_icons: ChevronIconsContent,
|
||||
#[serde(default)]
|
||||
pub file_stems: HashMap<String, String>,
|
||||
|
||||
@@ -298,6 +298,19 @@ impl ThemeRegistry {
|
||||
let mut file_suffixes = default_icon_theme.file_suffixes.clone();
|
||||
file_suffixes.extend(icon_theme.file_suffixes);
|
||||
|
||||
let mut named_directory_icons = default_icon_theme.named_directory_icons.clone();
|
||||
named_directory_icons.extend(icon_theme.named_directory_icons.into_iter().map(
|
||||
|(key, value)| {
|
||||
(
|
||||
key,
|
||||
DirectoryIcons {
|
||||
collapsed: value.collapsed.map(resolve_icon_path),
|
||||
expanded: value.expanded.map(resolve_icon_path),
|
||||
},
|
||||
)
|
||||
},
|
||||
));
|
||||
|
||||
let icon_theme = IconTheme {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
name: icon_theme.name.into(),
|
||||
@@ -309,6 +322,7 @@ impl ThemeRegistry {
|
||||
collapsed: icon_theme.directory_icons.collapsed.map(resolve_icon_path),
|
||||
expanded: icon_theme.directory_icons.expanded.map(resolve_icon_path),
|
||||
},
|
||||
named_directory_icons,
|
||||
chevron_icons: ChevronIcons {
|
||||
collapsed: icon_theme.chevron_icons.collapsed.map(resolve_icon_path),
|
||||
expanded: icon_theme.chevron_icons.expanded.map(resolve_icon_path),
|
||||
|
||||
Reference in New Issue
Block a user