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}