Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T03:59:04.327Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

toast_layer.rs

269 lines · 7.7 KB · rust
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}
269
Served at tenant.openagents/omega Member data and write actions are omitted.