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}