From 12a0e6db088504009d2810d402727233f86c3643 Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Tue, 10 Mar 2026 07:12:55 +0700 Subject: [PATCH] refactor notification comp --- crates/coop/src/workspace.rs | 8 +- crates/device/src/lib.rs | 11 +- crates/relay_auth/src/lib.rs | 18 +- crates/{ui => theme}/src/geometry.rs | 2 +- crates/theme/src/lib.rs | 8 + crates/theme/src/notification.rs | 31 +++ crates/ui/src/anchored.rs | 7 +- crates/ui/src/dock_area/stack_panel.rs | 8 +- crates/ui/src/dock_area/tab_panel.rs | 14 +- crates/ui/src/lib.rs | 4 +- crates/ui/src/menu/popup_menu.rs | 14 +- crates/ui/src/notification.rs | 268 ++++++++++++++--------- crates/ui/src/popover.rs | 11 +- crates/ui/src/resizable/panel.rs | 11 +- crates/ui/src/resizable/resize_handle.rs | 9 +- crates/ui/src/root.rs | 38 +++- crates/ui/src/scroll/scrollable_mask.rs | 9 +- crates/ui/src/scroll/scrollbar.rs | 4 +- crates/ui/src/switch.rs | 10 +- crates/ui/src/window_ext.rs | 39 ++-- 20 files changed, 331 insertions(+), 193 deletions(-) rename crates/{ui => theme}/src/geometry.rs (99%) create mode 100644 crates/theme/src/notification.rs diff --git a/crates/coop/src/workspace.rs b/crates/coop/src/workspace.rs index 1a38572..0731250 100644 --- a/crates/coop/src/workspace.rs +++ b/crates/coop/src/workspace.rs @@ -22,7 +22,7 @@ use ui::dock_area::dock::DockPlacement; use ui::dock_area::panel::PanelView; use ui::dock_area::{ClosePanel, DockArea, DockItem}; use ui::menu::{DropdownMenu, PopupMenuItem}; -use ui::notification::Notification; +use ui::notification::{Notification, NotificationKind}; use ui::{Disableable, IconName, Root, Sizable, WindowExtension, h_flex, v_flex}; use crate::dialogs::{accounts, settings}; @@ -111,6 +111,7 @@ impl Workspace { let note = Notification::new() .message("Connected to the bootstrap relay") .title("Relays") + .with_kind(NotificationKind::Success) .icon(IconName::Relay); window.push_notification(note, cx); @@ -122,6 +123,7 @@ impl Workspace { let note = Notification::new() .message("Connected to user's relay list") .title("Relays") + .with_kind(NotificationKind::Success) .icon(IconName::Relay); window.push_notification(note, cx); @@ -489,14 +491,14 @@ impl Workspace { .autohide(false) .icon(IconName::Relay) .title("Gossip Relays are required") - .content(move |_window, cx| { + .content(move |_this, _window, cx| { v_flex() .text_sm() .text_color(cx.theme().text_muted) .child(SharedString::from(BODY)) .into_any_element() }) - .action(move |_window, _cx| { + .action(move |_this, _window, _cx| { let entity = entity.clone(); let public_key = public_key.to_owned(); diff --git a/crates/device/src/lib.rs b/crates/device/src/lib.rs index 4cf0014..a35dd3e 100644 --- a/crates/device/src/lib.rs +++ b/crates/device/src/lib.rs @@ -591,7 +591,7 @@ impl DeviceRegistry { match task.await { Ok(_) => { cx.update(|window, cx| { - window.clear_notification(id, cx); + window.clear_notification_by_id::(id, cx); }) .ok(); } @@ -627,13 +627,14 @@ impl DeviceRegistry { let entity = cx.entity().downgrade(); let loading = Rc::new(Cell::new(false)); + let key = SharedString::from(event.id.to_hex()); Notification::new() - .custom_id(SharedString::from(event.id.to_hex())) + .type_id::(key) .autohide(false) .icon(IconName::UserKey) .title(SharedString::from("New request")) - .content(move |_window, cx| { + .content(move |_this, _window, cx| { v_flex() .gap_2() .text_sm() @@ -689,7 +690,7 @@ impl DeviceRegistry { ) .into_any_element() }) - .action(move |_window, _cx| { + .action(move |_this, _window, _cx| { let view = entity.clone(); let event = event.clone(); @@ -715,6 +716,8 @@ impl DeviceRegistry { } } +struct DeviceNotification; + /// Verify the author of an event async fn verify_author(client: &Client, event: &Event) -> bool { if let Some(signer) = client.signer() { diff --git a/crates/relay_auth/src/lib.rs b/crates/relay_auth/src/lib.rs index 7bf7617..cc0276d 100644 --- a/crates/relay_auth/src/lib.rs +++ b/crates/relay_auth/src/lib.rs @@ -260,7 +260,7 @@ impl RelayAuth { fn response(&self, req: &Arc, window: &Window, cx: &Context) { let settings = AppSettings::global(cx); let req = req.clone(); - let challenge = req.challenge().to_string(); + let challenge = SharedString::from(req.challenge().to_string()); // Create a task for authentication let task = self.auth(&req, cx); @@ -270,7 +270,7 @@ impl RelayAuth { let url = req.url(); this.update_in(cx, |this, window, cx| { - window.clear_notification(challenge, cx); + window.clear_notification_by_id::(challenge, cx); match result { Ok(_) => { @@ -282,7 +282,10 @@ impl RelayAuth { this.add_trusted_relay(url, cx); }); - window.push_notification(format!("{} has been authenticated", url), cx); + window.push_notification( + Notification::success(format!("{} has been authenticated", url)), + cx, + ); } Err(e) => { window.push_notification(Notification::error(e.to_string()), cx); @@ -310,16 +313,17 @@ impl RelayAuth { /// Build a notification for the authentication request. fn notification(&self, req: &Arc, cx: &Context) -> Notification { let req = req.clone(); + let challenge = SharedString::from(req.challenge.clone()); let url = SharedString::from(req.url().to_string()); let entity = cx.entity().downgrade(); let loading = Rc::new(Cell::new(false)); Notification::new() - .custom_id(SharedString::from(&req.challenge)) + .type_id::(challenge) .autohide(false) .icon(IconName::Warning) .title(SharedString::from("Authentication Required")) - .content(move |_window, cx| { + .content(move |_this, _window, cx| { v_flex() .gap_2() .text_sm() @@ -336,7 +340,7 @@ impl RelayAuth { ) .into_any_element() }) - .action(move |_window, _cx| { + .action(move |_this, _window, _cx| { let view = entity.clone(); let req = req.clone(); @@ -361,3 +365,5 @@ impl RelayAuth { }) } } + +struct AuthNotification; diff --git a/crates/ui/src/geometry.rs b/crates/theme/src/geometry.rs similarity index 99% rename from crates/ui/src/geometry.rs rename to crates/theme/src/geometry.rs index 4f6fbe7..7a5ad6d 100644 --- a/crates/ui/src/geometry.rs +++ b/crates/theme/src/geometry.rs @@ -138,7 +138,7 @@ impl Anchor { } } - pub(crate) fn other_side_corner_along(&self, axis: Axis) -> Anchor { + pub fn other_side_corner_along(&self, axis: Axis) -> Anchor { match axis { Axis::Vertical => match self { Self::TopLeft => Self::BottomLeft, diff --git a/crates/theme/src/lib.rs b/crates/theme/src/lib.rs index 7cc65d8..96c170b 100644 --- a/crates/theme/src/lib.rs +++ b/crates/theme/src/lib.rs @@ -4,6 +4,8 @@ use std::rc::Rc; use gpui::{App, Global, Pixels, SharedString, Window, px}; mod colors; +mod geometry; +mod notification; mod platform_kind; mod registry; mod scale; @@ -11,6 +13,8 @@ mod scrollbar_mode; mod theme; pub use colors::*; +pub use geometry::*; +pub use notification::*; pub use platform_kind::PlatformKind; pub use registry::*; pub use scale::*; @@ -82,6 +86,9 @@ pub struct Theme { /// Show the scrollbar mode, default: scrolling pub scrollbar_mode: ScrollbarMode, + /// Notification settings + pub notification: NotificationSettings, + /// Platform kind pub platform: PlatformKind, } @@ -204,6 +211,7 @@ impl From for Theme { radius_lg: px(10.), shadow: true, scrollbar_mode: ScrollbarMode::default(), + notification: NotificationSettings::default(), mode, colors: *colors, theme: Rc::new(family), diff --git a/crates/theme/src/notification.rs b/crates/theme/src/notification.rs new file mode 100644 index 0000000..59b242e --- /dev/null +++ b/crates/theme/src/notification.rs @@ -0,0 +1,31 @@ +use gpui::{Pixels, px}; +use serde::{Deserialize, Serialize}; + +use crate::{Anchor, Edges, TITLEBAR_HEIGHT}; + +/// The settings for notifications. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NotificationSettings { + /// The placement of the notification, default: [`Anchor::TopRight`] + pub placement: Anchor, + /// The margins of the notification with respect to the window edges. + pub margins: Edges, + /// The maximum number of notifications to show at once, default: 10 + pub max_items: usize, +} + +impl Default for NotificationSettings { + fn default() -> Self { + let offset = px(16.); + Self { + placement: Anchor::TopRight, + margins: Edges { + top: TITLEBAR_HEIGHT + offset, // avoid overlap with title bar + right: offset, + bottom: offset, + left: offset, + }, + max_items: 10, + } + } +} diff --git a/crates/ui/src/anchored.rs b/crates/ui/src/anchored.rs index 0b471b9..e23aec7 100644 --- a/crates/ui/src/anchored.rs +++ b/crates/ui/src/anchored.rs @@ -1,13 +1,12 @@ //! This is a fork of gpui's anchored element that adds support for offsetting //! https://github.com/zed-industries/zed/blob/b06f4088a3565c5e30663106ff79c1ced645d87a/crates/gpui/src/elements/anchored.rs use gpui::{ - point, px, AnyElement, App, Axis, Bounds, Display, Edges, Element, GlobalElementId, Half, + AnyElement, App, Axis, Bounds, Display, Edges, Element, GlobalElementId, Half, InspectorElementId, IntoElement, LayoutId, ParentElement, Pixels, Point, Position, Size, Style, - Window, + Window, point, px, }; use smallvec::SmallVec; - -use crate::Anchor; +use theme::Anchor; /// The state that the anchored element element uses to track its children. pub struct AnchoredState { diff --git a/crates/ui/src/dock_area/stack_panel.rs b/crates/ui/src/dock_area/stack_panel.rs index c2cb7f5..85d20e8 100644 --- a/crates/ui/src/dock_area/stack_panel.rs +++ b/crates/ui/src/dock_area/stack_panel.rs @@ -7,16 +7,16 @@ use gpui::{ Window, }; use smallvec::SmallVec; -use theme::{ActiveTheme, CLIENT_SIDE_DECORATION_ROUNDING}; +use theme::{ActiveTheme, AxisExt as _, CLIENT_SIDE_DECORATION_ROUNDING, Placement}; use super::{DockArea, PanelEvent}; use crate::dock_area::panel::{Panel, PanelView}; use crate::dock_area::tab_panel::TabPanel; +use crate::h_flex; use crate::resizable::{ - resizable_panel, ResizablePanelEvent, ResizablePanelGroup, ResizablePanelState, ResizableState, - PANEL_MIN_SIZE, + PANEL_MIN_SIZE, ResizablePanelEvent, ResizablePanelGroup, ResizablePanelState, ResizableState, + resizable_panel, }; -use crate::{h_flex, AxisExt as _, Placement}; pub struct StackPanel { pub(super) parent: Option>, diff --git a/crates/ui/src/dock_area/tab_panel.rs b/crates/ui/src/dock_area/tab_panel.rs index 252e8e5..7265dfe 100644 --- a/crates/ui/src/dock_area/tab_panel.rs +++ b/crates/ui/src/dock_area/tab_panel.rs @@ -2,12 +2,12 @@ use std::sync::Arc; use gpui::prelude::FluentBuilder; use gpui::{ - div, px, rems, App, AppContext, Context, Corner, DefiniteLength, DismissEvent, DragMoveEvent, - Empty, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement as _, IntoElement, - MouseButton, ParentElement, Pixels, Render, ScrollHandle, SharedString, - StatefulInteractiveElement, Styled, WeakEntity, Window, + App, AppContext, Context, Corner, DefiniteLength, DismissEvent, DragMoveEvent, Empty, Entity, + EventEmitter, FocusHandle, Focusable, InteractiveElement as _, IntoElement, MouseButton, + ParentElement, Pixels, Render, ScrollHandle, SharedString, StatefulInteractiveElement, Styled, + WeakEntity, Window, div, px, rems, }; -use theme::{ActiveTheme, CLIENT_SIDE_DECORATION_ROUNDING, TABBAR_HEIGHT}; +use theme::{ActiveTheme, AxisExt, CLIENT_SIDE_DECORATION_ROUNDING, Placement, TABBAR_HEIGHT}; use crate::button::{Button, ButtonVariants as _}; use crate::dock_area::dock::DockPlacement; @@ -15,9 +15,9 @@ use crate::dock_area::panel::{Panel, PanelView}; use crate::dock_area::stack_panel::StackPanel; use crate::dock_area::{ClosePanel, DockArea, PanelEvent, PanelStyle, ToggleZoom}; use crate::menu::{DropdownMenu, PopupMenu}; -use crate::tab::tab_bar::TabBar; use crate::tab::Tab; -use crate::{h_flex, v_flex, AxisExt, IconName, Placement, Selectable, Sizable, StyledExt}; +use crate::tab::tab_bar::TabBar; +use crate::{IconName, Selectable, Sizable, StyledExt, h_flex, v_flex}; #[derive(Clone)] struct TabState { diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs index 3c9c10b..d047e88 100644 --- a/crates/ui/src/lib.rs +++ b/crates/ui/src/lib.rs @@ -2,11 +2,10 @@ pub use anchored::*; pub use element_ext::ElementExt; pub use event::InteractiveElementExt; pub use focusable::FocusableCycle; -pub use geometry::*; pub use icon::*; pub use index_path::IndexPath; pub use kbd::*; -pub use root::{window_paddings, Root}; +pub use root::{Root, window_paddings}; pub use styled::*; pub use window_ext::*; @@ -39,7 +38,6 @@ mod anchored; mod element_ext; mod event; mod focusable; -mod geometry; mod icon; mod index_path; mod kbd; diff --git a/crates/ui/src/menu/popup_menu.rs b/crates/ui/src/menu/popup_menu.rs index e25f733..4638cda 100644 --- a/crates/ui/src/menu/popup_menu.rs +++ b/crates/ui/src/menu/popup_menu.rs @@ -2,19 +2,19 @@ use std::rc::Rc; use gpui::prelude::FluentBuilder; use gpui::{ - anchored, div, px, rems, Action, AnyElement, App, AppContext, Axis, Bounds, ClickEvent, - Context, Corner, DismissEvent, Edges, Entity, EventEmitter, FocusHandle, Focusable, Half, - InteractiveElement, IntoElement, KeyBinding, MouseDownEvent, OwnedMenuItem, ParentElement, - Pixels, Point, Render, ScrollHandle, SharedString, StatefulInteractiveElement, Styled, - Subscription, WeakEntity, Window, + Action, AnyElement, App, AppContext, Axis, Bounds, ClickEvent, Context, Corner, DismissEvent, + Edges, Entity, EventEmitter, FocusHandle, Focusable, Half, InteractiveElement, IntoElement, + KeyBinding, MouseDownEvent, OwnedMenuItem, ParentElement, Pixels, Point, Render, ScrollHandle, + SharedString, StatefulInteractiveElement, Styled, Subscription, WeakEntity, Window, anchored, + div, px, rems, }; -use theme::ActiveTheme; +use theme::{ActiveTheme, Side}; use crate::actions::{Cancel, Confirm, SelectDown, SelectLeft, SelectRight, SelectUp}; use crate::kbd::Kbd; use crate::menu::menu_item::MenuItemElement; use crate::scroll::ScrollableElement; -use crate::{h_flex, v_flex, ElementExt, Icon, IconName, Side, Sizable as _, Size, StyledExt}; +use crate::{ElementExt, Icon, IconName, Sizable as _, Size, StyledExt, h_flex, v_flex}; const CONTEXT: &str = "PopupMenu"; diff --git a/crates/ui/src/notification.rs b/crates/ui/src/notification.rs index 6b60319..4829d50 100644 --- a/crates/ui/src/notification.rs +++ b/crates/ui/src/notification.rs @@ -1,25 +1,23 @@ use std::any::TypeId; -use std::borrow::Cow; use std::collections::{HashMap, VecDeque}; use std::rc::Rc; use std::time::Duration; use gpui::prelude::FluentBuilder; use gpui::{ - div, px, Animation, AnimationExt, AnyElement, App, AppContext, ClickEvent, Context, - DismissEvent, ElementId, Entity, EventEmitter, InteractiveElement as _, IntoElement, - ParentElement as _, Render, SharedString, StatefulInteractiveElement, StyleRefinement, Styled, - Subscription, Window, + Animation, AnimationExt, AnyElement, App, AppContext, ClickEvent, Context, DismissEvent, + ElementId, Entity, EventEmitter, InteractiveElement as _, IntoElement, ParentElement as _, + Render, SharedString, StatefulInteractiveElement, StyleRefinement, Styled, Subscription, + Window, div, px, }; -use smol::Timer; -use theme::ActiveTheme; +use theme::{ActiveTheme, Anchor}; use crate::animation::cubic_bezier; use crate::button::{Button, ButtonVariants as _}; -use crate::{h_flex, v_flex, Icon, IconName, Sizable as _, StyledExt}; +use crate::{Icon, IconName, Sizable as _, StyledExt, h_flex, v_flex}; #[derive(Debug, Clone, Copy, Default)] -pub enum NotificationType { +pub enum NotificationKind { #[default] Info, Success, @@ -27,13 +25,13 @@ pub enum NotificationType { Error, } -impl NotificationType { +impl NotificationKind { fn icon(&self, cx: &App) -> Icon { match self { - Self::Info => Icon::new(IconName::Info).text_color(cx.theme().element_foreground), - Self::Success => Icon::new(IconName::Info).text_color(cx.theme().secondary_foreground), - Self::Warning => Icon::new(IconName::Warning).text_color(cx.theme().warning_foreground), - Self::Error => Icon::new(IconName::Warning).text_color(cx.theme().danger_foreground), + Self::Info => Icon::new(IconName::Info).text_color(cx.theme().icon), + Self::Success => Icon::new(IconName::CheckCircle).text_color(cx.theme().icon_accent), + Self::Warning => Icon::new(IconName::Warning).text_color(cx.theme().warning_active), + Self::Error => Icon::new(IconName::CloseCircle).text_color(cx.theme().danger_active), } } } @@ -56,6 +54,7 @@ impl From<(TypeId, ElementId)> for NotificationId { } } +#[allow(clippy::type_complexity)] /// A notification element. pub struct Notification { /// The id is used make the notification unique. @@ -64,16 +63,13 @@ pub struct Notification { /// None means the notification will be added to the end of the list. id: NotificationId, style: StyleRefinement, - type_: Option, + kind: Option, title: Option, message: Option, icon: Option, autohide: bool, - #[allow(clippy::type_complexity)] - action_builder: Option) -> Button>>, - #[allow(clippy::type_complexity)] - content_builder: Option) -> AnyElement>>, - #[allow(clippy::type_complexity)] + action_builder: Option) -> Button>>, + content_builder: Option) -> AnyElement>>, on_click: Option>, closing: bool, } @@ -84,12 +80,6 @@ impl From for Notification { } } -impl From> for Notification { - fn from(s: Cow<'static, str>) -> Self { - Self::new().message(s) - } -} - impl From for Notification { fn from(s: SharedString) -> Self { Self::new().message(s) @@ -102,24 +92,24 @@ impl From<&'static str> for Notification { } } -impl From<(NotificationType, &'static str)> for Notification { - fn from((type_, content): (NotificationType, &'static str)) -> Self { - Self::new().message(content).with_type(type_) +impl From<(NotificationKind, &'static str)> for Notification { + fn from((kind, content): (NotificationKind, &'static str)) -> Self { + Self::new().message(content).with_kind(kind) } } -impl From<(NotificationType, SharedString)> for Notification { - fn from((type_, content): (NotificationType, SharedString)) -> Self { - Self::new().message(content).with_type(type_) +impl From<(NotificationKind, SharedString)> for Notification { + fn from((kind, content): (NotificationKind, SharedString)) -> Self { + Self::new().message(content).with_kind(kind) } } struct DefaultIdType; impl Notification { - /// Create a new notification with the given content. + /// Create a new notification. /// - /// default width is 320px. + /// The default id is a random UUID. pub fn new() -> Self { let id: SharedString = uuid::Uuid::new_v4().to_string().into(); let id = (TypeId::of::(), id.into()); @@ -129,7 +119,7 @@ impl Notification { style: StyleRefinement::default(), title: None, message: None, - type_: None, + kind: None, icon: None, autohide: true, action_builder: None, @@ -139,33 +129,38 @@ impl Notification { } } + /// Set the message of the notification, default is None. pub fn message(mut self, message: impl Into) -> Self { self.message = Some(message.into()); self } + /// Create an info notification with the given message. pub fn info(message: impl Into) -> Self { Self::new() .message(message) - .with_type(NotificationType::Info) + .with_kind(NotificationKind::Info) } + /// Create a success notification with the given message. pub fn success(message: impl Into) -> Self { Self::new() .message(message) - .with_type(NotificationType::Success) + .with_kind(NotificationKind::Success) } + /// Create a warning notification with the given message. pub fn warning(message: impl Into) -> Self { Self::new() .message(message) - .with_type(NotificationType::Warning) + .with_kind(NotificationKind::Warning) } + /// Create an error notification with the given message. pub fn error(message: impl Into) -> Self { Self::new() .message(message) - .with_type(NotificationType::Error) + .with_kind(NotificationKind::Error) } /// Set the type for unique identification of the notification. @@ -180,8 +175,8 @@ impl Notification { } /// Set the type and id of the notification, used to uniquely identify the notification. - pub fn custom_id(mut self, key: impl Into) -> Self { - self.id = (TypeId::of::(), key.into()).into(); + pub fn type_id(mut self, key: impl Into) -> Self { + self.id = (TypeId::of::(), key.into()).into(); self } @@ -202,8 +197,8 @@ impl Notification { } /// Set the type of the notification, default is NotificationType::Info. - pub fn with_type(mut self, type_: NotificationType) -> Self { - self.type_ = Some(type_); + pub fn with_kind(mut self, kind: NotificationKind) -> Self { + self.kind = Some(kind); self } @@ -223,22 +218,31 @@ impl Notification { } /// Set the action button of the notification. + /// + /// When an action is set, the notification will not autohide. pub fn action(mut self, action: F) -> Self where - F: Fn(&mut Window, &mut Context) -> Button + 'static, + F: Fn(&mut Self, &mut Window, &mut Context) -> Button + 'static, { self.action_builder = Some(Rc::new(action)); + self.autohide = false; self } /// Dismiss the notification. pub fn dismiss(&mut self, _: &mut Window, cx: &mut Context) { + if self.closing { + return; + } self.closing = true; cx.notify(); // Dismiss the notification after 0.15s to show the animation. cx.spawn(async move |view, cx| { - Timer::after(Duration::from_secs_f32(0.15)).await; + cx.background_executor() + .timer(Duration::from_secs_f32(0.15)) + .await; + cx.update(|cx| { if let Some(view) = view.upgrade() { view.update(cx, |view, cx| { @@ -248,13 +252,13 @@ impl Notification { } }) }) - .detach() + .detach(); } /// Set the content of the notification. pub fn content( mut self, - content: impl Fn(&mut Window, &mut Context) -> AnyElement + 'static, + content: impl Fn(&mut Self, &mut Window, &mut Context) -> AnyElement + 'static, ) -> Self { self.content_builder = Some(Rc::new(content)); self @@ -276,52 +280,60 @@ impl Styled for Notification { &mut self.style } } - impl Render for Notification { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let closing = self.closing; - let icon = match self.type_ { + let content = self + .content_builder + .clone() + .map(|builder| builder(self, window, cx)); + + let action = self + .action_builder + .clone() + .map(|builder| builder(self, window, cx).small().mr_3p5()); + + let icon = match self.kind { None => self.icon.clone(), - Some(type_) => Some(type_.icon(cx)), + Some(kind) => Some(kind.icon(cx)), }; + let has_icon = icon.is_some(); + let closing = self.closing; + let placement = cx.theme().notification.placement; + h_flex() .id("notification") - .refine_style(&self.style) .group("") .occlude() .relative() - .w_96() + .w_112() .border_1() .border_color(cx.theme().border) .bg(cx.theme().surface_background) .rounded(cx.theme().radius_lg) .when(cx.theme().shadow, |this| this.shadow_md()) .p_2() - .gap_3() + .gap_2() .justify_start() .items_start() + .refine_style(&self.style) .when_some(icon, |this, icon| { this.child(div().flex_shrink_0().pt_1().child(icon)) }) .child( v_flex() .flex_1() - .gap_1() .overflow_hidden() + .when(has_icon, |this| this.pl_1()) .when_some(self.title.clone(), |this, title| { this.child(div().text_sm().font_semibold().child(title)) }) .when_some(self.message.clone(), |this, message| { this.child(div().text_sm().child(message)) }) - .when_some(self.content_builder.clone(), |this, child_builder| { - this.child(child_builder(window, cx)) - }) - .when_some(self.action_builder.clone(), |this, action_builder| { - this.child(action_builder(window, cx).small().w_full().my_2()) - }), + .when_some(content, |this, content| this.child(content)), ) + .when_some(action, |this, action| this.child(action)) .child( div() .absolute() @@ -334,9 +346,7 @@ impl Render for Notification { .icon(IconName::Close) .ghost() .xsmall() - .on_click(cx.listener(|this, _, window, cx| { - this.dismiss(window, cx); - })), + .on_click(cx.listener(|this, _, window, cx| this.dismiss(window, cx))), ), ) .when_some(self.on_click.clone(), |this, on_click| { @@ -345,20 +355,46 @@ impl Render for Notification { on_click(event, window, cx); })) }) + .on_aux_click(cx.listener(move |view, event: &ClickEvent, window, cx| { + if event.is_middle_click() { + view.dismiss(window, cx); + } + })) .with_animation( ElementId::NamedInteger("slide-down".into(), closing as u64), Animation::new(Duration::from_secs_f64(0.25)) .with_easing(cubic_bezier(0.4, 0., 0.2, 1.)), move |this, delta| { if closing { - let x_offset = px(0.) + delta * px(45.); let opacity = 1. - delta; - this.left(px(0.) + x_offset) + let that = this .shadow_none() .opacity(opacity) - .when(opacity < 0.85, |this| this.shadow_none()) + .when(opacity < 0.85, |this| this.shadow_none()); + match placement { + Anchor::TopRight | Anchor::BottomRight => { + let x_offset = px(0.) + delta * px(45.); + that.left(px(0.) + x_offset) + } + Anchor::TopLeft | Anchor::BottomLeft => { + let x_offset = px(0.) - delta * px(45.); + that.left(px(0.) + x_offset) + } + Anchor::TopCenter => { + let y_offset = px(0.) - delta * px(45.); + that.top(px(0.) + y_offset) + } + Anchor::BottomCenter => { + let y_offset = px(0.) + delta * px(45.); + that.top(px(0.) + y_offset) + } + } } else { - let y_offset = px(-45.) + delta * px(45.); + let y_offset = match placement { + placement if placement.is_top() => px(-45.) + delta * px(45.), + placement if placement.is_bottom() => px(45.) - delta * px(45.), + _ => px(0.), + }; let opacity = delta; this.top(px(0.) + y_offset) .opacity(opacity) @@ -373,7 +409,11 @@ impl Render for Notification { pub struct NotificationList { /// Notifications that will be auto hidden. pub(crate) notifications: VecDeque>, + + /// Whether the notification list is expanded. expanded: bool, + + /// Subscriptions _subscriptions: HashMap, } @@ -386,10 +426,12 @@ impl NotificationList { } } - pub fn push(&mut self, notification: T, window: &mut Window, cx: &mut Context) - where - T: Into, - { + pub fn push( + &mut self, + notification: impl Into, + window: &mut Window, + cx: &mut Context, + ) { let notification = notification.into(); let id = notification.id.clone(); let autohide = notification.autohide; @@ -411,36 +453,35 @@ impl NotificationList { if autohide { // Sleep for 5 seconds to autohide the notification - cx.spawn_in(window, async move |_, cx| { - Timer::after(Duration::from_secs(5)).await; + cx.spawn_in(window, async move |_this, cx| { + cx.background_executor().timer(Duration::from_secs(5)).await; - if let Err(error) = + if let Err(err) = notification.update_in(cx, |note, window, cx| note.dismiss(window, cx)) { - log::error!("Failed to auto hide notification: {error}"); + log::error!("failed to auto hide notification: {:?}", err); } }) .detach(); } + cx.notify(); } - pub fn close(&mut self, key: T, window: &mut Window, cx: &mut Context) - where - T: Into, - { - let id = (TypeId::of::(), key.into()).into(); - + pub(crate) fn close( + &mut self, + id: impl Into, + window: &mut Window, + cx: &mut Context, + ) { + let id: NotificationId = id.into(); if let Some(n) = self.notifications.iter().find(|n| n.read(cx).id == id) { - n.update(cx, |note, cx| { - note.dismiss(window, cx); - }); + n.update(cx, |note, cx| note.dismiss(window, cx)) } - cx.notify(); } - pub fn clear(&mut self, _window: &mut Window, cx: &mut Context) { + pub fn clear(&mut self, _: &mut Window, cx: &mut Context) { self.notifications.clear(); cx.notify(); } @@ -451,25 +492,46 @@ impl NotificationList { } impl Render for NotificationList { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + fn render( + &mut self, + window: &mut gpui::Window, + cx: &mut gpui::Context, + ) -> impl IntoElement { let size = window.viewport_size(); let items = self.notifications.iter().rev().take(10).rev().cloned(); - div() - .id("notification-wrapper") - .absolute() - .top_4() - .right_4() - .child( - v_flex() - .id("notification-list") - .h(size.height - px(8.)) - .gap_3() - .children(items) - .on_hover(cx.listener(|view, hovered, _, cx| { - view.expanded = *hovered; - cx.notify() - })), + let placement = cx.theme().notification.placement; + let margins = &cx.theme().notification.margins; + + v_flex() + .id("notification-list") + .max_h(size.height) + .pt(margins.top) + .pb(margins.bottom) + .gap_3() + .when( + matches!(placement, Anchor::TopRight), + |this| this.pr(margins.right), // ignore left ) + .when( + matches!(placement, Anchor::TopLeft), + |this| this.pl(margins.left), // ignore right + ) + .when( + matches!(placement, Anchor::BottomLeft), + |this| this.flex_col_reverse().pl(margins.left), // ignore right + ) + .when( + matches!(placement, Anchor::BottomRight), + |this| this.flex_col_reverse().pr(margins.right), // ignore left + ) + .when(matches!(placement, Anchor::BottomCenter), |this| { + this.flex_col_reverse() + }) + .on_hover(cx.listener(|view, hovered, _, cx| { + view.expanded = *hovered; + cx.notify() + })) + .children(items) } } diff --git a/crates/ui/src/popover.rs b/crates/ui/src/popover.rs index 9f5d826..33a5776 100644 --- a/crates/ui/src/popover.rs +++ b/crates/ui/src/popover.rs @@ -2,14 +2,15 @@ use std::rc::Rc; use gpui::prelude::FluentBuilder as _; use gpui::{ - deferred, div, px, AnyElement, App, Bounds, Context, Deferred, DismissEvent, Div, ElementId, - EventEmitter, FocusHandle, Focusable, Half, InteractiveElement as _, IntoElement, KeyBinding, - MouseButton, ParentElement, Pixels, Point, Render, RenderOnce, Stateful, StyleRefinement, - Styled, Subscription, Window, + AnyElement, App, Bounds, Context, Deferred, DismissEvent, Div, ElementId, EventEmitter, + FocusHandle, Focusable, Half, InteractiveElement as _, IntoElement, KeyBinding, MouseButton, + ParentElement, Pixels, Point, Render, RenderOnce, Stateful, StyleRefinement, Styled, + Subscription, Window, deferred, div, px, }; +use theme::Anchor; use crate::actions::Cancel; -use crate::{anchored, v_flex, Anchor, ElementExt, Selectable, StyledExt as _}; +use crate::{ElementExt, Selectable, StyledExt as _, anchored, v_flex}; const CONTEXT: &str = "Popover"; diff --git a/crates/ui/src/resizable/panel.rs b/crates/ui/src/resizable/panel.rs index c6a4c2c..3e50a63 100644 --- a/crates/ui/src/resizable/panel.rs +++ b/crates/ui/src/resizable/panel.rs @@ -3,14 +3,15 @@ use std::rc::Rc; use gpui::prelude::FluentBuilder; use gpui::{ - div, Along, AnyElement, App, AppContext, Axis, Bounds, Context, Element, ElementId, Empty, - Entity, EventEmitter, InteractiveElement as _, IntoElement, IsZero as _, MouseMoveEvent, - MouseUpEvent, ParentElement, Pixels, Render, RenderOnce, Style, Styled, Window, + Along, AnyElement, App, AppContext, Axis, Bounds, Context, Element, ElementId, Empty, Entity, + EventEmitter, InteractiveElement as _, IntoElement, IsZero as _, MouseMoveEvent, MouseUpEvent, + ParentElement, Pixels, Render, RenderOnce, Style, Styled, Window, div, }; +use theme::AxisExt; -use super::{resizable_panel, resize_handle, ResizableState}; +use super::{ResizableState, resizable_panel, resize_handle}; use crate::resizable::PANEL_MIN_SIZE; -use crate::{h_flex, v_flex, AxisExt, ElementExt}; +use crate::{ElementExt, h_flex, v_flex}; pub enum ResizablePanelEvent { Resized, diff --git a/crates/ui/src/resizable/resize_handle.rs b/crates/ui/src/resizable/resize_handle.rs index b84354d..8bc1ca9 100644 --- a/crates/ui/src/resizable/resize_handle.rs +++ b/crates/ui/src/resizable/resize_handle.rs @@ -3,14 +3,13 @@ use std::rc::Rc; use gpui::prelude::FluentBuilder as _; use gpui::{ - div, px, AnyElement, App, Axis, Element, ElementId, Entity, GlobalElementId, - InteractiveElement, IntoElement, MouseDownEvent, MouseUpEvent, ParentElement as _, Pixels, - Point, Render, StatefulInteractiveElement, Styled as _, Window, + AnyElement, App, Axis, Element, ElementId, Entity, GlobalElementId, InteractiveElement, + IntoElement, MouseDownEvent, MouseUpEvent, ParentElement as _, Pixels, Point, Render, + StatefulInteractiveElement, Styled as _, Window, div, px, }; -use theme::ActiveTheme; +use theme::{ActiveTheme, AxisExt}; use crate::dock_area::dock::DockPlacement; -use crate::AxisExt; pub(crate) const HANDLE_PADDING: Pixels = px(4.); pub(crate) const HANDLE_SIZE: Pixels = px(1.); diff --git a/crates/ui/src/root.rs b/crates/ui/src/root.rs index 9db3700..e6189ae 100644 --- a/crates/ui/src/root.rs +++ b/crates/ui/src/root.rs @@ -1,11 +1,12 @@ +use std::any::TypeId; use std::rc::Rc; use gpui::prelude::FluentBuilder; use gpui::{ - AnyView, App, AppContext, Bounds, Context, CursorStyle, Decorations, Edges, Entity, + AnyView, App, AppContext, Bounds, Context, CursorStyle, Decorations, Edges, ElementId, Entity, FocusHandle, HitboxBehavior, Hsla, InteractiveElement, IntoElement, MouseButton, - ParentElement as _, Pixels, Point, Render, ResizeEdge, SharedString, Size, Styled, Tiling, - WeakFocusHandle, Window, canvas, div, point, px, size, + ParentElement as _, Pixels, Point, Render, ResizeEdge, Size, Styled, Tiling, WeakFocusHandle, + Window, canvas, div, point, px, size, }; use theme::{ ActiveTheme, CLIENT_SIDE_DECORATION_BORDER, CLIENT_SIDE_DECORATION_ROUNDING, @@ -213,13 +214,30 @@ impl Root { cx.notify(); } - /// Clear a notification by its ID. - pub fn clear_notification(&mut self, id: T, window: &mut Window, cx: &mut Context) - where - T: Into, - { - self.notification - .update(cx, |view, cx| view.close(id.into(), window, cx)); + /// Clear a notification by its type. + pub fn clear_notification( + &mut self, + window: &mut Window, + cx: &mut Context<'_, Root>, + ) { + self.notification.update(cx, |view, cx| { + let id = TypeId::of::(); + view.close(id, window, cx); + }); + cx.notify(); + } + + /// Clear a notification by its type. + pub fn clear_notification_by_id( + &mut self, + key: impl Into, + window: &mut Window, + cx: &mut Context<'_, Root>, + ) { + self.notification.update(cx, |view, cx| { + let id = (TypeId::of::(), key.into()); + view.close(id, window, cx); + }); cx.notify(); } diff --git a/crates/ui/src/scroll/scrollable_mask.rs b/crates/ui/src/scroll/scrollable_mask.rs index f2e5f93..170f309 100644 --- a/crates/ui/src/scroll/scrollable_mask.rs +++ b/crates/ui/src/scroll/scrollable_mask.rs @@ -1,10 +1,9 @@ use gpui::{ - px, relative, App, Axis, BorderStyle, Bounds, ContentMask, Corners, Edges, Element, ElementId, - EntityId, GlobalElementId, Hitbox, Hsla, IntoElement, IsZero as _, LayoutId, PaintQuad, Pixels, - Point, Position, ScrollHandle, ScrollWheelEvent, Size, Style, Window, + App, Axis, BorderStyle, Bounds, ContentMask, Corners, Edges, Element, ElementId, EntityId, + GlobalElementId, Hitbox, Hsla, IntoElement, IsZero as _, LayoutId, PaintQuad, Pixels, Point, + Position, ScrollHandle, ScrollWheelEvent, Size, Style, Window, px, relative, }; - -use crate::AxisExt; +use theme::AxisExt; /// Make a scrollable mask element to cover the parent view with the mouse wheel event listening. /// diff --git a/crates/ui/src/scroll/scrollbar.rs b/crates/ui/src/scroll/scrollbar.rs index 9b67283..e48aeae 100644 --- a/crates/ui/src/scroll/scrollbar.rs +++ b/crates/ui/src/scroll/scrollbar.rs @@ -11,9 +11,7 @@ use gpui::{ Position, ScrollHandle, ScrollWheelEvent, Size, Style, UniformListScrollHandle, Window, fill, point, px, relative, size, }; -use theme::{ActiveTheme, ScrollbarMode}; - -use crate::AxisExt; +use theme::{ActiveTheme, AxisExt, ScrollbarMode}; /// The width of the scrollbar (THUMB_ACTIVE_INSET * 2 + THUMB_ACTIVE_WIDTH) const WIDTH: Pixels = px(1. * 2. + 8.); diff --git a/crates/ui/src/switch.rs b/crates/ui/src/switch.rs index a13728f..70aa3b6 100644 --- a/crates/ui/src/switch.rs +++ b/crates/ui/src/switch.rs @@ -4,13 +4,13 @@ use std::time::Duration; use gpui::prelude::FluentBuilder as _; use gpui::{ - div, px, white, Animation, AnimationExt as _, AnyElement, App, Element, ElementId, - GlobalElementId, InteractiveElement, IntoElement, LayoutId, ParentElement as _, SharedString, - Styled as _, Window, + Animation, AnimationExt as _, AnyElement, App, Element, ElementId, GlobalElementId, + InteractiveElement, IntoElement, LayoutId, ParentElement as _, SharedString, Styled as _, + Window, div, px, white, }; -use theme::ActiveTheme; +use theme::{ActiveTheme, Side}; -use crate::{Disableable, Side, Sizable, Size}; +use crate::{Disableable, Sizable, Size}; type OnClick = Option>; diff --git a/crates/ui/src/window_ext.rs b/crates/ui/src/window_ext.rs index dd71dd7..d4ed229 100644 --- a/crates/ui/src/window_ext.rs +++ b/crates/ui/src/window_ext.rs @@ -1,11 +1,11 @@ use std::rc::Rc; -use gpui::{App, Entity, SharedString, Window}; +use gpui::{App, ElementId, Entity, Window}; +use crate::Root; use crate::input::InputState; use crate::modal::Modal; use crate::notification::Notification; -use crate::Root; /// Extension trait for [`Window`] to add modal, notification .. functionality. pub trait WindowExtension: Sized { @@ -31,10 +31,15 @@ pub trait WindowExtension: Sized { where T: Into; - /// Clears a notification by its ID. - fn clear_notification(&mut self, id: T, cx: &mut App) - where - T: Into; + /// Clear the unique notification. + fn clear_notification(&mut self, cx: &mut App); + + /// Clear the unique notification with the given id. + fn clear_notification_by_id( + &mut self, + key: impl Into, + cx: &mut App, + ); /// Clear all notifications fn clear_notifications(&mut self, cx: &mut App); @@ -88,13 +93,21 @@ impl WindowExtension for Window { } #[inline] - fn clear_notification(&mut self, id: T, cx: &mut App) - where - T: Into, - { - let id = id.into(); - Root::update(self, cx, move |root, window, cx| { - root.clear_notification(id, window, cx); + fn clear_notification(&mut self, cx: &mut App) { + Root::update(self, cx, |root, window, cx| { + root.clear_notification::(window, cx); + }) + } + + #[inline] + fn clear_notification_by_id( + &mut self, + key: impl Into, + cx: &mut App, + ) { + let key: ElementId = key.into(); + Root::update(self, cx, |root, window, cx| { + root.clear_notification_by_id::(key, window, cx); }) }