1use crate::input::Copy;
3use crate::{StyledExt, TextSelectionLayer};
4use gpui::{
5 AnyElement, AnyView, App, AppContext, ClipboardItem, Context, Div, Entity, Global,
6 InteractiveElement, IntoElement, KeyBinding, ParentElement, Render, Stateful, StyleRefinement,
7 Styled, Window, actions, div,
8};
9use std::{any::TypeId, rc::Rc};
10
11actions!(root, [Tab, TabPrev]);
12const CONTEXT: &str = "Root";
13
14fn advance_focus(window: &mut Window, cx: &mut App, advance: fn(&mut Window, &mut App)) {
19 let Some(container) = crate::active_focus_trap(window, cx) else {
20 advance(window, cx);
21 return;
22 };
23
24 advance(window, cx);
25 let first_candidate = window.focused(cx);
26 while !container.contains_focused(window, cx) {
27 advance(window, cx);
28 if window.focused(cx) == first_candidate {
29 container.focus(window, cx);
32 break;
33 }
34 }
35}
36
37pub(crate) fn init(cx: &mut App) {
38 cx.bind_keys([
39 KeyBinding::new("tab", Tab, Some(CONTEXT)),
40 KeyBinding::new("shift-tab", TabPrev, Some(CONTEXT)),
41 #[cfg(target_os = "macos")]
42 KeyBinding::new("cmd-c", Copy, Some(CONTEXT)),
43 #[cfg(not(target_os = "macos"))]
44 KeyBinding::new("ctrl-c", Copy, Some(CONTEXT)),
45 ]);
46}
47
48pub trait RootPlugin: Render + Sized {
70 fn prepare(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {}
76
77 fn style(&self, _surface: &mut Stateful<Div>, _window: &mut Window, _cx: &mut App) {}
82
83 fn decorate(
90 &self,
91 surface: AnyElement,
92 _root: &Root,
93 _window: &mut Window,
94 _cx: &mut App,
95 ) -> impl IntoElement {
96 surface
97 }
98}
99
100type PluginFactory = Rc<dyn Fn(&mut Window, &mut Context<Root>) -> Plugin>;
101type Prepare = Rc<dyn Fn(&mut Window, &mut App)>;
102type SurfaceStyle = Rc<dyn Fn(&mut Stateful<Div>, &mut Window, &mut App)>;
103type Decorate = Rc<dyn Fn(AnyElement, &Root, &mut Window, &mut App) -> AnyElement>;
104#[derive(Default)]
105struct PluginRegistry(Vec<(TypeId, PluginFactory)>);
106impl Global for PluginRegistry {}
107struct Plugin {
108 view: AnyView,
109 prepare: Prepare,
110 style: SurfaceStyle,
111 decorate: Decorate,
112}
113
114pub struct Root {
116 view: AnyView,
117 style: StyleRefinement,
118 plugins: Vec<Plugin>,
119}
120
121impl Root {
122 pub fn register_plugin<V: RootPlugin>(
125 cx: &mut App,
126 build: fn(&mut Window, &mut Context<V>) -> V,
127 ) {
128 if !cx.has_global::<PluginRegistry>() {
129 cx.set_global(PluginRegistry::default());
130 }
131 let factory: PluginFactory = Rc::new(move |window, cx| {
132 let entity = cx.new(|cx| build(window, cx));
133 let root = cx.weak_entity();
134 let observed = entity.clone();
135 cx.defer(move |cx| {
139 cx.observe(&observed, move |_, cx| {
140 let _ = root.update(cx, |_, cx| cx.notify());
141 })
142 .detach();
143 });
144 let prepare = entity.clone();
145 let style = entity.clone();
146 let decorate = entity.clone();
147 Plugin {
148 view: entity.into(),
149 prepare: Rc::new(move |window, cx| {
150 prepare.update(cx, |state, cx| state.prepare(window, cx))
151 }),
152 style: Rc::new(move |surface, window, cx| {
153 style.update(cx, |state, cx| state.style(surface, window, cx))
154 }),
155 decorate: Rc::new(move |surface, root, window, cx| {
156 decorate.update(cx, |state, cx| {
157 state.decorate(surface, root, window, cx).into_any_element()
158 })
159 }),
160 }
161 });
162 let plugins = &mut cx.global_mut::<PluginRegistry>().0;
163 if let Some(entry) = plugins.iter_mut().find(|(id, _)| *id == TypeId::of::<V>()) {
164 entry.1 = factory;
165 } else {
166 plugins.push((TypeId::of::<V>(), factory));
167 }
168 }
169
170 pub fn new(view: impl Into<AnyView>, window: &mut Window, cx: &mut Context<Self>) -> Self {
171 #[cfg(all(target_os = "macos", not(test)))]
172 crate::install_window_hit_test_forwarder(window);
173 let factories = cx
174 .try_global::<PluginRegistry>()
175 .map(|e| e.0.clone())
176 .unwrap_or_default();
177 Self {
178 view: view.into(),
179 style: StyleRefinement::default(),
180 plugins: factories
181 .into_iter()
182 .map(|(_, build)| build(window, cx))
183 .collect(),
184 }
185 }
186
187 pub fn view(&self) -> &AnyView {
189 &self.view
190 }
191
192 pub fn plugin<V: RootPlugin>(&self) -> Option<Entity<V>> {
194 self.plugins
195 .iter()
196 .find_map(|entry| entry.view.clone().downcast::<V>().ok())
197 }
198
199 pub fn read<'a>(window: &'a Window, cx: &'a App) -> &'a Self {
200 window
201 .root::<Self>()
202 .flatten()
203 .expect("window must have a Base Root")
204 .read(cx)
205 }
206 pub fn update<R>(
207 window: &mut Window,
208 cx: &mut App,
209 f: impl FnOnce(&mut Self, &mut Window, &mut Context<Self>) -> R,
210 ) -> R {
211 let root = window
212 .root::<Self>()
213 .flatten()
214 .expect("window must have a Base Root");
215 root.update(cx, |root, cx| f(root, window, cx))
216 }
217 fn on_action_tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context<Self>) {
218 advance_focus(window, cx, Window::focus_next);
219 }
220
221 fn on_action_tab_prev(&mut self, _: &TabPrev, window: &mut Window, cx: &mut Context<Self>) {
222 advance_focus(window, cx, Window::focus_prev);
223 }
224
225 fn on_action_copy(&mut self, _: &Copy, window: &mut Window, cx: &mut Context<Self>) {
226 let text = crate::TextSelection::selected_text(window, cx)
227 .trim()
228 .to_string();
229 if text.is_empty() {
230 cx.propagate();
231 return;
232 }
233 cx.write_to_clipboard(ClipboardItem::new_string(text));
234 }
235}
236impl Styled for Root {
237 fn style(&mut self) -> &mut StyleRefinement {
238 &mut self.style
239 }
240}
241impl Render for Root {
242 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
243 for plugin in &self.plugins {
244 (plugin.prepare)(window, cx);
245 }
246 let mut content = div()
247 .id("root")
248 .key_context(CONTEXT)
249 .on_action(cx.listener(Self::on_action_tab))
250 .on_action(cx.listener(Self::on_action_tab_prev))
251 .on_action(cx.listener(Self::on_action_copy))
252 .relative()
253 .size_full()
254 .child(TextSelectionLayer)
255 .child(self.view.clone())
256 .child(
257 div()
258 .absolute()
259 .inset_0()
260 .children(self.plugins.iter().map(|plugin| plugin.view.clone())),
261 );
262 for plugin in &self.plugins {
263 (plugin.style)(&mut content, window, cx);
264 }
265 let mut content = content.refine_style(&self.style).into_any_element();
266 for plugin in &self.plugins {
267 content = (plugin.decorate)(content, self, window, cx);
268 }
269 content
270 }
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276 use crate::FocusTrapElement as _;
277 use gpui::{FocusHandle, Subscription, TestAppContext, VisualTestContext, px};
278 use std::cell::Cell;
279
280 struct TrapContent {
281 background: Vec<FocusHandle>,
282 container: FocusHandle,
283 controls: Vec<FocusHandle>,
284 _focus_subscriptions: Vec<Subscription>,
285 }
286
287 impl Render for TrapContent {
288 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
289 div()
290 .tab_group()
291 .size_full()
292 .children(self.background.iter().enumerate().map(|(index, handle)| {
293 div()
294 .id(("background", index))
295 .track_focus(handle)
296 .tab_index(0)
297 .size(px(1.))
298 }))
299 .child(
300 div()
301 .id("trap")
302 .track_focus(&self.container)
303 .tab_stop(false)
304 .children(self.controls.iter().enumerate().map(|(index, handle)| {
305 div()
306 .id(("control", index))
307 .track_focus(handle)
308 .tab_index(0)
309 .size(px(1.))
310 }))
311 .focus_trap("modal", &self.container),
312 )
313 }
314 }
315
316 fn trap_harness(
317 cx: &mut TestAppContext,
318 control_count: usize,
319 ) -> (
320 &mut VisualTestContext,
321 FocusHandle,
322 Vec<FocusHandle>,
323 Vec<FocusHandle>,
324 Rc<Cell<usize>>,
325 ) {
326 cx.update(crate::init);
327 let (container, controls, background) = cx.update(|cx| {
328 (
329 cx.focus_handle(),
330 (0..control_count)
331 .map(|_| cx.focus_handle().tab_stop(true))
332 .collect::<Vec<_>>(),
333 (0..256)
336 .map(|_| cx.focus_handle().tab_stop(true))
337 .collect::<Vec<_>>(),
338 )
339 });
340 let background_focuses = Rc::new(Cell::new(0));
341 let (_, cx) = cx.add_window_view({
342 let container = container.clone();
343 let controls = controls.clone();
344 let background = background.clone();
345 let background_focuses = background_focuses.clone();
346 move |window, cx| {
347 let content = cx.new(|cx| {
348 let subscriptions = background
349 .iter()
350 .map(|handle| {
351 let focuses = background_focuses.clone();
352 cx.on_focus(handle, window, move |_, _, _| {
353 focuses.set(focuses.get() + 1)
354 })
355 })
356 .collect();
357 TrapContent {
358 background,
359 container,
360 controls,
361 _focus_subscriptions: subscriptions,
362 }
363 });
364 Root::new(content, window, cx)
365 }
366 });
367 cx.update(|window, _| window.activate_window());
368 cx.executor().run_until_parked();
369 cx.update(|window, cx| window.draw(cx).clear(cx));
370 (cx, container, controls, background, background_focuses)
371 }
372
373 fn assert_trapped_focus(
374 cx: &mut VisualTestContext,
375 container: &FocusHandle,
376 expected: &FocusHandle,
377 ) {
378 cx.update(|window, cx| {
379 window.draw(cx).clear(cx);
380 assert_eq!(window.focused(cx).as_ref(), Some(expected));
381 assert!(container.contains_focused(window, cx));
382 });
383 }
384
385 #[gpui::test]
386 fn focus_trap_wraps_forward_past_more_than_100_background_stops(cx: &mut TestAppContext) {
387 let (cx, container, controls, _, background_focuses) = trap_harness(cx, 2);
388 cx.update(|window, cx| {
389 controls[1].focus(window, cx);
390 window.draw(cx).clear(cx);
391 });
392
393 cx.dispatch_action(Tab);
394 assert_trapped_focus(cx, &container, &controls[0]);
395 cx.dispatch_action(Tab);
396 assert_trapped_focus(cx, &container, &controls[1]);
397 assert_eq!(background_focuses.get(), 0);
398 }
399
400 #[gpui::test]
401 fn focus_trap_wraps_backward_past_more_than_100_background_stops(cx: &mut TestAppContext) {
402 let (cx, container, controls, _, background_focuses) = trap_harness(cx, 2);
403 cx.update(|window, cx| {
404 controls[0].focus(window, cx);
405 window.draw(cx).clear(cx);
406 });
407
408 cx.dispatch_action(TabPrev);
409 assert_trapped_focus(cx, &container, &controls[1]);
410 cx.dispatch_action(TabPrev);
411 assert_trapped_focus(cx, &container, &controls[0]);
412 assert_eq!(background_focuses.get(), 0);
413 }
414
415 #[gpui::test]
416 fn focus_trap_without_tab_stops_keeps_its_container_focused(cx: &mut TestAppContext) {
417 let (cx, container, _, _, background_focuses) = trap_harness(cx, 0);
418 cx.update(|window, cx| {
419 container.focus(window, cx);
420 window.draw(cx).clear(cx);
421 });
422
423 cx.dispatch_action(Tab);
424 assert_trapped_focus(cx, &container, &container);
425 cx.dispatch_action(TabPrev);
426 assert_trapped_focus(cx, &container, &container);
427 assert_eq!(background_focuses.get(), 0);
428 }
429
430 #[gpui::test]
431 fn inactive_focus_trap_does_not_capture_background_navigation(cx: &mut TestAppContext) {
432 let (cx, _, _, background, background_focuses) = trap_harness(cx, 2);
433 cx.update(|window, cx| {
434 background[0].focus(window, cx);
435 window.draw(cx).clear(cx);
436 });
437 assert_eq!(background_focuses.get(), 1);
440
441 cx.dispatch_action(Tab);
442 cx.update(|window, cx| {
443 window.draw(cx).clear(cx);
444 assert_eq!(window.focused(cx), Some(background[1].clone()));
445 });
446 cx.dispatch_action(TabPrev);
447 cx.update(|window, cx| {
448 window.draw(cx).clear(cx);
449 assert_eq!(window.focused(cx), Some(background[0].clone()));
450 });
451 assert_eq!(background_focuses.get(), 3);
452 }
453
454 struct Content;
455 impl Render for Content {
456 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
457 div()
458 }
459 }
460 struct Layer;
461 impl Render for Layer {
462 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
463 div()
464 }
465 }
466 impl RootPlugin for Layer {}
467 fn layer(_: &mut Window, _: &mut Context<Layer>) -> Layer {
468 Layer
469 }
470
471 #[gpui::test]
472 fn plugin_registration_is_idempotent_and_state_is_per_window(cx: &mut TestAppContext) {
473 cx.update(|cx| {
474 crate::init(cx);
475 Root::register_plugin(cx, layer);
476 Root::register_plugin(cx, layer);
477 });
478 let mut ids = Vec::new();
479 for _ in 0..2 {
480 let (root, _) = cx.add_window_view(|window, cx| {
481 let content = cx.new(|_| Content);
482 Root::new(content, window, cx)
483 });
484 let id = root.read_with(cx, |root, _| {
485 assert_eq!(root.plugins.len(), 1);
486 root.plugin::<Layer>().unwrap().entity_id()
487 });
488 ids.push(id);
489 }
490 assert_ne!(ids[0], ids[1]);
491 }
492
493 mod nested_traps {
494 use super::*;
495
496 struct NestedTraps {
497 containers: Vec<FocusHandle>,
498 controls: Vec<[FocusHandle; 2]>,
499 depth: usize,
500 }
501
502 impl Render for NestedTraps {
503 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
504 let mut child = div().into_any_element();
505 for index in (0..self.depth).rev() {
506 let control = |side: usize| {
507 div()
508 .id(("nested-control", index * 2 + side))
509 .track_focus(&self.controls[index][side])
510 .tab_index(0)
511 .size(px(10.))
512 };
513 child = div()
514 .tab_stop(false)
515 .child(control(0))
516 .child(child)
517 .child(control(1))
518 .focus_trap(("nested-trap", index), &self.containers[index])
519 .into_any_element();
520 }
521 div().tab_group().size_full().child(child)
522 }
523 }
524
525 #[gpui::test]
526 fn nested_traps_keep_the_innermost_scope_across_close_and_reopen(cx: &mut TestAppContext) {
527 cx.update(crate::init);
528 let (containers, controls) = cx.update(|cx| {
529 (
530 (0..6).map(|_| cx.focus_handle()).collect::<Vec<_>>(),
531 (0..6)
532 .map(|_| {
533 [
534 cx.focus_handle().tab_stop(true),
535 cx.focus_handle().tab_stop(true),
536 ]
537 })
538 .collect::<Vec<_>>(),
539 )
540 });
541 let (root, cx) = cx.add_window_view({
542 let containers = containers.clone();
543 let controls = controls.clone();
544 move |window, cx| {
545 let view = cx.new(|_| NestedTraps {
546 containers,
547 controls,
548 depth: 6,
549 });
550 Root::new(view, window, cx)
551 }
552 });
553 let view = root.read_with(cx, |root, _| {
554 root.view.clone().downcast::<NestedTraps>().unwrap()
555 });
556
557 for depth in [6, 5, 4, 3, 2, 1, 2, 3, 4, 5, 6] {
561 view.update(cx, |view, cx| {
562 view.depth = depth;
563 cx.notify();
564 });
565 let index = depth - 1;
566 cx.update(|window, cx| {
567 controls[index][1].focus(window, cx);
568 window.draw(cx).clear(cx);
569 });
570 cx.dispatch_action(Tab);
571 assert_trapped_focus(cx, &containers[index], &controls[index][0]);
572 cx.dispatch_action(TabPrev);
573 assert_trapped_focus(cx, &containers[index], &controls[index][1]);
574 }
575 }
576 }
577}