//! 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 _, Pixels, RenderOnce, SharedString, Styled as _, Window, div, px, svg, }; /// The semantic glyphs UI components need. #[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. #[derive(IntoElement)] pub struct Icon { role: IconRole, size: Pixels, color: Option, } /// Creates an icon for the role at the default size of 16px. pub fn icon(role: IconRole) -> Icon { Icon { role, size: px(16.0), color: None, } } impl Icon { pub fn size(mut self, size: Pixels) -> 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(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. 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>> { Ok(asset_bytes(path).map(std::borrow::Cow::Borrowed)) } fn list(&self, path: &str) -> gpui::Result> { 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:?}" ); } } }