gpui_component/
root.rs

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