split toast into alert and toast
This commit is contained in:
@@ -0,0 +1,330 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
Animation, AnimationExt as _, App, Context, ElementId, Entity, InteractiveElement as _,
|
||||
IntoElement, ParentElement as _, RenderOnce, SharedString, StatefulInteractiveElement as _,
|
||||
Styled as _, Window, deferred, div, ease_out_quint, px,
|
||||
};
|
||||
use gpui_native_icons::{IconRole, icon};
|
||||
use gpui_native_theme::{ActiveTheme as _, ThemeVariant};
|
||||
|
||||
use crate::common::DismissHandler;
|
||||
use crate::common::v_flex;
|
||||
|
||||
/// How alarming an [`Alert`] is; picks the icon and the accent color.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum AlertSeverity {
|
||||
/// The default; uses the info role.
|
||||
#[default]
|
||||
Info,
|
||||
/// Uses the success role.
|
||||
Success,
|
||||
/// Uses the warning role.
|
||||
Warning,
|
||||
/// Uses the destructive role.
|
||||
Danger,
|
||||
}
|
||||
|
||||
/// A static callout banner.
|
||||
#[derive(Clone, IntoElement)]
|
||||
pub struct Alert {
|
||||
id: ElementId,
|
||||
title: Option<SharedString>,
|
||||
message: Option<SharedString>,
|
||||
severity: AlertSeverity,
|
||||
icon: Option<IconRole>,
|
||||
show_icon: bool,
|
||||
on_dismiss: Option<DismissHandler>,
|
||||
}
|
||||
|
||||
impl Alert {
|
||||
pub fn new<I>(id: I) -> Self
|
||||
where
|
||||
I: Into<ElementId>,
|
||||
{
|
||||
Self {
|
||||
id: id.into(),
|
||||
title: None,
|
||||
message: None,
|
||||
severity: AlertSeverity::Info,
|
||||
icon: None,
|
||||
show_icon: true,
|
||||
on_dismiss: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the primary line.
|
||||
pub fn title<L>(mut self, title: L) -> Self
|
||||
where
|
||||
L: Into<SharedString>,
|
||||
{
|
||||
self.title = Some(title.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets a secondary line under the title.
|
||||
pub fn message<L>(mut self, message: L) -> Self
|
||||
where
|
||||
L: Into<SharedString>,
|
||||
{
|
||||
self.message = Some(message.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets how alarming the alert is.
|
||||
pub fn severity(mut self, severity: AlertSeverity) -> Self {
|
||||
self.severity = severity;
|
||||
self
|
||||
}
|
||||
|
||||
/// Overrides the severity-derived icon with this glyph.
|
||||
pub fn icon(mut self, role: IconRole) -> Self {
|
||||
self.icon = Some(role);
|
||||
self
|
||||
}
|
||||
|
||||
/// Hides the icon entirely.
|
||||
pub fn no_icon(mut self) -> Self {
|
||||
self.show_icon = false;
|
||||
self
|
||||
}
|
||||
|
||||
/// Runs when the user dismisses the alert.
|
||||
pub fn on_dismiss<F>(mut self, handler: F) -> Self
|
||||
where
|
||||
F: Fn(&mut App) + 'static,
|
||||
{
|
||||
self.on_dismiss = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
|
||||
fn rekey(mut self, id: usize) -> Self {
|
||||
self.id = ElementId::from(id);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Alert {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let theme = cx.theme();
|
||||
|
||||
let ramp = match self.severity {
|
||||
AlertSeverity::Info => theme.ramps.info,
|
||||
AlertSeverity::Success => theme.ramps.success,
|
||||
AlertSeverity::Warning => theme.ramps.warning,
|
||||
AlertSeverity::Danger => theme.ramps.destructive,
|
||||
};
|
||||
|
||||
let icon_role = self.icon.unwrap_or(match self.severity {
|
||||
AlertSeverity::Info => IconRole::Info,
|
||||
AlertSeverity::Success => IconRole::Check,
|
||||
AlertSeverity::Warning | AlertSeverity::Danger => IconRole::Warning,
|
||||
});
|
||||
|
||||
let foreground = theme.colors.foreground;
|
||||
let muted_foreground = theme.colors.muted_foreground;
|
||||
let muted = theme.colors.muted;
|
||||
let surface = theme.colors.surface;
|
||||
let border = theme.colors.border;
|
||||
|
||||
let mut elevation = theme.elevations.popover;
|
||||
if theme.variant == ThemeVariant::Light {
|
||||
elevation.color = elevation.color.alpha(0.12);
|
||||
}
|
||||
let shadows = elevation.shadows();
|
||||
|
||||
let text_size = theme.sizes.small.text_size;
|
||||
let icon_size = theme.sizes.small.icon_size;
|
||||
|
||||
div()
|
||||
.w_full()
|
||||
.flex()
|
||||
.items_start()
|
||||
.gap_2()
|
||||
.px_3()
|
||||
.py_2()
|
||||
.rounded(theme.radius.overlay)
|
||||
.bg(surface)
|
||||
.border_1()
|
||||
.border_color(border)
|
||||
.shadow(shadows)
|
||||
.when(self.show_icon, |alert| {
|
||||
alert.child(
|
||||
div()
|
||||
.flex_none()
|
||||
.flex()
|
||||
.items_center()
|
||||
.h(window.line_height())
|
||||
.child(icon(icon_role).size(icon_size).color(ramp.base)),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
v_flex()
|
||||
.flex_1()
|
||||
.gap_0p5()
|
||||
.when_some(self.title.clone(), |alert, title| {
|
||||
alert.child(
|
||||
div()
|
||||
.text_size(text_size)
|
||||
.text_color(foreground)
|
||||
.when(self.message.is_some(), |title| {
|
||||
title.font_weight(gpui::FontWeight::SEMIBOLD)
|
||||
})
|
||||
.child(title),
|
||||
)
|
||||
})
|
||||
.when_some(self.message.clone(), |alert, message| {
|
||||
// With no title, the message is the primary line.
|
||||
let color = if self.title.is_some() {
|
||||
muted_foreground
|
||||
} else {
|
||||
foreground
|
||||
};
|
||||
alert.child(div().text_size(text_size).text_color(color).child(message))
|
||||
}),
|
||||
)
|
||||
.when_some(self.on_dismiss, |alert, on_dismiss| {
|
||||
alert.child(
|
||||
div()
|
||||
.id((self.id, "alert-close"))
|
||||
.flex_none()
|
||||
.cursor_pointer()
|
||||
.p_0p5()
|
||||
.rounded(px(4.))
|
||||
.hover(|style| style.bg(muted))
|
||||
.child(
|
||||
icon(IconRole::Close)
|
||||
.size(theme.sizes.small.icon_size * 0.75)
|
||||
.color(foreground.alpha(0.55)),
|
||||
)
|
||||
.on_click(move |_, _window, cx| on_dismiss(cx)),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// One pushed alert, with its identity in the stack.
|
||||
#[derive(Clone)]
|
||||
pub struct AlertEntry {
|
||||
pub id: usize,
|
||||
pub alert: Alert,
|
||||
on_dismiss: Option<DismissHandler>,
|
||||
}
|
||||
|
||||
/// The stack of pushed alerts.
|
||||
#[derive(Default)]
|
||||
pub struct Alerts {
|
||||
entries: Vec<AlertEntry>,
|
||||
next_id: usize,
|
||||
}
|
||||
|
||||
impl Alerts {
|
||||
/// Pushes an alert onto the stack.
|
||||
pub fn push(&mut self, mut alert: Alert, cx: &mut Context<Self>) -> usize {
|
||||
let id = self.next_id;
|
||||
self.next_id += 1;
|
||||
|
||||
let on_dismiss = alert.on_dismiss.take();
|
||||
let stack = cx.entity().downgrade();
|
||||
|
||||
alert.on_dismiss = Some(Rc::new(move |cx: &mut App| {
|
||||
stack.update(cx, |alerts, cx| alerts.dismiss(id, cx)).ok();
|
||||
}));
|
||||
|
||||
self.entries.push(AlertEntry {
|
||||
id,
|
||||
alert: alert.rekey(id),
|
||||
on_dismiss,
|
||||
});
|
||||
|
||||
cx.notify();
|
||||
|
||||
id
|
||||
}
|
||||
|
||||
/// Dismisses one alert by id, running its dismiss handler.
|
||||
pub fn dismiss(&mut self, id: usize, cx: &mut Context<Self>) {
|
||||
if let Some(position) = self.entries.iter().position(|entry| entry.id == id) {
|
||||
let entry = self.entries.remove(position);
|
||||
cx.notify();
|
||||
if let Some(on_dismiss) = &entry.on_dismiss {
|
||||
(on_dismiss.clone())(cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Dismisses every alert.
|
||||
pub fn clear(&mut self, cx: &mut Context<Self>) {
|
||||
if !self.entries.is_empty() {
|
||||
self.entries.clear();
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
/// The stacked alerts, oldest first.
|
||||
pub fn entries(&self) -> &[AlertEntry] {
|
||||
&self.entries
|
||||
}
|
||||
|
||||
/// How many alerts are stacked.
|
||||
pub fn count(&self) -> usize {
|
||||
self.entries.len()
|
||||
}
|
||||
}
|
||||
|
||||
/// The floating stack of pushed alerts, topmost right.
|
||||
#[derive(IntoElement)]
|
||||
pub struct AlertsView {
|
||||
alerts: Entity<Alerts>,
|
||||
id: ElementId,
|
||||
}
|
||||
|
||||
impl AlertsView {
|
||||
/// Binds the view to an alert stack.
|
||||
pub fn new<I>(id: I, alerts: Entity<Alerts>) -> Self
|
||||
where
|
||||
I: Into<ElementId>,
|
||||
{
|
||||
Self {
|
||||
id: id.into(),
|
||||
alerts,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for AlertsView {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let theme = cx.theme();
|
||||
let enter_duration = theme.motion.overlay;
|
||||
|
||||
let entries: Vec<AlertEntry> = self.alerts.read(cx).entries().to_vec();
|
||||
|
||||
deferred(
|
||||
div()
|
||||
.id(self.id)
|
||||
.absolute()
|
||||
.top_4()
|
||||
.right_4()
|
||||
.w_80()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.items_end()
|
||||
.gap_2()
|
||||
.children(entries.iter().rev().map(|entry| {
|
||||
let id = entry.id;
|
||||
let ease_slide = ease_out_quint();
|
||||
|
||||
div()
|
||||
.relative()
|
||||
.w_full()
|
||||
.child(entry.alert.clone())
|
||||
.with_animation(
|
||||
("alert-enter", id),
|
||||
Animation::new(enter_duration),
|
||||
move |layer, delta| layer.left(px(24. * (1. - ease_slide(delta)))),
|
||||
)
|
||||
})),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,9 @@
|
||||
pub use gpui_native_theme::Size;
|
||||
pub use gpui_native_utils::{h_flex, v_flex};
|
||||
|
||||
/// A handler told to clean up after a dismissed surface.
|
||||
pub type DismissHandler = std::rc::Rc<dyn Fn(&mut gpui::App)>;
|
||||
|
||||
/// A component that can be told which size to render at.
|
||||
pub trait Sizable: Sized {
|
||||
/// Sets the size.
|
||||
|
||||
@@ -42,6 +42,9 @@ pub mod dialog;
|
||||
#[cfg(feature = "menu")]
|
||||
pub mod menu;
|
||||
|
||||
#[cfg(feature = "alert")]
|
||||
pub mod alert;
|
||||
|
||||
#[cfg(feature = "popover")]
|
||||
pub mod popover;
|
||||
|
||||
|
||||
+216
-176
@@ -1,3 +1,7 @@
|
||||
//! Toasts: succinct, temporary messages at the bottom center of the window
|
||||
//! — Adwaita Toast, the macOS banner. One toast at a time: content, an
|
||||
//! optional action, and a close button; fades in, then fades out.
|
||||
|
||||
use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -10,34 +14,38 @@ use gpui::{
|
||||
use gpui_native_icons::{IconRole, icon};
|
||||
use gpui_native_theme::ActiveTheme as _;
|
||||
|
||||
use crate::common::v_flex;
|
||||
use crate::common::DismissHandler;
|
||||
|
||||
/// The dismiss handler of a [`Toast`].
|
||||
pub type DismissHandler = Rc<dyn Fn(&mut App)>;
|
||||
/// The action handler of a [`Toast`].
|
||||
pub type ActionHandler = Rc<dyn Fn(&mut App)>;
|
||||
|
||||
/// How alarming a [`Toast`] is; picks the icon and the accent color.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum ToastSeverity {
|
||||
/// The default; uses the primary accent.
|
||||
#[default]
|
||||
Info,
|
||||
/// Uses the success role.
|
||||
Success,
|
||||
/// Uses the warning role.
|
||||
Warning,
|
||||
/// Uses the destructive role.
|
||||
Danger,
|
||||
/// One user action offered by a toast.
|
||||
#[derive(Clone)]
|
||||
pub struct ToastAction {
|
||||
label: SharedString,
|
||||
handler: ActionHandler,
|
||||
}
|
||||
|
||||
/// Toast notification.
|
||||
impl ToastAction {
|
||||
/// Creates an action with this label.
|
||||
pub fn new<L, F>(label: L, handler: F) -> Self
|
||||
where
|
||||
L: Into<SharedString>,
|
||||
F: Fn(&mut App) + 'static,
|
||||
{
|
||||
Self {
|
||||
label: label.into(),
|
||||
handler: Rc::new(handler),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A toast notification.
|
||||
#[derive(Clone)]
|
||||
pub struct Toast {
|
||||
title: SharedString,
|
||||
message: Option<SharedString>,
|
||||
severity: ToastSeverity,
|
||||
/// The painted glyph; `None` derives it from the severity.
|
||||
message: SharedString,
|
||||
icon: Option<IconRole>,
|
||||
show_icon: bool,
|
||||
action: Option<ToastAction>,
|
||||
duration: Option<Duration>,
|
||||
on_dismiss: Option<DismissHandler>,
|
||||
}
|
||||
@@ -46,46 +54,29 @@ impl Toast {
|
||||
/// How long a toast stays up by default.
|
||||
pub const DURATION: Duration = Duration::from_secs(5);
|
||||
|
||||
/// Creates an informational toast with this title.
|
||||
pub fn new<L>(title: L) -> Self
|
||||
/// Creates a toast with this one-line content.
|
||||
pub fn new<L>(message: L) -> Self
|
||||
where
|
||||
L: Into<SharedString>,
|
||||
{
|
||||
Self {
|
||||
title: title.into(),
|
||||
message: None,
|
||||
severity: ToastSeverity::Info,
|
||||
message: message.into(),
|
||||
icon: None,
|
||||
show_icon: true,
|
||||
action: None,
|
||||
duration: Some(Self::DURATION),
|
||||
on_dismiss: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets a secondary line under the title.
|
||||
pub fn message<L>(mut self, message: L) -> Self
|
||||
where
|
||||
L: Into<SharedString>,
|
||||
{
|
||||
self.message = Some(message.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets how alarming the toast is.
|
||||
pub fn severity(mut self, severity: ToastSeverity) -> Self {
|
||||
self.severity = severity;
|
||||
self
|
||||
}
|
||||
|
||||
/// Overrides the severity-derived icon with this glyph.
|
||||
/// Shows this glyph before the content.
|
||||
pub fn icon(mut self, role: IconRole) -> Self {
|
||||
self.icon = Some(role);
|
||||
self
|
||||
}
|
||||
|
||||
/// Hides the icon entirely.
|
||||
pub fn no_icon(mut self) -> Self {
|
||||
self.show_icon = false;
|
||||
/// Offers the user this action.
|
||||
pub fn action(mut self, action: ToastAction) -> Self {
|
||||
self.action = Some(action);
|
||||
self
|
||||
}
|
||||
|
||||
@@ -95,7 +86,7 @@ impl Toast {
|
||||
self
|
||||
}
|
||||
|
||||
/// Runs when the toast is dismissed by any means.
|
||||
/// Runs when the toast is dismissed by any means, once it is fully gone.
|
||||
pub fn on_dismiss<F>(mut self, handler: F) -> Self
|
||||
where
|
||||
F: Fn(&mut App) + 'static,
|
||||
@@ -115,19 +106,24 @@ pub struct ToastEntry {
|
||||
/// The store of raised toasts.
|
||||
#[derive(Default)]
|
||||
pub struct Toasts {
|
||||
entries: Vec<ToastEntry>,
|
||||
current: Option<ToastEntry>,
|
||||
exiting: Option<ToastEntry>,
|
||||
next_id: usize,
|
||||
}
|
||||
|
||||
impl Toasts {
|
||||
/// Raises a toast, starting its auto-dismiss timer. Returns its id.
|
||||
/// Raises a toast, replacing the one on screen. Returns its id.
|
||||
pub fn push(&mut self, toast: Toast, cx: &mut Context<Self>) -> usize {
|
||||
let id = self.next_id;
|
||||
self.next_id += 1;
|
||||
|
||||
let duration = toast.duration;
|
||||
|
||||
self.entries.push(ToastEntry { id, toast });
|
||||
if let Some(previous) = self.current.take() {
|
||||
self.start_exit(previous, cx);
|
||||
}
|
||||
|
||||
self.current = Some(ToastEntry { id, toast });
|
||||
cx.notify();
|
||||
|
||||
if let Some(duration) = duration.filter(|duration| !duration.is_zero()) {
|
||||
@@ -141,34 +137,65 @@ impl Toasts {
|
||||
id
|
||||
}
|
||||
|
||||
/// Dismisses one toast by id, running its dismiss handler.
|
||||
/// Dismisses one toast by id.
|
||||
pub fn dismiss(&mut self, id: usize, cx: &mut Context<Self>) {
|
||||
if let Some(position) = self.entries.iter().position(|entry| entry.id == id) {
|
||||
let entry = self.entries.remove(position);
|
||||
if self.exiting.as_ref().is_some_and(|entry| entry.id == id) {
|
||||
self.finish_exit(cx);
|
||||
return;
|
||||
}
|
||||
if self.current.as_ref().is_some_and(|entry| entry.id == id) {
|
||||
let entry = self.current.take().expect("current checked above");
|
||||
self.start_exit(entry, cx);
|
||||
}
|
||||
}
|
||||
|
||||
/// Dismisses the toast on screen.
|
||||
pub fn clear(&mut self, cx: &mut Context<Self>) {
|
||||
if let Some(entry) = self.current.take() {
|
||||
self.start_exit(entry, cx);
|
||||
} else {
|
||||
self.finish_exit(cx);
|
||||
}
|
||||
}
|
||||
|
||||
/// The toast now showing, if any.
|
||||
pub fn current(&self) -> Option<&ToastEntry> {
|
||||
self.current.as_ref()
|
||||
}
|
||||
|
||||
/// How many toasts are on screen, including one fading out.
|
||||
pub fn count(&self) -> usize {
|
||||
usize::from(self.current.is_some()) + usize::from(self.exiting.is_some())
|
||||
}
|
||||
|
||||
/// Moves a toast into the fade-out phase.
|
||||
fn start_exit(&mut self, entry: ToastEntry, cx: &mut Context<Self>) {
|
||||
if let Some(previous) = self.exiting.take()
|
||||
&& let Some(on_dismiss) = &previous.toast.on_dismiss
|
||||
{
|
||||
(on_dismiss.clone())(cx);
|
||||
}
|
||||
|
||||
self.exiting = Some(entry);
|
||||
cx.notify();
|
||||
|
||||
let fade = cx.theme().motion.overlay;
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
cx.background_executor().timer(fade).await;
|
||||
this.update(cx, |toasts, cx| toasts.finish_exit(cx)).ok();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn finish_exit(&mut self, cx: &mut Context<Self>) {
|
||||
if let Some(entry) = self.exiting.take() {
|
||||
cx.notify();
|
||||
if let Some(on_dismiss) = &entry.toast.on_dismiss {
|
||||
on_dismiss(cx);
|
||||
(on_dismiss.clone())(cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Dismisses every toast.
|
||||
pub fn clear(&mut self, cx: &mut Context<Self>) {
|
||||
if !self.entries.is_empty() {
|
||||
self.entries.clear();
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
/// The raised toasts, oldest first.
|
||||
pub fn entries(&self) -> &[ToastEntry] {
|
||||
&self.entries
|
||||
}
|
||||
|
||||
/// How many toasts are raised.
|
||||
pub fn count(&self) -> usize {
|
||||
self.entries.len()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
@@ -191,118 +218,131 @@ impl ToastsView {
|
||||
}
|
||||
|
||||
impl RenderOnce for ToastsView {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let theme = cx.theme();
|
||||
let surface = theme.colors.surface;
|
||||
let border = theme.colors.border;
|
||||
let foreground = theme.colors.foreground;
|
||||
let muted_foreground = theme.colors.muted_foreground;
|
||||
let muted = theme.colors.muted;
|
||||
let radius = theme.radius.overlay;
|
||||
let shadows = theme.elevations.popover.shadows();
|
||||
let icon_size = theme.sizes.small.icon_size;
|
||||
let enter_duration = theme.motion.overlay;
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let fade = cx.theme().motion.overlay;
|
||||
|
||||
let entries: Vec<ToastEntry> = self.toasts.read(cx).entries().to_vec();
|
||||
let exiting = self.toasts.read(cx).exiting.clone();
|
||||
let current = self.toasts.read(cx).current.clone();
|
||||
|
||||
deferred(
|
||||
div()
|
||||
.id(self.id)
|
||||
.absolute()
|
||||
.top_4()
|
||||
.right_4()
|
||||
.left_0()
|
||||
.bottom_4()
|
||||
.w_full()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.items_end()
|
||||
.gap_2()
|
||||
.w_80()
|
||||
.children(entries.into_iter().map(|entry| {
|
||||
let toast = &entry.toast;
|
||||
let toasts = self.toasts.clone();
|
||||
|
||||
let accent = match toast.severity {
|
||||
ToastSeverity::Info => theme.ramps.info.base,
|
||||
ToastSeverity::Success => theme.ramps.success.base,
|
||||
ToastSeverity::Warning => theme.ramps.warning.base,
|
||||
ToastSeverity::Danger => theme.ramps.destructive.base,
|
||||
};
|
||||
|
||||
let icon_role = toast.icon.unwrap_or(match toast.severity {
|
||||
ToastSeverity::Info => IconRole::Info,
|
||||
ToastSeverity::Success => IconRole::Check,
|
||||
ToastSeverity::Warning | ToastSeverity::Danger => IconRole::Warning,
|
||||
});
|
||||
|
||||
div()
|
||||
.id(("toast", entry.id))
|
||||
.flex()
|
||||
.items_start()
|
||||
.gap_2()
|
||||
.py_2()
|
||||
.px_3()
|
||||
.w_full()
|
||||
.bg(surface)
|
||||
.border_1()
|
||||
.border_color(border)
|
||||
.rounded(radius)
|
||||
.shadow(shadows.clone())
|
||||
.when(toast.show_icon, |toast| {
|
||||
toast.child(
|
||||
div()
|
||||
.flex_none()
|
||||
.flex()
|
||||
.items_center()
|
||||
.h(window.line_height())
|
||||
.child(icon(icon_role).size(icon_size).color(accent)),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
v_flex()
|
||||
.flex_1()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
div()
|
||||
.text_size(theme.sizes.small.text_size)
|
||||
.text_color(foreground)
|
||||
.when(toast.message.is_some(), |toast| {
|
||||
toast.font_weight(gpui::FontWeight::SEMIBOLD)
|
||||
})
|
||||
.child(toast.title.clone()),
|
||||
)
|
||||
.when_some(toast.message.clone(), |toast, message| {
|
||||
toast.child(
|
||||
div()
|
||||
.text_size(theme.sizes.small.text_size)
|
||||
.text_color(muted_foreground)
|
||||
.child(message),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id(("toast-close", entry.id))
|
||||
.flex_none()
|
||||
.cursor_pointer()
|
||||
.p_0p5()
|
||||
.rounded(px(4.))
|
||||
.hover(|style| style.bg(muted))
|
||||
.child(
|
||||
icon(IconRole::Close)
|
||||
.size(icon_size * 0.75)
|
||||
.color(foreground.alpha(0.55)),
|
||||
)
|
||||
.on_click(move |_, _window, cx| {
|
||||
toasts.update(cx, |toasts, cx| {
|
||||
toasts.dismiss(entry.id, cx);
|
||||
});
|
||||
}),
|
||||
)
|
||||
.with_animation(
|
||||
("toast-enter", entry.id),
|
||||
Animation::new(enter_duration).with_easing(ease_out_quint()),
|
||||
|toast, delta| toast.mt(px(8. * (1. - delta))),
|
||||
)
|
||||
})),
|
||||
.items_center()
|
||||
.when_some(exiting, |column, entry| {
|
||||
let id = entry.id;
|
||||
column.child(pill(&entry, &self.toasts, cx).with_animation(
|
||||
("toast-exit", id),
|
||||
Animation::new(fade),
|
||||
|pill, delta| pill.opacity(1. - delta),
|
||||
))
|
||||
})
|
||||
.when_some(current, |column, entry| {
|
||||
let id = entry.id;
|
||||
column.child(pill(&entry, &self.toasts, cx).with_animation(
|
||||
("toast-enter", id),
|
||||
Animation::new(fade).with_easing(ease_out_quint()),
|
||||
|pill, delta| pill.opacity(delta),
|
||||
))
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn pill(entry: &ToastEntry, toasts: &Entity<Toasts>, cx: &App) -> gpui::Stateful<gpui::Div> {
|
||||
let theme = cx.theme();
|
||||
let surface = theme.colors.surface;
|
||||
let border = theme.colors.border;
|
||||
let foreground = theme.colors.foreground;
|
||||
let muted = theme.colors.muted;
|
||||
let primary = theme.ramps.primary.base;
|
||||
let primary_hover = theme.ramps.primary.hover;
|
||||
let primary_foreground = theme.colors.primary_foreground;
|
||||
let radius = theme.radius.full;
|
||||
let shadows = theme.elevations.popover.shadows();
|
||||
let icon_size = theme.sizes.small.icon_size;
|
||||
let text_size = theme.sizes.small.text_size;
|
||||
let toast = &entry.toast;
|
||||
let toasts = toasts.clone();
|
||||
let id = entry.id;
|
||||
|
||||
div()
|
||||
.id(("toast", id))
|
||||
.max_w(px(480.))
|
||||
.flex()
|
||||
.items_center()
|
||||
.py_1p5()
|
||||
.px_3()
|
||||
.bg(surface)
|
||||
.border_1()
|
||||
.border_color(border)
|
||||
.rounded(radius)
|
||||
.shadow(shadows.clone())
|
||||
.when_some(toast.icon, |pill, icon_role| {
|
||||
pill.child(
|
||||
div()
|
||||
.flex_none()
|
||||
.mr_2()
|
||||
.flex()
|
||||
.items_center()
|
||||
.child(icon(icon_role).size(icon_size).color(foreground)),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.min_w(px(0.))
|
||||
.truncate()
|
||||
.text_size(text_size)
|
||||
.text_color(foreground)
|
||||
.child(toast.message.clone()),
|
||||
)
|
||||
.when_some(toast.action.clone(), |pill, action| {
|
||||
let toasts = toasts.clone();
|
||||
pill.child(
|
||||
div()
|
||||
.id(("toast-action", id))
|
||||
.flex_none()
|
||||
.ml_1p5()
|
||||
.cursor_pointer()
|
||||
.px_2()
|
||||
.py_0p5()
|
||||
.rounded(radius)
|
||||
.bg(primary)
|
||||
.hover(|style| style.bg(primary_hover))
|
||||
.text_size(text_size)
|
||||
.text_color(primary_foreground)
|
||||
.child(action.label.clone())
|
||||
.on_click(move |_, _window, cx| {
|
||||
(action.handler.clone())(cx);
|
||||
toasts.update(cx, |toasts, cx| {
|
||||
toasts.dismiss(id, cx);
|
||||
});
|
||||
}),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.id(("toast-close", id))
|
||||
.flex_none()
|
||||
.ml_4()
|
||||
.cursor_pointer()
|
||||
.p_0p5()
|
||||
.rounded(px(4.))
|
||||
.hover(|style| style.bg(muted))
|
||||
.child(
|
||||
icon(IconRole::Close)
|
||||
.size(icon_size * 0.75)
|
||||
.color(foreground.alpha(0.55)),
|
||||
)
|
||||
.on_click(move |_, _window, cx| {
|
||||
toasts.update(cx, |toasts, cx| {
|
||||
toasts.dismiss(id, cx);
|
||||
});
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user