Export with_type and impl FluentBuilder for Notification. (#154)

This commit is contained in:
Jason Lee 2024-08-15 19:52:37 +08:00 committed by GitHub
parent 57dab4d15a
commit b3f62bf096
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 45 additions and 509 deletions

View file

@ -485,10 +485,10 @@ impl Render for ModalStory {
Button::new("show-notify-warning", cx)
.label("Warning Notify...")
.on_click(cx.listener(|_, _, cx| {
cx.push_notification((
NotificationType::Warning,
struct WarningNotification;
cx.push_notification(Notification::warning(
"The network is not stable, please check your connection.",
))
).id1::<WarningNotification>("test"))
})),
)
.child(
@ -501,7 +501,7 @@ impl Render for ModalStory {
Notification::new(
"你已经成功保存了文件,但是有一些警告信息需要你注意。",
)
.type_id::<TestNotification>()
.id::<TestNotification>()
.title("保存成功")
.icon(IconName::Inbox)
.autohide(false)

View file

@ -1,7 +1,7 @@
use std::{any::TypeId, sync::Arc, time::Duration};
use gpui::{
div, prelude::FluentBuilder as _, px, Animation, AnimationExt, ClickEvent, DismissEvent,
div, prelude::FluentBuilder, px, Animation, AnimationExt, ClickEvent, DismissEvent, ElementId,
EventEmitter, InteractiveElement as _, IntoElement, ParentElement as _, Render, SharedString,
StatefulInteractiveElement, Styled, View, ViewContext, VisualContext, WindowContext,
};
@ -21,25 +21,19 @@ pub enum NotificationType {
#[derive(Debug, PartialEq, Clone)]
pub(crate) enum NotificationId {
Type(TypeId),
Id(SharedString),
Id(TypeId),
IdAndElementId(TypeId, ElementId),
}
impl From<TypeId> for NotificationId {
fn from(type_id: TypeId) -> Self {
Self::Type(type_id)
Self::Id(type_id)
}
}
impl From<SharedString> for NotificationId {
fn from(id: SharedString) -> Self {
Self::Id(id)
}
}
impl From<&'static str> for NotificationId {
fn from(id: &'static str) -> Self {
Self::Id(id.into())
impl From<(TypeId, ElementId)> for NotificationId {
fn from((type_id, id): (TypeId, ElementId)) -> Self {
Self::IdAndElementId(type_id, id)
}
}
@ -51,7 +45,7 @@ pub struct Notification {
id: NotificationId,
type_: NotificationType,
title: Option<SharedString>,
content: SharedString,
message: SharedString,
icon: Option<Icon>,
autohide: bool,
on_click: Option<Arc<dyn Fn(&ClickEvent, &mut WindowContext)>>,
@ -81,17 +75,19 @@ impl From<(NotificationType, SharedString)> for Notification {
}
}
struct DefaultIdType;
impl Notification {
/// Create a new notification with the given content.
///
/// default width is 320px.
pub fn new(content: impl Into<SharedString>) -> Self {
pub fn new(message: impl Into<SharedString>) -> Self {
let id: SharedString = uuid::Uuid::new_v4().to_string().into();
let id = (TypeId::of::<DefaultIdType>(), id.into());
Self {
id: id.into(),
title: None,
content: content.into(),
message: message.into(),
type_: NotificationType::Info,
icon: None,
autohide: true,
@ -99,22 +95,36 @@ impl Notification {
}
}
/// Set the id of the notification, used to uniquely identify the notification.
pub fn id(mut self, id: impl Into<SharedString>) -> Self {
let id: SharedString = id.into();
self.id = id.into();
self
pub fn info(message: impl Into<SharedString>) -> Self {
Self::new(message).with_type(NotificationType::Info)
}
/// Set the type id of the notification, used to uniquely identify the notification.
pub fn success(message: impl Into<SharedString>) -> Self {
Self::new(message).with_type(NotificationType::Success)
}
pub fn warning(message: impl Into<SharedString>) -> Self {
Self::new(message).with_type(NotificationType::Warning)
}
pub fn error(message: impl Into<SharedString>) -> Self {
Self::new(message).with_type(NotificationType::Error)
}
/// Set the type for unique identification of the notification.
///
/// ```rs
/// struct MyNotificationKind;
/// let notification = Notification::new("Hello").type_id::<MyNotificationKind>();
/// let notification = Notification::new("Hello").id::<MyNotificationKind>();
/// ```
pub fn type_id<T: Sized + 'static>(mut self) -> Self {
let type_id = TypeId::of::<T>();
self.id = type_id.into();
pub fn id<T: Sized + 'static>(mut self) -> Self {
self.id = TypeId::of::<T>().into();
self
}
/// Set the type and id of the notification, used to uniquely identify the notification.
pub fn id1<T: Sized + 'static>(mut self, key: impl Into<ElementId>) -> Self {
self.id = (TypeId::of::<T>(), key.into()).into();
self
}
@ -134,31 +144,12 @@ impl Notification {
self
}
fn with_type(mut self, type_: NotificationType) -> Self {
/// Set the type of the notification, default is NotificationType::Info.
pub fn with_type(mut self, type_: NotificationType) -> Self {
self.type_ = type_;
self
}
pub fn info(mut self) -> Self {
self.type_ = NotificationType::Info;
self
}
pub fn success(mut self) -> Self {
self.type_ = NotificationType::Success;
self
}
pub fn warning(mut self) -> Self {
self.type_ = NotificationType::Warning;
self
}
pub fn error(mut self) -> Self {
self.type_ = NotificationType::Error;
self
}
/// Set the auto hide of the notification, default is true.
pub fn autohide(mut self, autohide: bool) -> Self {
self.autohide = autohide;
@ -192,7 +183,7 @@ impl Notification {
}
}
impl EventEmitter<DismissEvent> for Notification {}
impl FluentBuilder for Notification {}
impl Render for Notification {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let group_id = "notification-group";
@ -245,7 +236,7 @@ impl Render for Notification {
this.child(div().text_sm().font_semibold().child(title))
})
.overflow_hidden()
.child(div().text_sm().child(self.content.clone())),
.child(div().text_sm().child(self.message.clone())),
)
.when_some(self.on_click.clone(), |this, on_click| {
this.cursor_pointer()

View file

@ -1,7 +1,5 @@
pub mod dock;
pub mod item;
pub mod model_layer;
pub mod notification;
pub mod pane;
pub mod pane_group;
mod title_bar;

View file

@ -1,179 +0,0 @@
use gpui::{
div, prelude::*, px, AnyView, DismissEvent, FocusHandle, ManagedView, Render, Subscription,
View, ViewContext, WindowContext,
};
use ui::theme::ActiveTheme as _;
use ui::{h_flex, v_flex};
pub enum DismissDecision {
Dismiss(bool),
Pending,
}
pub trait ModalView: ManagedView {
fn on_before_dismiss(&mut self, _: &mut ViewContext<Self>) -> DismissDecision {
DismissDecision::Dismiss(true)
}
fn fade_out_background(&self) -> bool {
false
}
}
trait ModalViewHandle {
fn on_before_dismiss(&mut self, cx: &mut WindowContext) -> DismissDecision;
fn view(&self) -> AnyView;
fn fade_out_background(&self, cx: &WindowContext) -> bool;
}
impl<V: ModalView> ModalViewHandle for View<V> {
fn on_before_dismiss(&mut self, cx: &mut WindowContext) -> DismissDecision {
self.update(cx, |this, cx| this.on_before_dismiss(cx))
}
fn view(&self) -> AnyView {
self.clone().into()
}
fn fade_out_background(&self, cx: &WindowContext) -> bool {
self.read(cx).fade_out_background()
}
}
pub struct ActiveModal {
modal: Box<dyn ModalViewHandle>,
_subscriptions: [Subscription; 2],
previous_focus_handle: Option<FocusHandle>,
focus_handle: FocusHandle,
}
pub struct ModalLayer {
active_modal: Option<ActiveModal>,
dismiss_on_focus_lost: bool,
}
impl ModalLayer {
pub fn new() -> Self {
Self {
active_modal: None,
dismiss_on_focus_lost: false,
}
}
pub fn toggle_modal<V, B>(&mut self, cx: &mut ViewContext<Self>, build_view: B)
where
V: ModalView,
B: FnOnce(&mut ViewContext<V>) -> V,
{
if let Some(active_modal) = &self.active_modal {
let is_close = active_modal.modal.view().downcast::<V>().is_ok();
let did_close = self.hide_modal(cx);
if is_close || !did_close {
return;
}
}
let new_modal = cx.new_view(build_view);
self.show_modal(new_modal, cx);
}
fn show_modal<V>(&mut self, new_modal: View<V>, cx: &mut ViewContext<Self>)
where
V: ModalView,
{
let focus_handle = cx.focus_handle();
self.active_modal = Some(ActiveModal {
modal: Box::new(new_modal.clone()),
_subscriptions: [
cx.subscribe(&new_modal, |this, _, _: &DismissEvent, cx| {
this.hide_modal(cx);
}),
cx.on_focus_out(&focus_handle, |this, _event, cx| {
if this.dismiss_on_focus_lost {
this.hide_modal(cx);
}
}),
],
previous_focus_handle: cx.focused(),
focus_handle,
});
cx.defer(move |_, cx| {
cx.focus_view(&new_modal);
});
cx.notify();
}
fn hide_modal(&mut self, cx: &mut ViewContext<Self>) -> bool {
let Some(active_modal) = self.active_modal.as_mut() else {
self.dismiss_on_focus_lost = false;
return false;
};
match active_modal.modal.on_before_dismiss(cx) {
DismissDecision::Dismiss(dismiss) => {
self.dismiss_on_focus_lost = !dismiss;
if !dismiss {
return false;
}
}
DismissDecision::Pending => {
self.dismiss_on_focus_lost = false;
return false;
}
}
if let Some(active_modal) = self.active_modal.take() {
if let Some(previous_focus) = active_modal.previous_focus_handle {
if active_modal.focus_handle.contains_focused(cx) {
previous_focus.focus(cx);
}
}
cx.notify();
}
true
}
pub fn active_modal<V>(&self) -> Option<View<V>>
where
V: 'static,
{
let active_modal = self.active_modal.as_ref()?;
active_modal.modal.view().downcast::<V>().ok()
}
pub fn has_active_modal(&self) -> bool {
self.active_modal.is_some()
}
}
impl Render for ModalLayer {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let Some(active_modal) = &self.active_modal else {
return div();
};
div()
.absolute()
.size_full()
.top_0()
.left_0()
.when(active_modal.modal.fade_out_background(cx), |el| {
let mut background = cx.theme().popover;
background.fade_out(0.2);
el.bg(background)
.occlude()
.on_mouse_down_out(cx.listener(|this, _, cx| {
this.hide_modal(cx);
}))
})
.child(
v_flex()
.h(px(0.0))
.top_20()
.flex()
.flex_col()
.items_center()
.track_focus(&active_modal.focus_handle)
.child(h_flex().occlude().child(active_modal.modal.view())),
)
}
}

View file

@ -1,235 +0,0 @@
use std::any::TypeId;
use std::borrow::Cow;
use std::sync::Arc;
use gpui::{
div, AnyView, DismissEvent, ElementId, Entity, EntityId, EventEmitter, InteractiveElement,
IntoElement, ParentElement, Render, SharedString, StatefulInteractiveElement, Styled, View,
ViewContext, VisualContext, WindowContext,
};
use ui::{h_flex, label::Label, theme::ActiveTheme, v_flex, Icon, IconName};
use crate::Workspace;
#[derive(Debug, PartialEq, Clone)]
pub struct NotificationId {
/// A [`TypeId`] used to uniquely identify this notification.
type_id: TypeId,
/// A supplementary ID used to distinguish between multiple
/// notifications that have the same [`type_id`](Self::type_id);
id: Option<ElementId>,
}
impl NotificationId {
/// Returns a unique [`NotificationId`] for the given type.
pub fn unique<T: 'static>() -> Self {
Self {
type_id: TypeId::of::<T>(),
id: None,
}
}
/// Returns a [`NotificationId`] for the given type that is also identified
/// by the provided ID.
pub fn identified<T: 'static>(id: impl Into<ElementId>) -> Self {
Self {
type_id: TypeId::of::<T>(),
id: Some(id.into()),
}
}
}
impl Workspace {
pub fn show_notification<V: Notification>(
&mut self,
id: NotificationId,
cx: &mut ViewContext<Self>,
build_notification: impl FnOnce(&mut ViewContext<Self>) -> View<V>,
) {
self.dismiss_notification_internal(&id, cx);
let notification = build_notification(cx);
cx.subscribe(&notification, {
let id = id.clone();
move |this, _, _: &DismissEvent, cx| {
this.dismiss_notification_internal(&id, cx);
}
})
.detach();
self.notifications.push((id, Box::new(notification)));
cx.notify();
}
pub fn dismiss_notification(&mut self, id: &NotificationId, cx: &mut ViewContext<Self>) {
self.dismiss_notification_internal(id, cx)
}
fn dismiss_notification_internal(&mut self, id: &NotificationId, cx: &mut ViewContext<Self>) {
self.notifications.retain(|(existing_id, _)| {
if existing_id == id {
cx.notify();
false
} else {
true
}
});
}
pub fn show_toast(&mut self, toast: Toast, cx: &mut ViewContext<Self>) {
self.dismiss_notification(&toast.id, cx);
self.show_notification(toast.id, cx, |cx| {
cx.new_view(|_cx| match toast.on_click.as_ref() {
Some((click_msg, on_click)) => {
let on_click = on_click.clone();
MessageNotification::new(toast.msg.clone())
.with_click_message(click_msg.clone())
.on_click(move |cx| on_click(cx))
}
None => MessageNotification::new(toast.msg.clone()),
})
})
}
pub fn dismiss_toast(&mut self, id: &NotificationId, cx: &mut ViewContext<Self>) {
self.dismiss_notification(id, cx);
}
pub fn clear_all_notifications(&mut self, cx: &mut ViewContext<Self>) {
self.notifications.clear();
cx.notify();
}
}
pub struct Toast {
id: NotificationId,
msg: Cow<'static, str>,
on_click: Option<(Cow<'static, str>, Arc<dyn Fn(&mut WindowContext)>)>,
}
impl Toast {
pub fn new<I: Into<Cow<'static, str>>>(id: NotificationId, msg: I) -> Self {
Toast {
id,
msg: msg.into(),
on_click: None,
}
}
pub fn on_click<F, M>(mut self, message: M, on_click: F) -> Self
where
M: Into<Cow<'static, str>>,
F: Fn(&mut WindowContext) + 'static,
{
self.on_click = Some((message.into(), Arc::new(on_click)));
self
}
}
impl PartialEq for Toast {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
&& self.msg == other.msg
&& self.on_click.is_some() == other.on_click.is_some()
}
}
impl Clone for Toast {
fn clone(&self) -> Self {
Toast {
id: self.id.clone(),
msg: self.msg.clone(),
on_click: self.on_click.clone(),
}
}
}
pub trait Notification: EventEmitter<DismissEvent> + Render {}
impl<V: EventEmitter<DismissEvent> + Render> Notification for V {}
pub trait NotificationHandle: Send {
fn id(&self) -> EntityId;
fn to_any(&self) -> AnyView;
}
impl<T: Notification> NotificationHandle for View<T> {
fn id(&self) -> EntityId {
self.entity_id()
}
fn to_any(&self) -> AnyView {
self.clone().into()
}
}
impl From<&dyn NotificationHandle> for AnyView {
fn from(val: &dyn NotificationHandle) -> Self {
val.to_any()
}
}
pub struct MessageNotification {
message: SharedString,
on_click: Option<Arc<dyn Fn(&mut ViewContext<Self>)>>,
click_message: Option<SharedString>,
}
impl EventEmitter<DismissEvent> for MessageNotification {}
impl MessageNotification {
pub fn new(message: impl Into<SharedString>) -> Self {
Self {
message: message.into(),
on_click: None,
click_message: None,
}
}
pub fn with_click_message<S>(mut self, message: S) -> Self
where
S: Into<SharedString>,
{
self.click_message = Some(message.into());
self
}
pub fn on_click<F>(mut self, on_click: F) -> Self
where
F: 'static + Fn(&mut ViewContext<Self>),
{
self.on_click = Some(Arc::new(on_click));
self
}
pub fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
cx.emit(DismissEvent);
}
}
impl Render for MessageNotification {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
v_flex()
.bg(cx.theme().popover)
.border_1()
.border_color(cx.theme().border)
.shadow_xl()
.rounded_xl()
.p_4()
.max_w_80()
.bg(cx.theme().background)
.child(
h_flex()
.justify_between()
.gap_2()
.child(div().max_w_80().child(Label::new(self.message.clone())))
.child(
div()
.id("cancel")
.child(Icon::new(IconName::Close))
.cursor_pointer()
.on_click(cx.listener(|this, _, cx| this.dismiss(cx))),
),
)
}
}

View file

@ -7,8 +7,6 @@ use std::{
use crate::{
dock::{Panel, PanelHandle},
model_layer::ModalLayer,
notification::{NotificationHandle, NotificationId},
pane_group,
};
use anyhow::Result;
@ -33,7 +31,6 @@ actions!(
[
ActivateNextPane,
ActivatePreviousPane,
ClearAllNotifications,
CloseAllDocks,
ToggleBottomDock,
ToggleCenteredLayout,
@ -83,8 +80,6 @@ pub struct Workspace {
pub(crate) zoomed_position: Option<DockPosition>,
database_id: Option<WorkspaceId>,
bounds: Bounds<Pixels>,
pub(crate) notifications: Vec<(NotificationId, Box<dyn NotificationHandle>)>,
modal_layer: View<ModalLayer>,
workspace_actions: Vec<Box<dyn Fn(Div, &mut ViewContext<Self>) -> Div>>,
bounds_save_task_queued: Option<Task<()>>,
_subscriptions: Vec<Subscription>,
@ -243,9 +238,7 @@ impl Render for Workspace {
Some(DockPosition::Bottom) => div.top_2().border_t_1(),
None => div.top_2().bottom_2().left_2().right_2().border_1(),
})
}))
.child(self.modal_layer.clone())
.children(self.render_notifications(cx)),
})),
)
}
}
@ -275,8 +268,6 @@ impl Workspace {
// let bottom_dock_buttons = cx.new_view(|cx| PanelButtons::new(bottom_dock.clone(), cx));
// let right_dock_buttons = cx.new_view(|cx| PanelButtons::new(right_dock.clone(), cx));
let modal_layer = cx.new_view(|_| ModalLayer::new());
let subscriptions = vec![
cx.observe_window_activation(Self::on_window_activation_changed),
cx.observe_window_bounds(move |this, cx| {
@ -337,8 +328,6 @@ impl Workspace {
panes_by_item: Default::default(),
active_pane: center_pane.clone(),
last_active_center_pane: Some(center_pane.downgrade()),
modal_layer,
notifications: Default::default(),
left_dock,
bottom_dock,
right_dock,
@ -425,11 +414,6 @@ impl Workspace {
workspace.close_all_docks(cx);
}),
)
.on_action(
cx.listener(|workspace: &mut Workspace, _: &ClearAllNotifications, cx| {
workspace.clear_all_notifications(cx);
}),
)
.on_action(cx.listener(Workspace::activate_pane_at_index))
.on_action(
cx.listener(|_workspace: &mut Workspace, _: &ReopenClosedItem, _cx| {
@ -1020,27 +1004,4 @@ impl Workspace {
// }));
// }
}
fn render_notifications(&self, _cx: &ViewContext<Self>) -> Option<Div> {
if self.notifications.is_empty() {
None
} else {
Some(
div()
.absolute()
.right_3()
.bottom_3()
.h_full()
.flex()
.flex_col()
.justify_end()
.gap_2()
.children(
self.notifications
.iter()
.map(|(_, notification)| notification.to_any()),
),
)
}
}
}