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.
|
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
|
## Demo
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ use super::story_case;
|
||||||
pub struct ButtonStory;
|
pub struct ButtonStory;
|
||||||
|
|
||||||
impl ButtonStory {
|
impl ButtonStory {
|
||||||
fn on_click(ev: &ClickEvent, cx: &mut WindowContext) {
|
fn on_click(ev: &ClickEvent, _: &mut WindowContext) {
|
||||||
println!("Button clicked! {:?}", ev);
|
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 button_story;
|
||||||
mod checkbox_story;
|
mod checkbox_story;
|
||||||
mod dropdown_story;
|
mod dropdown_story;
|
||||||
|
|
@ -14,6 +5,13 @@ mod input_story;
|
||||||
mod picker_story;
|
mod picker_story;
|
||||||
mod switch_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::{
|
use ui::{
|
||||||
divider::Divider,
|
divider::Divider,
|
||||||
label::Label,
|
label::Label,
|
||||||
|
|
@ -22,6 +20,7 @@ use ui::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use button_story::ButtonStory;
|
use button_story::ButtonStory;
|
||||||
|
use checkbox_story::CheckboxStory;
|
||||||
use dropdown_story::DropdownStory;
|
use dropdown_story::DropdownStory;
|
||||||
use input_story::InputStory;
|
use input_story::InputStory;
|
||||||
use picker_story::PickerStory;
|
use picker_story::PickerStory;
|
||||||
|
|
@ -55,7 +54,7 @@ impl StoryContainer {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderOnce for StoryContainer {
|
impl RenderOnce for StoryContainer {
|
||||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
fn render(self, _: &mut WindowContext) -> impl IntoElement {
|
||||||
div()
|
div()
|
||||||
.flex()
|
.flex()
|
||||||
.flex_col()
|
.flex_col()
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,11 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
button::ButtonSize,
|
button::ButtonSize,
|
||||||
label::Label,
|
|
||||||
stock::h_flex,
|
stock::h_flex,
|
||||||
theme::{ActiveTheme, Colorize},
|
theme::{ActiveTheme, Colorize},
|
||||||
Disableable,
|
Disableable,
|
||||||
};
|
};
|
||||||
use gpui::{
|
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,
|
ParentElement as _, RenderOnce, SharedString, Stateful, StatefulInteractiveElement,
|
||||||
Styled as _, WindowContext,
|
Styled as _, WindowContext,
|
||||||
};
|
};
|
||||||
|
|
@ -141,9 +140,8 @@ impl RenderOnce for Switch {
|
||||||
.when_some(
|
.when_some(
|
||||||
self.on_click.filter(|_| !self.disabled),
|
self.on_click.filter(|_| !self.disabled),
|
||||||
|this, on_click| {
|
|this, on_click| {
|
||||||
this.on_click(move |ev, cx| {
|
cx.stop_propagation();
|
||||||
on_click(ev, cx);
|
this.on_click(move |ev, cx| on_click(ev, cx))
|
||||||
})
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,10 @@
|
||||||
use gpui::*;
|
use gpui::*;
|
||||||
|
use notification::{NotificationHandle, NotificationId};
|
||||||
use prelude::FluentBuilder as _;
|
use prelude::FluentBuilder as _;
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use ui::{
|
use ui::{
|
||||||
button::ButtonSize,
|
button::ButtonSize,
|
||||||
input,
|
|
||||||
label::Label,
|
|
||||||
picker,
|
|
||||||
switch::{LabelSide, Switch},
|
switch::{LabelSide, Switch},
|
||||||
theme::{ActiveTheme, Theme},
|
theme::{ActiveTheme, Theme},
|
||||||
title_bar::TitleBar,
|
title_bar::TitleBar,
|
||||||
|
|
@ -15,12 +13,14 @@ use ui_story::Stories;
|
||||||
use util::ResultExt as _;
|
use util::ResultExt as _;
|
||||||
|
|
||||||
mod app_state;
|
mod app_state;
|
||||||
|
mod notification;
|
||||||
|
|
||||||
pub use app_state::AppState;
|
pub use app_state::AppState;
|
||||||
|
|
||||||
pub struct Workspace {
|
pub struct Workspace {
|
||||||
weak_self: WeakView<Self>,
|
weak_self: WeakView<Self>,
|
||||||
stories: View<Stories>,
|
stories: View<Stories>,
|
||||||
|
notifications: Vec<(NotificationId, Box<dyn NotificationHandle>)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Workspace {
|
impl Workspace {
|
||||||
|
|
@ -34,6 +34,7 @@ impl Workspace {
|
||||||
Workspace {
|
Workspace {
|
||||||
weak_self: weak_handle.clone(),
|
weak_self: weak_handle.clone(),
|
||||||
stories: Stories::view(cx),
|
stories: Stories::view(cx),
|
||||||
|
notifications: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -80,6 +81,29 @@ impl Workspace {
|
||||||
Ok(window)
|
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]);
|
actions!(workspace, [Open, CloseWindow]);
|
||||||
|
|
@ -110,10 +134,10 @@ impl Render for Workspace {
|
||||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
div()
|
div()
|
||||||
.relative()
|
.relative()
|
||||||
|
.size_full()
|
||||||
.flex()
|
.flex()
|
||||||
.flex_1()
|
.flex_1()
|
||||||
.flex_col()
|
.flex_col()
|
||||||
.size_full()
|
|
||||||
.bg(cx.theme().background)
|
.bg(cx.theme().background)
|
||||||
.text_color(cx.theme().foreground)
|
.text_color(cx.theme().foreground)
|
||||||
.gap_4()
|
.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()))
|
.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