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