chore: audit components
This commit is contained in:
@@ -4,7 +4,9 @@
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
use std::sync::{Mutex, OnceLock, PoisonError};
|
||||
|
||||
use gpui::SharedString;
|
||||
|
||||
use crate::IconRole;
|
||||
|
||||
@@ -144,14 +146,33 @@ fn icon_score(path: &Path, requested: u32) -> SizeScore {
|
||||
.unwrap_or(SizeScore::Any)
|
||||
}
|
||||
|
||||
/// The discovered icon file for a role at a size, cached for the process.
|
||||
type IconCache = Mutex<HashMap<(&'static str, u32), Option<PathBuf>>>;
|
||||
/// A resolved icon file and the asset key its bytes are served under.
|
||||
struct CachedIcon {
|
||||
path: PathBuf,
|
||||
asset_key: SharedString,
|
||||
}
|
||||
|
||||
/// The discovered icons, cached for the process.
|
||||
type IconCache = Mutex<HashMap<(&'static str, u32), Option<CachedIcon>>>;
|
||||
|
||||
pub(crate) fn cached_file(role: IconRole, requested: u32) -> Option<PathBuf> {
|
||||
cached_icon(role, requested, |icon| icon.path.clone())
|
||||
}
|
||||
|
||||
/// The asset key a resolved freedesktop file is served under.
|
||||
pub(crate) fn cached_asset_key(role: IconRole, requested: u32) -> Option<SharedString> {
|
||||
cached_icon(role, requested, |icon| icon.asset_key.clone())
|
||||
}
|
||||
|
||||
fn cached_icon<T, P>(role: IconRole, requested: u32, project: P) -> Option<T>
|
||||
where
|
||||
P: FnOnce(&CachedIcon) -> T,
|
||||
{
|
||||
static CACHE: OnceLock<IconCache> = OnceLock::new();
|
||||
|
||||
let cache = CACHE.get_or_init(|| Mutex::new(HashMap::new()));
|
||||
let mut cache = cache.lock().expect("icon cache poisoned");
|
||||
// A poisoned lock means a lookup panicked; its stale data beats a panic.
|
||||
let mut cache = cache.lock().unwrap_or_else(PoisonError::into_inner);
|
||||
|
||||
cache
|
||||
.entry((role.slug(), requested))
|
||||
@@ -162,8 +183,16 @@ pub(crate) fn cached_file(role: IconRole, requested: u32) -> Option<PathBuf> {
|
||||
requested,
|
||||
&base_dirs(),
|
||||
)
|
||||
.map(|path| CachedIcon {
|
||||
asset_key: SharedString::from(format!(
|
||||
"icons/freedesktop/{}-{requested}.svg",
|
||||
role.slug()
|
||||
)),
|
||||
path,
|
||||
})
|
||||
})
|
||||
.clone()
|
||||
.as_ref()
|
||||
.map(project)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
@@ -253,7 +282,12 @@ mod tests {
|
||||
fn scratch(name: &str) -> PathBuf {
|
||||
let root =
|
||||
std::env::temp_dir().join(format!("gpui-native-icons-{name}-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
// NotFound is the expected first run; anything else is a setup failure.
|
||||
if let Err(error) = std::fs::remove_dir_all(&root)
|
||||
&& error.kind() != std::io::ErrorKind::NotFound
|
||||
{
|
||||
panic!("failed to clear scratch dir {root:?}: {error}");
|
||||
}
|
||||
std::fs::create_dir_all(&root).expect("scratch dir");
|
||||
root
|
||||
}
|
||||
|
||||
+25
-9
@@ -92,7 +92,8 @@ impl IconRole {
|
||||
}
|
||||
}
|
||||
|
||||
fn slug(self) -> &'static str {
|
||||
/// The stable identifier used for element and debug ids.
|
||||
pub fn slug(self) -> &'static str {
|
||||
match self {
|
||||
Self::Check => "check",
|
||||
Self::Close => "close",
|
||||
@@ -223,11 +224,14 @@ pub struct Icon {
|
||||
transformation: Option<Transformation>,
|
||||
}
|
||||
|
||||
/// Creates an icon for the role at the default size of 16px.
|
||||
/// The default icon size; this crate sits beside the theme crate without depending on it.
|
||||
const DEFAULT_ICON_SIZE: Pixels = px(16.0);
|
||||
|
||||
/// Creates an icon for the role at the default size.
|
||||
pub fn icon(role: IconRole) -> Icon {
|
||||
Icon {
|
||||
glyph: Glyph::Role(role),
|
||||
size: px(16.0),
|
||||
size: DEFAULT_ICON_SIZE,
|
||||
color: None,
|
||||
transformation: None,
|
||||
}
|
||||
@@ -241,7 +245,7 @@ impl Icon {
|
||||
{
|
||||
Self {
|
||||
glyph: Glyph::Named(named.path()),
|
||||
size: px(16.0),
|
||||
size: DEFAULT_ICON_SIZE,
|
||||
color: None,
|
||||
transformation: None,
|
||||
}
|
||||
@@ -267,8 +271,7 @@ impl Icon {
|
||||
#[cfg(target_os = "linux")]
|
||||
fn freedesktop_asset_path(size: Pixels, role: &IconRole) -> Option<SharedString> {
|
||||
let size = size.as_f32().max(1.0).round() as u32;
|
||||
freedesktop::cached_file(*role, size)
|
||||
.map(|_| SharedString::from(format!("icons/freedesktop/{}-{size}.svg", role.slug())))
|
||||
freedesktop::cached_asset_key(*role, size)
|
||||
}
|
||||
|
||||
/// Roles resolve per platform.
|
||||
@@ -295,9 +298,9 @@ impl Icon {
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
/// Windows: the Segoe Fluent codepoint drawn as text in the system font.
|
||||
/// Windows: the Segoe Fluent glyph drawn as text in the system font.
|
||||
#[cfg(target_os = "windows")]
|
||||
fn render_glyph(size: Pixels, color: Option<Hsla>, glyph: char) -> AnyElement {
|
||||
fn render_glyph(size: Pixels, color: Option<Hsla>, glyph: &'static str) -> AnyElement {
|
||||
div()
|
||||
.flex_none()
|
||||
.size(size)
|
||||
@@ -309,7 +312,7 @@ impl Icon {
|
||||
.font_family(segoe::FONT_FAMILY)
|
||||
.text_size(size)
|
||||
.when_some(color, |el, color| el.text_color(color))
|
||||
.child(glyph.to_string()),
|
||||
.child(SharedString::new_static(glyph)),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
@@ -424,6 +427,19 @@ impl AssetSource for IconAssets {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn every_role_has_a_unique_slug() {
|
||||
for (index, role) in IconRole::all().iter().enumerate() {
|
||||
for other in &IconRole::all()[index + 1..] {
|
||||
assert_ne!(
|
||||
role.slug(),
|
||||
other.slug(),
|
||||
"{role:?} and {other:?} share a slug"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_role_resolves_to_a_bundled_asset() {
|
||||
for role in IconRole::all() {
|
||||
|
||||
+25
-26
@@ -3,34 +3,33 @@
|
||||
|
||||
use crate::IconRole;
|
||||
|
||||
/// The Segoe Fluent Icons codepoint for the role.
|
||||
pub(crate) fn glyph(role: IconRole) -> Option<char> {
|
||||
let codepoint = match role {
|
||||
// No Segoe spinner glyph; the icon falls back to the bundled SVG,
|
||||
// which the spinner rotates.
|
||||
/// The Segoe Fluent Icons glyph for the role.
|
||||
pub(crate) fn glyph(role: IconRole) -> Option<&'static str> {
|
||||
let glyph = match role {
|
||||
IconRole::Check => "\u{E73E}", // CheckMark
|
||||
IconRole::Close => "\u{E8BB}", // ChromeClose
|
||||
IconRole::ChevronDown => "\u{E70D}", // ChevronDown
|
||||
IconRole::ChevronLeft => "\u{E76B}", // ChevronLeft
|
||||
IconRole::ChevronRight => "\u{E76C}", // ChevronRight
|
||||
IconRole::ChevronUp => "\u{E70E}", // ChevronUp
|
||||
IconRole::Plus => "\u{E710}", // Add
|
||||
IconRole::Minus => "\u{E738}", // Remove
|
||||
IconRole::Search => "\u{E721}", // Search
|
||||
IconRole::Folder => "\u{E8B7}", // Folder
|
||||
IconRole::Warning => "\u{E7BA}", // Warning
|
||||
IconRole::Info => "\u{E946}", // Info
|
||||
IconRole::Copy => "\u{E8C8}", // Copy
|
||||
IconRole::Eye => "\u{E7B3}", // RedEye
|
||||
IconRole::EyeOff => "\u{ED1A}", // Hide
|
||||
IconRole::PanelLeft => "\u{E90C}", // DockLeft
|
||||
IconRole::PanelRight => "\u{E90D}", // DockRight
|
||||
IconRole::WindowMinimize => "\u{E921}", // ChromeMinimize
|
||||
IconRole::WindowMaximize => "\u{E922}", // ChromeMaximize
|
||||
IconRole::WindowRestore => "\u{E923}", // ChromeRestore
|
||||
// No Segoe spinner glyph, the icon falls back to the bundled SVG.
|
||||
IconRole::Loader => return None,
|
||||
IconRole::Check => 0xE73E, // CheckMark
|
||||
IconRole::Close => 0xE8BB, // ChromeClose
|
||||
IconRole::ChevronDown => 0xE70D, // ChevronDown
|
||||
IconRole::ChevronLeft => 0xE76B, // ChevronLeft
|
||||
IconRole::ChevronRight => 0xE76C, // ChevronRight
|
||||
IconRole::ChevronUp => 0xE70E, // ChevronUp
|
||||
IconRole::Plus => 0xE710, // Add
|
||||
IconRole::Minus => 0xE738, // Remove
|
||||
IconRole::Search => 0xE721, // Search
|
||||
IconRole::Folder => 0xE8B7, // Folder
|
||||
IconRole::Warning => 0xE7BA, // Warning
|
||||
IconRole::Info => 0xE946, // Info
|
||||
IconRole::Copy => 0xE8C8, // Copy
|
||||
IconRole::Eye => 0xE7B3, // RedEye
|
||||
IconRole::EyeOff => 0xED1A, // Hide
|
||||
IconRole::PanelLeft => 0xE90C, // DockLeft
|
||||
IconRole::PanelRight => 0xE90D, // DockRight
|
||||
IconRole::WindowMinimize => 0xE921, // ChromeMinimize
|
||||
IconRole::WindowMaximize => 0xE922, // ChromeMaximize
|
||||
IconRole::WindowRestore => 0xE923, // ChromeRestore
|
||||
};
|
||||
char::from_u32(codepoint)
|
||||
Some(glyph)
|
||||
}
|
||||
|
||||
/// The system font the codepoints resolve against (Windows 11+).
|
||||
|
||||
Reference in New Issue
Block a user