use std::{ rc::Rc, time::{Duration, Instant}, }; use gpui::{AnyView, DismissEvent, Entity, FocusHandle, ManagedView, Subscription, Task, actions}; use ui::{animation::DefaultAnimations, prelude::*}; use crate::Workspace; const DEFAULT_TOAST_DURATION: Duration = Duration::from_secs(10); const MINIMUM_RESUME_DURATION: Duration = Duration::from_millis(800); actions!(toast, [RunAction]); pub fn init(cx: &mut App) { cx.observe_new(|workspace: &mut Workspace, _window, _cx| { workspace.register_action(|_workspace, _: &RunAction, window, cx| { let workspace = cx.entity(); let window = window.window_handle(); cx.defer(move |cx| { let action = workspace .read(cx) .toast_layer .read(cx) .active_toast .as_ref() .and_then(|active_toast| active_toast.action.clone()); if let Some(on_click) = action.and_then(|action| action.on_click) { window .update(cx, |_, window, cx| { on_click(window, cx); }) .ok(); } }); }); }) .detach(); } pub trait ToastView: ManagedView { fn action(&self) -> Option; } #[derive(Clone)] pub struct ToastAction { pub id: ElementId, pub label: SharedString, pub on_click: Option>, } impl ToastAction { pub fn new( label: SharedString, on_click: Option>, ) -> Self { let id = ElementId::Name(label.clone()); Self { id, label, on_click, } } } trait ToastViewHandle { fn view(&self) -> AnyView; } impl ToastViewHandle for Entity { fn view(&self) -> AnyView { self.clone().into() } } pub struct ActiveToast { toast: Box, action: Option, _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, 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(cx); if is_close || !did_close { return; } } self.show_toast(new_toast, cx); } pub fn show_toast(&mut self, new_toast: Entity, cx: &mut Context) where V: ToastView, { let action = new_toast.read(cx).action(); let focus_handle = cx.focus_handle(); self.active_toast = Some(ActiveToast { toast: Box::new(new_toast.clone()), action, _subscriptions: [cx.subscribe(&new_toast, |this, _, _: &DismissEvent, cx| { this.hide_toast(cx); })], focus_handle, }); self.start_dismiss_timer(DEFAULT_TOAST_DURATION, cx); cx.notify(); } pub fn hide_toast(&mut self, cx: &mut Context) -> bool { self.active_toast.take(); 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, cx: &mut Context) { self.clear_dismiss_timer(cx); let instant_started = std::time::Instant::now(); let task = cx.spawn(async move |this, cx| { cx.background_executor().timer(duration).await; if let Some(this) = this.upgrade() { this.update(cx, |this, cx| this.hide_toast(cx)).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, cx: &mut Context) { let Some(duration) = self.duration_remaining else { return; }; self.start_dismiss_timer(duration, 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(cx)); } cx.stop_propagation(); }) .on_click(|_, _, cx| { cx.stop_propagation(); }) .child(active_toast.toast.view()), ) .animate_in(AnimationDirection::FromBottom, true), ) } }