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    ///
107    /// A dialog that is playing its exit transition is no longer active: it
108    /// accepts no input and will unmount when the transition finishes.
109    fn has_active_dialog(&mut self, cx: &mut App) -> bool;
110
111    /// Closes the last active Dialog.
112    fn close_dialog(&mut self, cx: &mut App);
113
114    /// Closes all active Dialogs.
115    fn close_all_dialogs(&mut self, cx: &mut App);
116
117    /// Pushes a notification to the notification list.
118    fn push_notification(&mut self, note: impl Into<Notification>, cx: &mut App);
119
120    /// Removes all notifications whose id matches `T`, including ones registered with
121    /// either `Notification::id` or `Notification::id1` (any key).
122    fn remove_notification<T: Sized + 'static>(&mut self, cx: &mut App);
123
124    /// Removes a single notification matching the given type `T` and `key` (paired with `Notification::id1`).
125    fn remove_notification1<T: Sized + 'static>(&mut self, key: impl Into<ElementId>, cx: &mut App);
126
127    /// Clears all notifications.
128    fn clear_notifications(&mut self, cx: &mut App);
129
130    /// Returns number of notifications.
131    fn notifications(&mut self, cx: &mut App) -> Rc<Vec<Entity<Notification>>>;
132
133    /// Return the currently focused input state.
134    ///
135    /// Covers `Input`, `Textarea`, `Editor` and `OtpInput`, use
136    /// [`AnyInputState::as_input`] and friends to get the concrete state.
137    /// A registration whose focus handle is no longer focused (e.g. the input
138    /// was removed from the tree while focused) is treated as `None`.
139    fn focused_input(&mut self, cx: &mut App) -> Option<AnyInputState>;
140    /// Returns true if there is a focused Input entity.
141    fn has_focused_input(&mut self, cx: &mut App) -> bool;
142}
143
144impl WindowExt for Window {
145    #[inline]
146    fn open_sheet<F>(&mut self, cx: &mut App, build: F)
147    where
148        F: Fn(Sheet, &mut Window, &mut App) -> Sheet + 'static,
149    {
150        self.open_sheet_at(Placement::Right, cx, build)
151    }
152
153    #[inline]
154    fn open_sheet_at<F>(&mut self, placement: Placement, cx: &mut App, build: F)
155    where
156        F: Fn(Sheet, &mut Window, &mut App) -> Sheet + 'static,
157    {
158        WindowState::update(self, cx, move |root, window, cx| {
159            root.open_sheet_at(placement, build, window, cx);
160        })
161    }
162
163    #[inline]
164    fn has_active_sheet(&mut self, cx: &mut App) -> bool {
165        WindowState::read(self, cx).active_sheet.is_some()
166    }
167
168    #[inline]
169    fn close_sheet(&mut self, cx: &mut App) {
170        WindowState::update(self, cx, |root, window, cx| {
171            root.close_sheet(window, cx);
172        })
173    }
174
175    #[inline]
176    fn open_dialog<F>(&mut self, cx: &mut App, build: F)
177    where
178        F: Fn(Dialog, &mut Window, &mut App) -> Dialog + 'static,
179    {
180        WindowState::update(self, cx, move |root, window, cx| {
181            root.open_dialog(build, window, cx);
182        })
183    }
184
185    #[inline]
186    fn open_alert_dialog<F>(&mut self, cx: &mut App, build: F)
187    where
188        F: Fn(AlertDialog, &mut Window, &mut App) -> AlertDialog + 'static,
189    {
190        self.open_dialog(cx, move |_, window, cx| {
191            build(AlertDialog::new(cx), window, cx).build_surface(window, cx)
192        })
193    }
194
195    #[inline]
196    fn has_active_dialog(&mut self, cx: &mut App) -> bool {
197        WindowState::read(self, cx)
198            .active_dialogs
199            .iter()
200            .any(|dialog| !dialog.closing)
201    }
202
203    #[inline]
204    fn close_dialog(&mut self, cx: &mut App) {
205        WindowState::update(self, cx, |root, window, cx| {
206            root.close_dialog(window, cx);
207        })
208    }
209
210    #[inline]
211    fn close_all_dialogs(&mut self, cx: &mut App) {
212        WindowState::update(self, cx, |root, window, cx| {
213            root.close_all_dialogs(window, cx);
214        })
215    }
216
217    #[inline]
218    fn push_notification(&mut self, note: impl Into<Notification>, cx: &mut App) {
219        let note = note.into();
220        WindowState::update(self, cx, |root, window, cx| {
221            root.push_notification(note, window, cx);
222        })
223    }
224
225    #[inline]
226    fn remove_notification<T: Sized + 'static>(&mut self, cx: &mut App) {
227        WindowState::update(self, cx, |root, window, cx| {
228            root.remove_notification::<T>(window, cx);
229        })
230    }
231
232    #[inline]
233    fn remove_notification1<T: Sized + 'static>(
234        &mut self,
235        key: impl Into<ElementId>,
236        cx: &mut App,
237    ) {
238        let key = key.into();
239        WindowState::update(self, cx, |root, window, cx| {
240            root.remove_notification1::<T>(key, window, cx);
241        })
242    }
243
244    #[inline]
245    fn clear_notifications(&mut self, cx: &mut App) {
246        WindowState::update(self, cx, |root, window, cx| {
247            root.clear_notifications(window, cx);
248        })
249    }
250
251    #[inline]
252    fn notifications(&mut self, cx: &mut App) -> Rc<Vec<Entity<Notification>>> {
253        Rc::new(
254            WindowState::read(self, cx)
255                .notification
256                .read(cx)
257                .notifications(),
258        )
259    }
260
261    #[inline]
262    fn has_focused_input(&mut self, cx: &mut App) -> bool {
263        self.focused_input(cx).is_some()
264    }
265
266    fn focused_input(&mut self, cx: &mut App) -> Option<AnyInputState> {
267        let state = WindowState::read(self, cx).focused_input.clone()?;
268        if state.focus_handle(cx).is_focused(self) {
269            return Some(state);
270        }
271
272        // An input removed from the tree while focused never re-renders to
273        // unregister itself; drop the stale registration lazily.
274        WindowState::try_update(self, cx, |root, _, cx| {
275            if root.focused_input.as_ref() == Some(&state) {
276                root.focused_input = None;
277                cx.notify();
278            }
279        });
280        None
281    }
282}