feat: support app-defined icon sets

This commit is contained in:
2026-11-09 20:35:04 +07:00
parent 1c091db699
commit ec906839ed
17 changed files with 274 additions and 173 deletions
+15 -10
View File
@@ -6,7 +6,7 @@ use gpui::{
IntoElement, ParentElement as _, RenderOnce, SharedString, StatefulInteractiveElement as _,
Styled as _, Window, deferred, div, ease_out_quint, px,
};
use gpui_native_icons::{IconRole, icon};
use gpui_native_icons::{Icon, IconRole, icon};
use gpui_native_theme::{ActiveTheme as _, Size};
use crate::common::DismissHandler;
@@ -33,7 +33,7 @@ pub struct Alert {
title: Option<SharedString>,
message: Option<SharedString>,
severity: AlertSeverity,
icon: Option<IconRole>,
icon: Option<Icon>,
show_icon: bool,
on_dismiss: Option<DismissHandler>,
}
@@ -78,9 +78,12 @@ impl Alert {
self
}
/// Overrides the severity-derived icon with this glyph.
pub fn icon(mut self, role: IconRole) -> Self {
self.icon = Some(role);
/// Overrides the severity-derived icon.
pub fn icon<I>(mut self, icon: I) -> Self
where
I: Into<Icon>,
{
self.icon = Some(icon.into());
self
}
@@ -116,10 +119,12 @@ impl RenderOnce for Alert {
AlertSeverity::Danger => theme.ramps.destructive,
};
let icon_role = self.icon.unwrap_or(match self.severity {
AlertSeverity::Info => IconRole::Info,
AlertSeverity::Success => IconRole::Check,
AlertSeverity::Warning | AlertSeverity::Danger => IconRole::Warning,
let icon_glyph = self.icon.unwrap_or_else(|| {
icon(match self.severity {
AlertSeverity::Info => IconRole::Info,
AlertSeverity::Success => IconRole::Check,
AlertSeverity::Warning | AlertSeverity::Danger => IconRole::Warning,
})
});
let foreground = theme.colors.foreground;
@@ -158,7 +163,7 @@ impl RenderOnce for Alert {
.flex()
.items_center()
.h(window.line_height())
.child(icon(icon_role).size(icon_size).color(ramp.base)),
.child(icon_glyph.size(icon_size).color(ramp.base)),
)
})
.child(
+9 -6
View File
@@ -6,7 +6,7 @@ use gpui::{
RenderOnce, Role, SharedString, StatefulInteractiveElement as _, Styled as _, Window, canvas,
relative, transparent_black,
};
use gpui_native_icons::{IconRole, icon};
use gpui_native_icons::Icon;
use gpui_native_theme::{ActiveTheme as _, BORDER_WIDTH, Size};
use gpui_native_utils::{focus_ring, record_anchor_bounds};
use std::rc::Rc;
@@ -38,7 +38,7 @@ pub enum ButtonVariant {
pub struct Button {
id: ElementId,
label: Option<SharedString>,
icon: Option<IconRole>,
icon: Option<Icon>,
variant: ButtonVariant,
size: Size,
disabled: bool,
@@ -82,8 +82,11 @@ impl Button {
}
/// Shows an icon beside the label.
pub fn icon(mut self, role: IconRole) -> Self {
self.icon = Some(role);
pub fn icon<I>(mut self, icon: I) -> Self
where
I: Into<Icon>,
{
self.icon = Some(icon.into());
self
}
@@ -236,8 +239,8 @@ impl RenderOnce for Button {
)
})
.when(!self.loading, |button| {
button.when_some(self.icon, |button, role| {
button.child(icon(role).size(metrics.icon_size).color(foreground))
button.when_some(self.icon, |button, icon| {
button.child(icon.size(metrics.icon_size).color(foreground))
})
})
.when_some(self.label, |button, label| button.child(label))
+9 -6
View File
@@ -9,7 +9,7 @@ use gpui::{
StatefulInteractiveElement as _, Styled as _, Subscription, Window, actions, canvas, deferred,
div, relative,
};
use gpui_native_icons::{IconRole, icon};
use gpui_native_icons::Icon;
use gpui_native_theme::{ActiveTheme as _, POPUP_MAX_HEIGHT, Size};
use crate::common::{h_flex, v_flex};
@@ -49,7 +49,7 @@ pub struct Command {
/// Gray text on the row's right edge, e.g. the keybinding.
pub hint: Option<SharedString>,
/// Leading glyph shown before the label.
pub icon: Option<IconRole>,
pub icon: Option<Icon>,
}
impl Command {
@@ -64,8 +64,11 @@ impl Command {
}
/// Shows an icon before the label.
pub fn icon(mut self, role: IconRole) -> Self {
self.icon = Some(role);
pub fn icon<I>(mut self, icon: I) -> Self
where
I: Into<Icon>,
{
self.icon = Some(icon.into());
self
}
@@ -361,8 +364,8 @@ impl Render for CommandPaletteState {
state.update(cx, |state, cx| state.choose_row(row_ix, window, cx));
}
})
.when_some(command.icon, |row, role| {
row.child(icon(role).size(metrics.icon_size).color(colors.foreground))
.when_some(command.icon.clone(), |row, icon| {
row.child(icon.size(metrics.icon_size).color(colors.foreground))
})
.child(
div()
+9 -6
View File
@@ -9,7 +9,7 @@ use gpui::{
MouseButton, MouseDownEvent, ParentElement as _, RenderOnce, Role, SharedString,
StatefulInteractiveElement as _, Styled as _, Window, canvas, div, relative,
};
use gpui_native_icons::{IconRole, icon};
use gpui_native_icons::{Icon, IconRole, icon};
use gpui_native_theme::{ActiveTheme as _, BORDER_WIDTH, Size};
use gpui_native_utils::{focus_handle, focus_ring};
@@ -27,7 +27,7 @@ type ItemsFn = Box<dyn Fn(&mut Window, &mut App) -> Vec<MenuItem>>;
pub struct DropdownMenu {
id: ElementId,
label: Option<SharedString>,
icon: Option<IconRole>,
icon: Option<Icon>,
variant: ButtonVariant,
size: Size,
disabled: bool,
@@ -73,8 +73,11 @@ impl DropdownMenu {
}
/// Shows an icon beside the label.
pub fn icon(mut self, role: IconRole) -> Self {
self.icon = Some(role);
pub fn icon<I>(mut self, icon: I) -> Self
where
I: Into<Icon>,
{
self.icon = Some(icon.into());
self
}
@@ -267,8 +270,8 @@ impl RenderOnce for DropdownMenu {
)
})
.when(!self.loading, |zone| {
zone.when_some(self.icon, |zone, role| {
zone.child(icon(role).size(metrics.icon_size).color(foreground))
zone.when_some(self.icon, |zone, icon| {
zone.child(icon.size(metrics.icon_size).color(foreground))
})
})
.when_some(self.label, |zone, label| zone.child(label));
+9 -6
View File
@@ -3,7 +3,7 @@ use gpui::{
App, IntoElement, ParentElement as _, RenderOnce, SharedString, StyleRefinement, Styled as _,
Window, div, relative,
};
use gpui_native_icons::{IconRole, icon};
use gpui_native_icons::Icon;
use gpui_native_theme::{ActiveTheme as _, Size};
use crate::common::{StyledExt, v_flex};
@@ -11,7 +11,7 @@ use crate::common::{StyledExt, v_flex};
/// The centered "nothing here yet" panel for lists, tables, and searches.
#[derive(IntoElement)]
pub struct EmptyState {
icon: Option<IconRole>,
icon: Option<Icon>,
headline: SharedString,
hint: Option<SharedString>,
action: Option<gpui::AnyElement>,
@@ -34,8 +34,11 @@ impl EmptyState {
}
/// Sets a large muted icon above the headline.
pub fn icon(mut self, role: IconRole) -> Self {
self.icon = Some(role);
pub fn icon<I>(mut self, icon: I) -> Self
where
I: Into<Icon>,
{
self.icon = Some(icon.into());
self
}
@@ -78,11 +81,11 @@ impl RenderOnce for EmptyState {
.justify_center()
.text_center()
.p_4()
.when_some(self.icon, |state, role| {
.when_some(self.icon, |state, icon| {
state.child(
div()
.mb_2()
.child(icon(role).size(illustration).color(muted_foreground)),
.child(icon.size(illustration).color(muted_foreground)),
)
})
.child(
+10 -7
View File
@@ -9,7 +9,7 @@ use gpui::{
StatefulInteractiveElement as _, Styled as _, Window, anchored, canvas, deferred, div,
ease_out_quint, point, px, relative,
};
use gpui_native_icons::{IconRole, icon as icon_glyph};
use gpui_native_icons::{Icon, IconRole, icon as icon_glyph};
use gpui_native_theme::{ActiveTheme as _, POPUP_MAX_HEIGHT};
use crate::common::v_flex;
@@ -30,7 +30,7 @@ type ItemsFn = Box<dyn Fn(&mut Window, &mut App) -> Vec<MenuItem>>;
pub struct MenuItem {
kind: MenuItemKind,
label: SharedString,
icon: Option<IconRole>,
icon: Option<Icon>,
shortcut: Option<SharedString>,
handler: Option<ItemHandler>,
disabled: bool,
@@ -86,8 +86,11 @@ impl MenuItem {
}
/// Shows an icon before the label.
pub fn icon(mut self, role: IconRole) -> Self {
self.icon = Some(role);
pub fn icon<I>(mut self, icon: I) -> Self
where
I: Into<Icon>,
{
self.icon = Some(icon.into());
self
}
@@ -526,7 +529,7 @@ fn render_item(
let destructive = item.destructive;
let checked = item.checked;
let shortcut = item.shortcut.clone();
let icon_role = item.icon;
let icon = item.icon.clone();
div()
.id(("menu-item", index))
@@ -576,8 +579,8 @@ fn render_item(
}
}
})
.when_some(icon_role, |row, role| {
row.child(icon_glyph(role).size(chrome.icon_size).color(label_color))
.when_some(icon, |row, icon| {
row.child(icon.size(chrome.icon_size).color(label_color))
})
.child(
div()
+9 -6
View File
@@ -7,7 +7,7 @@ use gpui::{
SpringConfig, StatefulInteractiveElement as _, StyleRefinement, Styled as _, Toggled, Window,
canvas, div,
};
use gpui_native_icons::{IconRole, icon};
use gpui_native_icons::Icon;
use gpui_native_theme::{ActiveTheme as _, Size};
use gpui_native_utils::{focus_handle, focus_ring};
@@ -21,7 +21,7 @@ type ChangeHandler<T> = Rc<dyn Fn(T, &mut Window, &mut App)>;
pub struct SegmentedOption<T> {
value: T,
label: SharedString,
icon: Option<IconRole>,
icon: Option<Icon>,
}
impl<T> SegmentedOption<T> {
@@ -38,8 +38,11 @@ impl<T> SegmentedOption<T> {
}
/// Sets a leading icon for the segment.
pub fn icon(mut self, role: IconRole) -> Self {
self.icon = Some(role);
pub fn icon<I>(mut self, icon: I) -> Self
where
I: Into<Icon>,
{
self.icon = Some(icon.into());
self
}
}
@@ -226,8 +229,8 @@ where
.role(Role::RadioButton)
.aria_label(segment.label.clone())
.aria_toggled(Toggled::from(is_selected))
.when_some(segment.icon, |segment, role| {
segment.child(icon(role).size(icon_size).color(content_color))
.when_some(segment.icon.clone(), |segment, icon| {
segment.child(icon.size(icon_size).color(content_color))
})
.child(segment.label.clone())
.child(
+9 -6
View File
@@ -5,7 +5,7 @@ use gpui::{
App, ElementId, InteractiveElement as _, IntoElement, ParentElement as _, RenderOnce, Role,
SharedString, StatefulInteractiveElement as _, StyleRefinement, Styled as _, Window,
};
use gpui_native_icons::{IconRole, icon};
use gpui_native_icons::{Icon, IconRole, icon};
use gpui_native_theme::{ActiveTheme as _, BORDER_WIDTH, Size};
use gpui_native_utils::focus_ring;
@@ -20,7 +20,7 @@ pub type SelectHandler<T> = Rc<dyn Fn(Option<T>, &mut Window, &mut App)>;
pub struct SelectOption<T> {
value: T,
label: SharedString,
icon: Option<IconRole>,
icon: Option<Icon>,
}
impl<T> SelectOption<T> {
@@ -37,8 +37,11 @@ impl<T> SelectOption<T> {
}
/// Sets a leading icon for the option's row.
pub fn icon(mut self, role: IconRole) -> Self {
self.icon = Some(role);
pub fn icon<I>(mut self, icon: I) -> Self
where
I: Into<Icon>,
{
self.icon = Some(icon.into());
self
}
}
@@ -272,8 +275,8 @@ where
};
let mut row = MenuItem::new(option.label.clone());
if let Some(role) = option.icon {
row = row.icon(role);
if let Some(icon) = option.icon.clone() {
row = row.icon(icon);
}
row.checked(is_selected).handler(handler)
})
+9 -6
View File
@@ -9,7 +9,7 @@ use gpui::{
IntoElement, ParentElement as _, RenderOnce, SharedString, StatefulInteractiveElement as _,
Styled as _, Task, Window, deferred, div, ease_out_quint, px,
};
use gpui_native_icons::{IconRole, icon};
use gpui_native_icons::{Icon, IconRole, icon};
use gpui_native_theme::ActiveTheme as _;
use crate::button::{Button, ButtonVariant};
@@ -43,7 +43,7 @@ impl ToastAction {
#[derive(Clone)]
pub struct Toast {
message: SharedString,
icon: Option<IconRole>,
icon: Option<Icon>,
action: Option<ToastAction>,
duration: Option<Duration>,
on_dismiss: Option<DismissHandler>,
@@ -68,8 +68,11 @@ impl Toast {
}
/// Shows this glyph before the content.
pub fn icon(mut self, role: IconRole) -> Self {
self.icon = Some(role);
pub fn icon<I>(mut self, icon: I) -> Self
where
I: Into<Icon>,
{
self.icon = Some(icon.into());
self
}
@@ -285,14 +288,14 @@ fn pill(entry: &ToastEntry, toasts: &Entity<Toasts>, cx: &App) -> gpui::Stateful
.border_color(border)
.rounded(radius)
.shadow(shadows.clone())
.when_some(toast.icon, |pill, icon_role| {
.when_some(toast.icon.clone(), |pill, icon| {
pill.child(
div()
.flex_none()
.mr_2()
.flex()
.items_center()
.child(icon(icon_role).size(icon_size).color(foreground)),
.child(icon.size(icon_size).color(foreground)),
)
})
.child(
+10 -11
View File
@@ -6,7 +6,7 @@ use gpui::{
ParentElement as _, RenderOnce, Role, SharedString, StatefulInteractiveElement as _,
Styled as _, Window, div,
};
use gpui_native_icons::{IconRole, icon};
use gpui_native_icons::Icon;
use gpui_native_theme::ActiveTheme as _;
use gpui_native_utils::{nav_key, paint_selectable_row, selectable_label_color};
@@ -17,7 +17,7 @@ type ItemsFn = Box<dyn Fn(&mut Window, &mut App) -> Vec<ListItem>>;
/// One row of a [`List`].
pub struct ListItem {
label: SharedString,
icon: Option<IconRole>,
icon: Option<Icon>,
detail: Option<SharedString>,
disabled: bool,
}
@@ -36,9 +36,12 @@ impl ListItem {
}
}
/// Shows an icon leading the label.
pub fn icon(mut self, role: IconRole) -> Self {
self.icon = Some(role);
/// Shows an icon leading the label, a role or any custom [`IconNamed`].
pub fn icon<I>(mut self, icon: I) -> Self
where
I: Into<Icon>,
{
self.icon = Some(icon.into());
self
}
@@ -222,12 +225,8 @@ fn render_row(
}
})
})
.when_some(item.icon, |row, role| {
row.child(
icon(role)
.size(theme.sizes.medium.icon_size)
.color(label_color),
)
.when_some(item.icon.clone(), |row, icon| {
row.child(icon.size(theme.sizes.medium.icon_size).color(label_color))
})
.child(
div()
+11 -12
View File
@@ -10,7 +10,7 @@ use gpui::{
Styled as _, Window, div,
};
use gpui_native_core::scroll_area::ScrollArea;
use gpui_native_icons::{IconRole, icon};
use gpui_native_icons::Icon;
use gpui_native_theme::ActiveTheme as _;
use gpui_native_utils::{nav_key, paint_selectable_row, selectable_label_color};
@@ -26,7 +26,7 @@ pub enum SidebarEntry {
/// A selectable row.
Row {
label: SharedString,
icon: Option<IconRole>,
icon: Option<Icon>,
detail: Option<SharedString>,
disabled: bool,
},
@@ -56,10 +56,13 @@ impl SidebarEntry {
}
}
/// Shows an icon leading the label.
pub fn icon(mut self, role: IconRole) -> Self {
if let Self::Row { icon, .. } = &mut self {
*icon = Some(role);
/// Shows an icon leading the label, a role or any custom [`IconNamed`].
pub fn icon<I>(mut self, icon: I) -> Self
where
I: Into<Icon>,
{
if let Self::Row { icon: slot, .. } = &mut self {
*slot = Some(icon.into());
}
self
}
@@ -337,12 +340,8 @@ fn render_entry(
});
}
row = row.when_some(*row_icon, |row, role| {
row.child(
icon(role)
.size(theme.sizes.medium.icon_size)
.color(label_color),
)
row = row.when_some(row_icon.clone(), |row, icon| {
row.child(icon.size(theme.sizes.medium.icon_size).color(label_color))
});
row = row.child(div().flex_1().text_color(label_color).child(label.clone()));
+15 -12
View File
@@ -10,7 +10,7 @@ use gpui_native_core::button::{Button, ButtonVariant};
use gpui_native_core::common::Sizable as _;
use gpui_native_core::spinner::Spinner;
use gpui_native_core::tooltip::Tooltip;
use gpui_native_icons::{IconRole, icon};
use gpui_native_icons::{Icon, IconRole};
use gpui_native_theme::{ActiveTheme as _, Size};
use gpui_native_utils::adjacent_selectable;
@@ -35,7 +35,7 @@ pub struct Tab {
/// The stable identity for element state; must survive reordering.
key: ElementId,
label: SharedString,
icon: Option<IconRole>,
icon: Option<Icon>,
lead: Option<AnyElement>,
busy: bool,
disabled: bool,
@@ -63,8 +63,11 @@ impl Tab {
}
/// Shows an icon leading the label.
pub fn icon(mut self, role: IconRole) -> Self {
self.icon = Some(role);
pub fn icon<I>(mut self, icon: I) -> Self
where
I: Into<Icon>,
{
self.icon = Some(icon.into());
self
}
@@ -344,18 +347,18 @@ fn render_tab(
};
let lead = tab.lead.take();
let icon = tab.icon.take();
strip_tab = match lead {
Some(lead) => strip_tab.child(lead),
None => match variant {
TabsVariant::Underline => strip_tab.child(div().flex_none().pb_0p5().when_some(
tab.icon,
|slot, role| {
slot.child(icon(role).size(metrics.dense_icon_size).color(label_color))
},
)),
TabsVariant::Pill => strip_tab.when_some(tab.icon, |strip_tab, role| {
strip_tab.child(icon(role).size(metrics.dense_icon_size).color(label_color))
TabsVariant::Underline => {
strip_tab.child(div().flex_none().pb_0p5().when_some(icon, |slot, icon| {
slot.child(icon.size(metrics.dense_icon_size).color(label_color))
}))
}
TabsVariant::Pill => strip_tab.when_some(icon, |strip_tab, icon| {
strip_tab.child(icon.size(metrics.dense_icon_size).color(label_color))
}),
},
};
+9 -10
View File
@@ -6,7 +6,7 @@ use gpui::{
ParentElement as _, RenderOnce, Role, SharedString, StatefulInteractiveElement as _,
Styled as _, Window, div,
};
use gpui_native_icons::{IconRole, icon};
use gpui_native_icons::{Icon, IconRole, icon};
use gpui_native_theme::ActiveTheme as _;
use gpui_native_utils::{nav_key, paint_selectable_row, selectable_label_color};
@@ -20,7 +20,7 @@ type ItemsFn = Box<dyn Fn(&mut Window, &mut App) -> Vec<TreeItem>>;
#[derive(Clone)]
pub struct TreeItem {
label: SharedString,
icon: Option<IconRole>,
icon: Option<Icon>,
depth: usize,
expandable: bool,
expanded: bool,
@@ -44,8 +44,11 @@ impl TreeItem {
}
/// Shows an icon leading the label.
pub fn icon(mut self, role: IconRole) -> Self {
self.icon = Some(role);
pub fn icon<I>(mut self, icon: I) -> Self
where
I: Into<Icon>,
{
self.icon = Some(icon.into());
self
}
@@ -317,12 +320,8 @@ fn render_row(
selected,
on_disclosure,
))
.when_some(item.icon, |row, role| {
row.child(
icon(role)
.size(theme.sizes.medium.icon_size)
.color(label_color),
)
.when_some(item.icon.clone(), |row, icon| {
row.child(icon.size(theme.sizes.medium.icon_size).color(label_color))
})
.child(
div()
+1 -1
View File
@@ -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
View File
@@ -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.
+13 -10
View File
@@ -15,7 +15,7 @@ use gpui_native_core::common::{h_flex, v_flex};
use gpui_native_core::context_menu::ContextMenu;
use gpui_native_core::menu::MenuItem;
use gpui_native_data::tabs::{Tab, Tabs, TabsVariant};
use gpui_native_icons::IconRole;
use gpui_native_icons::{Icon, IconRole};
use gpui_native_theme::{ActiveTheme as _, Size};
use crate::resize_handle::ResizeHandle;
@@ -35,7 +35,7 @@ type OpenRightSidebarHandler = Rc<dyn Fn(&mut App)>;
pub struct PanelDescriptor {
id: SharedString,
title: SharedString,
icon: Option<IconRole>,
icon: Option<Icon>,
build: PanelBuilder,
right_sidebar: Option<PanelBuilder>,
}
@@ -58,8 +58,11 @@ impl PanelDescriptor {
}
/// Shows an icon leading the tab label.
pub fn icon(mut self, role: IconRole) -> Self {
self.icon = Some(role);
pub fn icon<I>(mut self, icon: I) -> Self
where
I: Into<Icon>,
{
self.icon = Some(icon.into());
self
}
@@ -130,7 +133,7 @@ impl Render for DragPreview {
struct PanelTab {
panel: SharedString,
title: SharedString,
icon: Option<IconRole>,
icon: Option<Icon>,
view: AnyView,
right_view: Option<AnyView>,
}
@@ -215,7 +218,7 @@ impl DockState {
let tab = PanelTab {
panel: SharedString::from(id),
title: panel.title.clone(),
icon: panel.icon,
icon: panel.icon.clone(),
view: (panel.build)(window, cx),
right_view,
};
@@ -679,7 +682,7 @@ impl DockState {
.map(|tab| TabView {
panel: tab.panel.clone(),
title: tab.title.clone(),
icon: tab.icon,
icon: tab.icon.clone(),
view: tab.view.clone(),
})
.collect(),
@@ -747,7 +750,7 @@ fn take_node(node: &mut Node) -> Node {
struct TabView {
panel: SharedString,
title: SharedString,
icon: Option<IconRole>,
icon: Option<Icon>,
view: AnyView,
}
@@ -1039,8 +1042,8 @@ fn render_pane(
tabs.iter()
.map(|tab| {
let mut entry = Tab::new(tab.panel.clone(), tab.title.clone());
if let Some(role) = tab.icon {
entry = entry.icon(role);
if let Some(icon) = tab.icon.clone() {
entry = entry.icon(icon);
}
entry.closeable(true).wrap({
let panel = tab.panel.clone();