This commit is contained in:
2026-10-28 09:34:22 +07:00
parent 823f4b245c
commit b4d3359d4f
29 changed files with 6785 additions and 4 deletions
+166
View File
@@ -0,0 +1,166 @@
//! System icons for GPUI: components and apps reference semantic roles,
//! and the crate resolves each role to a glyph for the current platform.
//!
//! Today every role resolves to the bundled Lucide fallback. Platform
//! resolvers land with their phases: SF Symbols (macOS), Segoe Fluent
//! Icons (Windows), freedesktop icon themes (Linux/Omarchy).
use gpui::prelude::FluentBuilder as _;
use gpui::{
AssetSource, Hsla, IntoElement, ParentElement as _, RenderOnce, SharedString, Styled as _,
Window, div, px, svg,
};
/// The semantic glyphs UI components need. Grows one role at a time as
/// components call for it.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum IconRole {
Check,
Close,
ChevronDown,
ChevronLeft,
ChevronRight,
ChevronUp,
Plus,
Minus,
Search,
Folder,
Warning,
Info,
}
impl IconRole {
pub fn all() -> &'static [Self] {
&[
Self::Check,
Self::Close,
Self::ChevronDown,
Self::ChevronLeft,
Self::ChevronRight,
Self::ChevronUp,
Self::Plus,
Self::Minus,
Self::Search,
Self::Folder,
Self::Warning,
Self::Info,
]
}
fn asset_path(self) -> &'static str {
match self {
Self::Check => "icons/check.svg",
Self::Close => "icons/x.svg",
Self::ChevronDown => "icons/chevron-down.svg",
Self::ChevronLeft => "icons/chevron-left.svg",
Self::ChevronRight => "icons/chevron-right.svg",
Self::ChevronUp => "icons/chevron-up.svg",
Self::Plus => "icons/plus.svg",
Self::Minus => "icons/minus.svg",
Self::Search => "icons/search.svg",
Self::Folder => "icons/folder.svg",
Self::Warning => "icons/triangle-alert.svg",
Self::Info => "icons/info.svg",
}
}
}
/// An icon element. Unpainted unless a color is set (gpui paints SVGs from
/// the element's text color only).
#[derive(IntoElement)]
pub struct Icon {
role: IconRole,
size: f32,
color: Option<Hsla>,
}
/// Creates an icon for the role at the default size of 16px.
pub fn icon(role: IconRole) -> Icon {
Icon {
role,
size: 16.0,
color: None,
}
}
impl Icon {
pub fn size(mut self, size: f32) -> Self {
self.size = size;
self
}
pub fn color(mut self, color: Hsla) -> Self {
self.color = Some(color);
self
}
}
impl RenderOnce for Icon {
fn render(self, _window: &mut Window, _cx: &mut gpui::App) -> impl IntoElement {
div().flex_none().size(px(self.size)).child(
svg()
.size_full()
.path(self.role.asset_path())
.when_some(self.color, |el, color| el.text_color(color)),
)
}
}
/// The asset source serving this crate's bundled glyphs. Register it on the
/// application: `application().with_assets(IconAssets)`.
pub struct IconAssets;
fn asset_bytes(path: &str) -> Option<&'static [u8]> {
match path {
"icons/check.svg" => Some(include_bytes!("../assets/check.svg")),
"icons/x.svg" => Some(include_bytes!("../assets/x.svg")),
"icons/chevron-down.svg" => Some(include_bytes!("../assets/chevron-down.svg")),
"icons/chevron-left.svg" => Some(include_bytes!("../assets/chevron-left.svg")),
"icons/chevron-right.svg" => Some(include_bytes!("../assets/chevron-right.svg")),
"icons/chevron-up.svg" => Some(include_bytes!("../assets/chevron-up.svg")),
"icons/plus.svg" => Some(include_bytes!("../assets/plus.svg")),
"icons/minus.svg" => Some(include_bytes!("../assets/minus.svg")),
"icons/search.svg" => Some(include_bytes!("../assets/search.svg")),
"icons/folder.svg" => Some(include_bytes!("../assets/folder.svg")),
"icons/triangle-alert.svg" => Some(include_bytes!("../assets/triangle-alert.svg")),
"icons/info.svg" => Some(include_bytes!("../assets/info.svg")),
_ => None,
}
}
impl AssetSource for IconAssets {
fn load(&self, path: &str) -> gpui::Result<Option<std::borrow::Cow<'static, [u8]>>> {
Ok(asset_bytes(path).map(std::borrow::Cow::Borrowed))
}
fn list(&self, path: &str) -> gpui::Result<Vec<SharedString>> {
if path.is_empty() {
Ok(IconRole::all()
.iter()
.map(|role| SharedString::from(role.asset_path()))
.collect())
} else {
Ok(vec![])
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_role_resolves_to_a_bundled_asset() {
for role in IconRole::all() {
assert!(
asset_bytes(role.asset_path()).is_some(),
"missing bundled asset for {role:?}"
);
}
}
#[test]
fn unknown_assets_report_none() {
assert_eq!(asset_bytes("icons/nope.svg"), None);
}
}