scaffold
@@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "gpui-native-icons"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
description = "System icons for GPUI: semantic roles resolved per platform, with a bundled Lucide fallback"
|
||||
|
||||
[dependencies]
|
||||
gpui = { workspace = true }
|
||||
@@ -0,0 +1,9 @@
|
||||
# Licenses
|
||||
|
||||
## Bundled icons (`assets/*.svg`)
|
||||
|
||||
Lucide, ISC License, © Lucide Contributors. Fetched from https://github.com/lucide-icons/lucide (main) on 2026-10-28. These are the offline fallback glyphs, see https://lucide.dev/license.
|
||||
|
||||
## Platform glyph sets (not bundled)
|
||||
|
||||
SF Symbols (macOS) and Segoe Fluent Icons (Windows) are resolved through the operating system at runtime and are never extracted, modified, or redistributed. Freedesktop icons are loaded from the user's installed icon themes.
|
||||
@@ -0,0 +1,13 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M20 6 9 17l-5-5" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 239 B |
@@ -0,0 +1,13 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="m6 9 6 6 6-6" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 236 B |
@@ -0,0 +1,13 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="m15 18-6-6 6-6" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 238 B |
@@ -0,0 +1,13 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="m9 18 6-6-6-6" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 237 B |
@@ -0,0 +1,13 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="m18 15-6-6-6 6" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 238 B |
@@ -0,0 +1,13 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 342 B |
@@ -0,0 +1,15 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<path d="M12 16v-4" />
|
||||
<path d="M12 8h.01" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 294 B |
@@ -0,0 +1,13 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M5 12h14" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 232 B |
@@ -0,0 +1,14 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M5 12h14" />
|
||||
<path d="M12 5v14" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 256 B |
@@ -0,0 +1,14 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="m21 21-4.34-4.34" />
|
||||
<circle cx="11" cy="11" r="8" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 275 B |
@@ -0,0 +1,15 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" />
|
||||
<path d="M12 9v4" />
|
||||
<path d="M12 17h.01" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 345 B |
@@ -0,0 +1,14 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M18 6 6 18" />
|
||||
<path d="m6 6 12 12" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 260 B |
@@ -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);
|
||||
}
|
||||
}
|
||||