Skip to main content

gpui_base/
root.rs

1//! Window roots and presentation-layer plugins.
2use 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
14/// Scan at most one cycle of the rendered tab order, preserving containment
15/// regardless of how many controls exist behind a modal. GPUI exposes traversal
16/// but not candidate enumeration. This scan is synchronous: only the final
17/// focus path is published at the next draw, not the background candidates.
18fn 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    // GPUI's tab order can cycle short of the whole window (a stop whose
26    // previous stop is itself), so the walk ends at the first repeated stop,
27    // not only on a return to the one it began with.
28    let mut visited = Vec::new();
29    while !container.contains_focused(window, cx) {
30        let focused = window.focused(cx);
31        if visited.contains(&focused) {
32            // No tab stop in the trap is reachable. Its own handle remains a
33            // safe focus target even when excluded from ordinary tab order.
34            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
53/// A presentation layer's retained, per-window facilities.
54///
55/// Register during explicit application initialization, before creating windows.
56/// The view renders above application content. Base owns the root regardless of
57/// which plugins are registered; Cargo features never select its type.
58///
59/// On every root render, each plugin participates in three stages:
60///
61/// 1. [`RootPlugin::prepare`] synchronizes window state before elements are built.
62/// 2. [`RootPlugin::style`] supplies defaults for the root surface.
63/// 3. [`RootPlugin::decorate`] wraps the completed surface in presentation owned
64///    by the plugin.
65///
66/// The plugin's [`Render`] output is mounted as an overlay above application
67/// content. Plugins and their overlays are processed in registration order, so
68/// later plugins appear above earlier ones. Notify the plugin entity after its
69/// state changes to render the root again; `prepare` and `style` must not notify,
70/// because they run during that render.
71///
72/// Factories are captured when a root is created, so registration affects only
73/// future windows.
74pub trait RootPlugin: Render + Sized {
75    /// Synchronize settings derived from this plugin with the window.
76    ///
77    /// This runs before the root surface and plugin overlays are built. It is
78    /// intended for window-scoped state such as rem size or the active text
79    /// selection scope, not for producing elements.
80    fn prepare(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {}
81
82    /// Apply this plugin's default styles directly to the root surface.
83    ///
84    /// Styles set on the [`Root`] instance are refined onto the surface after
85    /// this hook and therefore take precedence over plugin defaults.
86    fn style(&self, _surface: &mut Stateful<Div>, _window: &mut Window, _cx: &mut App) {}
87
88    /// Add presentation around the completed root surface.
89    ///
90    /// This runs after plugin defaults and instance styles have been applied.
91    /// Return `surface` unchanged when no outer presentation is needed. Typical
92    /// uses include client-side window borders or another structural wrapper.
93    /// `root` provides read-only access to the root and its application view.
94    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
119/// The window's content and overlay host, independent of any styled component library.
120pub struct Root {
121    view: AnyView,
122    style: StyleRefinement,
123    plugins: Vec<Plugin>,
124}
125
126impl Root {
127    /// Register a presentation plugin once per application. Re-registering its
128    /// type replaces the factory for future windows rather than mounting it twice.
129    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            // Plugin construction can enqueue notifications. Observe only after
141            // that effect cycle so mounting a Root does not immediately render
142            // application content a second time.
143            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    /// The original application content entity.
193    pub fn view(&self) -> &AnyView {
194        &self.view
195    }
196
197    /// Find a presentation plugin owned by this window.
198    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                // Regression: traversing past the modal must cross more than
341                // the former 100-attempt limit before reaching its other end.
342                (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    /// Moves focus to the next scripted stop, in place of `focus_prev`. Running
443    /// out fails the test, so a search that never stops cannot hang it.
444    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        // A tab order in which the second stop is its own neighbor never
459        // returns to the first. Popped from the end: first, second, second.
460        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        // Prove that the observers used by the containment regressions really
476        // receive native focus notifications when a background control is used.
477        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            // Retain every container handle while inner scopes disappear and
596            // return. Rendered containment, not registration order or ownership
597            // lifetime, must decide which remaining scope handles Tab.
598            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}