7 Commits
Author SHA1 Message Date
reya b5ce99f4e6 update 2026-11-10 17:00:44 +07:00
reya 4de0029b03 add toast and alert overlays to the shell 2026-11-10 16:43:48 +07:00
reya 37d95d3c75 clean up 2026-11-09 16:00:19 +07:00
reya 2e7711ee2e Move layout metrics into theme and apply audit cleanups 2026-11-09 15:59:13 +07:00
reya 122f9b12ae Reduce render allocations and redundant work in core components 2026-11-09 14:35:15 +07:00
reya 73f7dc31d4 Fix shared element state across component instances 2026-11-09 14:04:25 +07:00
reya f022f08947 Add component audit report 2026-11-09 13:36:26 +07:00
23 changed files with 356 additions and 329 deletions
+1 -1
View File
@@ -63,7 +63,7 @@ badge = []
input = ["button"] input = ["button"]
clipboard = ["button"] clipboard = ["button"]
popover = [] popover = []
scroll_area = [] scroll_area = ["scrollbar"]
scrollbar = ["scroll_area"] scrollbar = ["scroll_area"]
segmented = [] segmented = []
separator = [] separator = []
+11 -15
View File
@@ -6,7 +6,7 @@ use gpui::{
IntoElement, ParentElement as _, RenderOnce, SharedString, StatefulInteractiveElement as _, IntoElement, ParentElement as _, RenderOnce, SharedString, StatefulInteractiveElement as _,
Styled as _, Window, deferred, div, ease_out_quint, px, Styled as _, Window, deferred, div, ease_out_quint, px,
}; };
use gpui_native_icons::{Icon, IconRole, icon}; use gpui_native_icons::{IconRole, icon};
use gpui_native_theme::{ActiveTheme as _, Size}; use gpui_native_theme::{ActiveTheme as _, Size};
use crate::common::DismissHandler; use crate::common::DismissHandler;
@@ -33,7 +33,7 @@ pub struct Alert {
title: Option<SharedString>, title: Option<SharedString>,
message: Option<SharedString>, message: Option<SharedString>,
severity: AlertSeverity, severity: AlertSeverity,
icon: Option<Icon>, icon: Option<IconRole>,
show_icon: bool, show_icon: bool,
on_dismiss: Option<DismissHandler>, on_dismiss: Option<DismissHandler>,
} }
@@ -78,12 +78,9 @@ impl Alert {
self self
} }
/// Overrides the severity-derived icon. /// Overrides the severity-derived icon with this glyph.
pub fn icon<I>(mut self, icon: I) -> Self pub fn icon(mut self, role: IconRole) -> Self {
where self.icon = Some(role);
I: Into<Icon>,
{
self.icon = Some(icon.into());
self self
} }
@@ -119,12 +116,10 @@ impl RenderOnce for Alert {
AlertSeverity::Danger => theme.ramps.destructive, AlertSeverity::Danger => theme.ramps.destructive,
}; };
let icon_glyph = self.icon.unwrap_or_else(|| { let icon_role = self.icon.unwrap_or(match self.severity {
icon(match self.severity { AlertSeverity::Info => IconRole::Info,
AlertSeverity::Info => IconRole::Info, AlertSeverity::Success => IconRole::Check,
AlertSeverity::Success => IconRole::Check, AlertSeverity::Warning | AlertSeverity::Danger => IconRole::Warning,
AlertSeverity::Warning | AlertSeverity::Danger => IconRole::Warning,
})
}); });
let foreground = theme.colors.foreground; let foreground = theme.colors.foreground;
@@ -163,7 +158,7 @@ impl RenderOnce for Alert {
.flex() .flex()
.items_center() .items_center()
.h(window.line_height()) .h(window.line_height())
.child(icon_glyph.size(icon_size).color(ramp.base)), .child(icon(icon_role).size(icon_size).color(ramp.base)),
) )
}) })
.child( .child(
@@ -337,6 +332,7 @@ impl RenderOnce for AlertsView {
div() div()
.relative() .relative()
.w_full() .w_full()
.debug_selector(|| "alert-entry".to_string())
.child(entry.alert.clone()) .child(entry.alert.clone())
.with_animation( .with_animation(
("alert-enter", id), ("alert-enter", id),
+6 -9
View File
@@ -6,7 +6,7 @@ use gpui::{
RenderOnce, Role, SharedString, StatefulInteractiveElement as _, Styled as _, Window, canvas, RenderOnce, Role, SharedString, StatefulInteractiveElement as _, Styled as _, Window, canvas,
relative, transparent_black, relative, transparent_black,
}; };
use gpui_native_icons::Icon; use gpui_native_icons::{IconRole, icon};
use gpui_native_theme::{ActiveTheme as _, BORDER_WIDTH, Size}; use gpui_native_theme::{ActiveTheme as _, BORDER_WIDTH, Size};
use gpui_native_utils::{focus_ring, record_anchor_bounds}; use gpui_native_utils::{focus_ring, record_anchor_bounds};
use std::rc::Rc; use std::rc::Rc;
@@ -38,7 +38,7 @@ pub enum ButtonVariant {
pub struct Button { pub struct Button {
id: ElementId, id: ElementId,
label: Option<SharedString>, label: Option<SharedString>,
icon: Option<Icon>, icon: Option<IconRole>,
variant: ButtonVariant, variant: ButtonVariant,
size: Size, size: Size,
disabled: bool, disabled: bool,
@@ -82,11 +82,8 @@ impl Button {
} }
/// Shows an icon beside the label. /// Shows an icon beside the label.
pub fn icon<I>(mut self, icon: I) -> Self pub fn icon(mut self, role: IconRole) -> Self {
where self.icon = Some(role);
I: Into<Icon>,
{
self.icon = Some(icon.into());
self self
} }
@@ -239,8 +236,8 @@ impl RenderOnce for Button {
) )
}) })
.when(!self.loading, |button| { .when(!self.loading, |button| {
button.when_some(self.icon, |button, icon| { button.when_some(self.icon, |button, role| {
button.child(icon.size(metrics.icon_size).color(foreground)) button.child(icon(role).size(metrics.icon_size).color(foreground))
}) })
}) })
.when_some(self.label, |button, label| button.child(label)) .when_some(self.label, |button, label| button.child(label))
+6 -9
View File
@@ -9,7 +9,7 @@ use gpui::{
StatefulInteractiveElement as _, Styled as _, Subscription, Window, actions, canvas, deferred, StatefulInteractiveElement as _, Styled as _, Subscription, Window, actions, canvas, deferred,
div, relative, div, relative,
}; };
use gpui_native_icons::Icon; use gpui_native_icons::{IconRole, icon};
use gpui_native_theme::{ActiveTheme as _, POPUP_MAX_HEIGHT, Size}; use gpui_native_theme::{ActiveTheme as _, POPUP_MAX_HEIGHT, Size};
use crate::common::{h_flex, v_flex}; 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. /// Gray text on the row's right edge, e.g. the keybinding.
pub hint: Option<SharedString>, pub hint: Option<SharedString>,
/// Leading glyph shown before the label. /// Leading glyph shown before the label.
pub icon: Option<Icon>, pub icon: Option<IconRole>,
} }
impl Command { impl Command {
@@ -64,11 +64,8 @@ impl Command {
} }
/// Shows an icon before the label. /// Shows an icon before the label.
pub fn icon<I>(mut self, icon: I) -> Self pub fn icon(mut self, role: IconRole) -> Self {
where self.icon = Some(role);
I: Into<Icon>,
{
self.icon = Some(icon.into());
self self
} }
@@ -364,8 +361,8 @@ impl Render for CommandPaletteState {
state.update(cx, |state, cx| state.choose_row(row_ix, window, cx)); state.update(cx, |state, cx| state.choose_row(row_ix, window, cx));
} }
}) })
.when_some(command.icon.clone(), |row, icon| { .when_some(command.icon, |row, role| {
row.child(icon.size(metrics.icon_size).color(colors.foreground)) row.child(icon(role).size(metrics.icon_size).color(colors.foreground))
}) })
.child( .child(
div() div()
+1
View File
@@ -14,6 +14,7 @@ use crate::common::v_flex;
pub type ChangeHandler = Rc<dyn Fn(bool, &mut Window, &mut App)>; pub type ChangeHandler = Rc<dyn Fn(bool, &mut Window, &mut App)>;
type ContentFn = Box<dyn Fn(&mut Window, &mut App) -> AnyElement>; type ContentFn = Box<dyn Fn(&mut Window, &mut App) -> AnyElement>;
type ActionsFn = Box<dyn Fn(&mut Window, &mut App) -> Vec<AnyElement>>; type ActionsFn = Box<dyn Fn(&mut Window, &mut App) -> Vec<AnyElement>>;
/// A centred panel over a dim scrim: the platform's modal. /// A centred panel over a dim scrim: the platform's modal.
#[derive(IntoElement)] #[derive(IntoElement)]
pub struct Dialog { pub struct Dialog {
+6 -9
View File
@@ -9,7 +9,7 @@ use gpui::{
MouseButton, MouseDownEvent, ParentElement as _, RenderOnce, Role, SharedString, MouseButton, MouseDownEvent, ParentElement as _, RenderOnce, Role, SharedString,
StatefulInteractiveElement as _, Styled as _, Window, canvas, div, relative, StatefulInteractiveElement as _, Styled as _, Window, canvas, div, relative,
}; };
use gpui_native_icons::{Icon, IconRole, icon}; use gpui_native_icons::{IconRole, icon};
use gpui_native_theme::{ActiveTheme as _, BORDER_WIDTH, Size}; use gpui_native_theme::{ActiveTheme as _, BORDER_WIDTH, Size};
use gpui_native_utils::{focus_handle, focus_ring}; 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 { pub struct DropdownMenu {
id: ElementId, id: ElementId,
label: Option<SharedString>, label: Option<SharedString>,
icon: Option<Icon>, icon: Option<IconRole>,
variant: ButtonVariant, variant: ButtonVariant,
size: Size, size: Size,
disabled: bool, disabled: bool,
@@ -73,11 +73,8 @@ impl DropdownMenu {
} }
/// Shows an icon beside the label. /// Shows an icon beside the label.
pub fn icon<I>(mut self, icon: I) -> Self pub fn icon(mut self, role: IconRole) -> Self {
where self.icon = Some(role);
I: Into<Icon>,
{
self.icon = Some(icon.into());
self self
} }
@@ -270,8 +267,8 @@ impl RenderOnce for DropdownMenu {
) )
}) })
.when(!self.loading, |zone| { .when(!self.loading, |zone| {
zone.when_some(self.icon, |zone, icon| { zone.when_some(self.icon, |zone, role| {
zone.child(icon.size(metrics.icon_size).color(foreground)) zone.child(icon(role).size(metrics.icon_size).color(foreground))
}) })
}) })
.when_some(self.label, |zone, label| zone.child(label)); .when_some(self.label, |zone, label| zone.child(label));
+6 -9
View File
@@ -3,7 +3,7 @@ use gpui::{
App, IntoElement, ParentElement as _, RenderOnce, SharedString, StyleRefinement, Styled as _, App, IntoElement, ParentElement as _, RenderOnce, SharedString, StyleRefinement, Styled as _,
Window, div, relative, Window, div, relative,
}; };
use gpui_native_icons::Icon; use gpui_native_icons::{IconRole, icon};
use gpui_native_theme::{ActiveTheme as _, Size}; use gpui_native_theme::{ActiveTheme as _, Size};
use crate::common::{StyledExt, v_flex}; 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. /// The centered "nothing here yet" panel for lists, tables, and searches.
#[derive(IntoElement)] #[derive(IntoElement)]
pub struct EmptyState { pub struct EmptyState {
icon: Option<Icon>, icon: Option<IconRole>,
headline: SharedString, headline: SharedString,
hint: Option<SharedString>, hint: Option<SharedString>,
action: Option<gpui::AnyElement>, action: Option<gpui::AnyElement>,
@@ -34,11 +34,8 @@ impl EmptyState {
} }
/// Sets a large muted icon above the headline. /// Sets a large muted icon above the headline.
pub fn icon<I>(mut self, icon: I) -> Self pub fn icon(mut self, role: IconRole) -> Self {
where self.icon = Some(role);
I: Into<Icon>,
{
self.icon = Some(icon.into());
self self
} }
@@ -81,11 +78,11 @@ impl RenderOnce for EmptyState {
.justify_center() .justify_center()
.text_center() .text_center()
.p_4() .p_4()
.when_some(self.icon, |state, icon| { .when_some(self.icon, |state, role| {
state.child( state.child(
div() div()
.mb_2() .mb_2()
.child(icon.size(illustration).color(muted_foreground)), .child(icon(role).size(illustration).color(muted_foreground)),
) )
}) })
.child( .child(
+7 -10
View File
@@ -9,7 +9,7 @@ use gpui::{
StatefulInteractiveElement as _, Styled as _, Window, anchored, canvas, deferred, div, StatefulInteractiveElement as _, Styled as _, Window, anchored, canvas, deferred, div,
ease_out_quint, point, px, relative, ease_out_quint, point, px, relative,
}; };
use gpui_native_icons::{Icon, IconRole, icon as icon_glyph}; use gpui_native_icons::{IconRole, icon as icon_glyph};
use gpui_native_theme::{ActiveTheme as _, POPUP_MAX_HEIGHT}; use gpui_native_theme::{ActiveTheme as _, POPUP_MAX_HEIGHT};
use crate::common::v_flex; use crate::common::v_flex;
@@ -30,7 +30,7 @@ type ItemsFn = Box<dyn Fn(&mut Window, &mut App) -> Vec<MenuItem>>;
pub struct MenuItem { pub struct MenuItem {
kind: MenuItemKind, kind: MenuItemKind,
label: SharedString, label: SharedString,
icon: Option<Icon>, icon: Option<IconRole>,
shortcut: Option<SharedString>, shortcut: Option<SharedString>,
handler: Option<ItemHandler>, handler: Option<ItemHandler>,
disabled: bool, disabled: bool,
@@ -86,11 +86,8 @@ impl MenuItem {
} }
/// Shows an icon before the label. /// Shows an icon before the label.
pub fn icon<I>(mut self, icon: I) -> Self pub fn icon(mut self, role: IconRole) -> Self {
where self.icon = Some(role);
I: Into<Icon>,
{
self.icon = Some(icon.into());
self self
} }
@@ -529,7 +526,7 @@ fn render_item(
let destructive = item.destructive; let destructive = item.destructive;
let checked = item.checked; let checked = item.checked;
let shortcut = item.shortcut.clone(); let shortcut = item.shortcut.clone();
let icon = item.icon.clone(); let icon_role = item.icon;
div() div()
.id(("menu-item", index)) .id(("menu-item", index))
@@ -579,8 +576,8 @@ fn render_item(
} }
} }
}) })
.when_some(icon, |row, icon| { .when_some(icon_role, |row, role| {
row.child(icon.size(chrome.icon_size).color(label_color)) row.child(icon_glyph(role).size(chrome.icon_size).color(label_color))
}) })
.child( .child(
div() div()
+6 -9
View File
@@ -7,7 +7,7 @@ use gpui::{
SpringConfig, StatefulInteractiveElement as _, StyleRefinement, Styled as _, Toggled, Window, SpringConfig, StatefulInteractiveElement as _, StyleRefinement, Styled as _, Toggled, Window,
canvas, div, canvas, div,
}; };
use gpui_native_icons::Icon; use gpui_native_icons::{IconRole, icon};
use gpui_native_theme::{ActiveTheme as _, Size}; use gpui_native_theme::{ActiveTheme as _, Size};
use gpui_native_utils::{focus_handle, focus_ring}; 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> { pub struct SegmentedOption<T> {
value: T, value: T,
label: SharedString, label: SharedString,
icon: Option<Icon>, icon: Option<IconRole>,
} }
impl<T> SegmentedOption<T> { impl<T> SegmentedOption<T> {
@@ -38,11 +38,8 @@ impl<T> SegmentedOption<T> {
} }
/// Sets a leading icon for the segment. /// Sets a leading icon for the segment.
pub fn icon<I>(mut self, icon: I) -> Self pub fn icon(mut self, role: IconRole) -> Self {
where self.icon = Some(role);
I: Into<Icon>,
{
self.icon = Some(icon.into());
self self
} }
} }
@@ -229,8 +226,8 @@ where
.role(Role::RadioButton) .role(Role::RadioButton)
.aria_label(segment.label.clone()) .aria_label(segment.label.clone())
.aria_toggled(Toggled::from(is_selected)) .aria_toggled(Toggled::from(is_selected))
.when_some(segment.icon.clone(), |segment, icon| { .when_some(segment.icon, |segment, role| {
segment.child(icon.size(icon_size).color(content_color)) segment.child(icon(role).size(icon_size).color(content_color))
}) })
.child(segment.label.clone()) .child(segment.label.clone())
.child( .child(
+6 -9
View File
@@ -5,7 +5,7 @@ use gpui::{
App, ElementId, InteractiveElement as _, IntoElement, ParentElement as _, RenderOnce, Role, App, ElementId, InteractiveElement as _, IntoElement, ParentElement as _, RenderOnce, Role,
SharedString, StatefulInteractiveElement as _, StyleRefinement, Styled as _, Window, SharedString, StatefulInteractiveElement as _, StyleRefinement, Styled as _, Window,
}; };
use gpui_native_icons::{Icon, IconRole, icon}; use gpui_native_icons::{IconRole, icon};
use gpui_native_theme::{ActiveTheme as _, BORDER_WIDTH, Size}; use gpui_native_theme::{ActiveTheme as _, BORDER_WIDTH, Size};
use gpui_native_utils::focus_ring; 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> { pub struct SelectOption<T> {
value: T, value: T,
label: SharedString, label: SharedString,
icon: Option<Icon>, icon: Option<IconRole>,
} }
impl<T> SelectOption<T> { impl<T> SelectOption<T> {
@@ -37,11 +37,8 @@ impl<T> SelectOption<T> {
} }
/// Sets a leading icon for the option's row. /// Sets a leading icon for the option's row.
pub fn icon<I>(mut self, icon: I) -> Self pub fn icon(mut self, role: IconRole) -> Self {
where self.icon = Some(role);
I: Into<Icon>,
{
self.icon = Some(icon.into());
self self
} }
} }
@@ -275,8 +272,8 @@ where
}; };
let mut row = MenuItem::new(option.label.clone()); let mut row = MenuItem::new(option.label.clone());
if let Some(icon) = option.icon.clone() { if let Some(role) = option.icon {
row = row.icon(icon); row = row.icon(role);
} }
row.checked(is_selected).handler(handler) row.checked(is_selected).handler(handler)
}) })
+7 -9
View File
@@ -9,7 +9,7 @@ use gpui::{
IntoElement, ParentElement as _, RenderOnce, SharedString, StatefulInteractiveElement as _, IntoElement, ParentElement as _, RenderOnce, SharedString, StatefulInteractiveElement as _,
Styled as _, Task, Window, deferred, div, ease_out_quint, px, Styled as _, Task, Window, deferred, div, ease_out_quint, px,
}; };
use gpui_native_icons::{Icon, IconRole, icon}; use gpui_native_icons::{IconRole, icon};
use gpui_native_theme::ActiveTheme as _; use gpui_native_theme::ActiveTheme as _;
use crate::button::{Button, ButtonVariant}; use crate::button::{Button, ButtonVariant};
@@ -43,7 +43,7 @@ impl ToastAction {
#[derive(Clone)] #[derive(Clone)]
pub struct Toast { pub struct Toast {
message: SharedString, message: SharedString,
icon: Option<Icon>, icon: Option<IconRole>,
action: Option<ToastAction>, action: Option<ToastAction>,
duration: Option<Duration>, duration: Option<Duration>,
on_dismiss: Option<DismissHandler>, on_dismiss: Option<DismissHandler>,
@@ -68,11 +68,8 @@ impl Toast {
} }
/// Shows this glyph before the content. /// Shows this glyph before the content.
pub fn icon<I>(mut self, icon: I) -> Self pub fn icon(mut self, role: IconRole) -> Self {
where self.icon = Some(role);
I: Into<Icon>,
{
self.icon = Some(icon.into());
self self
} }
@@ -278,6 +275,7 @@ fn pill(entry: &ToastEntry, toasts: &Entity<Toasts>, cx: &App) -> gpui::Stateful
div() div()
.id(("toast", id)) .id(("toast", id))
.debug_selector(|| "toast-pill".to_string())
.max_w(px(480.)) .max_w(px(480.))
.flex() .flex()
.items_center() .items_center()
@@ -288,14 +286,14 @@ fn pill(entry: &ToastEntry, toasts: &Entity<Toasts>, cx: &App) -> gpui::Stateful
.border_color(border) .border_color(border)
.rounded(radius) .rounded(radius)
.shadow(shadows.clone()) .shadow(shadows.clone())
.when_some(toast.icon.clone(), |pill, icon| { .when_some(toast.icon, |pill, icon_role| {
pill.child( pill.child(
div() div()
.flex_none() .flex_none()
.mr_2() .mr_2()
.flex() .flex()
.items_center() .items_center()
.child(icon.size(icon_size).color(foreground)), .child(icon(icon_role).size(icon_size).color(foreground)),
) )
}) })
.child( .child(
+11 -10
View File
@@ -6,7 +6,7 @@ use gpui::{
ParentElement as _, RenderOnce, Role, SharedString, StatefulInteractiveElement as _, ParentElement as _, RenderOnce, Role, SharedString, StatefulInteractiveElement as _,
Styled as _, Window, div, Styled as _, Window, div,
}; };
use gpui_native_icons::Icon; use gpui_native_icons::{IconRole, icon};
use gpui_native_theme::ActiveTheme as _; use gpui_native_theme::ActiveTheme as _;
use gpui_native_utils::{nav_key, paint_selectable_row, selectable_label_color}; 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`]. /// One row of a [`List`].
pub struct ListItem { pub struct ListItem {
label: SharedString, label: SharedString,
icon: Option<Icon>, icon: Option<IconRole>,
detail: Option<SharedString>, detail: Option<SharedString>,
disabled: bool, disabled: bool,
} }
@@ -36,12 +36,9 @@ impl ListItem {
} }
} }
/// Shows an icon leading the label, a role or any custom [`IconNamed`]. /// Shows an icon leading the label.
pub fn icon<I>(mut self, icon: I) -> Self pub fn icon(mut self, role: IconRole) -> Self {
where self.icon = Some(role);
I: Into<Icon>,
{
self.icon = Some(icon.into());
self self
} }
@@ -225,8 +222,12 @@ fn render_row(
} }
}) })
}) })
.when_some(item.icon.clone(), |row, icon| { .when_some(item.icon, |row, role| {
row.child(icon.size(theme.sizes.medium.icon_size).color(label_color)) row.child(
icon(role)
.size(theme.sizes.medium.icon_size)
.color(label_color),
)
}) })
.child( .child(
div() div()
+12 -11
View File
@@ -10,7 +10,7 @@ use gpui::{
Styled as _, Window, div, Styled as _, Window, div,
}; };
use gpui_native_core::scroll_area::ScrollArea; use gpui_native_core::scroll_area::ScrollArea;
use gpui_native_icons::Icon; use gpui_native_icons::{IconRole, icon};
use gpui_native_theme::ActiveTheme as _; use gpui_native_theme::ActiveTheme as _;
use gpui_native_utils::{nav_key, paint_selectable_row, selectable_label_color}; use gpui_native_utils::{nav_key, paint_selectable_row, selectable_label_color};
@@ -26,7 +26,7 @@ pub enum SidebarEntry {
/// A selectable row. /// A selectable row.
Row { Row {
label: SharedString, label: SharedString,
icon: Option<Icon>, icon: Option<IconRole>,
detail: Option<SharedString>, detail: Option<SharedString>,
disabled: bool, disabled: bool,
}, },
@@ -56,13 +56,10 @@ impl SidebarEntry {
} }
} }
/// Shows an icon leading the label, a role or any custom [`IconNamed`]. /// Shows an icon leading the label.
pub fn icon<I>(mut self, icon: I) -> Self pub fn icon(mut self, role: IconRole) -> Self {
where if let Self::Row { icon, .. } = &mut self {
I: Into<Icon>, *icon = Some(role);
{
if let Self::Row { icon: slot, .. } = &mut self {
*slot = Some(icon.into());
} }
self self
} }
@@ -340,8 +337,12 @@ fn render_entry(
}); });
} }
row = row.when_some(row_icon.clone(), |row, icon| { row = row.when_some(*row_icon, |row, role| {
row.child(icon.size(theme.sizes.medium.icon_size).color(label_color)) row.child(
icon(role)
.size(theme.sizes.medium.icon_size)
.color(label_color),
)
}); });
row = row.child(div().flex_1().text_color(label_color).child(label.clone())); row = row.child(div().flex_1().text_color(label_color).child(label.clone()));
+12 -15
View File
@@ -10,7 +10,7 @@ use gpui_native_core::button::{Button, ButtonVariant};
use gpui_native_core::common::Sizable as _; use gpui_native_core::common::Sizable as _;
use gpui_native_core::spinner::Spinner; use gpui_native_core::spinner::Spinner;
use gpui_native_core::tooltip::Tooltip; use gpui_native_core::tooltip::Tooltip;
use gpui_native_icons::{Icon, IconRole}; use gpui_native_icons::{IconRole, icon};
use gpui_native_theme::{ActiveTheme as _, Size}; use gpui_native_theme::{ActiveTheme as _, Size};
use gpui_native_utils::adjacent_selectable; use gpui_native_utils::adjacent_selectable;
@@ -35,7 +35,7 @@ pub struct Tab {
/// The stable identity for element state; must survive reordering. /// The stable identity for element state; must survive reordering.
key: ElementId, key: ElementId,
label: SharedString, label: SharedString,
icon: Option<Icon>, icon: Option<IconRole>,
lead: Option<AnyElement>, lead: Option<AnyElement>,
busy: bool, busy: bool,
disabled: bool, disabled: bool,
@@ -63,11 +63,8 @@ impl Tab {
} }
/// Shows an icon leading the label. /// Shows an icon leading the label.
pub fn icon<I>(mut self, icon: I) -> Self pub fn icon(mut self, role: IconRole) -> Self {
where self.icon = Some(role);
I: Into<Icon>,
{
self.icon = Some(icon.into());
self self
} }
@@ -347,18 +344,18 @@ fn render_tab(
}; };
let lead = tab.lead.take(); let lead = tab.lead.take();
let icon = tab.icon.take();
strip_tab = match lead { strip_tab = match lead {
Some(lead) => strip_tab.child(lead), Some(lead) => strip_tab.child(lead),
None => match variant { None => match variant {
TabsVariant::Underline => { TabsVariant::Underline => strip_tab.child(div().flex_none().pb_0p5().when_some(
strip_tab.child(div().flex_none().pb_0p5().when_some(icon, |slot, icon| { tab.icon,
slot.child(icon.size(metrics.dense_icon_size).color(label_color)) |slot, role| {
})) slot.child(icon(role).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)) TabsVariant::Pill => strip_tab.when_some(tab.icon, |strip_tab, role| {
strip_tab.child(icon(role).size(metrics.dense_icon_size).color(label_color))
}), }),
}, },
}; };
+10 -9
View File
@@ -6,7 +6,7 @@ use gpui::{
ParentElement as _, RenderOnce, Role, SharedString, StatefulInteractiveElement as _, ParentElement as _, RenderOnce, Role, SharedString, StatefulInteractiveElement as _,
Styled as _, Window, div, Styled as _, Window, div,
}; };
use gpui_native_icons::{Icon, IconRole, icon}; use gpui_native_icons::{IconRole, icon};
use gpui_native_theme::ActiveTheme as _; use gpui_native_theme::ActiveTheme as _;
use gpui_native_utils::{nav_key, paint_selectable_row, selectable_label_color}; 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)] #[derive(Clone)]
pub struct TreeItem { pub struct TreeItem {
label: SharedString, label: SharedString,
icon: Option<Icon>, icon: Option<IconRole>,
depth: usize, depth: usize,
expandable: bool, expandable: bool,
expanded: bool, expanded: bool,
@@ -44,11 +44,8 @@ impl TreeItem {
} }
/// Shows an icon leading the label. /// Shows an icon leading the label.
pub fn icon<I>(mut self, icon: I) -> Self pub fn icon(mut self, role: IconRole) -> Self {
where self.icon = Some(role);
I: Into<Icon>,
{
self.icon = Some(icon.into());
self self
} }
@@ -320,8 +317,12 @@ fn render_row(
selected, selected,
on_disclosure, on_disclosure,
)) ))
.when_some(item.icon.clone(), |row, icon| { .when_some(item.icon, |row, role| {
row.child(icon.size(theme.sizes.medium.icon_size).color(label_color)) row.child(
icon(role)
.size(theme.sizes.medium.icon_size)
.color(label_color),
)
}) })
.child( .child(
div() div()
+1 -1
View File
@@ -2,7 +2,7 @@
name = "gpui-native-icons" name = "gpui-native-icons"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
description = "System icons for GPUI: semantic roles resolved per platform, a bundled Lucide fallback, and app-defined icon sets" description = "System icons for GPUI: semantic roles resolved per platform, with a bundled Lucide fallback"
[dependencies] [dependencies]
anyhow = "1" anyhow = "1"
+58 -124
View File
@@ -1,11 +1,10 @@
//! System icons for GPUI: components and apps reference semantic roles, //! System icons for GPUI: components and apps reference semantic roles,
//! and the crate resolves each role to a glyph for the current platform. //! 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::prelude::FluentBuilder as _;
use gpui::{ use gpui::{
AnyElement, AssetSource, Hsla, IntoElement, ParentElement as _, Pixels, RenderOnce, AssetSource, Hsla, IntoElement, ParentElement as _, Pixels, RenderOnce, SharedString,
SharedString, Styled as _, Transformation, Window, div, px, svg, Styled as _, Transformation, Window, div, px, svg,
}; };
#[cfg_attr(not(target_os = "linux"), allow(dead_code))] #[cfg_attr(not(target_os = "linux"), allow(dead_code))]
@@ -195,29 +194,10 @@ 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. /// An icon element. Unpainted unless a color is set.
#[derive(Clone, IntoElement)] #[derive(IntoElement)]
pub struct Icon { pub struct Icon {
glyph: Glyph, role: IconRole,
size: Pixels, size: Pixels,
color: Option<Hsla>, color: Option<Hsla>,
transformation: Option<Transformation>, transformation: Option<Transformation>,
@@ -226,7 +206,7 @@ pub struct Icon {
/// Creates an icon for the role at the default size of 16px. /// Creates an icon for the role at the default size of 16px.
pub fn icon(role: IconRole) -> Icon { pub fn icon(role: IconRole) -> Icon {
Icon { Icon {
glyph: Glyph::Role(role), role,
size: px(16.0), size: px(16.0),
color: None, color: None,
transformation: None, transformation: None,
@@ -234,19 +214,6 @@ pub fn icon(role: IconRole) -> Icon {
} }
impl 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 { pub fn size(mut self, size: Pixels) -> Self {
self.size = size; self.size = size;
self self
@@ -265,99 +232,66 @@ impl Icon {
/// The asset key a resolved freedesktop file is served under. /// The asset key a resolved freedesktop file is served under.
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
fn freedesktop_asset_path(size: Pixels, role: &IconRole) -> Option<SharedString> { fn freedesktop_asset_path(&self) -> Option<SharedString> {
let size = size.as_f32().max(1.0).round() as u32; let size = self.size.as_f32().max(1.0).round() as u32;
freedesktop::cached_file(*role, size) freedesktop::cached_file(self.role, size).map(|_| {
.map(|_| SharedString::from(format!("icons/freedesktop/{}-{size}.svg", role.slug()))) SharedString::from(format!("icons/freedesktop/{}-{size}.svg", self.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 { impl RenderOnce for Icon {
fn render(self, _window: &mut Window, _cx: &mut gpui::App) -> impl IntoElement { fn render(self, _window: &mut Window, _cx: &mut gpui::App) -> impl IntoElement {
let Icon { #[cfg(target_os = "windows")]
glyph, return self.render_glyph();
size, #[cfg(not(target_os = "windows"))]
color, return self.render_svg();
transformation, }
} = self; }
match glyph {
Glyph::Named(path) => Self::render_svg_path(size, color, transformation, path), impl Icon {
Glyph::Role(role) => Self::render_role(size, color, transformation, role), /// 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(),
} }
} }
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. /// The asset source serving this crate's bundled glyphs.
+3 -1
View File
@@ -17,7 +17,7 @@ gpui = { workspace = true, features = ["test-support"] }
[features] [features]
default = ["full"] default = ["full"]
full = ["dock", "resize_handle", "resizable", "shell", "window_controls"] full = ["dock", "resize_handle", "resizable", "shell", "window_controls", "toast", "alert"]
dock = [ dock = [
"resize_handle", "resize_handle",
"dep:gpui-native-core", "dep:gpui-native-core",
@@ -36,4 +36,6 @@ shell = [
"dep:gpui-native-data", "dep:gpui-native-data",
"dep:gpui-native-icons", "dep:gpui-native-icons",
] ]
toast = ["shell", "gpui-native-core/toast"]
alert = ["shell", "gpui-native-core/alert"]
window_controls = ["dep:gpui-native-icons"] window_controls = ["dep:gpui-native-icons"]
+10 -13
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::context_menu::ContextMenu;
use gpui_native_core::menu::MenuItem; use gpui_native_core::menu::MenuItem;
use gpui_native_data::tabs::{Tab, Tabs, TabsVariant}; use gpui_native_data::tabs::{Tab, Tabs, TabsVariant};
use gpui_native_icons::{Icon, IconRole}; use gpui_native_icons::IconRole;
use gpui_native_theme::{ActiveTheme as _, Size}; use gpui_native_theme::{ActiveTheme as _, Size};
use crate::resize_handle::ResizeHandle; use crate::resize_handle::ResizeHandle;
@@ -35,7 +35,7 @@ type OpenRightSidebarHandler = Rc<dyn Fn(&mut App)>;
pub struct PanelDescriptor { pub struct PanelDescriptor {
id: SharedString, id: SharedString,
title: SharedString, title: SharedString,
icon: Option<Icon>, icon: Option<IconRole>,
build: PanelBuilder, build: PanelBuilder,
right_sidebar: Option<PanelBuilder>, right_sidebar: Option<PanelBuilder>,
} }
@@ -58,11 +58,8 @@ impl PanelDescriptor {
} }
/// Shows an icon leading the tab label. /// Shows an icon leading the tab label.
pub fn icon<I>(mut self, icon: I) -> Self pub fn icon(mut self, role: IconRole) -> Self {
where self.icon = Some(role);
I: Into<Icon>,
{
self.icon = Some(icon.into());
self self
} }
@@ -133,7 +130,7 @@ impl Render for DragPreview {
struct PanelTab { struct PanelTab {
panel: SharedString, panel: SharedString,
title: SharedString, title: SharedString,
icon: Option<Icon>, icon: Option<IconRole>,
view: AnyView, view: AnyView,
right_view: Option<AnyView>, right_view: Option<AnyView>,
} }
@@ -218,7 +215,7 @@ impl DockState {
let tab = PanelTab { let tab = PanelTab {
panel: SharedString::from(id), panel: SharedString::from(id),
title: panel.title.clone(), title: panel.title.clone(),
icon: panel.icon.clone(), icon: panel.icon,
view: (panel.build)(window, cx), view: (panel.build)(window, cx),
right_view, right_view,
}; };
@@ -682,7 +679,7 @@ impl DockState {
.map(|tab| TabView { .map(|tab| TabView {
panel: tab.panel.clone(), panel: tab.panel.clone(),
title: tab.title.clone(), title: tab.title.clone(),
icon: tab.icon.clone(), icon: tab.icon,
view: tab.view.clone(), view: tab.view.clone(),
}) })
.collect(), .collect(),
@@ -750,7 +747,7 @@ fn take_node(node: &mut Node) -> Node {
struct TabView { struct TabView {
panel: SharedString, panel: SharedString,
title: SharedString, title: SharedString,
icon: Option<Icon>, icon: Option<IconRole>,
view: AnyView, view: AnyView,
} }
@@ -1042,8 +1039,8 @@ fn render_pane(
tabs.iter() tabs.iter()
.map(|tab| { .map(|tab| {
let mut entry = Tab::new(tab.panel.clone(), tab.title.clone()); let mut entry = Tab::new(tab.panel.clone(), tab.title.clone());
if let Some(icon) = tab.icon.clone() { if let Some(role) = tab.icon {
entry = entry.icon(icon); entry = entry.icon(role);
} }
entry.closeable(true).wrap({ entry.closeable(true).wrap({
let panel = tab.panel.clone(); let panel = tab.panel.clone();
+58 -2
View File
@@ -6,8 +6,12 @@ use gpui::{
AnyElement, App, AppContext as _, Context, ElementId, Entity, InteractiveElement as _, AnyElement, App, AppContext as _, Context, ElementId, Entity, InteractiveElement as _,
IntoElement, ParentElement as _, Pixels, RenderOnce, Styled as _, Window, div, px, IntoElement, ParentElement as _, Pixels, RenderOnce, Styled as _, Window, div, px,
}; };
#[cfg(feature = "alert")]
use gpui_native_core::alert::{Alert, Alerts, AlertsView};
use gpui_native_core::button::{Button, ButtonVariant}; use gpui_native_core::button::{Button, ButtonVariant};
use gpui_native_core::common::{Sizable as _, h_flex, v_flex}; use gpui_native_core::common::{Sizable as _, h_flex, v_flex};
#[cfg(feature = "toast")]
use gpui_native_core::toast::{Toast, Toasts, ToastsView};
use gpui_native_data::sidebar::Sidebar; use gpui_native_data::sidebar::Sidebar;
use gpui_native_icons::IconRole; use gpui_native_icons::IconRole;
use gpui_native_theme::{ActiveTheme as _, Size}; use gpui_native_theme::{ActiveTheme as _, Size};
@@ -29,16 +33,24 @@ pub struct LayoutState {
right_visible: bool, right_visible: bool,
left_width: Pixels, left_width: Pixels,
right_width: Pixels, right_width: Pixels,
#[cfg(feature = "toast")]
toasts: Entity<Toasts>,
#[cfg(feature = "alert")]
alerts: Entity<Alerts>,
} }
impl LayoutState { impl LayoutState {
/// Creates the state: left sidebar visible, right sidebar hidden. /// Creates the state: left sidebar visible, right sidebar hidden.
pub fn new(cx: &mut App) -> Entity<Self> { pub fn new(cx: &mut App) -> Entity<Self> {
cx.new(|_| Self { cx.new(|cx| Self {
left_visible: true, left_visible: true,
right_visible: false, right_visible: false,
left_width: px(DEFAULT_SIDEBAR_WIDTH), left_width: px(DEFAULT_SIDEBAR_WIDTH),
right_width: px(DEFAULT_SIDEBAR_WIDTH), right_width: px(DEFAULT_SIDEBAR_WIDTH),
#[cfg(feature = "toast")]
toasts: cx.new(|_| Toasts::default()),
#[cfg(feature = "alert")]
alerts: cx.new(|_| Alerts::default()),
}) })
} }
@@ -91,6 +103,42 @@ impl LayoutState {
(self.right_width - delta).clamp(px(MIN_SIDEBAR_WIDTH), px(MAX_SIDEBAR_WIDTH)); (self.right_width - delta).clamp(px(MIN_SIDEBAR_WIDTH), px(MAX_SIDEBAR_WIDTH));
cx.notify(); cx.notify();
} }
/// Raises a toast in the shell's overlay. Returns its id.
#[cfg(feature = "toast")]
pub fn push_toast(&mut self, toast: Toast, cx: &mut Context<Self>) -> usize {
self.toasts.update(cx, |toasts, cx| toasts.push(toast, cx))
}
/// Dismisses one toast by id.
#[cfg(feature = "toast")]
pub fn dismiss_toast(&mut self, id: usize, cx: &mut Context<Self>) {
self.toasts.update(cx, |toasts, cx| toasts.dismiss(id, cx));
}
/// Dismisses the toast on screen.
#[cfg(feature = "toast")]
pub fn clear_toasts(&mut self, cx: &mut Context<Self>) {
self.toasts.update(cx, |toasts, cx| toasts.clear(cx));
}
/// Pushes an alert onto the shell's stack. Returns its id.
#[cfg(feature = "alert")]
pub fn push_alert(&mut self, alert: Alert, cx: &mut Context<Self>) -> usize {
self.alerts.update(cx, |alerts, cx| alerts.push(alert, cx))
}
/// Dismisses one alert by id, running its dismiss handler.
#[cfg(feature = "alert")]
pub fn dismiss_alert(&mut self, id: usize, cx: &mut Context<Self>) {
self.alerts.update(cx, |alerts, cx| alerts.dismiss(id, cx));
}
/// Dismisses every alert, running each entry's `on_dismiss` handler.
#[cfg(feature = "alert")]
pub fn clear_alerts(&mut self, cx: &mut Context<Self>) {
self.alerts.update(cx, |alerts, cx| alerts.clear(cx));
}
} }
/// Which sidebar a region or toggle belongs to. /// Which sidebar a region or toggle belongs to.
@@ -241,8 +289,16 @@ impl RenderOnce for Shell {
)) ))
}); });
// Wraps the layout in a window frame. // Wraps the layout in a window frame
frame(layout, window, cx) frame(layout, window, cx)
.when(cfg!(feature = "toast"), |this| {
let toasts = state.read(cx).toasts.clone();
this.child(ToastsView::new((id.clone(), "toasts"), toasts))
})
.when(cfg!(feature = "alert"), |this| {
let alerts = state.read(cx).alerts.clone();
this.child(AlertsView::new((id, "alerts"), alerts))
})
} }
} }
+85
View File
@@ -235,3 +235,88 @@ fn the_right_seam_grows_the_right_sidebar_leftwards() {
let region = bounds(&mut visual, "test-right").expect("right region missing"); let region = bounds(&mut visual, "test-right").expect("right region missing");
assert_eq!(region.size.width, grown); assert_eq!(region.size.width, grown);
} }
/// Toasts and alerts live on the shell's [`LayoutState`]; pushing one must
/// show up in the rendered shell with no observing or manual overlay.
#[cfg(feature = "toast")]
mod toasts {
use super::*;
use gpui_native_core::toast::{Toast, ToastAction};
use std::time::Duration;
#[test]
fn a_pushed_toast_renders_in_the_shell() {
let mut cx = TestAppContext::single();
let (state, mut visual) = layout_window(&mut cx);
let id = cx.update(|cx| {
state.update(cx, |state, cx| {
state.push_toast(
Toast::new("Draft deleted").action(ToastAction::new("Undo", |_| {
// A real app restores the draft here.
})),
cx,
)
})
});
// No redraw, no observation: the shell repaints its own overlay.
cx.run_until_parked();
assert!(
bounds(&mut visual, "toast-pill").is_some(),
"the pushed toast must appear in the shell's overlay"
);
cx.update(|cx| {
state.update(cx, |state, cx| state.dismiss_toast(id, cx));
});
cx.run_until_parked();
// The fade-out keeps the pill mounted briefly.
assert!(
bounds(&mut visual, "toast-pill").is_some(),
"the dismissed toast must stay mounted through its fade-out"
);
cx.executor().advance_clock(Duration::from_secs(2));
cx.run_until_parked();
assert!(
bounds(&mut visual, "toast-pill").is_none(),
"the dismissed toast must leave the overlay once faded"
);
}
}
#[cfg(feature = "alert")]
mod alerts {
use super::*;
use gpui_native_core::alert::{Alert, AlertSeverity};
#[test]
fn a_pushed_alert_renders_in_the_shell() {
let mut cx = TestAppContext::single();
let (state, mut visual) = layout_window(&mut cx);
cx.update(|cx| {
state.update(cx, |state, cx| {
state.push_alert(
Alert::new("shell-alert")
.title("Backed up")
.severity(AlertSeverity::Info),
cx,
);
})
});
cx.run_until_parked();
assert!(
bounds(&mut visual, "alert-entry").is_some(),
"the pushed alert must appear in the shell's overlay"
);
cx.update(|cx| {
state.update(cx, |state, cx| state.clear_alerts(cx));
});
cx.run_until_parked();
assert!(
bounds(&mut visual, "alert-entry").is_none(),
"cleared alerts must leave the overlay"
);
}
}
-3
View File
@@ -61,9 +61,6 @@ Gaps surfaced by the signed comparison; ordered by signed's usage.
- [x] `IconRole` + bundled Lucide fallback, Segoe Fluent codepoints (Windows), - [x] `IconRole` + bundled Lucide fallback, Segoe Fluent codepoints (Windows),
freedesktop theme walker (Linux/Omarchy) freedesktop theme walker (Linux/Omarchy)
- [x] `IconNamed` — app-defined icon sets: implement the trait for an enum of
asset paths, render it with `Icon::named`, and hand it to any icon
builder (`impl Into<Icon>`) alongside the built-in roles
### core (`crates/core`) ### core (`crates/core`)
+33 -51
View File
@@ -7,7 +7,7 @@ use gpui::{
SharedString, Styled as _, UniformListScrollHandle, WeakEntity, Window, WindowBounds, SharedString, Styled as _, UniformListScrollHandle, WeakEntity, Window, WindowBounds,
WindowOptions, actions, div, list, px, size, uniform_list, WindowOptions, actions, div, list, px, size, uniform_list,
}; };
use gpui_native_core::alert::{Alert, AlertSeverity, Alerts, AlertsView}; use gpui_native_core::alert::{Alert, AlertSeverity};
use gpui_native_core::avatar::Avatar; use gpui_native_core::avatar::Avatar;
use gpui_native_core::badge::{Badge, BadgeVariant}; use gpui_native_core::badge::{Badge, BadgeVariant};
use gpui_native_core::button::{Button, ButtonVariant}; use gpui_native_core::button::{Button, ButtonVariant};
@@ -36,7 +36,7 @@ use gpui_native_core::slider::Slider;
use gpui_native_core::spinner::{Spinner, SpinnerVariant}; use gpui_native_core::spinner::{Spinner, SpinnerVariant};
use gpui_native_core::status_dot::{StatusDot, StatusDotVariant}; use gpui_native_core::status_dot::{StatusDot, StatusDotVariant};
use gpui_native_core::switch::Switch; use gpui_native_core::switch::Switch;
use gpui_native_core::toast::{Toast, ToastAction, Toasts, ToastsView}; use gpui_native_core::toast::{Toast, ToastAction};
use gpui_native_core::toggle::Toggle; use gpui_native_core::toggle::Toggle;
use gpui_native_core::tooltip::Tooltip; use gpui_native_core::tooltip::Tooltip;
use gpui_native_data::list::{List, ListItem}; use gpui_native_data::list::{List, ListItem};
@@ -217,8 +217,6 @@ fn gallery_commands() -> Vec<Command> {
struct Gallery { struct Gallery {
layout_state: Entity<LayoutState>, layout_state: Entity<LayoutState>,
dock_state: Entity<DockState>, dock_state: Entity<DockState>,
alerts: Entity<Alerts>,
toasts: Entity<Toasts>,
palette: Option<Entity<CommandPaletteState>>, palette: Option<Entity<CommandPaletteState>>,
palette_runs: Vec<SharedString>, palette_runs: Vec<SharedString>,
dialog_open: bool, dialog_open: bool,
@@ -395,8 +393,6 @@ impl Gallery {
fn register_panels( fn register_panels(
dock: &mut DockState, dock: &mut DockState,
layout_state: Entity<LayoutState>, layout_state: Entity<LayoutState>,
alerts: Entity<Alerts>,
toasts: Entity<Toasts>,
palette: Entity<CommandPaletteState>, palette: Entity<CommandPaletteState>,
gallery: WeakEntity<Self>, gallery: WeakEntity<Self>,
cx: &mut Context<DockState>, cx: &mut Context<DockState>,
@@ -513,20 +509,20 @@ impl Gallery {
.into() .into()
})); }));
dock.register_panel(PanelDescriptor::new("alert", "Alert", { dock.register_panel(PanelDescriptor::new("alert", "Alert", {
let alerts = alerts.clone(); let layout_state = layout_state.clone();
move |_, cx| { move |_, cx| {
cx.new(|_| AlertDemo { cx.new(|_| AlertDemo {
alerts: alerts.clone(), layout_state: layout_state.clone(),
dismissed: false, dismissed: false,
}) })
.into() .into()
} }
})); }));
dock.register_panel(PanelDescriptor::new("toast", "Toast", { dock.register_panel(PanelDescriptor::new("toast", "Toast", {
let toasts = toasts.clone(); let layout_state = layout_state.clone();
move |_, cx| { move |_, cx| {
cx.new(|_| ToastDemo { cx.new(|_| ToastDemo {
toasts: toasts.clone(), layout_state: layout_state.clone(),
}) })
.into() .into()
} }
@@ -649,8 +645,6 @@ impl Render for Gallery {
.child(dialog) .child(dialog)
.child(alert_dialog) .child(alert_dialog)
.when_some(self.palette.clone(), |root, palette| root.child(palette)) .when_some(self.palette.clone(), |root, palette| root.child(palette))
.child(AlertsView::new("gallery-alerts", self.alerts.clone()))
.child(ToastsView::new("gallery-toasts", self.toasts.clone()))
} }
} }
@@ -2287,16 +2281,16 @@ impl Render for TabsDemo {
} }
struct AlertDemo { struct AlertDemo {
alerts: Entity<Alerts>, layout_state: Entity<LayoutState>,
dismissed: bool, dismissed: bool,
} }
impl Render for AlertDemo { impl Render for AlertDemo {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let view = cx.entity(); let view = cx.entity();
let alerts_info = self.alerts.clone(); let alerts_info = self.layout_state.clone();
let alerts_warning = self.alerts.clone(); let alerts_warning = self.layout_state.clone();
let alerts_clear = self.alerts.clone(); let alerts_clear = self.layout_state.clone();
let colors = cx.theme().colors; let colors = cx.theme().colors;
ScrollArea::new("alert-demo").size_full().p_4().child( ScrollArea::new("alert-demo").size_full().p_4().child(
@@ -2339,8 +2333,8 @@ impl Render for AlertDemo {
.label("Push info") .label("Push info")
.variant(ButtonVariant::Secondary) .variant(ButtonVariant::Secondary)
.on_click(move |_, _, cx| { .on_click(move |_, _, cx| {
alerts_info.update(cx, |alerts, cx| { alerts_info.update(cx, |state, cx| {
alerts.push( state.push_alert(
Alert::new("alert-demo-pushed-info") Alert::new("alert-demo-pushed-info")
.title("Backed up") .title("Backed up")
.message("Everything is in sync.") .message("Everything is in sync.")
@@ -2355,8 +2349,8 @@ impl Render for AlertDemo {
.label("Push warning") .label("Push warning")
.variant(ButtonVariant::Secondary) .variant(ButtonVariant::Secondary)
.on_click(move |_, _, cx| { .on_click(move |_, _, cx| {
alerts_warning.update(cx, |alerts, cx| { alerts_warning.update(cx, |state, cx| {
alerts.push( state.push_alert(
Alert::new("alert-demo-pushed-warning") Alert::new("alert-demo-pushed-warning")
.title("Storage almost full") .title("Storage almost full")
.message("1.2 GB remaining on this device.") .message("1.2 GB remaining on this device.")
@@ -2371,7 +2365,7 @@ impl Render for AlertDemo {
.label("Clear all") .label("Clear all")
.variant(ButtonVariant::Secondary) .variant(ButtonVariant::Secondary)
.on_click(move |_, _, cx| { .on_click(move |_, _, cx| {
alerts_clear.update(cx, |alerts, cx| alerts.clear(cx)); alerts_clear.update(cx, |state, cx| state.clear_alerts(cx));
}), }),
), ),
)), )),
@@ -2380,14 +2374,14 @@ impl Render for AlertDemo {
} }
struct ToastDemo { struct ToastDemo {
toasts: Entity<Toasts>, layout_state: Entity<LayoutState>,
} }
impl Render for ToastDemo { impl Render for ToastDemo {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let toasts_undo = self.toasts.clone(); let toasts_undo = self.layout_state.clone();
let toasts_saved = self.toasts.clone(); let toasts_saved = self.layout_state.clone();
let toasts_connection = self.toasts.clone(); let toasts_connection = self.layout_state.clone();
let colors = cx.theme().colors; let colors = cx.theme().colors;
ScrollArea::new("toast-demo") ScrollArea::new("toast-demo")
@@ -2404,8 +2398,8 @@ impl Render for ToastDemo {
.label("Deleted (with undo)") .label("Deleted (with undo)")
.variant(ButtonVariant::Secondary) .variant(ButtonVariant::Secondary)
.on_click(move |_, _, cx| { .on_click(move |_, _, cx| {
toasts_undo.update(cx, |toasts, cx| { toasts_undo.update(cx, |state, cx| {
toasts.push( state.push_toast(
Toast::new("Draft deleted").action(ToastAction::new( Toast::new("Draft deleted").action(ToastAction::new(
"Undo", "Undo",
|cx| { |cx| {
@@ -2423,8 +2417,8 @@ impl Render for ToastDemo {
.label("Saved (with icon)") .label("Saved (with icon)")
.variant(ButtonVariant::Secondary) .variant(ButtonVariant::Secondary)
.on_click(move |_, _, cx| { .on_click(move |_, _, cx| {
toasts_saved.update(cx, |toasts, cx| { toasts_saved.update(cx, |state, cx| {
toasts.push( state.push_toast(
Toast::new("Workspace saved").icon(IconRole::Check), Toast::new("Workspace saved").icon(IconRole::Check),
cx, cx,
); );
@@ -2436,8 +2430,8 @@ impl Render for ToastDemo {
.label("Connection lost (sticky)") .label("Connection lost (sticky)")
.variant(ButtonVariant::Secondary) .variant(ButtonVariant::Secondary)
.on_click(move |_, _, cx| { .on_click(move |_, _, cx| {
toasts_connection.update(cx, |toasts, cx| { toasts_connection.update(cx, |state, cx| {
toasts.push( state.push_toast(
Toast::new("Connection lost") Toast::new("Connection lost")
.icon(IconRole::Warning) .icon(IconRole::Warning)
.duration(Duration::ZERO), .duration(Duration::ZERO),
@@ -4025,24 +4019,14 @@ fn main() {
cx.refresh_windows(); cx.refresh_windows();
}) })
.detach(); .detach();
let toasts = cx.new(|_| Toasts::default()); let gallery = cx.new(|_| Gallery {
let alerts = cx.new(|_| Alerts::default()); layout_state: layout_state.clone(),
let gallery = cx.new(|cx| { dock_state: dock_state.clone(),
// Toast pushes and dismissals repaint the stack. palette: None,
cx.observe(&toasts, |_, _, cx| cx.notify()).detach(); palette_runs: Vec::new(),
// Alert pushes and dismissals repaint it. dialog_open: false,
cx.observe(&alerts, |_, _, cx| cx.notify()).detach(); alert_open: false,
Gallery { sidebar_transparent: true,
layout_state: layout_state.clone(),
dock_state: dock_state.clone(),
alerts: alerts.clone(),
toasts: toasts.clone(),
palette: None,
palette_runs: Vec::new(),
dialog_open: false,
alert_open: false,
sidebar_transparent: true,
}
}); });
let palette = cx.new(|cx| { let palette = cx.new(|cx| {
let matcher = |query: &str, commands: &[Command]| { let matcher = |query: &str, commands: &[Command]| {
@@ -4076,8 +4060,6 @@ fn main() {
Gallery::register_panels( Gallery::register_panels(
dock, dock,
layout_state.clone(), layout_state.clone(),
alerts.clone(),
toasts.clone(),
palette.clone(), palette.clone(),
gallery.downgrade(), gallery.downgrade(),
cx, cx,