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        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                // Regression: traversing past the modal must cross more than
339                // the former 100-attempt limit before reaching its other end.
340                (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    /// Moves focus to the next scripted stop, in place of `focus_prev`. Running
441    /// out fails the test, so a search that never stops cannot hang it.
442    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        // A tab order in which the second stop is its own neighbor never
457        // returns to the first. Popped from the end: first, second, second.
458        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        // Prove that the observers used by the containment regressions really
474        // receive native focus notifications when a background control is used.
475        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            // Retain every container handle while inner scopes disappear and
594            // return. Rendered containment, not registration order or ownership
595            // lifetime, must decide which remaining scope handles Tab.
596            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}