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
+8
View File
@@ -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 }
+9
View File
@@ -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.
+13
View File
@@ -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

+13
View File
@@ -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

+13
View File
@@ -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

+13
View File
@@ -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

+13
View File
@@ -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

+13
View File
@@ -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

+15
View File
@@ -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

+13
View File
@@ -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

+14
View File
@@ -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

+14
View File
@@ -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

+15
View File
@@ -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

+14
View File
@@ -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

+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);
}
}