1use gpui::{
2 AnyElement, App, Bounds, Element, ElementId, FocusHandle, Global, GlobalElementId,
3 InteractiveElement, Interactivity, IntoElement, LayoutId, ParentElement, Pixels,
4 StatefulInteractiveElement, StyleRefinement, Styled, WeakFocusHandle, Window,
5};
6use std::collections::HashMap;
7
8pub fn init(cx: &mut App) {
10 cx.set_global(FocusTrapManager::new());
11}
12
13pub trait FocusTrapElement: InteractiveElement + Sized {
15 fn focus_trap(
40 self,
41 id: impl Into<ElementId>,
42 focus_handle: &FocusHandle,
43 ) -> FocusTrapContainer<Self>
44 where
45 Self: ParentElement + Styled + Element + 'static,
46 {
47 FocusTrapContainer::new(id, focus_handle.clone(), self)
48 }
49}
50impl<T: InteractiveElement + Sized> FocusTrapElement for T {}
51
52pub(crate) struct FocusTrapManager {
54 traps: HashMap<GlobalElementId, WeakFocusHandle>,
56}
57
58impl Global for FocusTrapManager {}
59
60impl FocusTrapManager {
61 fn new() -> Self {
63 Self {
64 traps: HashMap::new(),
65 }
66 }
67
68 pub(crate) fn global(cx: &App) -> &Self {
69 cx.global::<FocusTrapManager>()
70 }
71
72 fn global_mut(cx: &mut App) -> &mut Self {
73 cx.global_mut::<FocusTrapManager>()
74 }
75
76 fn register_trap(id: &GlobalElementId, container_handle: WeakFocusHandle, cx: &mut App) {
78 let this = Self::global_mut(cx);
79 this.traps.insert(id.clone(), container_handle);
80 this.cleanup();
81 }
82
83 fn find_active_trap(window: &Window, cx: &App) -> Option<FocusHandle> {
85 let mut active: Option<FocusHandle> = None;
86 for container_handle in Self::global(cx).traps.values() {
87 let Some(container) = container_handle.upgrade() else {
88 continue;
89 };
90
91 if container.contains_focused(window, cx)
92 && active
93 .as_ref()
94 .is_none_or(|current| current.contains(&container, window))
95 {
96 active = Some(container);
101 }
102 }
103 active
104 }
105
106 fn cleanup(&mut self) {
108 self.traps.retain(|_, handle| handle.upgrade().is_some());
109 }
110}
111
112impl Default for FocusTrapManager {
113 fn default() -> Self {
114 Self::new()
115 }
116}
117
118pub struct FocusTrapContainer<E: InteractiveElement + ParentElement + Styled + Element> {
123 id: ElementId,
124 focus_handle: FocusHandle,
125 base: E,
126}
127
128impl<E: InteractiveElement + ParentElement + Styled + Element> FocusTrapContainer<E> {
129 pub(crate) fn new(id: impl Into<ElementId>, focus_handle: FocusHandle, child: E) -> Self {
130 Self {
131 id: id.into(),
132 base: child.track_focus(&focus_handle),
133 focus_handle,
134 }
135 }
136}
137
138impl<E: InteractiveElement + ParentElement + Styled + Element> IntoElement
139 for FocusTrapContainer<E>
140{
141 type Element = Self;
142
143 fn into_element(self) -> Self::Element {
144 self
145 }
146}
147impl<E: InteractiveElement + ParentElement + Styled + Element> ParentElement
148 for FocusTrapContainer<E>
149{
150 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
151 self.base.extend(elements);
152 }
153}
154impl<E: InteractiveElement + ParentElement + Styled + Element> InteractiveElement
155 for FocusTrapContainer<E>
156{
157 fn interactivity(&mut self) -> &mut Interactivity {
158 self.base.interactivity()
159 }
160}
161impl<E: InteractiveElement + ParentElement + Styled + Element> StatefulInteractiveElement
162 for FocusTrapContainer<E>
163{
164}
165impl<E: InteractiveElement + ParentElement + Styled + Element> Styled for FocusTrapContainer<E> {
166 fn style(&mut self) -> &mut StyleRefinement {
167 self.base.style()
168 }
169}
170
171impl<E: InteractiveElement + ParentElement + Styled + Element + 'static> Element
172 for FocusTrapContainer<E>
173{
174 type RequestLayoutState = E::RequestLayoutState;
175 type PrepaintState = E::PrepaintState;
176
177 fn id(&self) -> Option<ElementId> {
178 Some(self.id.clone())
179 }
180
181 fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
182 None
183 }
184
185 fn a11y_role(&self) -> Option<gpui::Role> {
186 self.base.a11y_role()
187 }
188
189 fn write_a11y_info(&self, node: &mut gpui::accesskit::Node) {
190 self.base.write_a11y_info(node);
191 }
192
193 fn a11y_synthetic_children(
194 &mut self,
195 prepaint: &mut Self::PrepaintState,
196 builder: &mut gpui::A11ySubtreeBuilder,
197 ) {
198 self.base.a11y_synthetic_children(prepaint, builder);
199 }
200
201 fn request_layout(
202 &mut self,
203 global_id: Option<&gpui::GlobalElementId>,
204 _inspector_id: Option<&gpui::InspectorElementId>,
205 window: &mut Window,
206 cx: &mut App,
207 ) -> (LayoutId, Self::RequestLayoutState) {
208 FocusTrapManager::register_trap(global_id.unwrap(), self.focus_handle.downgrade(), cx);
210
211 self.base.request_layout(global_id, None, window, cx)
212 }
213
214 fn prepaint(
215 &mut self,
216 global_id: Option<&gpui::GlobalElementId>,
217 inspector_id: Option<&gpui::InspectorElementId>,
218 bounds: Bounds<Pixels>,
219 request_layout: &mut Self::RequestLayoutState,
220 window: &mut Window,
221 cx: &mut App,
222 ) -> Self::PrepaintState {
223 self.base
224 .prepaint(global_id, inspector_id, bounds, request_layout, window, cx)
225 }
226
227 fn paint(
228 &mut self,
229 global_id: Option<&gpui::GlobalElementId>,
230 inspector_id: Option<&gpui::InspectorElementId>,
231 bounds: Bounds<Pixels>,
232 request_layout: &mut Self::RequestLayoutState,
233 prepaint: &mut Self::PrepaintState,
234 window: &mut Window,
235 cx: &mut App,
236 ) {
237 self.base.paint(
238 global_id,
239 inspector_id,
240 bounds,
241 request_layout,
242 prepaint,
243 window,
244 cx,
245 )
246 }
247}
248
249#[doc(hidden)]
251pub fn active_focus_trap(window: &Window, cx: &App) -> Option<FocusHandle> {
252 FocusTrapManager::find_active_trap(window, cx)
253}
254
255#[cfg(test)]
256mod tests {
257 use super::*;
258 use crate::TestSupportExt as _;
259 use gpui::{Role, TestAppContext, accesskit, div};
260
261 #[gpui::test]
262 fn trap_exports_inner_semantics_through_the_outer_native_hooks(cx: &mut TestAppContext) {
263 cx.update(|cx| {
264 for (role, name) in [
265 (Role::Dialog, "Edit server"),
266 (Role::AlertDialog, "Delete server?"),
267 ] {
268 let trap = div()
269 .id("inner-host")
270 .test_support()
271 .role(role)
272 .aria_label(name)
273 .aria_description("Server settings")
274 .focus_trap("outer-trap", &cx.focus_handle());
275 assert_eq!(Element::id(&trap), Some("outer-trap".into()));
279 assert_eq!(trap.a11y_role(), Some(role));
280 let mut node = accesskit::Node::new(role);
281 trap.write_a11y_info(&mut node);
282 assert_eq!(node.label(), Some(name));
283 assert_eq!(node.description(), Some("Server settings"));
284 }
285 });
286 }
287}