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
14pub(crate) fn init(cx: &mut App) {
15    cx.bind_keys([
16        KeyBinding::new("tab", Tab, Some(CONTEXT)),
17        KeyBinding::new("shift-tab", TabPrev, Some(CONTEXT)),
18        #[cfg(target_os = "macos")]
19        KeyBinding::new("cmd-c", Copy, Some(CONTEXT)),
20        #[cfg(not(target_os = "macos"))]
21        KeyBinding::new("ctrl-c", Copy, Some(CONTEXT)),
22    ]);
23}
24
25/// A presentation layer's retained, per-window facilities.
26///
27/// Register during explicit application initialization, before creating windows.
28/// The view renders above application content. Base owns the root regardless of
29/// which plugins are registered; Cargo features never select its type.
30///
31/// On every root render, each plugin participates in three stages:
32///
33/// 1. [`RootPlugin::prepare`] synchronizes window state before elements are built.
34/// 2. [`RootPlugin::style`] supplies defaults for the root surface.
35/// 3. [`RootPlugin::decorate`] wraps the completed surface in presentation owned
36///    by the plugin.
37///
38/// The plugin's [`Render`] output is mounted as an overlay above application
39/// content. Plugins and their overlays are processed in registration order, so
40/// later plugins appear above earlier ones. Notify the plugin entity after its
41/// state changes to render the root again; `prepare` and `style` must not notify,
42/// because they run during that render.
43///
44/// Factories are captured when a root is created, so registration affects only
45/// future windows.
46pub trait RootPlugin: Render + Sized {
47    /// Synchronize settings derived from this plugin with the window.
48    ///
49    /// This runs before the root surface and plugin overlays are built. It is
50    /// intended for window-scoped state such as rem size or the active text
51    /// selection scope, not for producing elements.
52    fn prepare(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {}
53
54    /// Apply this plugin's default styles directly to the root surface.
55    ///
56    /// Styles set on the [`Root`] instance are refined onto the surface after
57    /// this hook and therefore take precedence over plugin defaults.
58    fn style(&self, _surface: &mut Stateful<Div>, _window: &mut Window, _cx: &mut App) {}
59
60    /// Add presentation around the completed root surface.
61    ///
62    /// This runs after plugin defaults and instance styles have been applied.
63    /// Return `surface` unchanged when no outer presentation is needed. Typical
64    /// uses include client-side window borders or another structural wrapper.
65    /// `root` provides read-only access to the root and its application view.
66    fn decorate(
67        &self,
68        surface: AnyElement,
69        _root: &Root,
70        _window: &mut Window,
71        _cx: &mut App,
72    ) -> impl IntoElement {
73        surface
74    }
75}
76
77type PluginFactory = Rc<dyn Fn(&mut Window, &mut Context<Root>) -> Plugin>;
78type Prepare = Rc<dyn Fn(&mut Window, &mut App)>;
79type SurfaceStyle = Rc<dyn Fn(&mut Stateful<Div>, &mut Window, &mut App)>;
80type Decorate = Rc<dyn Fn(AnyElement, &Root, &mut Window, &mut App) -> AnyElement>;
81#[derive(Default)]
82struct PluginRegistry(Vec<(TypeId, PluginFactory)>);
83impl Global for PluginRegistry {}
84struct Plugin {
85    view: AnyView,
86    prepare: Prepare,
87    style: SurfaceStyle,
88    decorate: Decorate,
89}
90
91/// The window's content and overlay host, independent of any styled component library.
92pub struct Root {
93    view: AnyView,
94    style: StyleRefinement,
95    plugins: Vec<Plugin>,
96}
97
98impl Root {
99    /// Register a presentation plugin once per application. Re-registering its
100    /// type replaces the factory for future windows rather than mounting it twice.
101    pub fn register_plugin<V: RootPlugin>(
102        cx: &mut App,
103        build: fn(&mut Window, &mut Context<V>) -> V,
104    ) {
105        if !cx.has_global::<PluginRegistry>() {
106            cx.set_global(PluginRegistry::default());
107        }
108        let factory: PluginFactory = Rc::new(move |window, cx| {
109            let entity = cx.new(|cx| build(window, cx));
110            let root = cx.weak_entity();
111            let observed = entity.clone();
112            // Plugin construction can enqueue notifications. Observe only after
113            // that effect cycle so mounting a Root does not immediately render
114            // application content a second time.
115            cx.defer(move |cx| {
116                cx.observe(&observed, move |_, cx| {
117                    let _ = root.update(cx, |_, cx| cx.notify());
118                })
119                .detach();
120            });
121            let prepare = entity.clone();
122            let style = entity.clone();
123            let decorate = entity.clone();
124            Plugin {
125                view: entity.into(),
126                prepare: Rc::new(move |window, cx| {
127                    prepare.update(cx, |state, cx| state.prepare(window, cx))
128                }),
129                style: Rc::new(move |surface, window, cx| {
130                    style.update(cx, |state, cx| state.style(surface, window, cx))
131                }),
132                decorate: Rc::new(move |surface, root, window, cx| {
133                    decorate.update(cx, |state, cx| {
134                        state.decorate(surface, root, window, cx).into_any_element()
135                    })
136                }),
137            }
138        });
139        let plugins = &mut cx.global_mut::<PluginRegistry>().0;
140        if let Some(entry) = plugins.iter_mut().find(|(id, _)| *id == TypeId::of::<V>()) {
141            entry.1 = factory;
142        } else {
143            plugins.push((TypeId::of::<V>(), factory));
144        }
145    }
146
147    pub fn new(view: impl Into<AnyView>, window: &mut Window, cx: &mut Context<Self>) -> Self {
148        #[cfg(all(target_os = "macos", not(test)))]
149        crate::install_window_hit_test_forwarder(window);
150        let factories = cx
151            .try_global::<PluginRegistry>()
152            .map(|e| e.0.clone())
153            .unwrap_or_default();
154        Self {
155            view: view.into(),
156            style: StyleRefinement::default(),
157            plugins: factories
158                .into_iter()
159                .map(|(_, build)| build(window, cx))
160                .collect(),
161        }
162    }
163
164    /// The original application content entity.
165    pub fn view(&self) -> &AnyView {
166        &self.view
167    }
168
169    /// Find a presentation plugin owned by this window.
170    pub fn plugin<V: RootPlugin>(&self) -> Option<Entity<V>> {
171        self.plugins
172            .iter()
173            .find_map(|entry| entry.view.clone().downcast::<V>().ok())
174    }
175
176    pub fn read<'a>(window: &'a Window, cx: &'a App) -> &'a Self {
177        window
178            .root::<Self>()
179            .flatten()
180            .expect("window must have a Base Root")
181            .read(cx)
182    }
183    pub fn update<R>(
184        window: &mut Window,
185        cx: &mut App,
186        f: impl FnOnce(&mut Self, &mut Window, &mut Context<Self>) -> R,
187    ) -> R {
188        let root = window
189            .root::<Self>()
190            .flatten()
191            .expect("window must have a Base Root");
192        root.update(cx, |root, cx| f(root, window, cx))
193    }
194    fn on_action_tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context<Self>) {
195        // Check if we're inside a focus trap
196        if let Some(container_focus_handle) = crate::active_focus_trap(window, cx) {
197            // We're in a focus trap - try to focus next, then check if we're still inside
198            let before_focus = window.focused(cx);
199
200            // Try normal focus navigation
201            window.focus_next(cx);
202
203            // Check if we're still in the trap
204            if !container_focus_handle.contains_focused(window, cx) {
205                // We jumped out of the trap - need to cycle back to the beginning
206                // Find the first focusable element in the trap by continuing to focus_next
207                let mut attempts = 0;
208                const MAX_ATTEMPTS: usize = 100; // Prevent infinite loop
209
210                while !container_focus_handle.contains_focused(window, cx)
211                    && attempts < MAX_ATTEMPTS
212                {
213                    window.focus_next(cx);
214                    attempts += 1;
215
216                    // If we cycled back to where we started, restore original focus
217                    if window.focused(cx) == before_focus {
218                        break;
219                    }
220                }
221            }
222            return;
223        }
224
225        // Normal tab navigation
226        window.focus_next(cx);
227    }
228
229    fn on_action_tab_prev(&mut self, _: &TabPrev, window: &mut Window, cx: &mut Context<Self>) {
230        // Check if we're inside a focus trap
231        if let Some(container_focus_handle) = crate::active_focus_trap(window, cx) {
232            // We're in a focus trap - try to focus previous, then check if we're still inside
233            let before_focus = window.focused(cx);
234
235            // Try normal focus navigation
236            window.focus_prev(cx);
237
238            // Check if we're still in the trap
239            if !container_focus_handle.contains_focused(window, cx) {
240                // We jumped out of the trap - need to cycle back to the end
241                // Find the last focusable element in the trap by continuing to focus_prev
242                let mut attempts = 0;
243                const MAX_ATTEMPTS: usize = 100; // Prevent infinite loop
244
245                while !container_focus_handle.contains_focused(window, cx)
246                    && attempts < MAX_ATTEMPTS
247                {
248                    window.focus_prev(cx);
249                    attempts += 1;
250
251                    // If we cycled back to where we started, restore original focus
252                    if window.focused(cx) == before_focus {
253                        break;
254                    }
255                }
256            }
257            return;
258        }
259
260        // Normal tab navigation
261        window.focus_prev(cx);
262    }
263
264    fn on_action_copy(&mut self, _: &Copy, window: &mut Window, cx: &mut Context<Self>) {
265        let text = crate::TextSelection::selected_text(window, cx)
266            .trim()
267            .to_string();
268        if text.is_empty() {
269            cx.propagate();
270            return;
271        }
272        cx.write_to_clipboard(ClipboardItem::new_string(text));
273    }
274}
275impl Styled for Root {
276    fn style(&mut self) -> &mut StyleRefinement {
277        &mut self.style
278    }
279}
280impl Render for Root {
281    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
282        for plugin in &self.plugins {
283            (plugin.prepare)(window, cx);
284        }
285        let mut content = div()
286            .id("root")
287            .key_context(CONTEXT)
288            .on_action(cx.listener(Self::on_action_tab))
289            .on_action(cx.listener(Self::on_action_tab_prev))
290            .on_action(cx.listener(Self::on_action_copy))
291            .relative()
292            .size_full()
293            .child(TextSelectionLayer)
294            .child(self.view.clone())
295            .child(
296                div()
297                    .absolute()
298                    .inset_0()
299                    .children(self.plugins.iter().map(|plugin| plugin.view.clone())),
300            );
301        for plugin in &self.plugins {
302            (plugin.style)(&mut content, window, cx);
303        }
304        let mut content = content.refine_style(&self.style).into_any_element();
305        for plugin in &self.plugins {
306            content = (plugin.decorate)(content, self, window, cx);
307        }
308        content
309    }
310}
311
312#[cfg(test)]
313mod tests {
314    use super::*;
315    use gpui::TestAppContext;
316
317    struct Content;
318    impl Render for Content {
319        fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
320            div()
321        }
322    }
323    struct Layer;
324    impl Render for Layer {
325        fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
326            div()
327        }
328    }
329    impl RootPlugin for Layer {}
330    fn layer(_: &mut Window, _: &mut Context<Layer>) -> Layer {
331        Layer
332    }
333
334    #[gpui::test]
335    fn plugin_registration_is_idempotent_and_state_is_per_window(cx: &mut TestAppContext) {
336        cx.update(|cx| {
337            crate::init(cx);
338            Root::register_plugin(cx, layer);
339            Root::register_plugin(cx, layer);
340        });
341        let mut ids = Vec::new();
342        for _ in 0..2 {
343            let (root, _) = cx.add_window_view(|window, cx| {
344                let content = cx.new(|_| Content);
345                Root::new(content, window, cx)
346            });
347            let id = root.read_with(cx, |root, _| {
348                assert_eq!(root.plugins.len(), 1);
349                root.plugin::<Layer>().unwrap().entity_id()
350            });
351            ids.push(id);
352        }
353        assert_ne!(ids[0], ids[1]);
354    }
355}