refactor notification comp
This commit is contained in:
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -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),
|
||||||
|
|||||||
31
crates/theme/src/notification.rs
Normal file
31
crates/theme/src/notification.rs
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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>>,
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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.);
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -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.);
|
||||||
|
|||||||
@@ -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)>>;
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user