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 #[cfg(feature = "test-support")]
249 crate::test_support::begin_frame(window);
250 for plugin in &self.plugins {
251 (plugin.prepare)(window, cx);
252 }
253 let mut content = div()
254 .id("root")
255 .key_context(CONTEXT)
256 .on_action(cx.listener(Self::on_action_tab))
257 .on_action(cx.listener(Self::on_action_tab_prev))
258 .on_action(cx.listener(Self::on_action_copy))
259 .relative()
260 .size_full()
261 .child(TextSelectionLayer)
262 .child(self.view.clone())
263 .child(
264 div()
265 .absolute()
266 .inset_0()
267 .children(self.plugins.iter().map(|plugin| plugin.view.clone())),
268 );
269 for plugin in &self.plugins {
270 (plugin.style)(&mut content, window, cx);
271 }
272 let mut content = content.refine_style(&self.style).into_any_element();
273 for plugin in &self.plugins {
274 content = (plugin.decorate)(content, self, window, cx);
275 }
276 content
277 }
278}
279
280#[cfg(test)]
281mod tests {
282 use super::*;
283 use crate::FocusTrapElement as _;
284 use gpui::{FocusHandle, Subscription, TestAppContext, VisualTestContext, px};
285 use std::cell::Cell;
286
287 struct TrapContent {
288 background: Vec<FocusHandle>,
289 container: FocusHandle,
290 controls: Vec<FocusHandle>,
291 _focus_subscriptions: Vec<Subscription>,
292 }
293
294 impl Render for TrapContent {
295 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
296 div()
297 .tab_group()
298 .size_full()
299 .children(self.background.iter().enumerate().map(|(index, handle)| {
300 div()
301 .id(("background", index))
302 .track_focus(handle)
303 .tab_index(0)
304 .size(px(1.))
305 }))
306 .child(
307 div()
308 .id("trap")
309 .track_focus(&self.container)
310 .tab_stop(false)
311 .children(self.controls.iter().enumerate().map(|(index, handle)| {
312 div()
313 .id(("control", index))
314 .track_focus(handle)
315 .tab_index(0)
316 .size(px(1.))
317 }))
318 .focus_trap("modal", &self.container),
319 )
320 }
321 }
322
323 fn trap_harness(
324 cx: &mut TestAppContext,
325 control_count: usize,
326 ) -> (
327 &mut VisualTestContext,
328 FocusHandle,
329 Vec<FocusHandle>,
330 Vec<FocusHandle>,
331 Rc<Cell<usize>>,
332 ) {
333 cx.update(crate::init);
334 let (container, controls, background) = cx.update(|cx| {
335 (
336 cx.focus_handle(),
337 (0..control_count)
338 .map(|_| cx.focus_handle().tab_stop(true))
339 .collect::<Vec<_>>(),
340 (0..256)
343 .map(|_| cx.focus_handle().tab_stop(true))
344 .collect::<Vec<_>>(),
345 )
346 });
347 let background_focuses = Rc::new(Cell::new(0));
348 let (_, cx) = cx.add_window_view({
349 let container = container.clone();
350 let controls = controls.clone();
351 let background = background.clone();
352 let background_focuses = background_focuses.clone();
353 move |window, cx| {
354 let content = cx.new(|cx| {
355 let subscriptions = background
356 .iter()
357 .map(|handle| {
358 let focuses = background_focuses.clone();
359 cx.on_focus(handle, window, move |_, _, _| {
360 focuses.set(focuses.get() + 1)
361 })
362 })
363 .collect();
364 TrapContent {
365 background,
366 container,
367 controls,
368 _focus_subscriptions: subscriptions,
369 }
370 });
371 Root::new(content, window, cx)
372 }
373 });
374 cx.update(|window, _| window.activate_window());
375 cx.executor().run_until_parked();
376 cx.update(|window, cx| window.draw(cx).clear(cx));
377 (cx, container, controls, background, background_focuses)
378 }
379
380 fn assert_trapped_focus(
381 cx: &mut VisualTestContext,
382 container: &FocusHandle,
383 expected: &FocusHandle,
384 ) {
385 cx.update(|window, cx| {
386 window.draw(cx).clear(cx);
387 assert_eq!(window.focused(cx).as_ref(), Some(expected));
388 assert!(container.contains_focused(window, cx));
389 });
390 }
391
392 #[gpui::test]
393 fn focus_trap_wraps_forward_past_more_than_100_background_stops(cx: &mut TestAppContext) {
394 let (cx, container, controls, _, background_focuses) = trap_harness(cx, 2);
395 cx.update(|window, cx| {
396 controls[1].focus(window, cx);
397 window.draw(cx).clear(cx);
398 });
399
400 cx.dispatch_action(Tab);
401 assert_trapped_focus(cx, &container, &controls[0]);
402 cx.dispatch_action(Tab);
403 assert_trapped_focus(cx, &container, &controls[1]);
404 assert_eq!(background_focuses.get(), 0);
405 }
406
407 #[gpui::test]
408 fn focus_trap_wraps_backward_past_more_than_100_background_stops(cx: &mut TestAppContext) {
409 let (cx, container, controls, _, background_focuses) = trap_harness(cx, 2);
410 cx.update(|window, cx| {
411 controls[0].focus(window, cx);
412 window.draw(cx).clear(cx);
413 });
414
415 cx.dispatch_action(TabPrev);
416 assert_trapped_focus(cx, &container, &controls[1]);
417 cx.dispatch_action(TabPrev);
418 assert_trapped_focus(cx, &container, &controls[0]);
419 assert_eq!(background_focuses.get(), 0);
420 }
421
422 #[gpui::test]
423 fn focus_trap_without_tab_stops_keeps_its_container_focused(cx: &mut TestAppContext) {
424 let (cx, container, _, _, background_focuses) = trap_harness(cx, 0);
425 cx.update(|window, cx| {
426 container.focus(window, cx);
427 window.draw(cx).clear(cx);
428 });
429
430 cx.dispatch_action(Tab);
431 assert_trapped_focus(cx, &container, &container);
432 cx.dispatch_action(TabPrev);
433 assert_trapped_focus(cx, &container, &container);
434 assert_eq!(background_focuses.get(), 0);
435 }
436
437 thread_local! {
438 static SCRIPTED_STOPS: std::cell::RefCell<Vec<FocusHandle>> =
439 const { std::cell::RefCell::new(Vec::new()) };
440 }
441
442 fn scripted_advance(window: &mut Window, cx: &mut App) {
445 SCRIPTED_STOPS
446 .with(|stops| stops.borrow_mut().pop())
447 .expect("the search kept advancing after a stop repeated")
448 .focus(window, cx);
449 }
450
451 #[gpui::test]
452 fn focus_trap_search_ends_at_a_short_cycle_on_its_container(cx: &mut TestAppContext) {
453 let (cx, container, controls, background, _) = trap_harness(cx, 2);
454 cx.update(|window, cx| {
455 controls[0].focus(window, cx);
456 window.draw(cx).clear(cx);
457 });
458 let (first, second) = (background[0].clone(), background[1].clone());
461 SCRIPTED_STOPS.with(|stops| *stops.borrow_mut() = vec![second.clone(), second, first]);
462
463 cx.update(|window, cx| advance_focus(window, cx, scripted_advance));
464 assert_trapped_focus(cx, &container, &container);
465 assert!(SCRIPTED_STOPS.with(|stops| stops.borrow().is_empty()));
466 }
467
468 #[gpui::test]
469 fn inactive_focus_trap_does_not_capture_background_navigation(cx: &mut TestAppContext) {
470 let (cx, _, _, background, background_focuses) = trap_harness(cx, 2);
471 cx.update(|window, cx| {
472 background[0].focus(window, cx);
473 window.draw(cx).clear(cx);
474 });
475 assert_eq!(background_focuses.get(), 1);
478
479 cx.dispatch_action(Tab);
480 cx.update(|window, cx| {
481 window.draw(cx).clear(cx);
482 assert_eq!(window.focused(cx), Some(background[1].clone()));
483 });
484 cx.dispatch_action(TabPrev);
485 cx.update(|window, cx| {
486 window.draw(cx).clear(cx);
487 assert_eq!(window.focused(cx), Some(background[0].clone()));
488 });
489 assert_eq!(background_focuses.get(), 3);
490 }
491
492 struct Content;
493 impl Render for Content {
494 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
495 div()
496 }
497 }
498 struct Layer;
499 impl Render for Layer {
500 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
501 div()
502 }
503 }
504 impl RootPlugin for Layer {}
505 fn layer(_: &mut Window, _: &mut Context<Layer>) -> Layer {
506 Layer
507 }
508
509 #[gpui::test]
510 fn plugin_registration_is_idempotent_and_state_is_per_window(cx: &mut TestAppContext) {
511 cx.update(|cx| {
512 crate::init(cx);
513 Root::register_plugin(cx, layer);
514 Root::register_plugin(cx, layer);
515 });
516 let mut ids = Vec::new();
517 for _ in 0..2 {
518 let (root, _) = cx.add_window_view(|window, cx| {
519 let content = cx.new(|_| Content);
520 Root::new(content, window, cx)
521 });
522 let id = root.read_with(cx, |root, _| {
523 assert_eq!(root.plugins.len(), 1);
524 root.plugin::<Layer>().unwrap().entity_id()
525 });
526 ids.push(id);
527 }
528 assert_ne!(ids[0], ids[1]);
529 }
530
531 mod nested_traps {
532 use super::*;
533
534 struct NestedTraps {
535 containers: Vec<FocusHandle>,
536 controls: Vec<[FocusHandle; 2]>,
537 depth: usize,
538 }
539
540 impl Render for NestedTraps {
541 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
542 let mut child = div().into_any_element();
543 for index in (0..self.depth).rev() {
544 let control = |side: usize| {
545 div()
546 .id(("nested-control", index * 2 + side))
547 .track_focus(&self.controls[index][side])
548 .tab_index(0)
549 .size(px(10.))
550 };
551 child = div()
552 .tab_stop(false)
553 .child(control(0))
554 .child(child)
555 .child(control(1))
556 .focus_trap(("nested-trap", index), &self.containers[index])
557 .into_any_element();
558 }
559 div().tab_group().size_full().child(child)
560 }
561 }
562
563 #[gpui::test]
564 fn nested_traps_keep_the_innermost_scope_across_close_and_reopen(cx: &mut TestAppContext) {
565 cx.update(crate::init);
566 let (containers, controls) = cx.update(|cx| {
567 (
568 (0..6).map(|_| cx.focus_handle()).collect::<Vec<_>>(),
569 (0..6)
570 .map(|_| {
571 [
572 cx.focus_handle().tab_stop(true),
573 cx.focus_handle().tab_stop(true),
574 ]
575 })
576 .collect::<Vec<_>>(),
577 )
578 });
579 let (root, cx) = cx.add_window_view({
580 let containers = containers.clone();
581 let controls = controls.clone();
582 move |window, cx| {
583 let view = cx.new(|_| NestedTraps {
584 containers,
585 controls,
586 depth: 6,
587 });
588 Root::new(view, window, cx)
589 }
590 });
591 let view = root.read_with(cx, |root, _| {
592 root.view.clone().downcast::<NestedTraps>().unwrap()
593 });
594
595 for depth in [6, 5, 4, 3, 2, 1, 2, 3, 4, 5, 6] {
599 view.update(cx, |view, cx| {
600 view.depth = depth;
601 cx.notify();
602 });
603 let index = depth - 1;
604 cx.update(|window, cx| {
605 controls[index][1].focus(window, cx);
606 window.draw(cx).clear(cx);
607 });
608 cx.dispatch_action(Tab);
609 assert_trapped_focus(cx, &containers[index], &controls[index][0]);
610 cx.dispatch_action(TabPrev);
611 assert_trapped_focus(cx, &containers[index], &controls[index][1]);
612 }
613 }
614 }
615}