feat: support app-defined icon sets
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
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"
|
||||
description = "System icons for GPUI: semantic roles resolved per platform, a bundled Lucide fallback, and app-defined icon sets"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
|
||||
+124
-58
@@ -1,10 +1,11 @@
|
||||
//! System icons for GPUI: components and apps reference semantic roles,
|
||||
//! and the crate resolves each role to a glyph for the current platform.
|
||||
//! Apps ship their own icon set by implementing [`IconNamed`].
|
||||
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
AssetSource, Hsla, IntoElement, ParentElement as _, Pixels, RenderOnce, SharedString,
|
||||
Styled as _, Transformation, Window, div, px, svg,
|
||||
AnyElement, AssetSource, Hsla, IntoElement, ParentElement as _, Pixels, RenderOnce,
|
||||
SharedString, Styled as _, Transformation, Window, div, px, svg,
|
||||
};
|
||||
|
||||
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
|
||||
@@ -194,10 +195,29 @@ impl IconRole {
|
||||
}
|
||||
}
|
||||
|
||||
/// A glyph named by an application's own icon set.
|
||||
pub trait IconNamed {
|
||||
/// The SVG asset path, e.g. `icons/commit.svg`.
|
||||
fn path(self) -> SharedString;
|
||||
}
|
||||
|
||||
impl IconNamed for IconRole {
|
||||
fn path(self) -> SharedString {
|
||||
SharedString::from(self.asset_path())
|
||||
}
|
||||
}
|
||||
|
||||
/// The glyph an icon draws.
|
||||
#[derive(Clone)]
|
||||
enum Glyph {
|
||||
Role(IconRole),
|
||||
Named(SharedString),
|
||||
}
|
||||
|
||||
/// An icon element. Unpainted unless a color is set.
|
||||
#[derive(IntoElement)]
|
||||
#[derive(Clone, IntoElement)]
|
||||
pub struct Icon {
|
||||
role: IconRole,
|
||||
glyph: Glyph,
|
||||
size: Pixels,
|
||||
color: Option<Hsla>,
|
||||
transformation: Option<Transformation>,
|
||||
@@ -206,7 +226,7 @@ pub struct Icon {
|
||||
/// Creates an icon for the role at the default size of 16px.
|
||||
pub fn icon(role: IconRole) -> Icon {
|
||||
Icon {
|
||||
role,
|
||||
glyph: Glyph::Role(role),
|
||||
size: px(16.0),
|
||||
color: None,
|
||||
transformation: None,
|
||||
@@ -214,6 +234,19 @@ pub fn icon(role: IconRole) -> Icon {
|
||||
}
|
||||
|
||||
impl Icon {
|
||||
/// Creates an icon from an application-defined icon set.
|
||||
pub fn named<I>(named: I) -> Self
|
||||
where
|
||||
I: IconNamed,
|
||||
{
|
||||
Self {
|
||||
glyph: Glyph::Named(named.path()),
|
||||
size: px(16.0),
|
||||
color: None,
|
||||
transformation: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn size(mut self, size: Pixels) -> Self {
|
||||
self.size = size;
|
||||
self
|
||||
@@ -232,66 +265,99 @@ impl Icon {
|
||||
|
||||
/// The asset key a resolved freedesktop file is served under.
|
||||
#[cfg(target_os = "linux")]
|
||||
fn freedesktop_asset_path(&self) -> Option<SharedString> {
|
||||
let size = self.size.as_f32().max(1.0).round() as u32;
|
||||
freedesktop::cached_file(self.role, size).map(|_| {
|
||||
SharedString::from(format!("icons/freedesktop/{}-{size}.svg", self.role.slug()))
|
||||
})
|
||||
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())))
|
||||
}
|
||||
|
||||
/// Roles resolve per platform.
|
||||
fn render_role(
|
||||
size: Pixels,
|
||||
color: Option<Hsla>,
|
||||
transformation: Option<Transformation>,
|
||||
role: IconRole,
|
||||
) -> AnyElement {
|
||||
#[cfg(target_os = "windows")]
|
||||
if let Some(glyph) = segoe::glyph(role) {
|
||||
return Self::render_glyph(size, color, glyph).into_any_element();
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
if let Some(path) = Self::freedesktop_asset_path(size, &role) {
|
||||
return Self::render_svg_path(size, color, transformation, path).into_any_element();
|
||||
}
|
||||
Self::render_svg_path(
|
||||
size,
|
||||
color,
|
||||
transformation,
|
||||
SharedString::from(role.asset_path()),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
/// Windows: the Segoe Fluent codepoint drawn as text in the system font.
|
||||
#[cfg(target_os = "windows")]
|
||||
fn render_glyph(size: Pixels, color: Option<Hsla>, glyph: char) -> AnyElement {
|
||||
div()
|
||||
.flex_none()
|
||||
.size(size)
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.child(
|
||||
div()
|
||||
.font_family(segoe::FONT_FAMILY)
|
||||
.text_size(size)
|
||||
.when_some(color, |el, color| el.text_color(color))
|
||||
.child(glyph.to_string()),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_svg_path(
|
||||
size: Pixels,
|
||||
color: Option<Hsla>,
|
||||
transformation: Option<Transformation>,
|
||||
path: SharedString,
|
||||
) -> AnyElement {
|
||||
div()
|
||||
.flex_none()
|
||||
.size(size)
|
||||
.child(
|
||||
svg()
|
||||
.size_full()
|
||||
.path(path)
|
||||
.when_some(color, |el, color| el.text_color(color))
|
||||
.when_some(transformation, |el, transformation| {
|
||||
el.with_transformation(transformation)
|
||||
}),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
impl<I> From<I> for Icon
|
||||
where
|
||||
I: IconNamed,
|
||||
{
|
||||
fn from(named: I) -> Self {
|
||||
Icon::named(named)
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Icon {
|
||||
fn render(self, _window: &mut Window, _cx: &mut gpui::App) -> impl IntoElement {
|
||||
#[cfg(target_os = "windows")]
|
||||
return self.render_glyph();
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
return self.render_svg();
|
||||
}
|
||||
}
|
||||
|
||||
impl Icon {
|
||||
/// Windows: the Segoe Fluent codepoint drawn as text in the system font;
|
||||
/// roles without a codepoint fall back to the bundled SVG.
|
||||
#[cfg(target_os = "windows")]
|
||||
fn render_glyph(self) -> impl IntoElement {
|
||||
match segoe::glyph(self.role) {
|
||||
Some(glyph) => div()
|
||||
.flex_none()
|
||||
.size(self.size)
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.child(
|
||||
div()
|
||||
.font_family(segoe::FONT_FAMILY)
|
||||
.text_size(self.size)
|
||||
.when_some(self.color, |el, color| el.text_color(color))
|
||||
.child(glyph.to_string()),
|
||||
)
|
||||
.into_any_element(),
|
||||
None => self.render_svg().into_any_element(),
|
||||
let Icon {
|
||||
glyph,
|
||||
size,
|
||||
color,
|
||||
transformation,
|
||||
} = self;
|
||||
match glyph {
|
||||
Glyph::Named(path) => Self::render_svg_path(size, color, transformation, path),
|
||||
Glyph::Role(role) => Self::render_role(size, color, transformation, role),
|
||||
}
|
||||
}
|
||||
|
||||
fn render_svg(self) -> impl IntoElement {
|
||||
#[cfg(target_os = "linux")]
|
||||
let path: SharedString = self
|
||||
.freedesktop_asset_path()
|
||||
.unwrap_or_else(|| SharedString::from(self.role.asset_path()));
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let path: SharedString = SharedString::from(self.role.asset_path());
|
||||
|
||||
div().flex_none().size(self.size).child(
|
||||
svg()
|
||||
.size_full()
|
||||
.path(path)
|
||||
.when_some(self.color, |el, color| el.text_color(color))
|
||||
.when_some(self.transformation, |el, transformation| {
|
||||
el.with_transformation(transformation)
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// The asset source serving this crate's bundled glyphs.
|
||||
|
||||
Reference in New Issue
Block a user