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}