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}