logiguard fork: GPUI xdg-activation keyboard-focus serial fix
Some checks failed
Update All Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Triage Project Sync (#84) / Sync triage project (push) Has been cancelled
release_nightly / notify_on_failure (push) Has been cancelled
release_nightly / check_style (push) Has been cancelled
release_nightly / run_tests_windows (push) Has been cancelled
release_nightly / clippy_windows (push) Has been cancelled
release_nightly / bundle_linux_aarch64 (push) Has been cancelled
release_nightly / bundle_linux_x86_64 (push) Has been cancelled
release_nightly / bundle_mac_aarch64 (push) Has been cancelled
release_nightly / bundle_mac_x86_64 (push) Has been cancelled
release_nightly / bundle_windows_aarch64 (push) Has been cancelled
release_nightly / bundle_windows_x86_64 (push) Has been cancelled
release_nightly / build_nix_linux_x86_64 (push) Has been cancelled
release_nightly / build_nix_mac_aarch64 (push) Has been cancelled
release_nightly / update_nightly_tag (push) Has been cancelled
Hotfix Review Monitor / check-hotfix-reviews (push) Has been cancelled
Stale PR Review Reminder / check-stale-prs (push) Has been cancelled
Update Weekly Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Bump collab-staging Tag / update-collab-staging-tag (push) Has been cancelled
compliance_check / scheduled_compliance_check (push) Has been cancelled
Some checks failed
Update All Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Triage Project Sync (#84) / Sync triage project (push) Has been cancelled
release_nightly / notify_on_failure (push) Has been cancelled
release_nightly / check_style (push) Has been cancelled
release_nightly / run_tests_windows (push) Has been cancelled
release_nightly / clippy_windows (push) Has been cancelled
release_nightly / bundle_linux_aarch64 (push) Has been cancelled
release_nightly / bundle_linux_x86_64 (push) Has been cancelled
release_nightly / bundle_mac_aarch64 (push) Has been cancelled
release_nightly / bundle_mac_x86_64 (push) Has been cancelled
release_nightly / bundle_windows_aarch64 (push) Has been cancelled
release_nightly / bundle_windows_x86_64 (push) Has been cancelled
release_nightly / build_nix_linux_x86_64 (push) Has been cancelled
release_nightly / build_nix_mac_aarch64 (push) Has been cancelled
release_nightly / update_nightly_tag (push) Has been cancelled
Hotfix Review Monitor / check-hotfix-reviews (push) Has been cancelled
Stale PR Review Reminder / check-stale-prs (push) Has been cancelled
Update Weekly Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Bump collab-staging Tag / update-collab-staging-tag (push) Has been cancelled
compliance_check / scheduled_compliance_check (push) Has been cancelled
Single-commit orphan branch: full zed-industries/zed @ 8c74db0 source tree with a 3-file patch applied (no upstream history). Patch (crates/gpui_linux/src/linux/wayland/): - serial.rs: add SerialKind::KeyboardEnter - client.rs: store wl_keyboard.enter serial; add latest_serial_of() - window.rs: activate() uses keyboard-enter serial (Mutter focus gate) Mutter honors window activation only when the token carries the keyboard- focus serial from wl_keyboard.enter; GPUI used a stale mouse-press serial. See docs/tray-window-focus-wayland.md in logiguard. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
42
crates/notifications/Cargo.toml
Normal file
42
crates/notifications/Cargo.toml
Normal file
@@ -0,0 +1,42 @@
|
||||
[package]
|
||||
name = "notifications"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/notifications.rs"
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
test-support = [
|
||||
"channel/test-support",
|
||||
|
||||
"gpui/test-support",
|
||||
"rpc/test-support",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
channel.workspace = true
|
||||
client.workspace = true
|
||||
futures-lite.workspace = true
|
||||
component.workspace = true
|
||||
gpui.workspace = true
|
||||
rpc.workspace = true
|
||||
sum_tree.workspace = true
|
||||
time.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
client = { workspace = true, features = ["test-support"] }
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
rpc = { workspace = true, features = ["test-support"] }
|
||||
util = { workspace = true, features = ["test-support"] }
|
||||
1
crates/notifications/LICENSE-GPL
Symbolic link
1
crates/notifications/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
428
crates/notifications/src/notification_store.rs
Normal file
428
crates/notifications/src/notification_store.rs
Normal file
@@ -0,0 +1,428 @@
|
||||
use anyhow::{Context as _, Result};
|
||||
use channel::ChannelStore;
|
||||
use client::{ChannelId, Client, UserStore};
|
||||
use futures_lite::stream::StreamExt;
|
||||
use gpui::{App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Global, Task};
|
||||
use rpc::{Notification, TypedEnvelope, proto};
|
||||
use std::{ops::Range, sync::Arc};
|
||||
use sum_tree::{Bias, Dimensions, SumTree};
|
||||
use time::OffsetDateTime;
|
||||
use util::ResultExt;
|
||||
|
||||
pub fn init(client: Arc<Client>, user_store: Entity<UserStore>, cx: &mut App) {
|
||||
let notification_store = cx.new(|cx| NotificationStore::new(client, user_store, cx));
|
||||
cx.set_global(GlobalNotificationStore(notification_store));
|
||||
}
|
||||
|
||||
struct GlobalNotificationStore(Entity<NotificationStore>);
|
||||
|
||||
impl Global for GlobalNotificationStore {}
|
||||
|
||||
pub struct NotificationStore {
|
||||
client: Arc<Client>,
|
||||
user_store: Entity<UserStore>,
|
||||
channel_store: Entity<ChannelStore>,
|
||||
notifications: SumTree<NotificationEntry>,
|
||||
loaded_all_notifications: bool,
|
||||
_watch_connection_status: Task<Option<()>>,
|
||||
_subscriptions: Vec<client::Subscription>,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub enum NotificationEvent {
|
||||
NotificationsUpdated {
|
||||
old_range: Range<usize>,
|
||||
new_count: usize,
|
||||
},
|
||||
NewNotification {
|
||||
entry: NotificationEntry,
|
||||
},
|
||||
NotificationRemoved {
|
||||
entry: NotificationEntry,
|
||||
},
|
||||
NotificationRead {
|
||||
entry: NotificationEntry,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct NotificationEntry {
|
||||
pub id: u64,
|
||||
pub notification: Notification,
|
||||
pub timestamp: OffsetDateTime,
|
||||
pub is_read: bool,
|
||||
pub response: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct NotificationSummary {
|
||||
max_id: u64,
|
||||
count: usize,
|
||||
unread_count: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
|
||||
struct Count(usize);
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
|
||||
struct NotificationId(u64);
|
||||
|
||||
impl NotificationStore {
|
||||
pub fn global(cx: &App) -> Entity<Self> {
|
||||
cx.global::<GlobalNotificationStore>().0.clone()
|
||||
}
|
||||
|
||||
pub fn new(client: Arc<Client>, user_store: Entity<UserStore>, cx: &mut Context<Self>) -> Self {
|
||||
let mut connection_status = client.status();
|
||||
let watch_connection_status = cx.spawn(async move |this, cx| {
|
||||
while let Some(status) = connection_status.next().await {
|
||||
let this = this.upgrade()?;
|
||||
match status {
|
||||
client::Status::Connected { .. } => {
|
||||
if let Some(task) = this.update(cx, |this, cx| this.handle_connect(cx)) {
|
||||
task.await.log_err()?;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
this.update(cx, |this, cx| this.handle_disconnect(cx));
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(())
|
||||
});
|
||||
|
||||
Self {
|
||||
channel_store: ChannelStore::global(cx),
|
||||
notifications: Default::default(),
|
||||
loaded_all_notifications: false,
|
||||
_watch_connection_status: watch_connection_status,
|
||||
_subscriptions: vec![
|
||||
client.add_message_handler(cx.weak_entity(), Self::handle_new_notification),
|
||||
client.add_message_handler(cx.weak_entity(), Self::handle_delete_notification),
|
||||
],
|
||||
user_store,
|
||||
client,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn notification_count(&self) -> usize {
|
||||
self.notifications.summary().count
|
||||
}
|
||||
|
||||
pub fn unread_notification_count(&self) -> usize {
|
||||
self.notifications.summary().unread_count
|
||||
}
|
||||
|
||||
// Get the nth newest notification.
|
||||
pub fn notification_at(&self, ix: usize) -> Option<&NotificationEntry> {
|
||||
let count = self.notifications.summary().count;
|
||||
if ix >= count {
|
||||
return None;
|
||||
}
|
||||
let ix = count - 1 - ix;
|
||||
let (.., item) = self
|
||||
.notifications
|
||||
.find::<Count, _>((), &Count(ix), Bias::Right);
|
||||
item
|
||||
}
|
||||
pub fn notification_for_id(&self, id: u64) -> Option<&NotificationEntry> {
|
||||
let (.., item) =
|
||||
self.notifications
|
||||
.find::<NotificationId, _>((), &NotificationId(id), Bias::Left);
|
||||
if let Some(item) = item
|
||||
&& item.id == id
|
||||
{
|
||||
return Some(item);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn load_more_notifications(
|
||||
&self,
|
||||
clear_old: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<Task<Result<()>>> {
|
||||
if self.loaded_all_notifications && !clear_old {
|
||||
return None;
|
||||
}
|
||||
|
||||
let before_id = if clear_old {
|
||||
None
|
||||
} else {
|
||||
self.notifications.first().map(|entry| entry.id)
|
||||
};
|
||||
let request = self.client.request(proto::GetNotifications { before_id });
|
||||
Some(cx.spawn(async move |this, cx| {
|
||||
let this = this
|
||||
.upgrade()
|
||||
.context("Notification store was dropped while loading notifications")?;
|
||||
|
||||
let response = request.await?;
|
||||
this.update(cx, |this, _| this.loaded_all_notifications = response.done);
|
||||
Self::add_notifications(
|
||||
this,
|
||||
response.notifications,
|
||||
AddNotificationsOptions {
|
||||
is_new: false,
|
||||
clear_old,
|
||||
includes_first: response.done,
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}))
|
||||
}
|
||||
|
||||
fn handle_connect(&mut self, cx: &mut Context<Self>) -> Option<Task<Result<()>>> {
|
||||
self.notifications = Default::default();
|
||||
cx.notify();
|
||||
self.load_more_notifications(true, cx)
|
||||
}
|
||||
|
||||
fn handle_disconnect(&mut self, cx: &mut Context<Self>) {
|
||||
cx.notify()
|
||||
}
|
||||
|
||||
async fn handle_new_notification(
|
||||
this: Entity<Self>,
|
||||
envelope: TypedEnvelope<proto::AddNotification>,
|
||||
mut cx: AsyncApp,
|
||||
) -> Result<()> {
|
||||
Self::add_notifications(
|
||||
this,
|
||||
envelope.payload.notification.into_iter().collect(),
|
||||
AddNotificationsOptions {
|
||||
is_new: true,
|
||||
clear_old: false,
|
||||
includes_first: false,
|
||||
},
|
||||
&mut cx,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn handle_delete_notification(
|
||||
this: Entity<Self>,
|
||||
envelope: TypedEnvelope<proto::DeleteNotification>,
|
||||
mut cx: AsyncApp,
|
||||
) -> Result<()> {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.splice_notifications([(envelope.payload.notification_id, None)], false, cx);
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn add_notifications(
|
||||
this: Entity<Self>,
|
||||
notifications: Vec<proto::Notification>,
|
||||
options: AddNotificationsOptions,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<()> {
|
||||
let mut user_ids = Vec::new();
|
||||
|
||||
let notifications = notifications
|
||||
.into_iter()
|
||||
.filter_map(|message| {
|
||||
Some(NotificationEntry {
|
||||
id: message.id,
|
||||
is_read: message.is_read,
|
||||
timestamp: OffsetDateTime::from_unix_timestamp(message.timestamp as i64)
|
||||
.ok()?,
|
||||
notification: Notification::from_proto(&message)?,
|
||||
response: message.response,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if notifications.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
for entry in ¬ifications {
|
||||
match entry.notification {
|
||||
Notification::ChannelInvitation { inviter_id, .. } => {
|
||||
user_ids.push(inviter_id);
|
||||
}
|
||||
Notification::ContactRequest {
|
||||
sender_id: requester_id,
|
||||
} => {
|
||||
user_ids.push(requester_id);
|
||||
}
|
||||
Notification::ContactRequestAccepted {
|
||||
responder_id: contact_id,
|
||||
} => {
|
||||
user_ids.push(contact_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let user_store = this.read_with(cx, |this, _| this.user_store.clone());
|
||||
|
||||
user_store
|
||||
.update(cx, |store, cx| store.get_users(user_ids, cx))
|
||||
.await?;
|
||||
this.update(cx, |this, cx| {
|
||||
if options.clear_old {
|
||||
cx.emit(NotificationEvent::NotificationsUpdated {
|
||||
old_range: 0..this.notifications.summary().count,
|
||||
new_count: 0,
|
||||
});
|
||||
this.notifications = SumTree::default();
|
||||
this.loaded_all_notifications = false;
|
||||
}
|
||||
|
||||
if options.includes_first {
|
||||
this.loaded_all_notifications = true;
|
||||
}
|
||||
|
||||
this.splice_notifications(
|
||||
notifications
|
||||
.into_iter()
|
||||
.map(|notification| (notification.id, Some(notification))),
|
||||
options.is_new,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn splice_notifications(
|
||||
&mut self,
|
||||
notifications: impl IntoIterator<Item = (u64, Option<NotificationEntry>)>,
|
||||
is_new: bool,
|
||||
cx: &mut Context<NotificationStore>,
|
||||
) {
|
||||
let mut cursor = self
|
||||
.notifications
|
||||
.cursor::<Dimensions<NotificationId, Count>>(());
|
||||
let mut new_notifications = SumTree::default();
|
||||
let mut old_range = 0..0;
|
||||
|
||||
for (i, (id, new_notification)) in notifications.into_iter().enumerate() {
|
||||
new_notifications.append(cursor.slice(&NotificationId(id), Bias::Left), ());
|
||||
|
||||
if i == 0 {
|
||||
old_range.start = cursor.start().1.0;
|
||||
}
|
||||
|
||||
let old_notification = cursor.item();
|
||||
if let Some(old_notification) = old_notification {
|
||||
if old_notification.id == id {
|
||||
cursor.next();
|
||||
|
||||
if let Some(new_notification) = &new_notification {
|
||||
if new_notification.is_read {
|
||||
cx.emit(NotificationEvent::NotificationRead {
|
||||
entry: new_notification.clone(),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
cx.emit(NotificationEvent::NotificationRemoved {
|
||||
entry: old_notification.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if let Some(new_notification) = &new_notification
|
||||
&& is_new
|
||||
{
|
||||
cx.emit(NotificationEvent::NewNotification {
|
||||
entry: new_notification.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(notification) = new_notification {
|
||||
new_notifications.push(notification, ());
|
||||
}
|
||||
}
|
||||
|
||||
old_range.end = cursor.start().1.0;
|
||||
let new_count = new_notifications.summary().count - old_range.start;
|
||||
new_notifications.append(cursor.suffix(), ());
|
||||
drop(cursor);
|
||||
|
||||
self.notifications = new_notifications;
|
||||
cx.emit(NotificationEvent::NotificationsUpdated {
|
||||
old_range,
|
||||
new_count,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn respond_to_notification(
|
||||
&mut self,
|
||||
notification: Notification,
|
||||
response: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match notification {
|
||||
Notification::ContactRequest { sender_id } => {
|
||||
self.user_store
|
||||
.update(cx, |store, cx| {
|
||||
store.respond_to_contact_request(sender_id, response, cx)
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
Notification::ChannelInvitation { channel_id, .. } => {
|
||||
self.channel_store
|
||||
.update(cx, |store, cx| {
|
||||
store.respond_to_channel_invite(ChannelId(channel_id), response, cx)
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<NotificationEvent> for NotificationStore {}
|
||||
|
||||
impl sum_tree::Item for NotificationEntry {
|
||||
type Summary = NotificationSummary;
|
||||
|
||||
fn summary(&self, _cx: ()) -> Self::Summary {
|
||||
NotificationSummary {
|
||||
max_id: self.id,
|
||||
count: 1,
|
||||
unread_count: if self.is_read { 0 } else { 1 },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl sum_tree::ContextLessSummary for NotificationSummary {
|
||||
fn zero() -> Self {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
fn add_summary(&mut self, summary: &Self) {
|
||||
self.max_id = self.max_id.max(summary.max_id);
|
||||
self.count += summary.count;
|
||||
self.unread_count += summary.unread_count;
|
||||
}
|
||||
}
|
||||
|
||||
impl sum_tree::Dimension<'_, NotificationSummary> for NotificationId {
|
||||
fn zero(_cx: ()) -> Self {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
fn add_summary(&mut self, summary: &NotificationSummary, _: ()) {
|
||||
debug_assert!(summary.max_id > self.0);
|
||||
self.0 = summary.max_id;
|
||||
}
|
||||
}
|
||||
|
||||
impl sum_tree::Dimension<'_, NotificationSummary> for Count {
|
||||
fn zero(_cx: ()) -> Self {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
fn add_summary(&mut self, summary: &NotificationSummary, _: ()) {
|
||||
self.0 += summary.count;
|
||||
}
|
||||
}
|
||||
|
||||
struct AddNotificationsOptions {
|
||||
is_new: bool,
|
||||
clear_old: bool,
|
||||
includes_first: bool,
|
||||
}
|
||||
4
crates/notifications/src/notifications.rs
Normal file
4
crates/notifications/src/notifications.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
mod notification_store;
|
||||
|
||||
pub use notification_store::*;
|
||||
pub mod status_toast;
|
||||
257
crates/notifications/src/status_toast.rs
Normal file
257
crates/notifications/src/status_toast.rs
Normal file
@@ -0,0 +1,257 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, IntoElement};
|
||||
use ui::{Tooltip, prelude::*};
|
||||
use workspace::{ToastAction, ToastView};
|
||||
use zed_actions::toast;
|
||||
|
||||
#[derive(RegisterComponent)]
|
||||
pub struct StatusToast {
|
||||
icon: Option<Icon>,
|
||||
text: SharedString,
|
||||
action: Option<ToastAction>,
|
||||
show_dismiss: bool,
|
||||
auto_dismiss: bool,
|
||||
this_handle: Entity<Self>,
|
||||
focus_handle: FocusHandle,
|
||||
}
|
||||
|
||||
impl StatusToast {
|
||||
pub fn new(
|
||||
text: impl Into<SharedString>,
|
||||
cx: &mut App,
|
||||
f: impl FnOnce(Self, &mut Context<Self>) -> Self,
|
||||
) -> Entity<Self> {
|
||||
cx.new(|cx| {
|
||||
let focus_handle = cx.focus_handle();
|
||||
|
||||
f(
|
||||
Self {
|
||||
text: text.into(),
|
||||
icon: None,
|
||||
action: None,
|
||||
show_dismiss: false,
|
||||
auto_dismiss: true,
|
||||
this_handle: cx.entity(),
|
||||
focus_handle,
|
||||
},
|
||||
cx,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn icon(mut self, icon: Icon) -> Self {
|
||||
self.icon = Some(icon);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn auto_dismiss(mut self, auto_dismiss: bool) -> Self {
|
||||
self.auto_dismiss = auto_dismiss;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn action(
|
||||
mut self,
|
||||
label: impl Into<SharedString>,
|
||||
f: impl Fn(&mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
let this_handle = self.this_handle.clone();
|
||||
self.action = Some(ToastAction::new(
|
||||
label.into(),
|
||||
Some(Rc::new(move |window, cx| {
|
||||
this_handle.update(cx, |_, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
});
|
||||
f(window, cx);
|
||||
})),
|
||||
));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn dismiss_button(mut self, show: bool) -> Self {
|
||||
self.show_dismiss = show;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for StatusToast {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let has_action_or_dismiss = self.action.is_some() || self.show_dismiss;
|
||||
|
||||
h_flex()
|
||||
.id("status-toast")
|
||||
.elevation_3(cx)
|
||||
.gap_2()
|
||||
.py_1p5()
|
||||
.pl_2p5()
|
||||
.map(|this| {
|
||||
if has_action_or_dismiss {
|
||||
this.pr_1p5()
|
||||
} else {
|
||||
this.pr_2p5()
|
||||
}
|
||||
})
|
||||
.flex_none()
|
||||
.bg(cx.theme().colors().surface_background)
|
||||
.shadow_lg()
|
||||
.when_some(self.icon.clone(), |this, icon| this.child(icon))
|
||||
.child(Label::new(self.text.clone()).color(Color::Default))
|
||||
.when_some(self.action.as_ref(), |this, action| {
|
||||
this.child(
|
||||
Button::new(action.id.clone(), action.label.clone())
|
||||
.tooltip(Tooltip::for_action_title(
|
||||
action.label.clone(),
|
||||
&toast::RunAction,
|
||||
))
|
||||
.color(Color::Muted)
|
||||
.when_some(action.on_click.clone(), |el, handler| {
|
||||
el.on_click(move |_click_event, window, cx| handler(window, cx))
|
||||
}),
|
||||
)
|
||||
})
|
||||
.when(self.show_dismiss, |this| {
|
||||
let handle = self.this_handle.clone();
|
||||
this.child(
|
||||
IconButton::new("dismiss", IconName::Close)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.tooltip(Tooltip::text("Dismiss"))
|
||||
.on_click(move |_click_event, _window, cx| {
|
||||
handle.update(cx, |_, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
});
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ToastView for StatusToast {
|
||||
fn action(&self) -> Option<ToastAction> {
|
||||
self.action.clone()
|
||||
}
|
||||
|
||||
fn auto_dismiss(&self) -> bool {
|
||||
self.auto_dismiss
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for StatusToast {
|
||||
fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for StatusToast {}
|
||||
|
||||
impl Component for StatusToast {
|
||||
fn scope() -> ComponentScope {
|
||||
ComponentScope::Notification
|
||||
}
|
||||
|
||||
fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
|
||||
let text_example = StatusToast::new("Operation completed", cx, |this, _| this);
|
||||
|
||||
let action_example = StatusToast::new("Update ready to install", cx, |this, _cx| {
|
||||
this.action("Restart", |_, _| {})
|
||||
});
|
||||
|
||||
let dismiss_button_example =
|
||||
StatusToast::new("Dismiss Button", cx, |this, _| this.dismiss_button(true));
|
||||
|
||||
let icon_example = StatusToast::new(
|
||||
"Nathan Sobo accepted your contact request",
|
||||
cx,
|
||||
|this, _| {
|
||||
this.icon(
|
||||
Icon::new(IconName::Check)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
let success_example = StatusToast::new("Pushed 4 changes to `zed/main`", cx, |this, _| {
|
||||
this.icon(
|
||||
Icon::new(IconName::Check)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Success),
|
||||
)
|
||||
});
|
||||
|
||||
let error_example = StatusToast::new(
|
||||
"git push: Couldn't find remote origin `iamnbutler/zed`",
|
||||
cx,
|
||||
|this, _cx| {
|
||||
this.icon(
|
||||
Icon::new(IconName::XCircle)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Error),
|
||||
)
|
||||
.action("More Info", |_, _| {})
|
||||
},
|
||||
);
|
||||
|
||||
let warning_example = StatusToast::new("You have outdated settings", cx, |this, _cx| {
|
||||
this.icon(
|
||||
Icon::new(IconName::Warning)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Warning),
|
||||
)
|
||||
.action("More Info", |_, _| {})
|
||||
});
|
||||
|
||||
let pr_example =
|
||||
StatusToast::new("`zed/new-notification-system` created!", cx, |this, _cx| {
|
||||
this.icon(
|
||||
Icon::new(IconName::GitBranch)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.action("Open Pull Request", |_, cx| {
|
||||
cx.open_url("https://github.com/")
|
||||
})
|
||||
});
|
||||
|
||||
Some(
|
||||
v_flex()
|
||||
.gap_6()
|
||||
.p_4()
|
||||
.children(vec![
|
||||
example_group_with_title(
|
||||
"Basic Toast",
|
||||
vec![
|
||||
single_example("Text", div().child(text_example).into_any_element()),
|
||||
single_example(
|
||||
"Action",
|
||||
div().child(action_example).into_any_element(),
|
||||
),
|
||||
single_example("Icon", div().child(icon_example).into_any_element()),
|
||||
single_example(
|
||||
"Dismiss Button",
|
||||
div().child(dismiss_button_example).into_any_element(),
|
||||
),
|
||||
],
|
||||
),
|
||||
example_group_with_title(
|
||||
"Examples",
|
||||
vec![
|
||||
single_example(
|
||||
"Success",
|
||||
div().child(success_example).into_any_element(),
|
||||
),
|
||||
single_example("Error", div().child(error_example).into_any_element()),
|
||||
single_example(
|
||||
"Warning",
|
||||
div().child(warning_example).into_any_element(),
|
||||
),
|
||||
single_example("Create PR", div().child(pr_example).into_any_element()),
|
||||
],
|
||||
)
|
||||
.vertical(),
|
||||
])
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user