use std::rc::Rc; use std::time::Duration; use gpui::prelude::FluentBuilder as _; use gpui::{ App, Context, ElementId, Entity, InteractiveElement as _, IntoElement, ParentElement as _, RenderOnce, SharedString, StatefulInteractiveElement as _, Styled as _, Window, deferred, div, px, }; use gpui_native_icons::{IconRole, icon}; use gpui_native_theme::ActiveTheme as _; use crate::common::v_flex; /// The dismiss handler of a [`Toast`]. pub type DismissHandler = Rc; /// 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, } /// Toast notification. #[derive(Clone)] pub struct Toast { title: SharedString, message: Option, severity: ToastSeverity, /// The painted glyph; `None` derives it from the severity. icon: Option, show_icon: bool, duration: Option, on_dismiss: Option, } 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(title: L) -> Self where L: Into, { Self { title: title.into(), message: None, severity: ToastSeverity::Info, icon: None, show_icon: true, duration: Some(Self::DURATION), on_dismiss: None, } } /// Sets a secondary line under the title. pub fn message(mut self, message: L) -> Self where L: Into, { 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. 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 } /// Sets the time before the toast dismisses itself. pub fn duration(mut self, duration: Duration) -> Self { self.duration = Some(duration); self } /// Runs when the toast is dismissed by any means. pub fn on_dismiss(mut self, handler: F) -> Self where F: Fn(&mut App) + 'static, { self.on_dismiss = Some(Rc::new(handler)); self } } /// One raised toast, with its identity in the store. #[derive(Clone)] pub struct ToastEntry { pub id: usize, pub toast: Toast, } /// The store of raised toasts. #[derive(Default)] pub struct Toasts { entries: Vec, next_id: usize, } impl Toasts { /// Raises a toast, starting its auto-dismiss timer. Returns its id. pub fn push(&mut self, toast: Toast, cx: &mut Context) -> usize { let id = self.next_id; self.next_id += 1; let duration = toast.duration; self.entries.push(ToastEntry { id, toast }); cx.notify(); if let Some(duration) = duration.filter(|duration| !duration.is_zero()) { cx.spawn(async move |this, cx| { cx.background_executor().timer(duration).await; this.update(cx, |toasts, cx| toasts.dismiss(id, cx)).ok(); }) .detach(); } id } /// Dismisses one toast by id, running its dismiss handler. pub fn dismiss(&mut self, id: usize, cx: &mut Context) { 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.toast.on_dismiss { on_dismiss(cx); } } } /// Dismisses every toast. pub fn clear(&mut self, cx: &mut Context) { 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)] pub struct ToastsView { toasts: Entity, id: ElementId, } impl ToastsView { /// Binds the view to a toast store. pub fn new(id: I, toasts: Entity) -> Self where I: Into, { Self { id: id.into(), toasts, } } } 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 entries: Vec = self.toasts.read(cx).entries().to_vec(); deferred( div() .id(self.id) .absolute() .top_4() .right_4() .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.primary.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); }); }), ) })), ) } }