ZIm/crates/notifications/src/status_toast.rs
Julia Ryan 2b94a35aaa
Rework git toasts (#26420)
The notifications from git output could take up variable amounts of
screen space, and they were quite obnoxious when a git command printed
lots of output, such as fetching many new branches or verbose push
hooks.

This change makes the push/pull/fetch buttons trigger a small
notification toast, based on the output of the command that was ran. For
errors or commands with more output the user may want to see, there's an
"Open Log" button which opens a new buffer with the output of that
command.

It also uses this behavior for long error notifications for other git
commands like `commit` and `checkout`. The output of those commands can
be quite long due to arbitrary githooks running.

Release Notes:

- N/A

---------

Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
2025-03-11 21:39:29 +00:00

203 lines
6.3 KiB
Rust

use std::rc::Rc;
use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, IntoElement};
use ui::{prelude::*, Tooltip};
use workspace::{ToastAction, ToastView};
#[derive(Clone, Copy)]
pub struct ToastIcon {
icon: IconName,
color: Color,
}
impl ToastIcon {
pub fn new(icon: IconName) -> Self {
Self {
icon,
color: Color::default(),
}
}
pub fn color(mut self, color: Color) -> Self {
self.color = color;
self
}
}
impl From<IconName> for ToastIcon {
fn from(icon: IconName) -> Self {
Self {
icon,
color: Color::default(),
}
}
}
#[derive(IntoComponent)]
#[component(scope = "Notification")]
pub struct StatusToast {
icon: Option<ToastIcon>,
text: SharedString,
action: Option<ToastAction>,
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,
this_handle: cx.entity(),
focus_handle,
},
cx,
)
})
}
pub fn icon(mut self, icon: ToastIcon) -> Self {
self.icon = Some(icon);
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
}
}
impl Render for StatusToast {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
h_flex()
.id("status-toast")
.elevation_3(cx)
.gap_2()
.py_1p5()
.px_2p5()
.flex_none()
.bg(cx.theme().colors().surface_background)
.shadow_lg()
.items_center()
.when_some(self.icon.as_ref(), |this, icon| {
this.child(Icon::new(icon.icon).color(icon.color))
})
.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(),
&workspace::RunAction,
))
.color(Color::Muted)
.when_some(action.on_click.clone(), |el, handler| {
el.on_click(move |_click_event, window, cx| handler(window, cx))
}),
)
})
}
}
impl ToastView for StatusToast {
fn action(&self) -> Option<ToastAction> {
self.action.clone()
}
}
impl Focusable for StatusToast {
fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
self.focus_handle.clone()
}
}
impl EventEmitter<DismissEvent> for StatusToast {}
impl ComponentPreview for StatusToast {
fn preview(_window: &mut Window, cx: &mut App) -> 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 icon_example = StatusToast::new(
"Nathan Sobo accepted your contact request",
cx,
|this, _| this.icon(ToastIcon::new(IconName::Check).color(Color::Muted)),
);
let success_example = StatusToast::new("Pushed 4 changes to `zed/main`", cx, |this, _| {
this.icon(ToastIcon::new(IconName::Check).color(Color::Success))
});
let error_example = StatusToast::new(
"git push: Couldn't find remote origin `iamnbutler/zed`",
cx,
|this, _cx| {
this.icon(ToastIcon::new(IconName::XCircle).color(Color::Error))
.action("More Info", |_, _| {})
},
);
let warning_example = StatusToast::new("You have outdated settings", cx, |this, _cx| {
this.icon(ToastIcon::new(IconName::Warning).color(Color::Warning))
.action("More Info", |_, _| {})
});
let pr_example =
StatusToast::new("`zed/new-notification-system` created!", cx, |this, _cx| {
this.icon(ToastIcon::new(IconName::GitBranchSmall).color(Color::Muted))
.action("Open Pull Request", |_, cx| {
cx.open_url("https://github.com/")
})
});
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()),
],
),
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()
}
}