Add Notification (#4)

This commit is contained in:
Floyd Wang 2024-06-28 14:08:06 +08:00 committed by GitHub
parent 5393580789
commit 11901dc1e4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 267 additions and 23 deletions

View file

@ -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

View file

@ -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);
}
}

View file

@ -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()

View file

@ -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"

View file

@ -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))
},
)
}

View file

@ -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()))
}
}

View 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(&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();
}
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))),
),
)
}
}