toast_layer.rs

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