forked from reya/gpui-native-kit
add windows theme source and platform icon resolvers
This commit is contained in:
+156
-4
@@ -1,9 +1,10 @@
|
||||
//! 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).
|
||||
//! Resolver cascade (plan §4.4): Segoe Fluent Icons (Windows), freedesktop
|
||||
//! icon themes (Linux/Omarchy), and the bundled Lucide subset everywhere
|
||||
//! else — macOS rides the Lucide fallback until SF Symbols shows it matters.
|
||||
//! A missing glyph falls back silently; nothing ever fails to paint.
|
||||
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
@@ -11,6 +12,11 @@ use gpui::{
|
||||
Styled as _, Window, div, px, svg,
|
||||
};
|
||||
|
||||
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
|
||||
mod freedesktop;
|
||||
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
|
||||
mod segoe;
|
||||
|
||||
/// The semantic glyphs UI components need.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum IconRole {
|
||||
@@ -62,6 +68,73 @@ impl IconRole {
|
||||
Self::Info => "icons/info.svg",
|
||||
}
|
||||
}
|
||||
|
||||
fn slug(self) -> &'static str {
|
||||
match self {
|
||||
Self::Check => "check",
|
||||
Self::Close => "close",
|
||||
Self::ChevronDown => "chevron-down",
|
||||
Self::ChevronLeft => "chevron-left",
|
||||
Self::ChevronRight => "chevron-right",
|
||||
Self::ChevronUp => "chevron-up",
|
||||
Self::Plus => "plus",
|
||||
Self::Minus => "minus",
|
||||
Self::Search => "search",
|
||||
Self::Folder => "folder",
|
||||
Self::Warning => "warning",
|
||||
Self::Info => "info",
|
||||
}
|
||||
}
|
||||
|
||||
/// Freedesktop names the role resolves through, best first.
|
||||
///
|
||||
/// Symbolic names lead (Adwaita), with Breeze and legacy names as fallbacks.
|
||||
fn freedesktop_names(self) -> &'static [&'static str] {
|
||||
match self {
|
||||
Self::Check => &[
|
||||
"object-select-symbolic",
|
||||
"object-select",
|
||||
"emblem-ok-symbolic",
|
||||
"checkmark",
|
||||
],
|
||||
Self::Close => &[
|
||||
"window-close-symbolic",
|
||||
"window-close",
|
||||
"close-symbolic",
|
||||
"gtk-close",
|
||||
],
|
||||
Self::ChevronDown => &[
|
||||
"pan-down-symbolic",
|
||||
"pan-down",
|
||||
"go-down-symbolic",
|
||||
"arrow-down",
|
||||
],
|
||||
Self::ChevronLeft => &[
|
||||
"pan-start-symbolic",
|
||||
"pan-start",
|
||||
"go-previous-symbolic",
|
||||
"arrow-left",
|
||||
],
|
||||
Self::ChevronRight => &[
|
||||
"pan-end-symbolic",
|
||||
"pan-end",
|
||||
"go-next-symbolic",
|
||||
"arrow-right",
|
||||
],
|
||||
Self::ChevronUp => &["pan-up-symbolic", "pan-up", "go-up-symbolic", "arrow-up"],
|
||||
Self::Plus => &["list-add-symbolic", "list-add", "add"],
|
||||
Self::Minus => &["list-remove-symbolic", "list-remove", "remove"],
|
||||
Self::Search => &[
|
||||
"system-search-symbolic",
|
||||
"system-search",
|
||||
"search",
|
||||
"edit-find",
|
||||
],
|
||||
Self::Folder => &["folder-symbolic", "folder"],
|
||||
Self::Warning => &["dialog-warning-symbolic", "dialog-warning", "warning"],
|
||||
Self::Info => &["dialog-information-symbolic", "dialog-information", "info"],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An icon element. Unpainted unless a color is set.
|
||||
@@ -91,14 +164,61 @@ impl Icon {
|
||||
self.color = Some(color);
|
||||
self
|
||||
}
|
||||
|
||||
/// 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()))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
#[cfg(target_os = "windows")]
|
||||
fn render_glyph(self) -> impl IntoElement {
|
||||
div()
|
||||
.flex_none()
|
||||
.size(self.size)
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.when_some(segoe::glyph(self.role), |el, glyph| {
|
||||
el.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()),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
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(self.role.asset_path())
|
||||
.path(path)
|
||||
.when_some(self.color, |el, color| el.text_color(color)),
|
||||
)
|
||||
}
|
||||
@@ -125,8 +245,23 @@ fn asset_bytes(path: &str) -> Option<&'static [u8]> {
|
||||
}
|
||||
}
|
||||
|
||||
fn freedesktop_file(path: &str) -> Option<std::io::Result<Vec<u8>>> {
|
||||
let rest = path.strip_prefix("icons/freedesktop/")?;
|
||||
let stem = rest.strip_suffix(".svg")?;
|
||||
let (slug, size) = stem.rsplit_once('-')?;
|
||||
let size = size.parse::<u32>().ok()?;
|
||||
let role = IconRole::all().iter().find(|role| role.slug() == slug)?;
|
||||
freedesktop::cached_file(*role, size).map(std::fs::read)
|
||||
}
|
||||
|
||||
impl AssetSource for IconAssets {
|
||||
fn load(&self, path: &str) -> gpui::Result<Option<std::borrow::Cow<'static, [u8]>>> {
|
||||
if let Some(file) = freedesktop_file(path) {
|
||||
let bytes = file.map_err(|error| {
|
||||
anyhow::anyhow!("failed to read freedesktop icon {path}: {error}")
|
||||
})?;
|
||||
return Ok(Some(std::borrow::Cow::Owned(bytes)));
|
||||
}
|
||||
Ok(asset_bytes(path).map(std::borrow::Cow::Borrowed))
|
||||
}
|
||||
|
||||
@@ -155,4 +290,21 @@ mod tests {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn freedesktop_asset_keys_round_trip() {
|
||||
let role = IconRole::ChevronDown;
|
||||
let key = format!("icons/freedesktop/{}-16.svg", role.slug());
|
||||
let stem = key
|
||||
.strip_prefix("icons/freedesktop/")
|
||||
.unwrap()
|
||||
.strip_suffix(".svg")
|
||||
.unwrap();
|
||||
let (slug, parsed_size) = stem.rsplit_once('-').unwrap();
|
||||
let resolved = IconRole::all()
|
||||
.iter()
|
||||
.find(|candidate| candidate.slug() == slug);
|
||||
assert_eq!(resolved, Some(&role));
|
||||
assert_eq!(parsed_size, "16");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user