use std::time::{Duration, Instant}; use gpui::{AnyView, DismissEvent, Entity, FocusHandle, ManagedView, Subscription, Task}; use ui::{animation::DefaultAnimations, prelude::*}; const DEFAULT_TOAST_DURATION: Duration = Duration::from_millis(2400); const MINIMUM_RESUME_DURATION: Duration = Duration::from_millis(800); pub trait ToastView: ManagedView {} trait ToastViewHandle { fn view(&self) -> AnyView; } impl ToastViewHandle for Entity { fn view(&self) -> AnyView { self.clone().into() } } pub struct ActiveToast { toast: Box, _subscriptions: [Subscription; 1], focus_handle: FocusHandle, } struct DismissTimer { instant_started: Instant, _task: Task<()>, } pub struct ToastLayer { active_toast: Option, duration_remaining: Option, dismiss_timer: Option, } impl Default for ToastLayer { fn default() -> Self { Self::new() } } impl ToastLayer { pub fn new() -> Self { Self { active_toast: None, duration_remaining: None, dismiss_timer: None, } } pub fn toggle_toast( &mut self, window: &mut Window, cx: &mut Context, new_toast: Entity, ) where V: ToastView, { if let Some(active_toast) = &self.active_toast { let is_close = active_toast.toast.view().downcast::().is_ok(); let did_close = self.hide_toast(window, cx); if is_close || !did_close { return; } } self.show_toast(new_toast, window, cx); } pub fn show_toast( &mut self, new_toast: Entity, window: &mut Window, cx: &mut Context, ) where V: ToastView, { let focus_handle = cx.focus_handle(); self.active_toast = Some(ActiveToast { toast: Box::new(new_toast.clone()), _subscriptions: [cx.subscribe_in( &new_toast, window, |this, _, _: &DismissEvent, window, cx| { this.hide_toast(window, cx); }, )], focus_handle, }); self.start_dismiss_timer(DEFAULT_TOAST_DURATION, window, cx); cx.notify(); } pub fn hide_toast(&mut self, _window: &mut Window, cx: &mut Context) -> bool { cx.notify(); true } pub fn active_toast(&self) -> Option> where V: 'static, { let active_toast = self.active_toast.as_ref()?; active_toast.toast.view().downcast::().ok() } pub fn has_active_toast(&self) -> bool { self.active_toast.is_some() } fn pause_dismiss_timer(&mut self) { let Some(dismiss_timer) = self.dismiss_timer.take() else { return; }; let Some(duration_remaining) = self.duration_remaining.as_mut() else { return; }; *duration_remaining = duration_remaining.saturating_sub(dismiss_timer.instant_started.elapsed()); if *duration_remaining < MINIMUM_RESUME_DURATION { *duration_remaining = MINIMUM_RESUME_DURATION; } } /// Starts a timer to automatically dismiss the toast after the specified duration pub fn start_dismiss_timer( &mut self, duration: Duration, _window: &mut Window, cx: &mut Context, ) { self.clear_dismiss_timer(cx); let instant_started = std::time::Instant::now(); let task = cx.spawn(|this, mut cx| async move { cx.background_executor().timer(duration).await; if let Some(this) = this.upgrade() { this.update(&mut cx, |this, cx| { this.active_toast.take(); cx.notify(); }) .ok(); } }); self.duration_remaining = Some(duration); self.dismiss_timer = Some(DismissTimer { instant_started, _task: task, }); cx.notify(); } /// Restarts the dismiss timer with a new duration pub fn restart_dismiss_timer(&mut self, window: &mut Window, cx: &mut Context) { let Some(duration) = self.duration_remaining else { return; }; self.start_dismiss_timer(duration, window, cx); cx.notify(); } /// Clears the dismiss timer if one exists pub fn clear_dismiss_timer(&mut self, cx: &mut Context) { self.dismiss_timer.take(); cx.notify(); } } impl Render for ToastLayer { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let Some(active_toast) = &self.active_toast else { return div(); }; let handle = cx.weak_entity(); div().absolute().size_full().bottom_0().left_0().child( v_flex() .id("toast-layer-container") .absolute() .w_full() .bottom(px(0.)) .flex() .flex_col() .items_center() .track_focus(&active_toast.focus_handle) .child( h_flex() .id("active-toast-container") .occlude() .on_hover(move |hover_start, window, cx| { let Some(this) = handle.upgrade() else { return; }; if *hover_start { this.update(cx, |this, _| this.pause_dismiss_timer()); } else { this.update(cx, |this, cx| this.restart_dismiss_timer(window, cx)); } cx.stop_propagation(); }) .on_click(|_, _, cx| { cx.stop_propagation(); }) .child(active_toast.toast.view()), ) .animate_in(AnimationDirection::FromBottom, true), ) } }