toast_layer.rs

  1use std::{
  2    rc::Rc,
  3    time::{Duration, Instant},
  4};
  5
  6use gpui::{AnyView, DismissEvent, Entity, FocusHandle, ManagedView, Subscription, Task, actions};
  7use ui::{animation::DefaultAnimations, prelude::*};
  8
  9use crate::Workspace;
 10
 11const DEFAULT_TOAST_DURATION: Duration = Duration::from_secs(10);
 12const MINIMUM_RESUME_DURATION: Duration = Duration::from_millis(800);
 13
 14actions!(toast, [RunAction]);
 15
 16pub fn init(cx: &mut App) {
 17    cx.observe_new(|workspace: &mut Workspace, _window, _cx| {
 18        workspace.register_action(|_workspace, _: &RunAction, window, cx| {
 19            let workspace = cx.entity();
 20            let window = window.window_handle();
 21            cx.defer(move |cx| {
 22                let action = workspace
 23                    .read(cx)
 24                    .toast_layer
 25                    .read(cx)
 26                    .active_toast
 27                    .as_ref()
 28                    .and_then(|active_toast| active_toast.action.clone());
 29
 30                if let Some(on_click) = action.and_then(|action| action.on_click) {
 31                    window
 32                        .update(cx, |_, window, cx| {
 33                            on_click(window, cx);
 34                        })
 35                        .ok();
 36                }
 37            });
 38        });
 39    })
 40    .detach();
 41}
 42
 43pub trait ToastView: ManagedView {
 44    fn action(&self) -> Option<ToastAction>;
 45}
 46
 47#[derive(Clone)]
 48pub struct ToastAction {
 49    pub id: ElementId,
 50    pub label: SharedString,
 51    pub on_click: Option<Rc<dyn Fn(&mut Window, &mut App) + 'static>>,
 52}
 53
 54impl ToastAction {
 55    pub fn new(
 56        label: SharedString,
 57        on_click: Option<Rc<dyn Fn(&mut Window, &mut App) + 'static>>,
 58    ) -> Self {
 59        let id = ElementId::Name(label.clone());
 60
 61        Self {
 62            id,
 63            label,
 64            on_click,
 65        }
 66    }
 67}
 68
 69trait ToastViewHandle {
 70    fn view(&self) -> AnyView;
 71}
 72
 73impl<V: ToastView> ToastViewHandle for Entity<V> {
 74    fn view(&self) -> AnyView {
 75        self.clone().into()
 76    }
 77}
 78
 79pub struct ActiveToast {
 80    toast: Box<dyn ToastViewHandle>,
 81    action: Option<ToastAction>,
 82    _subscriptions: [Subscription; 1],
 83    focus_handle: FocusHandle,
 84}
 85
 86struct DismissTimer {
 87    instant_started: Instant,
 88    _task: Task<()>,
 89}
 90
 91pub struct ToastLayer {
 92    active_toast: Option<ActiveToast>,
 93    duration_remaining: Option<Duration>,
 94    dismiss_timer: Option<DismissTimer>,
 95}
 96
 97impl Default for ToastLayer {
 98    fn default() -> Self {
 99        Self::new()
100    }
101}
102
103impl ToastLayer {
104    pub fn new() -> Self {
105        Self {
106            active_toast: None,
107            duration_remaining: None,
108            dismiss_timer: None,
109        }
110    }
111
112    pub fn toggle_toast<V>(&mut self, cx: &mut Context<Self>, new_toast: Entity<V>)
113    where
114        V: ToastView,
115    {
116        if let Some(active_toast) = &self.active_toast {
117            let is_close = active_toast.toast.view().downcast::<V>().is_ok();
118            let did_close = self.hide_toast(cx);
119            if is_close || !did_close {
120                return;
121            }
122        }
123        self.show_toast(new_toast, cx);
124    }
125
126    pub fn show_toast<V>(&mut self, new_toast: Entity<V>, cx: &mut Context<Self>)
127    where
128        V: ToastView,
129    {
130        let action = new_toast.read(cx).action();
131        let focus_handle = cx.focus_handle();
132
133        self.active_toast = Some(ActiveToast {
134            toast: Box::new(new_toast.clone()),
135            action,
136            _subscriptions: [cx.subscribe(&new_toast, |this, _, _: &DismissEvent, cx| {
137                this.hide_toast(cx);
138            })],
139            focus_handle,
140        });
141
142        self.start_dismiss_timer(DEFAULT_TOAST_DURATION, cx);
143
144        cx.notify();
145    }
146
147    pub fn hide_toast(&mut self, cx: &mut Context<Self>) -> bool {
148        self.active_toast.take();
149        cx.notify();
150
151        true
152    }
153
154    pub fn active_toast<V>(&self) -> Option<Entity<V>>
155    where
156        V: 'static,
157    {
158        let active_toast = self.active_toast.as_ref()?;
159        active_toast.toast.view().downcast::<V>().ok()
160    }
161
162    pub fn has_active_toast(&self) -> bool {
163        self.active_toast.is_some()
164    }
165
166    fn pause_dismiss_timer(&mut self) {
167        let Some(dismiss_timer) = self.dismiss_timer.take() else {
168            return;
169        };
170        let Some(duration_remaining) = self.duration_remaining.as_mut() else {
171            return;
172        };
173        *duration_remaining =
174            duration_remaining.saturating_sub(dismiss_timer.instant_started.elapsed());
175        if *duration_remaining < MINIMUM_RESUME_DURATION {
176            *duration_remaining = MINIMUM_RESUME_DURATION;
177        }
178    }
179
180    /// Starts a timer to automatically dismiss the toast after the specified duration
181    pub fn start_dismiss_timer(&mut self, duration: Duration, cx: &mut Context<Self>) {
182        self.clear_dismiss_timer(cx);
183
184        let instant_started = std::time::Instant::now();
185        let task = cx.spawn(async move |this, cx| {
186            cx.background_executor().timer(duration).await;
187
188            if let Some(this) = this.upgrade() {
189                this.update(cx, |this, cx| this.hide_toast(cx)).ok();
190            }
191        });
192
193        self.duration_remaining = Some(duration);
194        self.dismiss_timer = Some(DismissTimer {
195            instant_started,
196            _task: task,
197        });
198        cx.notify();
199    }
200
201    /// Restarts the dismiss timer with a new duration
202    pub fn restart_dismiss_timer(&mut self, cx: &mut Context<Self>) {
203        let Some(duration) = self.duration_remaining else {
204            return;
205        };
206        self.start_dismiss_timer(duration, cx);
207        cx.notify();
208    }
209
210    /// Clears the dismiss timer if one exists
211    pub fn clear_dismiss_timer(&mut self, cx: &mut Context<Self>) {
212        self.dismiss_timer.take();
213        cx.notify();
214    }
215}
216
217impl Render for ToastLayer {
218    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
219        let Some(active_toast) = &self.active_toast else {
220            return div();
221        };
222        let handle = cx.weak_entity();
223
224        div().absolute().size_full().bottom_0().left_0().child(
225            v_flex()
226                .id("toast-layer-container")
227                .absolute()
228                .w_full()
229                .bottom(px(0.))
230                .flex()
231                .flex_col()
232                .items_center()
233                .track_focus(&active_toast.focus_handle)
234                .child(
235                    h_flex()
236                        .id("active-toast-container")
237                        .occlude()
238                        .on_hover(move |hover_start, _window, cx| {
239                            let Some(this) = handle.upgrade() else {
240                                return;
241                            };
242                            if *hover_start {
243                                this.update(cx, |this, _| this.pause_dismiss_timer());
244                            } else {
245                                this.update(cx, |this, cx| this.restart_dismiss_timer(cx));
246                            }
247                            cx.stop_propagation();
248                        })
249                        .on_click(|_, _, cx| {
250                            cx.stop_propagation();
251                        })
252                        .child(active_toast.toast.view()),
253                )
254                .animate_in(AnimationDirection::FromBottom, true),
255        )
256    }
257}