toast_layer.rs

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