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