gpui_component/
root.rs

1use crate::{
2    dialog::Dialog,
3    input::InputState,
4    notification::{Notification, NotificationList},
5    sheet::Sheet,
6    window_border, ActiveTheme, Placement,
7};
8use gpui::{
9    actions, canvas, div, prelude::FluentBuilder as _, AnyView, App, AppContext, Context,
10    DefiniteLength, Entity, FocusHandle, InteractiveElement, IntoElement, KeyBinding,
11    ParentElement as _, Render, Styled, Window,
12};
13use std::{any::TypeId, rc::Rc};
14
15actions!(root, [Tab, TabPrev]);
16
17const CONTEXT: &str = "Root";
18pub(crate) fn init(cx: &mut App) {
19    cx.bind_keys([
20        KeyBinding::new("tab", Tab, Some(CONTEXT)),
21        KeyBinding::new("shift-tab", TabPrev, Some(CONTEXT)),
22    ]);
23}
24
25/// Extension trait for [`Window`] to add dialog, sheet .. functionality.
26pub trait WindowExt: Sized {
27    /// Opens a Sheet at right placement.
28    fn open_sheet<F>(&mut self, cx: &mut App, build: F)
29    where
30        F: Fn(Sheet, &mut Window, &mut App) -> Sheet + 'static;
31
32    /// Opens a Sheet at the given placement.
33    fn open_sheet_at<F>(&mut self, placement: Placement, cx: &mut App, build: F)
34    where
35        F: Fn(Sheet, &mut Window, &mut App) -> Sheet + 'static;
36
37    /// Return true, if there is an active Sheet.
38    fn has_active_sheet(&mut self, cx: &mut App) -> bool;
39
40    /// Closes the active Sheet.
41    fn close_sheet(&mut self, cx: &mut App);
42
43    /// Opens a Dialog.
44    fn open_dialog<F>(&mut self, cx: &mut App, build: F)
45    where
46        F: Fn(Dialog, &mut Window, &mut App) -> Dialog + 'static;
47
48    /// Return true, if there is an active Dialog.
49    fn has_active_dialog(&mut self, cx: &mut App) -> bool;
50
51    /// Closes the last active Dialog.
52    fn close_dialog(&mut self, cx: &mut App);
53
54    /// Closes all active Dialogs.
55    fn close_all_dialogs(&mut self, cx: &mut App);
56
57    /// Pushes a notification to the notification list.
58    fn push_notification(&mut self, note: impl Into<Notification>, cx: &mut App);
59
60    /// Removes the notification with the given id.
61    fn remove_notification<T: Sized + 'static>(&mut self, cx: &mut App);
62
63    /// Clears all notifications.
64    fn clear_notifications(&mut self, cx: &mut App);
65
66    /// Returns number of notifications.
67    fn notifications(&mut self, cx: &mut App) -> Rc<Vec<Entity<Notification>>>;
68
69    /// Return current focused Input entity.
70    fn focused_input(&mut self, cx: &mut App) -> Option<Entity<InputState>>;
71    /// Returns true if there is a focused Input entity.
72    fn has_focused_input(&mut self, cx: &mut App) -> bool;
73}
74
75impl WindowExt for Window {
76    fn open_sheet<F>(&mut self, cx: &mut App, build: F)
77    where
78        F: Fn(Sheet, &mut Window, &mut App) -> Sheet + 'static,
79    {
80        self.open_sheet_at(Placement::Right, cx, build)
81    }
82
83    fn open_sheet_at<F>(&mut self, placement: Placement, cx: &mut App, build: F)
84    where
85        F: Fn(Sheet, &mut Window, &mut App) -> Sheet + 'static,
86    {
87        Root::update(self, cx, move |root, window, cx| {
88            if root.active_sheet.is_none() {
89                root.previous_focus_handle = window.focused(cx);
90            }
91
92            let focus_handle = cx.focus_handle();
93            focus_handle.focus(window);
94
95            root.active_sheet = Some(ActiveSheet {
96                focus_handle,
97                placement,
98                builder: Rc::new(build),
99            });
100            cx.notify();
101        })
102    }
103
104    fn has_active_sheet(&mut self, cx: &mut App) -> bool {
105        Root::read(self, cx).active_sheet.is_some()
106    }
107
108    fn close_sheet(&mut self, cx: &mut App) {
109        Root::update(self, cx, |root, window, cx| {
110            root.focused_input = None;
111            root.active_sheet = None;
112            root.focus_back(window, cx);
113            cx.notify();
114        })
115    }
116
117    fn open_dialog<F>(&mut self, cx: &mut App, build: F)
118    where
119        F: Fn(Dialog, &mut Window, &mut App) -> Dialog + 'static,
120    {
121        Root::update(self, cx, move |root, window, cx| {
122            // Only save focus handle if there are no active dialogs.
123            // This is used to restore focus when all dialogs are closed.
124            if root.active_dialogs.len() == 0 {
125                root.previous_focus_handle = window.focused(cx);
126            }
127
128            let focus_handle = cx.focus_handle();
129            focus_handle.focus(window);
130
131            root.active_dialogs.push(ActiveDialog {
132                focus_handle,
133                builder: Rc::new(build),
134            });
135            cx.notify();
136        })
137    }
138
139    fn has_active_dialog(&mut self, cx: &mut App) -> bool {
140        Root::read(self, cx).active_dialogs.len() > 0
141    }
142
143    fn close_dialog(&mut self, cx: &mut App) {
144        Root::update(self, cx, move |root, window, cx| {
145            root.focused_input = None;
146            root.active_dialogs.pop();
147
148            if let Some(top_dialog) = root.active_dialogs.last() {
149                // Focus the next dialog.
150                top_dialog.focus_handle.focus(window);
151            } else {
152                // Restore focus if there are no more dialogs.
153                root.focus_back(window, cx);
154            }
155            cx.notify();
156        })
157    }
158
159    fn close_all_dialogs(&mut self, cx: &mut App) {
160        Root::update(self, cx, |root, window, cx| {
161            root.focused_input = None;
162            root.active_dialogs.clear();
163            root.focus_back(window, cx);
164            cx.notify();
165        })
166    }
167
168    fn push_notification(&mut self, note: impl Into<Notification>, cx: &mut App) {
169        let note = note.into();
170        Root::update(self, cx, move |root, window, cx| {
171            root.notification
172                .update(cx, |view, cx| view.push(note, window, cx));
173            cx.notify();
174        })
175    }
176
177    fn remove_notification<T: Sized + 'static>(&mut self, cx: &mut App) {
178        Root::update(self, cx, move |root, window, cx| {
179            root.notification.update(cx, |view, cx| {
180                let id = TypeId::of::<T>();
181                view.close(id, window, cx);
182            });
183            cx.notify();
184        })
185    }
186
187    fn clear_notifications(&mut self, cx: &mut App) {
188        Root::update(self, cx, move |root, window, cx| {
189            root.notification
190                .update(cx, |view, cx| view.clear(window, cx));
191            cx.notify();
192        })
193    }
194
195    fn notifications(&mut self, cx: &mut App) -> Rc<Vec<Entity<Notification>>> {
196        let entity = Root::read(self, cx).notification.clone();
197        Rc::new(entity.read(cx).notifications())
198    }
199
200    fn has_focused_input(&mut self, cx: &mut App) -> bool {
201        Root::read(self, cx).focused_input.is_some()
202    }
203
204    fn focused_input(&mut self, cx: &mut App) -> Option<Entity<InputState>> {
205        Root::read(self, cx).focused_input.clone()
206    }
207}
208
209/// Root is a view for the App window for as the top level view (Must be the first view in the window).
210///
211/// It is used to manage the Sheet, Dialog, and Notification.
212pub struct Root {
213    /// Used to store the focus handle of the previous view.
214    /// When the Dialog, Sheet closes, we will focus back to the previous view.
215    previous_focus_handle: Option<FocusHandle>,
216    active_sheet: Option<ActiveSheet>,
217    pub(crate) active_dialogs: Vec<ActiveDialog>,
218    pub(super) focused_input: Option<Entity<InputState>>,
219    pub notification: Entity<NotificationList>,
220    sheet_size: Option<DefiniteLength>,
221    view: AnyView,
222}
223
224#[derive(Clone)]
225struct ActiveSheet {
226    focus_handle: FocusHandle,
227    placement: Placement,
228    builder: Rc<dyn Fn(Sheet, &mut Window, &mut App) -> Sheet + 'static>,
229}
230
231#[derive(Clone)]
232pub(crate) struct ActiveDialog {
233    focus_handle: FocusHandle,
234    builder: Rc<dyn Fn(Dialog, &mut Window, &mut App) -> Dialog + 'static>,
235}
236
237impl Root {
238    /// Create a new Root view.
239    pub fn new(view: impl Into<AnyView>, window: &mut Window, cx: &mut Context<Self>) -> Self {
240        Self {
241            previous_focus_handle: None,
242            active_sheet: None,
243            active_dialogs: Vec::new(),
244            focused_input: None,
245            notification: cx.new(|cx| NotificationList::new(window, cx)),
246            sheet_size: None,
247            view: view.into(),
248        }
249    }
250
251    pub fn update<F, R>(window: &mut Window, cx: &mut App, f: F) -> R
252    where
253        F: FnOnce(&mut Self, &mut Window, &mut Context<Self>) -> R,
254    {
255        let root = window
256            .root::<Root>()
257            .flatten()
258            .expect("BUG: window first layer should be a gpui_component::Root.");
259
260        root.update(cx, |root, cx| f(root, window, cx))
261    }
262
263    pub fn read<'a>(window: &'a Window, cx: &'a App) -> &'a Self {
264        &window
265            .root::<Root>()
266            .expect("The window root view should be of type `ui::Root`.")
267            .unwrap()
268            .read(cx)
269    }
270
271    fn focus_back(&mut self, window: &mut Window, _: &mut App) {
272        if let Some(handle) = self.previous_focus_handle.clone() {
273            window.focus(&handle);
274        }
275    }
276
277    // Render Notification layer.
278    pub fn render_notification_layer(
279        window: &mut Window,
280        cx: &mut App,
281    ) -> Option<impl IntoElement> {
282        let root = window.root::<Root>()??;
283
284        let active_sheet_placement = root.read(cx).active_sheet.clone().map(|d| d.placement);
285
286        let (mt, mr) = match active_sheet_placement {
287            Some(Placement::Right) => (None, root.read(cx).sheet_size),
288            Some(Placement::Top) => (root.read(cx).sheet_size, None),
289            _ => (None, None),
290        };
291
292        Some(
293            div()
294                .absolute()
295                .top_0()
296                .right_0()
297                .when_some(mt, |this, offset| this.mt(offset))
298                .when_some(mr, |this, offset| this.mr(offset))
299                .child(root.read(cx).notification.clone()),
300        )
301    }
302
303    /// Render the Sheet layer.
304    pub fn render_sheet_layer(window: &mut Window, cx: &mut App) -> Option<impl IntoElement> {
305        let root = window.root::<Root>()??;
306
307        if let Some(active_sheet) = root.read(cx).active_sheet.clone() {
308            let mut sheet = Sheet::new(window, cx);
309            sheet = (active_sheet.builder)(sheet, window, cx);
310            sheet.focus_handle = active_sheet.focus_handle.clone();
311            sheet.placement = active_sheet.placement;
312
313            let size = sheet.size;
314
315            return Some(
316                div().relative().child(sheet).child(
317                    canvas(
318                        move |_, _, cx| root.update(cx, |r, _| r.sheet_size = Some(size)),
319                        |_, _, _, _| {},
320                    )
321                    .absolute()
322                    .size_full(),
323                ),
324            );
325        }
326
327        None
328    }
329
330    /// Render the Dialog layer.
331    pub fn render_dialog_layer(window: &mut Window, cx: &mut App) -> Option<impl IntoElement> {
332        let root = window.root::<Root>()??;
333
334        let active_dialogs = root.read(cx).active_dialogs.clone();
335
336        if active_dialogs.is_empty() {
337            return None;
338        }
339
340        let mut show_overlay_ix = None;
341
342        let mut dialogs = active_dialogs
343            .iter()
344            .enumerate()
345            .map(|(i, active_dialog)| {
346                let mut dialog = Dialog::new(window, cx);
347
348                dialog = (active_dialog.builder)(dialog, window, cx);
349
350                // Give the dialog the focus handle, because `dialog` is a temporary value, is not possible to
351                // keep the focus handle in the dialog.
352                //
353                // So we keep the focus handle in the `active_dialog`, this is owned by the `Root`.
354                dialog.focus_handle = active_dialog.focus_handle.clone();
355
356                dialog.layer_ix = i;
357                // Find the dialog which one needs to show overlay.
358                if dialog.has_overlay() {
359                    show_overlay_ix = Some(i);
360                }
361
362                dialog
363            })
364            .collect::<Vec<_>>();
365
366        if let Some(ix) = show_overlay_ix {
367            if let Some(dialog) = dialogs.get_mut(ix) {
368                dialog.overlay_visible = true;
369            }
370        }
371
372        Some(div().children(dialogs))
373    }
374
375    /// Return the root view of the Root.
376    pub fn view(&self) -> &AnyView {
377        &self.view
378    }
379
380    fn on_action_tab(&mut self, _: &Tab, window: &mut Window, _: &mut Context<Self>) {
381        window.focus_next();
382    }
383
384    fn on_action_tab_prev(&mut self, _: &TabPrev, window: &mut Window, _: &mut Context<Self>) {
385        window.focus_prev();
386    }
387}
388
389impl Render for Root {
390    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
391        let base_font_size = cx.theme().font_size;
392        window.set_rem_size(base_font_size);
393
394        window_border().child(
395            div()
396                .id("root")
397                .key_context(CONTEXT)
398                .on_action(cx.listener(Self::on_action_tab))
399                .on_action(cx.listener(Self::on_action_tab_prev))
400                .relative()
401                .size_full()
402                .font_family(".SystemUIFont")
403                .bg(cx.theme().background)
404                .text_color(cx.theme().foreground)
405                .child(self.view.clone()),
406        )
407    }
408}