Skip to main content

gpui_component/
window_ext.rs

1use crate::root::WindowState;
2use crate::{
3    Placement,
4    dialog::{AlertDialog, Dialog},
5    input::AnyInputState,
6    notification::Notification,
7    sheet::Sheet,
8};
9use gpui::{App, ElementId, Entity, Window};
10use std::rc::Rc;
11
12/// Extension trait for [`Window`] to add dialog, sheet .. functionality.
13pub trait WindowExt: Sized {
14    /// Opens a Sheet at right placement.
15    ///
16    /// The builder follows the same rendering contract as
17    /// [`WindowExt::open_dialog`]: it may run again when the sheet layer is
18    /// rebuilt. Keep it cheap and create persistent entities before opening,
19    /// not inside the builder.
20    fn open_sheet<F>(&mut self, cx: &mut App, build: F)
21    where
22        F: Fn(Sheet, &mut Window, &mut App) -> Sheet + 'static;
23
24    /// Opens a Sheet at the given placement.
25    ///
26    /// The builder follows the same rendering contract as
27    /// [`WindowExt::open_dialog`]: it may run again when the sheet layer is
28    /// rebuilt. Keep it cheap and create persistent entities before opening,
29    /// not inside the builder.
30    fn open_sheet_at<F>(&mut self, placement: Placement, cx: &mut App, build: F)
31    where
32        F: Fn(Sheet, &mut Window, &mut App) -> Sheet + 'static;
33
34    /// Return true, if there is an active Sheet.
35    fn has_active_sheet(&mut self, cx: &mut App) -> bool;
36
37    /// Closes the active Sheet.
38    fn close_sheet(&mut self, cx: &mut App);
39
40    /// Opens a Dialog.
41    ///
42    /// The builder is a render closure, not a one-shot constructor. It may
43    /// run again when the dialog layer is rebuilt while the dialog is open.
44    /// View caching means a displayed frame need not rebuild the layer; the
45    /// builder is not a periodic update callback. Keep it cheap and idempotent.
46    ///
47    /// Create persistent entities, such as an [`InputState`](crate::input::InputState)
48    /// or a child view, before opening and capture their handles in the builder.
49    /// Creating them with `cx.new(...)` inside the builder replaces them on
50    /// each rebuild and loses their editing state.
51    ///
52    /// `open_dialog` initially focuses the dialog shell, replacing any focus
53    /// set before the call. To choose initial content focus, set it only on
54    /// the builder's first invocation, with a guard captured separately for
55    /// each opening. Never focus unconditionally on subsequent rebuilds: that
56    /// would steal focus from another control or a nested dialog.
57    ///
58    /// # Examples
59    ///
60    /// ```ignore
61    /// use std::cell::Cell;
62    /// use gpui_kit::{AppContext as _, Focusable as _};
63    /// use gpui_kit::component::{WindowExt as _, input::{Input, InputState}};
64    ///
65    /// // Both the input and the one-time focus guard belong to this opening.
66    /// let input = cx.new(|cx| InputState::new(window, cx).placeholder("URL"));
67    /// let initial_focus = Cell::new(true);
68    /// window.open_dialog(cx, move |dialog, window, cx| {
69    ///     if initial_focus.replace(false) {
70    ///         input.focus_handle(cx).focus(window, cx);
71    ///     }
72    ///     dialog.title("Import").child(Input::new(&input))
73    /// });
74    /// ```
75    fn open_dialog<F>(&mut self, cx: &mut App, build: F)
76    where
77        F: Fn(Dialog, &mut Window, &mut App) -> Dialog + 'static;
78
79    /// Opens an AlertDialog.
80    ///
81    /// This is a convenience method for opening an alert dialog with opinionated defaults.
82    /// The footer buttons are center-aligned and include an icon based on the variant.
83    ///
84    /// The builder follows the same rendering contract as
85    /// [`WindowExt::open_dialog`]: it may run again when the dialog layer is
86    /// rebuilt. Keep it cheap and create persistent entities before opening,
87    /// not inside the builder.
88    ///
89    /// # Examples
90    ///
91    /// ```ignore
92    /// use gpui_kit::component::{AlertDialog, alert::AlertVariant};
93    ///
94    /// window.open_alert_dialog(cx, |alert, _, _| {
95    ///     alert.warning()
96    ///         .title("Unsaved Changes")
97    ///         .description("You have unsaved changes. Are you sure you want to leave?")
98    ///         .show_cancel(true)
99    /// });
100    /// ```
101    fn open_alert_dialog<F>(&mut self, cx: &mut App, build: F)
102    where
103        F: Fn(AlertDialog, &mut Window, &mut App) -> AlertDialog + 'static;
104
105    /// Return true, if there is an active Dialog.
106    fn has_active_dialog(&mut self, cx: &mut App) -> bool;
107
108    /// Closes the last active Dialog.
109    fn close_dialog(&mut self, cx: &mut App);
110
111    /// Closes all active Dialogs.
112    fn close_all_dialogs(&mut self, cx: &mut App);
113
114    /// Pushes a notification to the notification list.
115    fn push_notification(&mut self, note: impl Into<Notification>, cx: &mut App);
116
117    /// Removes all notifications whose id matches `T`, including ones registered with
118    /// either `Notification::id` or `Notification::id1` (any key).
119    fn remove_notification<T: Sized + 'static>(&mut self, cx: &mut App);
120
121    /// Removes a single notification matching the given type `T` and `key` (paired with `Notification::id1`).
122    fn remove_notification1<T: Sized + 'static>(&mut self, key: impl Into<ElementId>, cx: &mut App);
123
124    /// Clears all notifications.
125    fn clear_notifications(&mut self, cx: &mut App);
126
127    /// Returns number of notifications.
128    fn notifications(&mut self, cx: &mut App) -> Rc<Vec<Entity<Notification>>>;
129
130    /// Return the currently focused input state.
131    ///
132    /// Covers `Input`, `Textarea`, `Editor` and `OtpInput`, use
133    /// [`AnyInputState::as_input`] and friends to get the concrete state.
134    /// A registration whose focus handle is no longer focused (e.g. the input
135    /// was removed from the tree while focused) is treated as `None`.
136    fn focused_input(&mut self, cx: &mut App) -> Option<AnyInputState>;
137    /// Returns true if there is a focused Input entity.
138    fn has_focused_input(&mut self, cx: &mut App) -> bool;
139}
140
141impl WindowExt for Window {
142    #[inline]
143    fn open_sheet<F>(&mut self, cx: &mut App, build: F)
144    where
145        F: Fn(Sheet, &mut Window, &mut App) -> Sheet + 'static,
146    {
147        self.open_sheet_at(Placement::Right, cx, build)
148    }
149
150    #[inline]
151    fn open_sheet_at<F>(&mut self, placement: Placement, cx: &mut App, build: F)
152    where
153        F: Fn(Sheet, &mut Window, &mut App) -> Sheet + 'static,
154    {
155        WindowState::update(self, cx, move |root, window, cx| {
156            root.open_sheet_at(placement, build, window, cx);
157        })
158    }
159
160    #[inline]
161    fn has_active_sheet(&mut self, cx: &mut App) -> bool {
162        WindowState::read(self, cx).active_sheet.is_some()
163    }
164
165    #[inline]
166    fn close_sheet(&mut self, cx: &mut App) {
167        WindowState::update(self, cx, |root, window, cx| {
168            root.close_sheet(window, cx);
169        })
170    }
171
172    #[inline]
173    fn open_dialog<F>(&mut self, cx: &mut App, build: F)
174    where
175        F: Fn(Dialog, &mut Window, &mut App) -> Dialog + 'static,
176    {
177        WindowState::update(self, cx, move |root, window, cx| {
178            root.open_dialog(build, window, cx);
179        })
180    }
181
182    #[inline]
183    fn open_alert_dialog<F>(&mut self, cx: &mut App, build: F)
184    where
185        F: Fn(AlertDialog, &mut Window, &mut App) -> AlertDialog + 'static,
186    {
187        self.open_dialog(cx, move |_, window, cx| {
188            build(AlertDialog::new(cx), window, cx).build_surface(window, cx)
189        })
190    }
191
192    #[inline]
193    fn has_active_dialog(&mut self, cx: &mut App) -> bool {
194        !WindowState::read(self, cx).active_dialogs.is_empty()
195    }
196
197    #[inline]
198    fn close_dialog(&mut self, cx: &mut App) {
199        WindowState::update(self, cx, |root, window, cx| {
200            root.close_dialog(window, cx);
201        })
202    }
203
204    #[inline]
205    fn close_all_dialogs(&mut self, cx: &mut App) {
206        WindowState::update(self, cx, |root, window, cx| {
207            root.close_all_dialogs(window, cx);
208        })
209    }
210
211    #[inline]
212    fn push_notification(&mut self, note: impl Into<Notification>, cx: &mut App) {
213        let note = note.into();
214        WindowState::update(self, cx, |root, window, cx| {
215            root.push_notification(note, window, cx);
216        })
217    }
218
219    #[inline]
220    fn remove_notification<T: Sized + 'static>(&mut self, cx: &mut App) {
221        WindowState::update(self, cx, |root, window, cx| {
222            root.remove_notification::<T>(window, cx);
223        })
224    }
225
226    #[inline]
227    fn remove_notification1<T: Sized + 'static>(
228        &mut self,
229        key: impl Into<ElementId>,
230        cx: &mut App,
231    ) {
232        let key = key.into();
233        WindowState::update(self, cx, |root, window, cx| {
234            root.remove_notification1::<T>(key, window, cx);
235        })
236    }
237
238    #[inline]
239    fn clear_notifications(&mut self, cx: &mut App) {
240        WindowState::update(self, cx, |root, window, cx| {
241            root.clear_notifications(window, cx);
242        })
243    }
244
245    #[inline]
246    fn notifications(&mut self, cx: &mut App) -> Rc<Vec<Entity<Notification>>> {
247        Rc::new(
248            WindowState::read(self, cx)
249                .notification
250                .read(cx)
251                .notifications(),
252        )
253    }
254
255    #[inline]
256    fn has_focused_input(&mut self, cx: &mut App) -> bool {
257        self.focused_input(cx).is_some()
258    }
259
260    fn focused_input(&mut self, cx: &mut App) -> Option<AnyInputState> {
261        let state = WindowState::read(self, cx).focused_input.clone()?;
262        if state.focus_handle(cx).is_focused(self) {
263            return Some(state);
264        }
265
266        // An input removed from the tree while focused never re-renders to
267        // unregister itself; drop the stale registration lazily.
268        WindowState::try_update(self, cx, |root, _, cx| {
269            if root.focused_input.as_ref() == Some(&state) {
270                root.focused_input = None;
271                cx.notify();
272            }
273        });
274        None
275    }
276}