gpui_component/
root.rs

1use crate::{
2    drawer::Drawer,
3    input::InputState,
4    modal::Modal,
5    notification::{Notification, NotificationList},
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 CONTENT: &str = "Root";
18
19pub(crate) fn init(cx: &mut App) {
20    cx.bind_keys([
21        KeyBinding::new("tab", Tab, Some(CONTENT)),
22        KeyBinding::new("shift-tab", TabPrev, Some(CONTENT)),
23    ]);
24}
25
26/// Extension trait for [`WindowContext`] and [`ViewContext`] to add drawer functionality.
27pub trait ContextModal: Sized {
28    /// Opens a Drawer at right placement.
29    fn open_drawer<F>(&mut self, cx: &mut App, build: F)
30    where
31        F: Fn(Drawer, &mut Window, &mut App) -> Drawer + 'static;
32
33    /// Opens a Drawer at the given placement.
34    fn open_drawer_at<F>(&mut self, placement: Placement, cx: &mut App, build: F)
35    where
36        F: Fn(Drawer, &mut Window, &mut App) -> Drawer + 'static;
37
38    /// Return true, if there is an active Drawer.
39    fn has_active_drawer(&mut self, cx: &mut App) -> bool;
40
41    /// Closes the active Drawer.
42    fn close_drawer(&mut self, cx: &mut App);
43
44    /// Opens a Modal.
45    fn open_modal<F>(&mut self, cx: &mut App, build: F)
46    where
47        F: Fn(Modal, &mut Window, &mut App) -> Modal + 'static;
48
49    /// Return true, if there is an active Modal.
50    fn has_active_modal(&mut self, cx: &mut App) -> bool;
51
52    /// Closes the last active Modal.
53    fn close_modal(&mut self, cx: &mut App);
54
55    /// Closes all active Modals.
56    fn close_all_modals(&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 ContextModal for Window {
77    fn open_drawer<F>(&mut self, cx: &mut App, build: F)
78    where
79        F: Fn(Drawer, &mut Window, &mut App) -> Drawer + 'static,
80    {
81        self.open_drawer_at(Placement::Right, cx, build)
82    }
83
84    fn open_drawer_at<F>(&mut self, placement: Placement, cx: &mut App, build: F)
85    where
86        F: Fn(Drawer, &mut Window, &mut App) -> Drawer + 'static,
87    {
88        Root::update(self, cx, move |root, window, cx| {
89            if root.active_drawer.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_drawer = Some(ActiveDrawer {
97                focus_handle,
98                placement,
99                builder: Rc::new(build),
100            });
101            cx.notify();
102        })
103    }
104
105    fn has_active_drawer(&mut self, cx: &mut App) -> bool {
106        Root::read(self, cx).active_drawer.is_some()
107    }
108
109    fn close_drawer(&mut self, cx: &mut App) {
110        Root::update(self, cx, |root, window, cx| {
111            root.focused_input = None;
112            root.active_drawer = None;
113            root.focus_back(window, cx);
114            cx.notify();
115        })
116    }
117
118    fn open_modal<F>(&mut self, cx: &mut App, build: F)
119    where
120        F: Fn(Modal, &mut Window, &mut App) -> Modal + 'static,
121    {
122        Root::update(self, cx, move |root, window, cx| {
123            // Only save focus handle if there are no active modals.
124            // This is used to restore focus when all modals are closed.
125            if root.active_modals.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_modals.push(ActiveModal {
133                focus_handle,
134                builder: Rc::new(build),
135            });
136            cx.notify();
137        })
138    }
139
140    fn has_active_modal(&mut self, cx: &mut App) -> bool {
141        Root::read(self, cx).active_modals.len() > 0
142    }
143
144    fn close_modal(&mut self, cx: &mut App) {
145        Root::update(self, cx, move |root, window, cx| {
146            root.focused_input = None;
147            root.active_modals.pop();
148
149            if let Some(top_modal) = root.active_modals.last() {
150                // Focus the next modal.
151                top_modal.focus_handle.focus(window);
152            } else {
153                // Restore focus if there are no more modals.
154                root.focus_back(window, cx);
155            }
156            cx.notify();
157        })
158    }
159
160    fn close_all_modals(&mut self, cx: &mut App) {
161        Root::update(self, cx, |root, window, cx| {
162            root.focused_input = None;
163            root.active_modals.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 Drawer, Modal, and Notification.
213pub struct Root {
214    /// Used to store the focus handle of the previous view.
215    /// When the Modal, Drawer closes, we will focus back to the previous view.
216    previous_focus_handle: Option<FocusHandle>,
217    active_drawer: Option<ActiveDrawer>,
218    pub(crate) active_modals: Vec<ActiveModal>,
219    pub(super) focused_input: Option<Entity<InputState>>,
220    pub notification: Entity<NotificationList>,
221    drawer_size: Option<DefiniteLength>,
222    view: AnyView,
223}
224
225#[derive(Clone)]
226struct ActiveDrawer {
227    focus_handle: FocusHandle,
228    placement: Placement,
229    builder: Rc<dyn Fn(Drawer, &mut Window, &mut App) -> Drawer + 'static>,
230}
231
232#[derive(Clone)]
233pub(crate) struct ActiveModal {
234    focus_handle: FocusHandle,
235    builder: Rc<dyn Fn(Modal, &mut Window, &mut App) -> Modal + 'static>,
236}
237
238impl Root {
239    pub fn new(view: AnyView, window: &mut Window, cx: &mut Context<Self>) -> Self {
240        Self {
241            previous_focus_handle: None,
242            active_drawer: None,
243            active_modals: Vec::new(),
244            focused_input: None,
245            notification: cx.new(|cx| NotificationList::new(window, cx)),
246            drawer_size: None,
247            view,
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_drawer_placement = root.read(cx).active_drawer.clone().map(|d| d.placement);
285
286        let (mt, mr) = match active_drawer_placement {
287            Some(Placement::Right) => (None, root.read(cx).drawer_size),
288            Some(Placement::Top) => (root.read(cx).drawer_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 Drawer layer.
304    pub fn render_drawer_layer(window: &mut Window, cx: &mut App) -> Option<impl IntoElement> {
305        let root = window.root::<Root>()??;
306
307        if let Some(active_drawer) = root.read(cx).active_drawer.clone() {
308            let mut drawer = Drawer::new(window, cx);
309            drawer = (active_drawer.builder)(drawer, window, cx);
310            drawer.focus_handle = active_drawer.focus_handle.clone();
311            drawer.placement = active_drawer.placement;
312
313            let drawer_size = drawer.size;
314
315            return Some(
316                div().relative().child(drawer).child(
317                    canvas(
318                        move |_, _, cx| root.update(cx, |r, _| r.drawer_size = Some(drawer_size)),
319                        |_, _, _, _| {},
320                    )
321                    .absolute()
322                    .size_full(),
323                ),
324            );
325        }
326
327        None
328    }
329
330    /// Render the Modal layer.
331    pub fn render_modal_layer(window: &mut Window, cx: &mut App) -> Option<impl IntoElement> {
332        let root = window.root::<Root>()??;
333
334        let active_modals = root.read(cx).active_modals.clone();
335
336        if active_modals.is_empty() {
337            return None;
338        }
339
340        let mut show_overlay_ix = None;
341
342        let mut modals = active_modals
343            .iter()
344            .enumerate()
345            .map(|(i, active_modal)| {
346                let mut modal = Modal::new(window, cx);
347
348                modal = (active_modal.builder)(modal, window, cx);
349
350                // Give the modal the focus handle, because `modal` is a temporary value, is not possible to
351                // keep the focus handle in the modal.
352                //
353                // So we keep the focus handle in the `active_modal`, this is owned by the `Root`.
354                modal.focus_handle = active_modal.focus_handle.clone();
355
356                modal.layer_ix = i;
357                // Find the modal which one needs to show overlay.
358                if modal.has_overlay() {
359                    show_overlay_ix = Some(i);
360                }
361
362                modal
363            })
364            .collect::<Vec<_>>();
365
366        if let Some(ix) = show_overlay_ix {
367            if let Some(modal) = modals.get_mut(ix) {
368                modal.overlay_visible = true;
369            }
370        }
371
372        Some(div().children(modals))
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(CONTENT)
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}