Skip to repository content328 lines · 11.5 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T01:50:33.841Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
modal_layer.rs
1use gpui::{
2 AnyView, App, DismissEvent, Entity, EventEmitter, FocusHandle, Global, ManagedView,
3 MouseButton, Subscription, WeakFocusHandle,
4};
5use ui::prelude::*;
6
7#[derive(Debug)]
8pub enum DismissDecision {
9 Dismiss(bool),
10 Pending,
11}
12
13// A modal that hosts a picker forwards its own focus handle to the inner picker,
14// so the modal layer recognizes a reopenable picker by focus identity rather than
15// requiring each modal wrapper to implement its own opt-in.
16#[derive(Default)]
17struct ReopenablePickerRegistry {
18 handles: Vec<WeakFocusHandle>,
19}
20
21impl Global for ReopenablePickerRegistry {}
22
23pub fn register_reopenable_picker(focus_handle: &FocusHandle, cx: &mut App) {
24 let registry = cx.default_global::<ReopenablePickerRegistry>();
25 registry.handles.retain(|handle| handle.upgrade().is_some());
26 if !registry.handles.iter().any(|handle| handle == focus_handle) {
27 registry.handles.push(focus_handle.downgrade());
28 }
29}
30
31pub fn deregister_reopenable_picker(focus_handle: &FocusHandle, cx: &mut App) {
32 let registry = cx.default_global::<ReopenablePickerRegistry>();
33 registry
34 .handles
35 .retain(|handle| handle.upgrade().is_some() && handle != focus_handle);
36}
37
38fn focus_handle_is_reopenable(focus_handle: &FocusHandle, cx: &App) -> bool {
39 cx.try_global::<ReopenablePickerRegistry>()
40 .is_some_and(|registry| {
41 registry
42 .handles
43 .iter()
44 .filter_map(|handle| handle.upgrade())
45 .any(|handle| &handle == focus_handle)
46 })
47}
48
49pub trait ModalView: ManagedView {
50 fn on_before_dismiss(
51 &mut self,
52 _window: &mut Window,
53 _: &mut Context<Self>,
54 ) -> DismissDecision {
55 DismissDecision::Dismiss(true)
56 }
57
58 fn fade_out_background(&self) -> bool {
59 false
60 }
61
62 fn render_bare(&self) -> bool {
63 false
64 }
65}
66
67trait ModalViewHandle {
68 fn on_before_dismiss(&mut self, window: &mut Window, cx: &mut App) -> DismissDecision;
69 fn view(&self) -> AnyView;
70 fn focus_handle(&self, cx: &App) -> FocusHandle;
71 fn subscribe_dismiss(&self, window: &mut Window, cx: &mut Context<ModalLayer>) -> Subscription;
72 fn fade_out_background(&self, cx: &mut App) -> bool;
73 fn render_bare(&self, cx: &mut App) -> bool;
74}
75
76impl<V: ModalView> ModalViewHandle for Entity<V> {
77 fn on_before_dismiss(&mut self, window: &mut Window, cx: &mut App) -> DismissDecision {
78 self.update(cx, |this, cx| this.on_before_dismiss(window, cx))
79 }
80
81 fn view(&self) -> AnyView {
82 self.clone().into()
83 }
84
85 fn focus_handle(&self, cx: &App) -> FocusHandle {
86 self.read(cx).focus_handle(cx)
87 }
88
89 fn subscribe_dismiss(&self, window: &mut Window, cx: &mut Context<ModalLayer>) -> Subscription {
90 cx.subscribe_in(self, window, |this, _, _: &DismissEvent, window, cx| {
91 this.hide_modal(window, cx);
92 })
93 }
94
95 fn fade_out_background(&self, cx: &mut App) -> bool {
96 self.read(cx).fade_out_background()
97 }
98
99 fn render_bare(&self, cx: &mut App) -> bool {
100 self.read(cx).render_bare()
101 }
102}
103
104pub struct ActiveModal {
105 modal: Box<dyn ModalViewHandle>,
106 _subscriptions: [Subscription; 2],
107 previous_focus_handle: Option<FocusHandle>,
108 focus_handle: FocusHandle,
109}
110
111pub struct ModalLayer {
112 active_modal: Option<ActiveModal>,
113 // Kept alive (hidden) rather than dropped on dismissal so `ReopenLastPicker`
114 // can reveal it again with its exact prior state. Left intact across
115 // non-reopenable modals (e.g. the command palette), which may trigger the reopen.
116 stashed_modal: Option<Box<dyn ModalViewHandle>>,
117 // Set when a reveal was requested while another modal was still active (e.g. the
118 // which-key popup that is dismissed only after the reopen action fires). The reveal
119 // then happens once that modal closes, so it doesn't matter whether the triggering
120 // modal is dismissed before or after the action dispatches.
121 reveal_stash_when_free: bool,
122 dismiss_on_focus_lost: bool,
123}
124
125pub(crate) struct ModalOpenedEvent;
126
127impl EventEmitter<ModalOpenedEvent> for ModalLayer {}
128
129impl Default for ModalLayer {
130 fn default() -> Self {
131 Self::new()
132 }
133}
134
135impl ModalLayer {
136 pub fn new() -> Self {
137 Self {
138 active_modal: None,
139 stashed_modal: None,
140 reveal_stash_when_free: false,
141 dismiss_on_focus_lost: false,
142 }
143 }
144
145 /// Toggles a modal of type `V`. If a modal of the same type is currently active,
146 /// it will be hidden. If a different modal is active, it will be replaced with the new one.
147 /// If no modal is active, the new modal will be shown.
148 ///
149 /// If closing the current modal fails (e.g., due to `on_before_dismiss` returning
150 /// `DismissDecision::Dismiss(false)` or `DismissDecision::Pending`), the new modal
151 /// will not be shown.
152 pub fn toggle_modal<V, B>(&mut self, window: &mut Window, cx: &mut Context<Self>, build_view: B)
153 where
154 V: ModalView,
155 B: FnOnce(&mut Window, &mut Context<V>) -> V,
156 {
157 // Opening a modal explicitly supersedes any reveal that was waiting for the
158 // layer to become free.
159 self.reveal_stash_when_free = false;
160 if let Some(active_modal) = &self.active_modal {
161 let should_close = active_modal.modal.view().downcast::<V>().is_ok();
162 let did_close = self.hide_modal(window, cx);
163 if should_close || !did_close {
164 return;
165 }
166 }
167 let new_modal = cx.new(|cx| build_view(window, cx));
168 self.show_modal(Box::new(new_modal), window, cx);
169 cx.emit(ModalOpenedEvent);
170 }
171
172 /// Shows a modal and sets up subscriptions for dismiss events and focus tracking.
173 /// The modal is automatically focused after being shown.
174 fn show_modal(
175 &mut self,
176 modal: Box<dyn ModalViewHandle>,
177 window: &mut Window,
178 cx: &mut Context<Self>,
179 ) {
180 let focus_handle = cx.focus_handle();
181 let modal_focus_handle = modal.focus_handle(cx);
182 let dismiss_subscription = modal.subscribe_dismiss(window, cx);
183 self.active_modal = Some(ActiveModal {
184 modal,
185 _subscriptions: [
186 dismiss_subscription,
187 cx.on_focus_out(&focus_handle, window, |this, _event, window, cx| {
188 if this.dismiss_on_focus_lost {
189 this.hide_modal(window, cx);
190 }
191 }),
192 ],
193 previous_focus_handle: window.focused(cx),
194 focus_handle,
195 });
196 cx.defer_in(window, move |_, window, cx| {
197 window.focus(&modal_focus_handle, cx);
198 });
199 cx.notify();
200 }
201
202 /// Reveals the most recently stashed reopenable modal, if any. Returns whether
203 /// a modal was revealed.
204 pub fn reveal_stashed_modal(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
205 if self.stashed_modal.is_none() {
206 return false;
207 }
208 if self.active_modal.is_some() {
209 // Another modal is still closing (e.g. the which-key popup that dismisses
210 // only after this action fires). Reveal once it is gone; see `hide_modal`.
211 self.reveal_stash_when_free = true;
212 return false;
213 }
214 let Some(modal) = self.stashed_modal.take() else {
215 return false;
216 };
217 self.show_modal(modal, window, cx);
218 cx.emit(ModalOpenedEvent);
219 true
220 }
221
222 /// Attempts to hide the currently active modal.
223 ///
224 /// The modal's `on_before_dismiss` method is called to determine if dismissal should proceed.
225 /// If dismissal is allowed, the modal is removed and focus is restored to the previously
226 /// focused element.
227 ///
228 /// Returns `true` if the modal was successfully hidden, `false` otherwise.
229 pub fn hide_modal(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
230 let Some(active_modal) = self.active_modal.as_mut() else {
231 self.dismiss_on_focus_lost = false;
232 return false;
233 };
234
235 match active_modal.modal.on_before_dismiss(window, cx) {
236 DismissDecision::Dismiss(should_dismiss) => {
237 if !should_dismiss {
238 self.dismiss_on_focus_lost = !should_dismiss;
239 return false;
240 }
241 }
242 DismissDecision::Pending => {
243 self.dismiss_on_focus_lost = false;
244 return false;
245 }
246 }
247
248 if let Some(active_modal) = self.active_modal.take() {
249 let reopenable = focus_handle_is_reopenable(&active_modal.modal.focus_handle(cx), cx);
250 if let Some(previous_focus) = active_modal.previous_focus_handle
251 && active_modal.focus_handle.contains_focused(window, cx)
252 {
253 previous_focus.focus(window, cx);
254 }
255 if reopenable {
256 self.stashed_modal = Some(active_modal.modal);
257 }
258 cx.notify();
259 }
260 self.dismiss_on_focus_lost = false;
261 // A reveal was requested while this modal was still active; now that the layer
262 // is free, honor it.
263 if self.reveal_stash_when_free && self.active_modal.is_none() {
264 self.reveal_stash_when_free = false;
265 self.reveal_stashed_modal(window, cx);
266 }
267 true
268 }
269
270 /// Returns the currently active modal if it is of type `V`.
271 pub fn active_modal<V>(&self) -> Option<Entity<V>>
272 where
273 V: 'static,
274 {
275 let active_modal = self.active_modal.as_ref()?;
276 active_modal.modal.view().downcast::<V>().ok()
277 }
278
279 pub fn has_active_modal(&self) -> bool {
280 self.active_modal.is_some()
281 }
282}
283
284impl Render for ModalLayer {
285 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
286 let Some(active_modal) = &self.active_modal else {
287 return div().into_any_element();
288 };
289
290 if active_modal.modal.render_bare(cx) {
291 return active_modal.modal.view().into_any_element();
292 }
293
294 div()
295 .absolute()
296 .size_full()
297 .inset_0()
298 .occlude()
299 .when(active_modal.modal.fade_out_background(cx), |this| {
300 let mut background = cx.theme().colors().elevated_surface_background;
301 background.fade_out(0.2);
302 this.bg(background)
303 })
304 .on_mouse_down(
305 MouseButton::Left,
306 cx.listener(|this, _, window, cx| {
307 this.hide_modal(window, cx);
308 }),
309 )
310 .child(
311 v_flex()
312 .h(px(0.0))
313 .top_20()
314 .items_center()
315 .track_focus(&active_modal.focus_handle)
316 .child(
317 h_flex()
318 .occlude()
319 .child(active_modal.modal.view())
320 .on_mouse_down(MouseButton::Left, |_, _, cx| {
321 cx.stop_propagation();
322 }),
323 ),
324 )
325 .into_any_element()
326 }
327}
328