refactor notification comp

This commit is contained in:
2026-03-10 07:12:55 +07:00
parent 4b4f6913bb
commit 12a0e6db08
20 changed files with 331 additions and 193 deletions

View File

@@ -22,7 +22,7 @@ use ui::dock_area::dock::DockPlacement;
use ui::dock_area::panel::PanelView; use ui::dock_area::panel::PanelView;
use ui::dock_area::{ClosePanel, DockArea, DockItem}; use ui::dock_area::{ClosePanel, DockArea, DockItem};
use ui::menu::{DropdownMenu, PopupMenuItem}; 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 ui::{Disableable, IconName, Root, Sizable, WindowExtension, h_flex, v_flex};
use crate::dialogs::{accounts, settings}; use crate::dialogs::{accounts, settings};
@@ -111,6 +111,7 @@ impl Workspace {
let note = Notification::new() let note = Notification::new()
.message("Connected to the bootstrap relay") .message("Connected to the bootstrap relay")
.title("Relays") .title("Relays")
.with_kind(NotificationKind::Success)
.icon(IconName::Relay); .icon(IconName::Relay);
window.push_notification(note, cx); window.push_notification(note, cx);
@@ -122,6 +123,7 @@ impl Workspace {
let note = Notification::new() let note = Notification::new()
.message("Connected to user's relay list") .message("Connected to user's relay list")
.title("Relays") .title("Relays")
.with_kind(NotificationKind::Success)
.icon(IconName::Relay); .icon(IconName::Relay);
window.push_notification(note, cx); window.push_notification(note, cx);
@@ -489,14 +491,14 @@ impl Workspace {
.autohide(false) .autohide(false)
.icon(IconName::Relay) .icon(IconName::Relay)
.title("Gossip Relays are required") .title("Gossip Relays are required")
.content(move |_window, cx| { .content(move |_this, _window, cx| {
v_flex() v_flex()
.text_sm() .text_sm()
.text_color(cx.theme().text_muted) .text_color(cx.theme().text_muted)
.child(SharedString::from(BODY)) .child(SharedString::from(BODY))
.into_any_element() .into_any_element()
}) })
.action(move |_window, _cx| { .action(move |_this, _window, _cx| {
let entity = entity.clone(); let entity = entity.clone();
let public_key = public_key.to_owned(); let public_key = public_key.to_owned();

View File

@@ -591,7 +591,7 @@ impl DeviceRegistry {
match task.await { match task.await {
Ok(_) => { Ok(_) => {
cx.update(|window, cx| { cx.update(|window, cx| {
window.clear_notification(id, cx); window.clear_notification_by_id::<DeviceNotification>(id, cx);
}) })
.ok(); .ok();
} }
@@ -627,13 +627,14 @@ impl DeviceRegistry {
let entity = cx.entity().downgrade(); let entity = cx.entity().downgrade();
let loading = Rc::new(Cell::new(false)); let loading = Rc::new(Cell::new(false));
let key = SharedString::from(event.id.to_hex());
Notification::new() Notification::new()
.custom_id(SharedString::from(event.id.to_hex())) .type_id::<DeviceNotification>(key)
.autohide(false) .autohide(false)
.icon(IconName::UserKey) .icon(IconName::UserKey)
.title(SharedString::from("New request")) .title(SharedString::from("New request"))
.content(move |_window, cx| { .content(move |_this, _window, cx| {
v_flex() v_flex()
.gap_2() .gap_2()
.text_sm() .text_sm()
@@ -689,7 +690,7 @@ impl DeviceRegistry {
) )
.into_any_element() .into_any_element()
}) })
.action(move |_window, _cx| { .action(move |_this, _window, _cx| {
let view = entity.clone(); let view = entity.clone();
let event = event.clone(); let event = event.clone();
@@ -715,6 +716,8 @@ impl DeviceRegistry {
} }
} }
struct DeviceNotification;
/// Verify the author of an event /// Verify the author of an event
async fn verify_author(client: &Client, event: &Event) -> bool { async fn verify_author(client: &Client, event: &Event) -> bool {
if let Some(signer) = client.signer() { if let Some(signer) = client.signer() {

View File

@@ -260,7 +260,7 @@ impl RelayAuth {
fn response(&self, req: &Arc<AuthRequest>, window: &Window, cx: &Context<Self>) { fn response(&self, req: &Arc<AuthRequest>, window: &Window, cx: &Context<Self>) {
let settings = AppSettings::global(cx); let settings = AppSettings::global(cx);
let req = req.clone(); let req = req.clone();
let challenge = req.challenge().to_string(); let challenge = SharedString::from(req.challenge().to_string());
// Create a task for authentication // Create a task for authentication
let task = self.auth(&req, cx); let task = self.auth(&req, cx);
@@ -270,7 +270,7 @@ impl RelayAuth {
let url = req.url(); let url = req.url();
this.update_in(cx, |this, window, cx| { this.update_in(cx, |this, window, cx| {
window.clear_notification(challenge, cx); window.clear_notification_by_id::<AuthNotification>(challenge, cx);
match result { match result {
Ok(_) => { Ok(_) => {
@@ -282,7 +282,10 @@ impl RelayAuth {
this.add_trusted_relay(url, cx); 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) => { Err(e) => {
window.push_notification(Notification::error(e.to_string()), cx); window.push_notification(Notification::error(e.to_string()), cx);
@@ -310,16 +313,17 @@ impl RelayAuth {
/// Build a notification for the authentication request. /// Build a notification for the authentication request.
fn notification(&self, req: &Arc<AuthRequest>, cx: &Context<Self>) -> Notification { fn notification(&self, req: &Arc<AuthRequest>, cx: &Context<Self>) -> Notification {
let req = req.clone(); let req = req.clone();
let challenge = SharedString::from(req.challenge.clone());
let url = SharedString::from(req.url().to_string()); let url = SharedString::from(req.url().to_string());
let entity = cx.entity().downgrade(); let entity = cx.entity().downgrade();
let loading = Rc::new(Cell::new(false)); let loading = Rc::new(Cell::new(false));
Notification::new() Notification::new()
.custom_id(SharedString::from(&req.challenge)) .type_id::<AuthNotification>(challenge)
.autohide(false) .autohide(false)
.icon(IconName::Warning) .icon(IconName::Warning)
.title(SharedString::from("Authentication Required")) .title(SharedString::from("Authentication Required"))
.content(move |_window, cx| { .content(move |_this, _window, cx| {
v_flex() v_flex()
.gap_2() .gap_2()
.text_sm() .text_sm()
@@ -336,7 +340,7 @@ impl RelayAuth {
) )
.into_any_element() .into_any_element()
}) })
.action(move |_window, _cx| { .action(move |_this, _window, _cx| {
let view = entity.clone(); let view = entity.clone();
let req = req.clone(); let req = req.clone();
@@ -361,3 +365,5 @@ impl RelayAuth {
}) })
} }
} }
struct AuthNotification;

View File

@@ -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 { match axis {
Axis::Vertical => match self { Axis::Vertical => match self {
Self::TopLeft => Self::BottomLeft, Self::TopLeft => Self::BottomLeft,

View File

@@ -4,6 +4,8 @@ use std::rc::Rc;
use gpui::{App, Global, Pixels, SharedString, Window, px}; use gpui::{App, Global, Pixels, SharedString, Window, px};
mod colors; mod colors;
mod geometry;
mod notification;
mod platform_kind; mod platform_kind;
mod registry; mod registry;
mod scale; mod scale;
@@ -11,6 +13,8 @@ mod scrollbar_mode;
mod theme; mod theme;
pub use colors::*; pub use colors::*;
pub use geometry::*;
pub use notification::*;
pub use platform_kind::PlatformKind; pub use platform_kind::PlatformKind;
pub use registry::*; pub use registry::*;
pub use scale::*; pub use scale::*;
@@ -82,6 +86,9 @@ pub struct Theme {
/// Show the scrollbar mode, default: scrolling /// Show the scrollbar mode, default: scrolling
pub scrollbar_mode: ScrollbarMode, pub scrollbar_mode: ScrollbarMode,
/// Notification settings
pub notification: NotificationSettings,
/// Platform kind /// Platform kind
pub platform: PlatformKind, pub platform: PlatformKind,
} }
@@ -204,6 +211,7 @@ impl From<ThemeFamily> for Theme {
radius_lg: px(10.), radius_lg: px(10.),
shadow: true, shadow: true,
scrollbar_mode: ScrollbarMode::default(), scrollbar_mode: ScrollbarMode::default(),
notification: NotificationSettings::default(),
mode, mode,
colors: *colors, colors: *colors,
theme: Rc::new(family), theme: Rc::new(family),

View File

@@ -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<Pixels>,
/// 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,
}
}
}

View File

@@ -1,13 +1,12 @@
//! This is a fork of gpui's anchored element that adds support for offsetting //! 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 //! https://github.com/zed-industries/zed/blob/b06f4088a3565c5e30663106ff79c1ced645d87a/crates/gpui/src/elements/anchored.rs
use gpui::{ 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, InspectorElementId, IntoElement, LayoutId, ParentElement, Pixels, Point, Position, Size, Style,
Window, Window, point, px,
}; };
use smallvec::SmallVec; use smallvec::SmallVec;
use theme::Anchor;
use crate::Anchor;
/// The state that the anchored element element uses to track its children. /// The state that the anchored element element uses to track its children.
pub struct AnchoredState { pub struct AnchoredState {

View File

@@ -7,16 +7,16 @@ use gpui::{
Window, Window,
}; };
use smallvec::SmallVec; 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 super::{DockArea, PanelEvent};
use crate::dock_area::panel::{Panel, PanelView}; use crate::dock_area::panel::{Panel, PanelView};
use crate::dock_area::tab_panel::TabPanel; use crate::dock_area::tab_panel::TabPanel;
use crate::h_flex;
use crate::resizable::{ use crate::resizable::{
resizable_panel, ResizablePanelEvent, ResizablePanelGroup, ResizablePanelState, ResizableState, PANEL_MIN_SIZE, ResizablePanelEvent, ResizablePanelGroup, ResizablePanelState, ResizableState,
PANEL_MIN_SIZE, resizable_panel,
}; };
use crate::{h_flex, AxisExt as _, Placement};
pub struct StackPanel { pub struct StackPanel {
pub(super) parent: Option<WeakEntity<StackPanel>>, pub(super) parent: Option<WeakEntity<StackPanel>>,

View File

@@ -2,12 +2,12 @@ use std::sync::Arc;
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
div, px, rems, App, AppContext, Context, Corner, DefiniteLength, DismissEvent, DragMoveEvent, App, AppContext, Context, Corner, DefiniteLength, DismissEvent, DragMoveEvent, Empty, Entity,
Empty, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement as _, IntoElement, EventEmitter, FocusHandle, Focusable, InteractiveElement as _, IntoElement, MouseButton,
MouseButton, ParentElement, Pixels, Render, ScrollHandle, SharedString, ParentElement, Pixels, Render, ScrollHandle, SharedString, StatefulInteractiveElement, Styled,
StatefulInteractiveElement, Styled, WeakEntity, Window, 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::button::{Button, ButtonVariants as _};
use crate::dock_area::dock::DockPlacement; 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::stack_panel::StackPanel;
use crate::dock_area::{ClosePanel, DockArea, PanelEvent, PanelStyle, ToggleZoom}; use crate::dock_area::{ClosePanel, DockArea, PanelEvent, PanelStyle, ToggleZoom};
use crate::menu::{DropdownMenu, PopupMenu}; use crate::menu::{DropdownMenu, PopupMenu};
use crate::tab::tab_bar::TabBar;
use crate::tab::Tab; 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)] #[derive(Clone)]
struct TabState { struct TabState {

View File

@@ -2,11 +2,10 @@ pub use anchored::*;
pub use element_ext::ElementExt; pub use element_ext::ElementExt;
pub use event::InteractiveElementExt; pub use event::InteractiveElementExt;
pub use focusable::FocusableCycle; pub use focusable::FocusableCycle;
pub use geometry::*;
pub use icon::*; pub use icon::*;
pub use index_path::IndexPath; pub use index_path::IndexPath;
pub use kbd::*; pub use kbd::*;
pub use root::{window_paddings, Root}; pub use root::{Root, window_paddings};
pub use styled::*; pub use styled::*;
pub use window_ext::*; pub use window_ext::*;
@@ -39,7 +38,6 @@ mod anchored;
mod element_ext; mod element_ext;
mod event; mod event;
mod focusable; mod focusable;
mod geometry;
mod icon; mod icon;
mod index_path; mod index_path;
mod kbd; mod kbd;

View File

@@ -2,19 +2,19 @@ use std::rc::Rc;
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
anchored, div, px, rems, Action, AnyElement, App, AppContext, Axis, Bounds, ClickEvent, Action, AnyElement, App, AppContext, Axis, Bounds, ClickEvent, Context, Corner, DismissEvent,
Context, Corner, DismissEvent, Edges, Entity, EventEmitter, FocusHandle, Focusable, Half, Edges, Entity, EventEmitter, FocusHandle, Focusable, Half, InteractiveElement, IntoElement,
InteractiveElement, IntoElement, KeyBinding, MouseDownEvent, OwnedMenuItem, ParentElement, KeyBinding, MouseDownEvent, OwnedMenuItem, ParentElement, Pixels, Point, Render, ScrollHandle,
Pixels, Point, Render, ScrollHandle, SharedString, StatefulInteractiveElement, Styled, SharedString, StatefulInteractiveElement, Styled, Subscription, WeakEntity, Window, anchored,
Subscription, WeakEntity, Window, div, px, rems,
}; };
use theme::ActiveTheme; use theme::{ActiveTheme, Side};
use crate::actions::{Cancel, Confirm, SelectDown, SelectLeft, SelectRight, SelectUp}; use crate::actions::{Cancel, Confirm, SelectDown, SelectLeft, SelectRight, SelectUp};
use crate::kbd::Kbd; use crate::kbd::Kbd;
use crate::menu::menu_item::MenuItemElement; use crate::menu::menu_item::MenuItemElement;
use crate::scroll::ScrollableElement; 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"; const CONTEXT: &str = "PopupMenu";

View File

@@ -1,25 +1,23 @@
use std::any::TypeId; use std::any::TypeId;
use std::borrow::Cow;
use std::collections::{HashMap, VecDeque}; use std::collections::{HashMap, VecDeque};
use std::rc::Rc; use std::rc::Rc;
use std::time::Duration; use std::time::Duration;
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
div, px, Animation, AnimationExt, AnyElement, App, AppContext, ClickEvent, Context, Animation, AnimationExt, AnyElement, App, AppContext, ClickEvent, Context, DismissEvent,
DismissEvent, ElementId, Entity, EventEmitter, InteractiveElement as _, IntoElement, ElementId, Entity, EventEmitter, InteractiveElement as _, IntoElement, ParentElement as _,
ParentElement as _, Render, SharedString, StatefulInteractiveElement, StyleRefinement, Styled, Render, SharedString, StatefulInteractiveElement, StyleRefinement, Styled, Subscription,
Subscription, Window, Window, div, px,
}; };
use smol::Timer; use theme::{ActiveTheme, Anchor};
use theme::ActiveTheme;
use crate::animation::cubic_bezier; use crate::animation::cubic_bezier;
use crate::button::{Button, ButtonVariants as _}; 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)] #[derive(Debug, Clone, Copy, Default)]
pub enum NotificationType { pub enum NotificationKind {
#[default] #[default]
Info, Info,
Success, Success,
@@ -27,13 +25,13 @@ pub enum NotificationType {
Error, Error,
} }
impl NotificationType { impl NotificationKind {
fn icon(&self, cx: &App) -> Icon { fn icon(&self, cx: &App) -> Icon {
match self { match self {
Self::Info => Icon::new(IconName::Info).text_color(cx.theme().element_foreground), Self::Info => Icon::new(IconName::Info).text_color(cx.theme().icon),
Self::Success => Icon::new(IconName::Info).text_color(cx.theme().secondary_foreground), Self::Success => Icon::new(IconName::CheckCircle).text_color(cx.theme().icon_accent),
Self::Warning => Icon::new(IconName::Warning).text_color(cx.theme().warning_foreground), Self::Warning => Icon::new(IconName::Warning).text_color(cx.theme().warning_active),
Self::Error => Icon::new(IconName::Warning).text_color(cx.theme().danger_foreground), 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. /// A notification element.
pub struct Notification { pub struct Notification {
/// The id is used make the notification unique. /// 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. /// None means the notification will be added to the end of the list.
id: NotificationId, id: NotificationId,
style: StyleRefinement, style: StyleRefinement,
type_: Option<NotificationType>, kind: Option<NotificationKind>,
title: Option<SharedString>, title: Option<SharedString>,
message: Option<SharedString>, message: Option<SharedString>,
icon: Option<Icon>, icon: Option<Icon>,
autohide: bool, autohide: bool,
#[allow(clippy::type_complexity)] action_builder: Option<Rc<dyn Fn(&mut Self, &mut Window, &mut Context<Self>) -> Button>>,
action_builder: Option<Rc<dyn Fn(&mut Window, &mut Context<Self>) -> Button>>, content_builder: Option<Rc<dyn Fn(&mut Self, &mut Window, &mut Context<Self>) -> AnyElement>>,
#[allow(clippy::type_complexity)]
content_builder: Option<Rc<dyn Fn(&mut Window, &mut Context<Self>) -> AnyElement>>,
#[allow(clippy::type_complexity)]
on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>, on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
closing: bool, closing: bool,
} }
@@ -84,12 +80,6 @@ impl From<String> for Notification {
} }
} }
impl From<Cow<'static, str>> for Notification {
fn from(s: Cow<'static, str>) -> Self {
Self::new().message(s)
}
}
impl From<SharedString> for Notification { impl From<SharedString> for Notification {
fn from(s: SharedString) -> Self { fn from(s: SharedString) -> Self {
Self::new().message(s) Self::new().message(s)
@@ -102,24 +92,24 @@ impl From<&'static str> for Notification {
} }
} }
impl From<(NotificationType, &'static str)> for Notification { impl From<(NotificationKind, &'static str)> for Notification {
fn from((type_, content): (NotificationType, &'static str)) -> Self { fn from((kind, content): (NotificationKind, &'static str)) -> Self {
Self::new().message(content).with_type(type_) Self::new().message(content).with_kind(kind)
} }
} }
impl From<(NotificationType, SharedString)> for Notification { impl From<(NotificationKind, SharedString)> for Notification {
fn from((type_, content): (NotificationType, SharedString)) -> Self { fn from((kind, content): (NotificationKind, SharedString)) -> Self {
Self::new().message(content).with_type(type_) Self::new().message(content).with_kind(kind)
} }
} }
struct DefaultIdType; struct DefaultIdType;
impl Notification { 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 { pub fn new() -> Self {
let id: SharedString = uuid::Uuid::new_v4().to_string().into(); let id: SharedString = uuid::Uuid::new_v4().to_string().into();
let id = (TypeId::of::<DefaultIdType>(), id.into()); let id = (TypeId::of::<DefaultIdType>(), id.into());
@@ -129,7 +119,7 @@ impl Notification {
style: StyleRefinement::default(), style: StyleRefinement::default(),
title: None, title: None,
message: None, message: None,
type_: None, kind: None,
icon: None, icon: None,
autohide: true, autohide: true,
action_builder: None, 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<SharedString>) -> Self { pub fn message(mut self, message: impl Into<SharedString>) -> Self {
self.message = Some(message.into()); self.message = Some(message.into());
self self
} }
/// Create an info notification with the given message.
pub fn info(message: impl Into<SharedString>) -> Self { pub fn info(message: impl Into<SharedString>) -> Self {
Self::new() Self::new()
.message(message) .message(message)
.with_type(NotificationType::Info) .with_kind(NotificationKind::Info)
} }
/// Create a success notification with the given message.
pub fn success(message: impl Into<SharedString>) -> Self { pub fn success(message: impl Into<SharedString>) -> Self {
Self::new() Self::new()
.message(message) .message(message)
.with_type(NotificationType::Success) .with_kind(NotificationKind::Success)
} }
/// Create a warning notification with the given message.
pub fn warning(message: impl Into<SharedString>) -> Self { pub fn warning(message: impl Into<SharedString>) -> Self {
Self::new() Self::new()
.message(message) .message(message)
.with_type(NotificationType::Warning) .with_kind(NotificationKind::Warning)
} }
/// Create an error notification with the given message.
pub fn error(message: impl Into<SharedString>) -> Self { pub fn error(message: impl Into<SharedString>) -> Self {
Self::new() Self::new()
.message(message) .message(message)
.with_type(NotificationType::Error) .with_kind(NotificationKind::Error)
} }
/// Set the type for unique identification of the notification. /// 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. /// Set the type and id of the notification, used to uniquely identify the notification.
pub fn custom_id(mut self, key: impl Into<ElementId>) -> Self { pub fn type_id<T: Sized + 'static>(mut self, key: impl Into<ElementId>) -> Self {
self.id = (TypeId::of::<DefaultIdType>(), key.into()).into(); self.id = (TypeId::of::<T>(), key.into()).into();
self self
} }
@@ -202,8 +197,8 @@ impl Notification {
} }
/// Set the type of the notification, default is NotificationType::Info. /// Set the type of the notification, default is NotificationType::Info.
pub fn with_type(mut self, type_: NotificationType) -> Self { pub fn with_kind(mut self, kind: NotificationKind) -> Self {
self.type_ = Some(type_); self.kind = Some(kind);
self self
} }
@@ -223,22 +218,31 @@ impl Notification {
} }
/// Set the action button of the notification. /// Set the action button of the notification.
///
/// When an action is set, the notification will not autohide.
pub fn action<F>(mut self, action: F) -> Self pub fn action<F>(mut self, action: F) -> Self
where where
F: Fn(&mut Window, &mut Context<Self>) -> Button + 'static, F: Fn(&mut Self, &mut Window, &mut Context<Self>) -> Button + 'static,
{ {
self.action_builder = Some(Rc::new(action)); self.action_builder = Some(Rc::new(action));
self.autohide = false;
self self
} }
/// Dismiss the notification. /// Dismiss the notification.
pub fn dismiss(&mut self, _: &mut Window, cx: &mut Context<Self>) { pub fn dismiss(&mut self, _: &mut Window, cx: &mut Context<Self>) {
if self.closing {
return;
}
self.closing = true; self.closing = true;
cx.notify(); cx.notify();
// Dismiss the notification after 0.15s to show the animation. // Dismiss the notification after 0.15s to show the animation.
cx.spawn(async move |view, cx| { 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| { cx.update(|cx| {
if let Some(view) = view.upgrade() { if let Some(view) = view.upgrade() {
view.update(cx, |view, cx| { view.update(cx, |view, cx| {
@@ -248,13 +252,13 @@ impl Notification {
} }
}) })
}) })
.detach() .detach();
} }
/// Set the content of the notification. /// Set the content of the notification.
pub fn content( pub fn content(
mut self, mut self,
content: impl Fn(&mut Window, &mut Context<Self>) -> AnyElement + 'static, content: impl Fn(&mut Self, &mut Window, &mut Context<Self>) -> AnyElement + 'static,
) -> Self { ) -> Self {
self.content_builder = Some(Rc::new(content)); self.content_builder = Some(Rc::new(content));
self self
@@ -276,52 +280,60 @@ impl Styled for Notification {
&mut self.style &mut self.style
} }
} }
impl Render for Notification { impl Render for Notification {
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 closing = self.closing; let content = self
let icon = match self.type_ { .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(), 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() h_flex()
.id("notification") .id("notification")
.refine_style(&self.style)
.group("") .group("")
.occlude() .occlude()
.relative() .relative()
.w_96() .w_112()
.border_1() .border_1()
.border_color(cx.theme().border) .border_color(cx.theme().border)
.bg(cx.theme().surface_background) .bg(cx.theme().surface_background)
.rounded(cx.theme().radius_lg) .rounded(cx.theme().radius_lg)
.when(cx.theme().shadow, |this| this.shadow_md()) .when(cx.theme().shadow, |this| this.shadow_md())
.p_2() .p_2()
.gap_3() .gap_2()
.justify_start() .justify_start()
.items_start() .items_start()
.refine_style(&self.style)
.when_some(icon, |this, icon| { .when_some(icon, |this, icon| {
this.child(div().flex_shrink_0().pt_1().child(icon)) this.child(div().flex_shrink_0().pt_1().child(icon))
}) })
.child( .child(
v_flex() v_flex()
.flex_1() .flex_1()
.gap_1()
.overflow_hidden() .overflow_hidden()
.when(has_icon, |this| this.pl_1())
.when_some(self.title.clone(), |this, title| { .when_some(self.title.clone(), |this, title| {
this.child(div().text_sm().font_semibold().child(title)) this.child(div().text_sm().font_semibold().child(title))
}) })
.when_some(self.message.clone(), |this, message| { .when_some(self.message.clone(), |this, message| {
this.child(div().text_sm().child(message)) this.child(div().text_sm().child(message))
}) })
.when_some(self.content_builder.clone(), |this, child_builder| { .when_some(content, |this, content| this.child(content)),
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(action, |this, action| this.child(action))
.child( .child(
div() div()
.absolute() .absolute()
@@ -334,9 +346,7 @@ impl Render for Notification {
.icon(IconName::Close) .icon(IconName::Close)
.ghost() .ghost()
.xsmall() .xsmall()
.on_click(cx.listener(|this, _, window, cx| { .on_click(cx.listener(|this, _, window, cx| this.dismiss(window, cx))),
this.dismiss(window, cx);
})),
), ),
) )
.when_some(self.on_click.clone(), |this, on_click| { .when_some(self.on_click.clone(), |this, on_click| {
@@ -345,20 +355,46 @@ impl Render for Notification {
on_click(event, window, cx); 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( .with_animation(
ElementId::NamedInteger("slide-down".into(), closing as u64), ElementId::NamedInteger("slide-down".into(), closing as u64),
Animation::new(Duration::from_secs_f64(0.25)) Animation::new(Duration::from_secs_f64(0.25))
.with_easing(cubic_bezier(0.4, 0., 0.2, 1.)), .with_easing(cubic_bezier(0.4, 0., 0.2, 1.)),
move |this, delta| { move |this, delta| {
if closing { if closing {
let x_offset = px(0.) + delta * px(45.);
let opacity = 1. - delta; let opacity = 1. - delta;
this.left(px(0.) + x_offset) let that = this
.shadow_none() .shadow_none()
.opacity(opacity) .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 { } 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; let opacity = delta;
this.top(px(0.) + y_offset) this.top(px(0.) + y_offset)
.opacity(opacity) .opacity(opacity)
@@ -373,7 +409,11 @@ impl Render for Notification {
pub struct NotificationList { pub struct NotificationList {
/// Notifications that will be auto hidden. /// Notifications that will be auto hidden.
pub(crate) notifications: VecDeque<Entity<Notification>>, pub(crate) notifications: VecDeque<Entity<Notification>>,
/// Whether the notification list is expanded.
expanded: bool, expanded: bool,
/// Subscriptions
_subscriptions: HashMap<NotificationId, Subscription>, _subscriptions: HashMap<NotificationId, Subscription>,
} }
@@ -386,10 +426,12 @@ impl NotificationList {
} }
} }
pub fn push<T>(&mut self, notification: T, window: &mut Window, cx: &mut Context<Self>) pub fn push(
where &mut self,
T: Into<Notification>, notification: impl Into<Notification>,
{ window: &mut Window,
cx: &mut Context<Self>,
) {
let notification = notification.into(); let notification = notification.into();
let id = notification.id.clone(); let id = notification.id.clone();
let autohide = notification.autohide; let autohide = notification.autohide;
@@ -411,36 +453,35 @@ impl NotificationList {
if autohide { if autohide {
// Sleep for 5 seconds to autohide the notification // Sleep for 5 seconds to autohide the notification
cx.spawn_in(window, async move |_, cx| { cx.spawn_in(window, async move |_this, cx| {
Timer::after(Duration::from_secs(5)).await; 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)) 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(); .detach();
} }
cx.notify(); cx.notify();
} }
pub fn close<T>(&mut self, key: T, window: &mut Window, cx: &mut Context<Self>) pub(crate) fn close(
where &mut self,
T: Into<ElementId>, id: impl Into<NotificationId>,
{ window: &mut Window,
let id = (TypeId::of::<DefaultIdType>(), key.into()).into(); cx: &mut Context<Self>,
) {
let id: NotificationId = id.into();
if let Some(n) = self.notifications.iter().find(|n| n.read(cx).id == id) { if let Some(n) = self.notifications.iter().find(|n| n.read(cx).id == id) {
n.update(cx, |note, cx| { n.update(cx, |note, cx| note.dismiss(window, cx))
note.dismiss(window, cx);
});
} }
cx.notify(); cx.notify();
} }
pub fn clear(&mut self, _window: &mut Window, cx: &mut Context<Self>) { pub fn clear(&mut self, _: &mut Window, cx: &mut Context<Self>) {
self.notifications.clear(); self.notifications.clear();
cx.notify(); cx.notify();
} }
@@ -451,25 +492,46 @@ impl NotificationList {
} }
impl Render for NotificationList { impl Render for NotificationList {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(
&mut self,
window: &mut gpui::Window,
cx: &mut gpui::Context<Self>,
) -> impl IntoElement {
let size = window.viewport_size(); let size = window.viewport_size();
let items = self.notifications.iter().rev().take(10).rev().cloned(); let items = self.notifications.iter().rev().take(10).rev().cloned();
div() let placement = cx.theme().notification.placement;
.id("notification-wrapper") let margins = &cx.theme().notification.margins;
.absolute()
.top_4() v_flex()
.right_4() .id("notification-list")
.child( .max_h(size.height)
v_flex() .pt(margins.top)
.id("notification-list") .pb(margins.bottom)
.h(size.height - px(8.)) .gap_3()
.gap_3() .when(
.children(items) matches!(placement, Anchor::TopRight),
.on_hover(cx.listener(|view, hovered, _, cx| { |this| this.pr(margins.right), // ignore left
view.expanded = *hovered;
cx.notify()
})),
) )
.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)
} }
} }

View File

@@ -2,14 +2,15 @@ use std::rc::Rc;
use gpui::prelude::FluentBuilder as _; use gpui::prelude::FluentBuilder as _;
use gpui::{ use gpui::{
deferred, div, px, AnyElement, App, Bounds, Context, Deferred, DismissEvent, Div, ElementId, AnyElement, App, Bounds, Context, Deferred, DismissEvent, Div, ElementId, EventEmitter,
EventEmitter, FocusHandle, Focusable, Half, InteractiveElement as _, IntoElement, KeyBinding, FocusHandle, Focusable, Half, InteractiveElement as _, IntoElement, KeyBinding, MouseButton,
MouseButton, ParentElement, Pixels, Point, Render, RenderOnce, Stateful, StyleRefinement, ParentElement, Pixels, Point, Render, RenderOnce, Stateful, StyleRefinement, Styled,
Styled, Subscription, Window, Subscription, Window, deferred, div, px,
}; };
use theme::Anchor;
use crate::actions::Cancel; 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"; const CONTEXT: &str = "Popover";

View File

@@ -3,14 +3,15 @@ use std::rc::Rc;
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
div, Along, AnyElement, App, AppContext, Axis, Bounds, Context, Element, ElementId, Empty, Along, AnyElement, App, AppContext, Axis, Bounds, Context, Element, ElementId, Empty, Entity,
Entity, EventEmitter, InteractiveElement as _, IntoElement, IsZero as _, MouseMoveEvent, EventEmitter, InteractiveElement as _, IntoElement, IsZero as _, MouseMoveEvent, MouseUpEvent,
MouseUpEvent, ParentElement, Pixels, Render, RenderOnce, Style, Styled, Window, 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::resizable::PANEL_MIN_SIZE;
use crate::{h_flex, v_flex, AxisExt, ElementExt}; use crate::{ElementExt, h_flex, v_flex};
pub enum ResizablePanelEvent { pub enum ResizablePanelEvent {
Resized, Resized,

View File

@@ -3,14 +3,13 @@ use std::rc::Rc;
use gpui::prelude::FluentBuilder as _; use gpui::prelude::FluentBuilder as _;
use gpui::{ use gpui::{
div, px, AnyElement, App, Axis, Element, ElementId, Entity, GlobalElementId, AnyElement, App, Axis, Element, ElementId, Entity, GlobalElementId, InteractiveElement,
InteractiveElement, IntoElement, MouseDownEvent, MouseUpEvent, ParentElement as _, Pixels, IntoElement, MouseDownEvent, MouseUpEvent, ParentElement as _, Pixels, Point, Render,
Point, Render, StatefulInteractiveElement, Styled as _, Window, StatefulInteractiveElement, Styled as _, Window, div, px,
}; };
use theme::ActiveTheme; use theme::{ActiveTheme, AxisExt};
use crate::dock_area::dock::DockPlacement; use crate::dock_area::dock::DockPlacement;
use crate::AxisExt;
pub(crate) const HANDLE_PADDING: Pixels = px(4.); pub(crate) const HANDLE_PADDING: Pixels = px(4.);
pub(crate) const HANDLE_SIZE: Pixels = px(1.); pub(crate) const HANDLE_SIZE: Pixels = px(1.);

View File

@@ -1,11 +1,12 @@
use std::any::TypeId;
use std::rc::Rc; use std::rc::Rc;
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ 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, FocusHandle, HitboxBehavior, Hsla, InteractiveElement, IntoElement, MouseButton,
ParentElement as _, Pixels, Point, Render, ResizeEdge, SharedString, Size, Styled, Tiling, ParentElement as _, Pixels, Point, Render, ResizeEdge, Size, Styled, Tiling, WeakFocusHandle,
WeakFocusHandle, Window, canvas, div, point, px, size, Window, canvas, div, point, px, size,
}; };
use theme::{ use theme::{
ActiveTheme, CLIENT_SIDE_DECORATION_BORDER, CLIENT_SIDE_DECORATION_ROUNDING, ActiveTheme, CLIENT_SIDE_DECORATION_BORDER, CLIENT_SIDE_DECORATION_ROUNDING,
@@ -213,13 +214,30 @@ impl Root {
cx.notify(); cx.notify();
} }
/// Clear a notification by its ID. /// Clear a notification by its type.
pub fn clear_notification<T>(&mut self, id: T, window: &mut Window, cx: &mut Context<Self>) pub fn clear_notification<T: Sized + 'static>(
where &mut self,
T: Into<SharedString>, window: &mut Window,
{ cx: &mut Context<'_, Root>,
self.notification ) {
.update(cx, |view, cx| view.close(id.into(), window, cx)); self.notification.update(cx, |view, cx| {
let id = TypeId::of::<T>();
view.close(id, window, cx);
});
cx.notify();
}
/// Clear a notification by its type.
pub fn clear_notification_by_id<T: Sized + 'static>(
&mut self,
key: impl Into<ElementId>,
window: &mut Window,
cx: &mut Context<'_, Root>,
) {
self.notification.update(cx, |view, cx| {
let id = (TypeId::of::<T>(), key.into());
view.close(id, window, cx);
});
cx.notify(); cx.notify();
} }

View File

@@ -1,10 +1,9 @@
use gpui::{ use gpui::{
px, relative, App, Axis, BorderStyle, Bounds, ContentMask, Corners, Edges, Element, ElementId, App, Axis, BorderStyle, Bounds, ContentMask, Corners, Edges, Element, ElementId, EntityId,
EntityId, GlobalElementId, Hitbox, Hsla, IntoElement, IsZero as _, LayoutId, PaintQuad, Pixels, GlobalElementId, Hitbox, Hsla, IntoElement, IsZero as _, LayoutId, PaintQuad, Pixels, Point,
Point, Position, ScrollHandle, ScrollWheelEvent, Size, Style, Window, Position, ScrollHandle, ScrollWheelEvent, Size, Style, Window, px, relative,
}; };
use theme::AxisExt;
use crate::AxisExt;
/// Make a scrollable mask element to cover the parent view with the mouse wheel event listening. /// Make a scrollable mask element to cover the parent view with the mouse wheel event listening.
/// ///

View File

@@ -11,9 +11,7 @@ use gpui::{
Position, ScrollHandle, ScrollWheelEvent, Size, Style, UniformListScrollHandle, Window, fill, Position, ScrollHandle, ScrollWheelEvent, Size, Style, UniformListScrollHandle, Window, fill,
point, px, relative, size, point, px, relative, size,
}; };
use theme::{ActiveTheme, ScrollbarMode}; use theme::{ActiveTheme, AxisExt, ScrollbarMode};
use crate::AxisExt;
/// The width of the scrollbar (THUMB_ACTIVE_INSET * 2 + THUMB_ACTIVE_WIDTH) /// The width of the scrollbar (THUMB_ACTIVE_INSET * 2 + THUMB_ACTIVE_WIDTH)
const WIDTH: Pixels = px(1. * 2. + 8.); const WIDTH: Pixels = px(1. * 2. + 8.);

View File

@@ -4,13 +4,13 @@ use std::time::Duration;
use gpui::prelude::FluentBuilder as _; use gpui::prelude::FluentBuilder as _;
use gpui::{ use gpui::{
div, px, white, Animation, AnimationExt as _, AnyElement, App, Element, ElementId, Animation, AnimationExt as _, AnyElement, App, Element, ElementId, GlobalElementId,
GlobalElementId, InteractiveElement, IntoElement, LayoutId, ParentElement as _, SharedString, InteractiveElement, IntoElement, LayoutId, ParentElement as _, SharedString, Styled as _,
Styled as _, Window, 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<Rc<dyn Fn(&bool, &mut Window, &mut App)>>; type OnClick = Option<Rc<dyn Fn(&bool, &mut Window, &mut App)>>;

View File

@@ -1,11 +1,11 @@
use std::rc::Rc; 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::input::InputState;
use crate::modal::Modal; use crate::modal::Modal;
use crate::notification::Notification; use crate::notification::Notification;
use crate::Root;
/// Extension trait for [`Window`] to add modal, notification .. functionality. /// Extension trait for [`Window`] to add modal, notification .. functionality.
pub trait WindowExtension: Sized { pub trait WindowExtension: Sized {
@@ -31,10 +31,15 @@ pub trait WindowExtension: Sized {
where where
T: Into<Notification>; T: Into<Notification>;
/// Clears a notification by its ID. /// Clear the unique notification.
fn clear_notification<T>(&mut self, id: T, cx: &mut App) fn clear_notification<T: Sized + 'static>(&mut self, cx: &mut App);
where
T: Into<SharedString>; /// Clear the unique notification with the given id.
fn clear_notification_by_id<T: Sized + 'static>(
&mut self,
key: impl Into<ElementId>,
cx: &mut App,
);
/// Clear all notifications /// Clear all notifications
fn clear_notifications(&mut self, cx: &mut App); fn clear_notifications(&mut self, cx: &mut App);
@@ -88,13 +93,21 @@ impl WindowExtension for Window {
} }
#[inline] #[inline]
fn clear_notification<T>(&mut self, id: T, cx: &mut App) fn clear_notification<T: Sized + 'static>(&mut self, cx: &mut App) {
where Root::update(self, cx, |root, window, cx| {
T: Into<SharedString>, root.clear_notification::<T>(window, cx);
{ })
let id = id.into(); }
Root::update(self, cx, move |root, window, cx| {
root.clear_notification(id, window, cx); #[inline]
fn clear_notification_by_id<T: Sized + 'static>(
&mut self,
key: impl Into<ElementId>,
cx: &mut App,
) {
let key: ElementId = key.into();
Root::update(self, cx, |root, window, cx| {
root.clear_notification_by_id::<T>(key, window, cx);
}) })
} }