Add Notification (#4)
This commit is contained in:
parent
5393580789
commit
11901dc1e4
7 changed files with 267 additions and 23 deletions
|
|
@ -2,9 +2,9 @@
|
|||
|
||||
This is an example of build app by using GPUI.
|
||||
|
||||
https://github.com/huacnlee/gpui-app/assets/5518/ad103f02-697a-40ed-a876-8b13e4242a72
|
||||
<https://github.com/huacnlee/gpui-app/assets/5518/ad103f02-697a-40ed-a876-8b13e4242a72>
|
||||
|
||||
https://github.com/huacnlee/gpui-app/assets/5518/5316f9f0-58c8-4b99-bd79-eafffb38c3fc
|
||||
<https://github.com/huacnlee/gpui-app/assets/5518/5316f9f0-58c8-4b99-bd79-eafffb38c3fc>
|
||||
|
||||
## Demo
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ use super::story_case;
|
|||
pub struct ButtonStory;
|
||||
|
||||
impl ButtonStory {
|
||||
fn on_click(ev: &ClickEvent, cx: &mut WindowContext) {
|
||||
fn on_click(ev: &ClickEvent, _: &mut WindowContext) {
|
||||
println!("Button clicked! {:?}", ev);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,3 @@
|
|||
use std::fmt::{self, Display, Formatter};
|
||||
|
||||
use checkbox_story::CheckboxStory;
|
||||
use gpui::{
|
||||
div, prelude::FluentBuilder as _, px, AnyElement, IntoElement, ParentElement, Render,
|
||||
RenderOnce, SharedString, StatefulInteractiveElement as _, Styled as _, View, ViewContext,
|
||||
VisualContext, WindowContext,
|
||||
};
|
||||
|
||||
mod button_story;
|
||||
mod checkbox_story;
|
||||
mod dropdown_story;
|
||||
|
|
@ -14,6 +5,13 @@ mod input_story;
|
|||
mod picker_story;
|
||||
mod switch_story;
|
||||
|
||||
use gpui::{
|
||||
div, prelude::FluentBuilder as _, px, AnyElement, IntoElement, ParentElement, Render,
|
||||
RenderOnce, SharedString, StatefulInteractiveElement as _, Styled as _, View, ViewContext,
|
||||
VisualContext, WindowContext,
|
||||
};
|
||||
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
use ui::{
|
||||
divider::Divider,
|
||||
label::Label,
|
||||
|
|
@ -22,6 +20,7 @@ use ui::{
|
|||
};
|
||||
|
||||
use button_story::ButtonStory;
|
||||
use checkbox_story::CheckboxStory;
|
||||
use dropdown_story::DropdownStory;
|
||||
use input_story::InputStory;
|
||||
use picker_story::PickerStory;
|
||||
|
|
@ -55,7 +54,7 @@ impl StoryContainer {
|
|||
}
|
||||
|
||||
impl RenderOnce for StoryContainer {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
fn render(self, _: &mut WindowContext) -> impl IntoElement {
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
|
|
|
|||
|
|
@ -12,4 +12,4 @@ serde_json = "1"
|
|||
wry = "0"
|
||||
smallvec = "1.13.2"
|
||||
windows = "0.57.0"
|
||||
unicode-segmentation = "1.11.0"
|
||||
unicode-segmentation = "1.11.0"
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
use crate::{
|
||||
button::ButtonSize,
|
||||
label::Label,
|
||||
stock::h_flex,
|
||||
theme::{ActiveTheme, Colorize},
|
||||
Disableable,
|
||||
};
|
||||
use gpui::{
|
||||
div, prelude::FluentBuilder as _, px, ClickEvent, Div, InteractiveElement, IntoElement,
|
||||
div, prelude::FluentBuilder as _, ClickEvent, Div, InteractiveElement, IntoElement,
|
||||
ParentElement as _, RenderOnce, SharedString, Stateful, StatefulInteractiveElement,
|
||||
Styled as _, WindowContext,
|
||||
};
|
||||
|
|
@ -141,9 +140,8 @@ impl RenderOnce for Switch {
|
|||
.when_some(
|
||||
self.on_click.filter(|_| !self.disabled),
|
||||
|this, on_click| {
|
||||
this.on_click(move |ev, cx| {
|
||||
on_click(ev, cx);
|
||||
})
|
||||
cx.stop_propagation();
|
||||
this.on_click(move |ev, cx| on_click(ev, cx))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
use gpui::*;
|
||||
use notification::{NotificationHandle, NotificationId};
|
||||
use prelude::FluentBuilder as _;
|
||||
|
||||
use std::sync::Arc;
|
||||
use ui::{
|
||||
button::ButtonSize,
|
||||
input,
|
||||
label::Label,
|
||||
picker,
|
||||
switch::{LabelSide, Switch},
|
||||
theme::{ActiveTheme, Theme},
|
||||
title_bar::TitleBar,
|
||||
|
|
@ -15,12 +13,14 @@ use ui_story::Stories;
|
|||
use util::ResultExt as _;
|
||||
|
||||
mod app_state;
|
||||
mod notification;
|
||||
|
||||
pub use app_state::AppState;
|
||||
|
||||
pub struct Workspace {
|
||||
weak_self: WeakView<Self>,
|
||||
stories: View<Stories>,
|
||||
notifications: Vec<(NotificationId, Box<dyn NotificationHandle>)>,
|
||||
}
|
||||
|
||||
impl Workspace {
|
||||
|
|
@ -34,6 +34,7 @@ impl Workspace {
|
|||
Workspace {
|
||||
weak_self: weak_handle.clone(),
|
||||
stories: Stories::view(cx),
|
||||
notifications: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -80,6 +81,29 @@ impl Workspace {
|
|||
Ok(window)
|
||||
})
|
||||
}
|
||||
|
||||
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()),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
actions!(workspace, [Open, CloseWindow]);
|
||||
|
|
@ -110,10 +134,10 @@ impl Render for Workspace {
|
|||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.relative()
|
||||
.size_full()
|
||||
.flex()
|
||||
.flex_1()
|
||||
.flex_col()
|
||||
.size_full()
|
||||
.bg(cx.theme().background)
|
||||
.text_color(cx.theme().foreground)
|
||||
.gap_4()
|
||||
|
|
@ -159,6 +183,7 @@ impl Render for Workspace {
|
|||
),
|
||||
),
|
||||
)
|
||||
.children(self.render_notifications(cx))
|
||||
.child(div().flex().px_4().gap_2().child(self.stories.clone()))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
222
crates/workspace/src/notification.rs
Normal file
222
crates/workspace/src/notification.rs
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
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, StyledExt};
|
||||
|
||||
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(¬ification, {
|
||||
let id = id.clone();
|
||||
move |this, _, _: &DismissEvent, cx| {
|
||||
this.dismiss_notification_internal(&id, cx);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
self.notifications.push((id, Box::new(notification)));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
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 dismiss_notification(&mut self, id: &NotificationId, cx: &mut ViewContext<Self>) {
|
||||
self.dismiss_notification_internal(id, cx)
|
||||
}
|
||||
|
||||
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 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()
|
||||
.elevation_3(cx)
|
||||
.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))),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue