Skip to main content

gpui_component/
root.rs

1use crate::{
2    ActiveTheme, ElementExt, Placement, StyledExt,
3    dialog::{ANIMATION_DURATION, Dialog},
4    input::{AnyInputState, Copy},
5    native_menu::FallbackMenuOverlay,
6    notification::{Notification, NotificationList},
7    sheet::Sheet,
8    tooltip::render_tooltip,
9    window_border,
10};
11use gpui::{
12    AnyView, App, AppContext, ClipboardItem, Context, DefiniteLength, ElementId, Entity,
13    FocusHandle, InteractiveElement, IntoElement, KeyBinding, ParentElement as _, Pixels, Render,
14    StyleRefinement, Styled, WeakFocusHandle, Window, actions, div, prelude::FluentBuilder as _,
15};
16use gpui_base::{TextSelection, TextSelectionLayer, TextSelectionScopeId};
17use std::{any::TypeId, rc::Rc};
18
19actions!(root, [Tab, TabPrev]);
20
21const CONTEXT: &str = "Root";
22pub(crate) fn init(cx: &mut App) {
23    cx.bind_keys([
24        KeyBinding::new("tab", Tab, Some(CONTEXT)),
25        KeyBinding::new("shift-tab", TabPrev, Some(CONTEXT)),
26        #[cfg(target_os = "macos")]
27        KeyBinding::new("cmd-c", Copy, Some(CONTEXT)),
28        #[cfg(not(target_os = "macos"))]
29        KeyBinding::new("ctrl-c", Copy, Some(CONTEXT)),
30    ]);
31}
32
33/// Root is a view for the App window for as the top level view (Must be the first view in the window).
34///
35/// It is used to manage the Sheet, Dialog, and Notification.
36pub struct Root {
37    style: StyleRefinement,
38    view: AnyView,
39    pub(crate) active_sheet: Option<ActiveSheet>,
40    pub(crate) active_dialogs: Vec<ActiveDialog>,
41    pub(super) focused_input: Option<AnyInputState>,
42    pub notification: Entity<NotificationList>,
43    pub(crate) tooltip_overlay: Entity<gpui_base::TooltipOverlay>,
44    pub(crate) native_menu_overlay: Entity<FallbackMenuOverlay>,
45    sheet_size: Option<DefiniteLength>,
46    window_shadow_size: Pixels,
47    /// Render the Linux CSD `window_border` wrapper.
48    bordered: bool,
49    /// The focus handle that will be restored after a dialog is closed with animation.
50    /// Used to handle rapid dialog opening/closing to maintain correct focus chain.
51    pending_focus_restore: Option<WeakFocusHandle>,
52    window_id: gpui::WindowId,
53}
54
55#[derive(Clone)]
56pub(crate) struct ActiveSheet {
57    focus_handle: FocusHandle,
58    /// The previous focused handle before opening the Sheet.
59    previous_focused_handle: Option<WeakFocusHandle>,
60    placement: Placement,
61    selection_scope: TextSelectionScopeId,
62    builder: Rc<dyn Fn(Sheet, &mut Window, &mut App) -> Sheet + 'static>,
63}
64
65#[derive(Clone)]
66pub(crate) struct ActiveDialog {
67    focus_handle: FocusHandle,
68    /// The previous focused handle before opening the Dialog.
69    previous_focused_handle: Option<WeakFocusHandle>,
70    selection_scope: TextSelectionScopeId,
71    builder: Rc<dyn Fn(Dialog, &mut Window, &mut App) -> Dialog + 'static>,
72}
73
74impl ActiveDialog {
75    pub(crate) fn new(
76        focus_handle: FocusHandle,
77        previous_focused_handle: Option<WeakFocusHandle>,
78        selection_scope: TextSelectionScopeId,
79        builder: impl Fn(Dialog, &mut Window, &mut App) -> Dialog + 'static,
80    ) -> Self {
81        Self {
82            focus_handle,
83            previous_focused_handle,
84            selection_scope,
85            builder: Rc::new(builder),
86        }
87    }
88}
89
90impl Root {
91    /// Clears window-owned text selection synchronously.
92    #[deprecated(note = "use gpui_base::TextSelection::clear instead")]
93    pub fn clear_text_selection(&mut self, cx: &mut Context<Self>) {
94        gpui_base::TextSelection::clear_for_window(self.window_id, cx);
95    }
96
97    /// Create a new Root view.
98    pub fn new(view: impl Into<AnyView>, window: &mut Window, cx: &mut Context<Self>) -> Self {
99        #[cfg(all(target_os = "macos", not(test)))]
100        gpui_base::install_window_hit_test_forwarder(window);
101
102        Self {
103            style: StyleRefinement::default(),
104            view: view.into(),
105            active_sheet: None,
106            active_dialogs: Vec::new(),
107            focused_input: None,
108            notification: cx.new(|cx| NotificationList::new(window, cx)),
109            tooltip_overlay: cx
110                .new(|_| gpui_base::TooltipOverlay::new().render_with(render_tooltip)),
111            native_menu_overlay: cx.new(|_| FallbackMenuOverlay::new()),
112            sheet_size: None,
113            window_shadow_size: window_border::SHADOW_SIZE,
114            bordered: true,
115            pending_focus_restore: None,
116            window_id: window.window_handle().window_id(),
117        }
118    }
119
120    fn allocate_text_selection_scope(&mut self) -> TextSelectionScopeId {
121        TextSelectionScopeId::new()
122    }
123
124    pub(crate) fn active_text_selection_scope(&self) -> TextSelectionScopeId {
125        self.active_dialogs
126            .last()
127            .map(|dialog| dialog.selection_scope)
128            .or_else(|| {
129                self.active_sheet
130                    .as_ref()
131                    .map(|sheet| sheet.selection_scope)
132            })
133            .unwrap_or_default()
134    }
135
136    /// Enable or disable the Linux client-side window border wrapper.
137    ///
138    /// Defaults to `true`. Use `bordered(false)` for layer-shell fullscreen windows
139    /// or other surfaces that should not render GPUI Component's window border.
140    pub fn bordered(mut self, bordered: bool) -> Self {
141        self.bordered = bordered;
142        self
143    }
144
145    /// Set the window border shadow size for Linux client-side decorations.
146    ///
147    /// Default: [`window_border::SHADOW_SIZE`]
148    pub fn window_shadow_size(mut self, size: impl Into<Pixels>) -> Self {
149        self.window_shadow_size = size.into();
150        self
151    }
152
153    pub fn update<F, R>(window: &mut Window, cx: &mut App, f: F) -> R
154    where
155        F: FnOnce(&mut Self, &mut Window, &mut Context<Self>) -> R,
156    {
157        let root = window
158            .root::<Root>()
159            .flatten()
160            .expect("BUG: window first layer should be a gpui_component::Root.");
161
162        root.update(cx, |root, cx| f(root, window, cx))
163    }
164
165    pub(crate) fn try_update<F, R>(window: &mut Window, cx: &mut App, f: F) -> Option<R>
166    where
167        F: FnOnce(&mut Self, &mut Window, &mut Context<Self>) -> R,
168    {
169        let root = window.root::<Root>().flatten()?;
170        Some(root.update(cx, |root, cx| f(root, window, cx)))
171    }
172
173    pub fn read<'a>(window: &'a Window, cx: &'a App) -> &'a Self {
174        &window
175            .root::<Root>()
176            .expect("The window root view should be of type `ui::Root`.")
177            .unwrap()
178            .read(cx)
179    }
180
181    // Render Notification layer.
182    pub fn render_notification_layer(
183        window: &mut Window,
184        cx: &mut App,
185    ) -> Option<impl IntoElement + use<>> {
186        let root = window.root::<Root>()??;
187
188        let active_sheet_placement = root.read(cx).active_sheet.clone().map(|d| d.placement);
189
190        let sheet_size = root.read(cx).sheet_size;
191        let (mt, mr, mb, ml) = match active_sheet_placement {
192            Some(Placement::Top) => (sheet_size, None, None, None),
193            Some(Placement::Right) => (None, sheet_size, None, None),
194            Some(Placement::Bottom) => (None, None, sheet_size, None),
195            Some(Placement::Left) => (None, None, None, sheet_size),
196            _ => (None, None, None, None),
197        };
198
199        Some(
200            div()
201                .absolute()
202                .inset_0()
203                .when_some(mt, |this, offset| this.mt(offset))
204                .when_some(mr, |this, offset| this.mr(offset))
205                .when_some(mb, |this, offset| this.mb(offset))
206                .when_some(ml, |this, offset| this.ml(offset))
207                .child(root.read(cx).notification.clone()),
208        )
209    }
210
211    /// Render the Sheet layer.
212    pub fn render_sheet_layer(
213        window: &mut Window,
214        cx: &mut App,
215    ) -> Option<impl IntoElement + use<>> {
216        let root = window.root::<Root>()??;
217
218        if let Some(active_sheet) = root.read(cx).active_sheet.clone() {
219            let mut sheet = Sheet::new(window, cx);
220            sheet = (active_sheet.builder)(sheet, window, cx);
221            sheet.focus_handle = active_sheet.focus_handle.clone();
222            sheet.placement = active_sheet.placement;
223            sheet.selection_scope = active_sheet.selection_scope;
224
225            let size = sheet.size;
226
227            return Some(
228                div()
229                    .relative()
230                    .child(sheet)
231                    .on_prepaint(move |_, _, cx| root.update(cx, |r, _| r.sheet_size = Some(size))),
232            );
233        }
234
235        None
236    }
237
238    /// Render the Dialog layer.
239    pub fn render_dialog_layer(
240        window: &mut Window,
241        cx: &mut App,
242    ) -> Option<impl IntoElement + use<>> {
243        let root = window.root::<Root>()??;
244
245        let active_dialogs = root.read(cx).active_dialogs.clone();
246
247        if active_dialogs.is_empty() {
248            return None;
249        }
250
251        let mut show_overlay_ix = None;
252
253        let mut dialogs = active_dialogs
254            .iter()
255            .enumerate()
256            .map(|(i, active_dialog)| {
257                let mut dialog = Dialog::new(cx);
258
259                dialog = (active_dialog.builder)(dialog, window, cx);
260
261                // Give the dialog the focus handle, because `dialog` is a temporary value, is not possible to
262                // keep the focus handle in the dialog.
263                //
264                // So we keep the focus handle in the `active_dialog`, this is owned by the `Root`.
265                dialog.focus_handle = active_dialog.focus_handle.clone();
266                dialog.selection_scope = active_dialog.selection_scope;
267
268                dialog.layer_ix = i;
269                // Find the dialog which one needs to show overlay.
270                if dialog.has_overlay() {
271                    show_overlay_ix = Some(i);
272                }
273
274                dialog
275            })
276            .collect::<Vec<_>>();
277
278        if let Some(ix) = show_overlay_ix {
279            if let Some(dialog) = dialogs.get_mut(ix) {
280                dialog.props.overlay_visible = true;
281            }
282        }
283
284        // Named so a test can assert the layer actually reached the screen. A
285        // dialog that opens into a root which never renders this layer looks
286        // exactly like one that does not open.
287        Some(
288            div()
289                .debug_selector(|| "dialog-layer".to_string())
290                .children(dialogs),
291        )
292    }
293
294    pub fn open_dialog<F>(&mut self, build: F, window: &mut Window, cx: &mut Context<'_, Root>)
295    where
296        F: Fn(Dialog, &mut Window, &mut App) -> Dialog + 'static,
297    {
298        let mut previous_focused_handle = window.focused(cx).map(|h| h.downgrade());
299
300        // Use pending focus restore if available to maintain correct focus chain
301        // when a new dialog is opened immediately after closing another dialog.
302        if let Some(pending_handle) = self.pending_focus_restore.take() {
303            previous_focused_handle = Some(pending_handle);
304        }
305
306        let focus_handle = cx.focus_handle();
307        focus_handle.focus(window, cx);
308
309        let selection_scope = self.allocate_text_selection_scope();
310        self.active_dialogs.push(ActiveDialog::new(
311            focus_handle,
312            previous_focused_handle,
313            selection_scope,
314            build,
315        ));
316        // Opening a modal confines selection to it; drop any background
317        // selection so it cannot linger (or be copied) under the modal.
318        gpui_base::TextSelection::clear(window, cx);
319        cx.notify();
320    }
321
322    fn close_dialog_internal(&mut self) -> Option<FocusHandle> {
323        self.focused_input = None;
324        self.active_dialogs
325            .pop()
326            .and_then(|d| d.previous_focused_handle)
327            .and_then(|h| h.upgrade())
328    }
329
330    pub fn close_dialog(&mut self, window: &mut Window, cx: &mut Context<'_, Root>) {
331        if let Some(handle) = self.close_dialog_internal() {
332            window.focus(&handle, cx);
333        }
334        gpui_base::TextSelection::clear(window, cx);
335        cx.notify();
336    }
337
338    pub(crate) fn defer_close_dialog(&mut self, window: &mut Window, cx: &mut Context<'_, Root>) {
339        if let Some(handle) = self.close_dialog_internal() {
340            let dialogs_count = self.active_dialogs.len();
341
342            // Save for new dialogs opened during animation to maintain focus chain
343            self.pending_focus_restore = Some(handle.downgrade());
344
345            cx.spawn_in(window, async move |this, cx| {
346                cx.background_executor().timer(*ANIMATION_DURATION).await;
347                let _ = this.update_in(cx, |this, window, cx| {
348                    let current_dialogs_count = this.active_dialogs.len();
349                    // Only restore focus if no new dialogs were opened during animation
350                    if current_dialogs_count == dialogs_count {
351                        window.focus(&handle, cx);
352                    }
353                    this.pending_focus_restore = None;
354                });
355            })
356            .detach();
357        }
358        gpui_base::TextSelection::clear(window, cx);
359        cx.notify();
360    }
361
362    pub fn close_all_dialogs(&mut self, window: &mut Window, cx: &mut Context<'_, Root>) {
363        self.focused_input = None;
364        let previous_focused_handle = self
365            .active_dialogs
366            .first()
367            .and_then(|d| d.previous_focused_handle.clone());
368        self.active_dialogs.clear();
369        if let Some(handle) = previous_focused_handle.and_then(|h| h.upgrade()) {
370            window.focus(&handle, cx);
371        }
372        gpui_base::TextSelection::clear(window, cx);
373        cx.notify();
374    }
375
376    pub fn open_sheet_at<F>(
377        &mut self,
378        placement: Placement,
379        build: F,
380        window: &mut Window,
381        cx: &mut Context<'_, Root>,
382    ) where
383        F: Fn(Sheet, &mut Window, &mut App) -> Sheet + 'static,
384    {
385        let previous_focused_handle = self
386            .active_sheet
387            .take()
388            .and_then(|s| s.previous_focused_handle)
389            .or_else(|| window.focused(cx).map(|h| h.downgrade()));
390
391        let focus_handle = cx.focus_handle();
392        focus_handle.focus(window, cx);
393        let selection_scope = self.allocate_text_selection_scope();
394        self.active_sheet = Some(ActiveSheet {
395            focus_handle,
396            previous_focused_handle,
397            placement,
398            selection_scope,
399            builder: Rc::new(build),
400        });
401        // Opening a modal confines selection to it; drop any background
402        // selection so it cannot linger (or be copied) under the modal.
403        gpui_base::TextSelection::clear(window, cx);
404        cx.notify();
405    }
406
407    pub fn close_sheet(&mut self, window: &mut Window, cx: &mut Context<'_, Root>) {
408        self.focused_input = None;
409        if let Some(previous_handle) = self
410            .active_sheet
411            .as_ref()
412            .and_then(|s| s.previous_focused_handle.as_ref())
413            .and_then(|h| h.upgrade())
414        {
415            window.focus(&previous_handle, cx);
416        }
417        self.active_sheet = None;
418        gpui_base::TextSelection::clear(window, cx);
419        cx.notify();
420    }
421
422    pub fn push_notification(
423        &mut self,
424        note: impl Into<Notification>,
425        window: &mut Window,
426        cx: &mut Context<'_, Root>,
427    ) {
428        self.notification
429            .update(cx, |view, cx| view.push(note, window, cx));
430        cx.notify();
431    }
432
433    /// Removes all notifications whose id matches `T`, including ones registered with
434    /// either [`Notification::id`] or [`Notification::id1`] (any key).
435    pub fn remove_notification<T: Sized + 'static>(
436        &mut self,
437        window: &mut Window,
438        cx: &mut Context<'_, Root>,
439    ) {
440        self.notification.update(cx, |view, cx| {
441            view.close_by_type(TypeId::of::<T>(), window, cx);
442        });
443        cx.notify();
444    }
445
446    /// Removes the notification matching the given type and element id (paired with [`Notification::id1`]).
447    pub fn remove_notification1<T: Sized + 'static>(
448        &mut self,
449        key: impl Into<ElementId>,
450        window: &mut Window,
451        cx: &mut Context<'_, Root>,
452    ) {
453        let key = key.into();
454        self.notification.update(cx, |view, cx| {
455            view.close((TypeId::of::<T>(), key), window, cx);
456        });
457        cx.notify();
458    }
459
460    pub fn clear_notifications(&mut self, window: &mut Window, cx: &mut Context<'_, Root>) {
461        self.notification
462            .update(cx, |view, cx| view.clear(window, cx));
463        cx.notify();
464    }
465
466    /// Get the tooltip overlay entity for this window.
467    pub(crate) fn tooltip_overlay(
468        window: &Window,
469        cx: &App,
470    ) -> Option<Entity<gpui_base::TooltipOverlay>> {
471        let root = window.root::<Root>()??;
472        Some(root.read(cx).tooltip_overlay.clone())
473    }
474
475    /// Get the fallback native-menu overlay entity for this window.
476    pub(crate) fn native_menu_overlay(
477        window: &Window,
478        cx: &App,
479    ) -> Option<Entity<FallbackMenuOverlay>> {
480        let root = window.root::<Root>()??;
481        Some(root.read(cx).native_menu_overlay.clone())
482    }
483
484    /// Return the root view of the Root.
485    pub fn view(&self) -> &AnyView {
486        &self.view
487    }
488
489    fn on_action_tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context<Self>) {
490        // Check if we're inside a focus trap
491        if let Some(container_focus_handle) = gpui_base::active_focus_trap(window, cx) {
492            // We're in a focus trap - try to focus next, then check if we're still inside
493            let before_focus = window.focused(cx);
494
495            // Try normal focus navigation
496            window.focus_next(cx);
497
498            // Check if we're still in the trap
499            if !container_focus_handle.contains_focused(window, cx) {
500                // We jumped out of the trap - need to cycle back to the beginning
501                // Find the first focusable element in the trap by continuing to focus_next
502                let mut attempts = 0;
503                const MAX_ATTEMPTS: usize = 100; // Prevent infinite loop
504
505                while !container_focus_handle.contains_focused(window, cx)
506                    && attempts < MAX_ATTEMPTS
507                {
508                    window.focus_next(cx);
509                    attempts += 1;
510
511                    // If we cycled back to where we started, restore original focus
512                    if window.focused(cx) == before_focus {
513                        break;
514                    }
515                }
516            }
517            return;
518        }
519
520        // Normal tab navigation
521        window.focus_next(cx);
522    }
523
524    fn on_action_tab_prev(&mut self, _: &TabPrev, window: &mut Window, cx: &mut Context<Self>) {
525        // Check if we're inside a focus trap
526        if let Some(container_focus_handle) = gpui_base::active_focus_trap(window, cx) {
527            // We're in a focus trap - try to focus previous, then check if we're still inside
528            let before_focus = window.focused(cx);
529
530            // Try normal focus navigation
531            window.focus_prev(cx);
532
533            // Check if we're still in the trap
534            if !container_focus_handle.contains_focused(window, cx) {
535                // We jumped out of the trap - need to cycle back to the end
536                // Find the last focusable element in the trap by continuing to focus_prev
537                let mut attempts = 0;
538                const MAX_ATTEMPTS: usize = 100; // Prevent infinite loop
539
540                while !container_focus_handle.contains_focused(window, cx)
541                    && attempts < MAX_ATTEMPTS
542                {
543                    window.focus_prev(cx);
544                    attempts += 1;
545
546                    // If we cycled back to where we started, restore original focus
547                    if window.focused(cx) == before_focus {
548                        break;
549                    }
550                }
551            }
552            return;
553        }
554
555        // Normal tab navigation
556        window.focus_prev(cx);
557    }
558
559    fn on_action_copy(&mut self, _: &Copy, window: &mut Window, cx: &mut Context<Self>) {
560        let text = gpui_base::TextSelection::selected_text(window, cx)
561            .trim()
562            .to_string();
563        if text.is_empty() {
564            cx.propagate();
565            return;
566        }
567        cx.write_to_clipboard(ClipboardItem::new_string(text));
568    }
569}
570
571impl Styled for Root {
572    fn style(&mut self) -> &mut StyleRefinement {
573        &mut self.style
574    }
575}
576
577impl Render for Root {
578    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
579        window.set_rem_size(cx.theme().font_size);
580        let active_scope = self.active_text_selection_scope();
581        TextSelection::activate_scope(active_scope, window, cx);
582
583        let inner = div()
584            .id("root")
585            .key_context(CONTEXT)
586            .on_action(cx.listener(Self::on_action_tab))
587            .on_action(cx.listener(Self::on_action_tab_prev))
588            .on_action(cx.listener(Self::on_action_copy))
589            .relative()
590            .size_full()
591            .font_family(cx.theme().font_family.clone())
592            .bg(cx.theme().tokens.background)
593            .text_color(cx.theme().foreground)
594            .refine_style(&self.style)
595            .child(TextSelectionLayer)
596            .child(self.view.clone())
597            .child(self.tooltip_overlay.clone())
598            .child(self.native_menu_overlay.clone());
599
600        if self.bordered {
601            window_border()
602                .shadow_size(self.window_shadow_size)
603                .child(inner)
604                .into_any_element()
605        } else {
606            inner.into_any_element()
607        }
608    }
609}
610
611#[cfg(test)]
612mod tests {
613    use super::*;
614    use gpui::TestAppContext;
615
616    struct TestView;
617
618    impl Render for TestView {
619        fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
620            div()
621        }
622    }
623
624    #[gpui::test]
625    fn bordered_builder_toggles_window_border(cx: &mut TestAppContext) {
626        cx.update(crate::init);
627
628        let (default_root, _) = cx.add_window_view(|window, cx| {
629            let view = cx.new(|_| TestView);
630            Root::new(view, window, cx)
631        });
632        assert!(default_root.read_with(cx, |root, _| root.bordered));
633
634        let (root, _) = cx.add_window_view(|window, cx| {
635            let view = cx.new(|_| TestView);
636            Root::new(view, window, cx).bordered(false)
637        });
638        assert!(!root.read_with(cx, |root, _| root.bordered));
639
640        let (root, _) = cx.add_window_view(|window, cx| {
641            let view = cx.new(|_| TestView);
642            Root::new(view, window, cx).bordered(false).bordered(true)
643        });
644        assert!(root.read_with(cx, |root, _| root.bordered));
645    }
646}