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