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    pub fn new(view: AnyView, window: &mut Window, cx: &mut Context<Self>) -> Self {
239        Self {
240            previous_focus_handle: None,
241            active_sheet: None,
242            active_dialogs: Vec::new(),
243            focused_input: None,
244            notification: cx.new(|cx| NotificationList::new(window, cx)),
245            sheet_size: None,
246            view,
247        }
248    }
249
250    pub fn update<F, R>(window: &mut Window, cx: &mut App, f: F) -> R
251    where
252        F: FnOnce(&mut Self, &mut Window, &mut Context<Self>) -> R,
253    {
254        let root = window
255            .root::<Root>()
256            .flatten()
257            .expect("BUG: window first layer should be a gpui_component::Root.");
258
259        root.update(cx, |root, cx| f(root, window, cx))
260    }
261
262    pub fn read<'a>(window: &'a Window, cx: &'a App) -> &'a Self {
263        &window
264            .root::<Root>()
265            .expect("The window root view should be of type `ui::Root`.")
266            .unwrap()
267            .read(cx)
268    }
269
270    fn focus_back(&mut self, window: &mut Window, _: &mut App) {
271        if let Some(handle) = self.previous_focus_handle.clone() {
272            window.focus(&handle);
273        }
274    }
275
276    // Render Notification layer.
277    pub fn render_notification_layer(
278        window: &mut Window,
279        cx: &mut App,
280    ) -> Option<impl IntoElement> {
281        let root = window.root::<Root>()??;
282
283        let active_sheet_placement = root.read(cx).active_sheet.clone().map(|d| d.placement);
284
285        let (mt, mr) = match active_sheet_placement {
286            Some(Placement::Right) => (None, root.read(cx).sheet_size),
287            Some(Placement::Top) => (root.read(cx).sheet_size, None),
288            _ => (None, None),
289        };
290
291        Some(
292            div()
293                .absolute()
294                .top_0()
295                .right_0()
296                .when_some(mt, |this, offset| this.mt(offset))
297                .when_some(mr, |this, offset| this.mr(offset))
298                .child(root.read(cx).notification.clone()),
299        )
300    }
301
302    /// Render the Sheet layer.
303    pub fn render_sheet_layer(window: &mut Window, cx: &mut App) -> Option<impl IntoElement> {
304        let root = window.root::<Root>()??;
305
306        if let Some(active_sheet) = root.read(cx).active_sheet.clone() {
307            let mut sheet = Sheet::new(window, cx);
308            sheet = (active_sheet.builder)(sheet, window, cx);
309            sheet.focus_handle = active_sheet.focus_handle.clone();
310            sheet.placement = active_sheet.placement;
311
312            let size = sheet.size;
313
314            return Some(
315                div().relative().child(sheet).child(
316                    canvas(
317                        move |_, _, cx| root.update(cx, |r, _| r.sheet_size = Some(size)),
318                        |_, _, _, _| {},
319                    )
320                    .absolute()
321                    .size_full(),
322                ),
323            );
324        }
325
326        None
327    }
328
329    /// Render the Dialog layer.
330    pub fn render_dialog_layer(window: &mut Window, cx: &mut App) -> Option<impl IntoElement> {
331        let root = window.root::<Root>()??;
332
333        let active_dialogs = root.read(cx).active_dialogs.clone();
334
335        if active_dialogs.is_empty() {
336            return None;
337        }
338
339        let mut show_overlay_ix = None;
340
341        let mut dialogs = active_dialogs
342            .iter()
343            .enumerate()
344            .map(|(i, active_dialog)| {
345                let mut dialog = Dialog::new(window, cx);
346
347                dialog = (active_dialog.builder)(dialog, window, cx);
348
349                // Give the dialog the focus handle, because `dialog` is a temporary value, is not possible to
350                // keep the focus handle in the dialog.
351                //
352                // So we keep the focus handle in the `active_dialog`, this is owned by the `Root`.
353                dialog.focus_handle = active_dialog.focus_handle.clone();
354
355                dialog.layer_ix = i;
356                // Find the dialog which one needs to show overlay.
357                if dialog.has_overlay() {
358                    show_overlay_ix = Some(i);
359                }
360
361                dialog
362            })
363            .collect::<Vec<_>>();
364
365        if let Some(ix) = show_overlay_ix {
366            if let Some(dialog) = dialogs.get_mut(ix) {
367                dialog.overlay_visible = true;
368            }
369        }
370
371        Some(div().children(dialogs))
372    }
373
374    /// Return the root view of the Root.
375    pub fn view(&self) -> &AnyView {
376        &self.view
377    }
378
379    fn on_action_tab(&mut self, _: &Tab, window: &mut Window, _: &mut Context<Self>) {
380        window.focus_next();
381    }
382
383    fn on_action_tab_prev(&mut self, _: &TabPrev, window: &mut Window, _: &mut Context<Self>) {
384        window.focus_prev();
385    }
386}
387
388impl Render for Root {
389    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
390        let base_font_size = cx.theme().font_size;
391        window.set_rem_size(base_font_size);
392
393        window_border().child(
394            div()
395                .id("root")
396                .key_context(CONTEXT)
397                .on_action(cx.listener(Self::on_action_tab))
398                .on_action(cx.listener(Self::on_action_tab_prev))
399                .relative()
400                .size_full()
401                .font_family(".SystemUIFont")
402                .bg(cx.theme().background)
403                .text_color(cx.theme().foreground)
404                .child(self.view.clone()),
405        )
406    }
407}