Skip to main content

gpui/window/
prompts.rs

1use std::ops::Deref;
2use std::rc::Rc;
3
4use futures::channel::oneshot;
5
6use crate::{
7    AnyView, App, AppContext as _, Context, Entity, EventEmitter, FocusHandle, Focusable,
8    InteractiveElement, IntoElement, ParentElement, PromptButton, PromptLevel, Render,
9    StatefulInteractiveElement, Styled, div, opaque_grey, white,
10};
11
12use super::Window;
13
14/// The event a prompt emits when the user chooses one of its buttons.
15///
16/// The value is the index of the chosen button, counted from the first button passed to
17/// [`Window::prompt`](crate::Window::prompt).
18pub struct PromptResponse(pub usize);
19
20/// A view that can be shown as a prompt in a window.
21///
22/// A prompt is a view that emits a [`PromptResponse`] and can take focus. Every view that
23/// meets those bounds is a `Prompt`, so you don't implement this trait yourself. Pass the view
24/// to [`PromptHandle::with_view`] from a prompt builder.
25pub trait Prompt: EventEmitter<PromptResponse> + Focusable {}
26
27impl<V: EventEmitter<PromptResponse> + Focusable> Prompt for V {}
28
29/// A handle that a prompt builder uses to show a prompt and report the user's answer.
30///
31/// GPUI creates a `PromptHandle` for each prompt that it draws in the window, and passes it to
32/// the builder set with [`App::set_prompt_builder`]. The builder calls
33/// [`PromptHandle::with_view`] with the view it renders, and returns the result. The answer
34/// goes to the receiver that `Window::prompt` returned.
35pub struct PromptHandle {
36    sender: oneshot::Sender<usize>,
37}
38
39impl PromptHandle {
40    pub(crate) fn new(sender: oneshot::Sender<usize>) -> Self {
41        Self { sender }
42    }
43
44    /// Shows `view` as the prompt, and returns the handle the window renders.
45    ///
46    /// The window focuses `view`. When `view` emits a [`PromptResponse`], its index is sent to
47    /// the prompt's receiver, the window drops the prompt, and focus returns to the element that
48    /// had it before. The handle is consumed, so a prompt can be answered once. Later responses
49    /// from `view` are ignored. If a newer prompt replaces `view` before it is answered, its
50    /// response is ignored too, and the newer prompt keeps the window.
51    pub fn with_view<V: Prompt + Render>(
52        self,
53        view: Entity<V>,
54        window: &mut Window,
55        cx: &mut App,
56    ) -> RenderablePromptHandle {
57        let mut sender = Some(self.sender);
58        // A prompt that replaces another one restores the focus from before the first.
59        let previous_focus = window.prompt.as_ref().map_or_else(
60            || window.focused(cx),
61            |prompt| prompt.previous_focus.clone(),
62        );
63        let restore_focus = previous_focus.clone();
64        let token = Rc::new(());
65        let response_token = token.clone();
66        let window_handle = window.window_handle();
67        cx.subscribe(&view, move |_: Entity<V>, e: &PromptResponse, cx| {
68            if let Some(sender) = sender.take() {
69                sender.send(e.0).ok();
70                window_handle
71                    .update(cx, |_, window, cx| {
72                        // A response from a replaced prompt must not close its replacement.
73                        if !window
74                            .prompt
75                            .as_ref()
76                            .is_some_and(|prompt| Rc::ptr_eq(&prompt.token, &response_token))
77                        {
78                            return;
79                        }
80                        window.prompt.take();
81                        if let Some(previous_focus) = &restore_focus {
82                            window.focus(previous_focus, cx);
83                        } else {
84                            window.blur(cx);
85                        }
86                        window.refresh();
87                    })
88                    .ok();
89            }
90        })
91        .detach();
92
93        window.focus(&view.focus_handle(cx), cx);
94
95        RenderablePromptHandle {
96            view: Box::new(view),
97            token,
98            previous_focus,
99        }
100    }
101}
102
103/// A prompt that a window can render, as returned by a prompt builder.
104///
105/// Get one from [`PromptHandle::with_view`]. The builder set with [`App::set_prompt_builder`]
106/// returns it, and the window keeps it until the user answers.
107pub struct RenderablePromptHandle {
108    pub(crate) view: Box<dyn PromptViewHandle>,
109    token: Rc<()>,
110    previous_focus: Option<FocusHandle>,
111}
112
113/// Shows a prompt drawn inside the window, rather than a platform dialog.
114///
115/// Pass it to [`App::set_prompt_builder`] to draw every prompt in the window on every
116/// platform. [`Window::prompt`](crate::Window::prompt) also uses it when the platform has
117/// no native dialog, which is the case on the web. The prompt shows the message, the optional
118/// detail, and one button per answer. Its [`PromptLevel`](crate::PromptLevel) isn't shown.
119///
120/// Use [`App::reset_prompt_builder`] to go back to the platform's dialogs.
121///
122/// # Examples
123///
124/// ```
125/// # use gpui::{Context, PromptLevel, Render, TestApp, Window, div, fallback_prompt_renderer, prelude::*};
126/// # struct Root;
127/// # impl Render for Root {
128/// #     fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement { div() }
129/// # }
130/// # let mut app = TestApp::new();
131/// app.update(|cx| cx.set_prompt_builder(fallback_prompt_renderer));
132/// let mut window = app.open_window(|_, _| Root);
133///
134/// // The prompt is drawn in the window, so no platform dialog opens.
135/// window.update(|_, window, cx| {
136///     window.prompt(PromptLevel::Info, "Saved", None, &["OK"], cx);
137/// });
138/// assert!(window.update(|_, window, _| window.has_active_prompt()));
139/// ```
140pub fn fallback_prompt_renderer(
141    level: PromptLevel,
142    message: &str,
143    detail: Option<&str>,
144    actions: &[PromptButton],
145    handle: PromptHandle,
146    window: &mut Window,
147    cx: &mut App,
148) -> RenderablePromptHandle {
149    let renderer = cx.new(|cx| FallbackPromptRenderer {
150        _level: level,
151        message: message.to_string(),
152        detail: detail.map(ToString::to_string),
153        actions: actions.to_vec(),
154        focus: cx.focus_handle(),
155    });
156
157    handle.with_view(renderer, window, cx)
158}
159
160/// The view that [`fallback_prompt_renderer`] shows inside the window.
161///
162/// You don't create this type yourself. Clicking a button emits a [`PromptResponse`] with that
163/// button's index, and the window then closes the prompt.
164pub struct FallbackPromptRenderer {
165    _level: PromptLevel,
166    message: String,
167    detail: Option<String>,
168    actions: Vec<PromptButton>,
169    focus: FocusHandle,
170}
171
172impl Render for FallbackPromptRenderer {
173    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
174        let prompt = div()
175            .cursor_default()
176            .track_focus(&self.focus)
177            .w_72()
178            .bg(white())
179            .rounded_lg()
180            .overflow_hidden()
181            .p_3()
182            .child(
183                div()
184                    .w_full()
185                    .flex()
186                    .flex_row()
187                    .justify_around()
188                    .child(div().overflow_hidden().child(self.message.clone())),
189            )
190            .children(self.detail.clone().map(|detail| {
191                div()
192                    .w_full()
193                    .flex()
194                    .flex_row()
195                    .justify_around()
196                    .text_sm()
197                    .mb_2()
198                    .child(div().child(detail))
199            }))
200            .children(self.actions.iter().enumerate().map(|(ix, action)| {
201                div()
202                    .flex()
203                    .flex_row()
204                    .justify_around()
205                    .border_1()
206                    .border_color(opaque_grey(0.2, 0.5))
207                    .mt_1()
208                    .rounded_xs()
209                    .cursor_pointer()
210                    .text_sm()
211                    .child(action.label().clone())
212                    .id(ix)
213                    .on_click(cx.listener(move |_, _, _, cx| {
214                        cx.emit(PromptResponse(ix));
215                        cx.stop_propagation();
216                    }))
217            }));
218
219        div()
220            .occlude()
221            .size_full()
222            .child(
223                div()
224                    .size_full()
225                    .bg(opaque_grey(0.5, 0.6))
226                    .absolute()
227                    .top_0()
228                    .left_0(),
229            )
230            .child(
231                div()
232                    .size_full()
233                    .absolute()
234                    .top_0()
235                    .left_0()
236                    .flex()
237                    .flex_col()
238                    .justify_around()
239                    .child(
240                        div()
241                            .w_full()
242                            .flex()
243                            .flex_row()
244                            .justify_around()
245                            .child(prompt),
246                    ),
247            )
248    }
249}
250
251impl EventEmitter<PromptResponse> for FallbackPromptRenderer {}
252
253impl Focusable for FallbackPromptRenderer {
254    fn focus_handle(&self, _: &crate::App) -> FocusHandle {
255        self.focus.clone()
256    }
257}
258
259pub(crate) trait PromptViewHandle {
260    fn any_view(&self) -> AnyView;
261}
262
263impl<V: Prompt + Render> PromptViewHandle for Entity<V> {
264    fn any_view(&self) -> AnyView {
265        self.clone().into()
266    }
267}
268
269pub(crate) enum PromptBuilder {
270    Default,
271    Custom(
272        Box<
273            dyn Fn(
274                PromptLevel,
275                &str,
276                Option<&str>,
277                &[PromptButton],
278                PromptHandle,
279                &mut Window,
280                &mut App,
281            ) -> RenderablePromptHandle,
282        >,
283    ),
284}
285
286impl Deref for PromptBuilder {
287    type Target = dyn Fn(
288        PromptLevel,
289        &str,
290        Option<&str>,
291        &[PromptButton],
292        PromptHandle,
293        &mut Window,
294        &mut App,
295    ) -> RenderablePromptHandle;
296
297    fn deref(&self) -> &Self::Target {
298        match self {
299            Self::Default => &fallback_prompt_renderer,
300            Self::Custom(f) => f.as_ref(),
301        }
302    }
303}
304
305#[cfg(test)]
306mod tests {
307    use super::*;
308    use crate::{TestApp, TestAppWindow};
309    use std::cell::RefCell;
310
311    struct Root {
312        focus: FocusHandle,
313    }
314
315    impl Render for Root {
316        fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
317            div().size_full().track_focus(&self.focus)
318        }
319    }
320
321    struct CustomPrompt {
322        focus: FocusHandle,
323    }
324
325    impl Render for CustomPrompt {
326        fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
327            div().track_focus(&self.focus)
328        }
329    }
330
331    impl Focusable for CustomPrompt {
332        fn focus_handle(&self, _: &App) -> FocusHandle {
333            self.focus.clone()
334        }
335    }
336
337    impl EventEmitter<PromptResponse> for CustomPrompt {}
338
339    /// A window whose prompts are `CustomPrompt` views, which are also collected in the returned list.
340    fn prompt_window() -> (
341        TestApp,
342        TestAppWindow<Root>,
343        Rc<RefCell<Vec<Entity<CustomPrompt>>>>,
344    ) {
345        let mut app = TestApp::new();
346        let views = Rc::new(RefCell::new(Vec::new()));
347        app.update({
348            let views = views.clone();
349            move |cx| {
350                cx.set_prompt_builder(move |_, _, _, _, handle, window, cx| {
351                    let view = cx.new(|cx| CustomPrompt {
352                        focus: cx.focus_handle(),
353                    });
354                    views.borrow_mut().push(view.clone());
355                    handle.with_view(view, window, cx)
356                })
357            }
358        });
359        let window = app.open_window(|window, cx| {
360            let focus = cx.focus_handle();
361            window.focus(&focus, cx);
362            Root { focus }
363        });
364        (app, window, views)
365    }
366
367    fn show_prompt(window: &mut TestAppWindow<Root>) -> oneshot::Receiver<usize> {
368        window
369            .update(|_, window, cx| window.prompt(PromptLevel::Info, "Message", None, &["OK"], cx))
370    }
371
372    #[test]
373    fn showing_and_answering_a_prompt_requests_a_draw() {
374        let (mut app, mut window, views) = prompt_window();
375        window.draw();
376        assert!(!window.update(|_, window, _| window.invalidator.is_dirty()));
377
378        // Checked inside the same update: `TestAppWindow::update` runs until parked, which draws.
379        let mut response = window.update(|_, window, cx| {
380            let response = window.prompt(PromptLevel::Info, "Message", None, &["OK"], cx);
381            assert!(window.invalidator.is_dirty());
382            response
383        });
384
385        window.draw();
386        app.update_entity(&views.borrow()[0], |_, cx| cx.emit(PromptResponse(0)));
387        assert_eq!(response.try_recv().unwrap(), Some(0));
388        window.update(|root, window, _| {
389            assert!(!window.has_active_prompt());
390            assert!(root.focus.is_focused(window));
391        });
392    }
393
394    #[test]
395    fn superseded_response_cannot_close_new_prompt_or_restore_stale_focus() {
396        let (mut app, mut window, views) = prompt_window();
397        let mut first = show_prompt(&mut window);
398        let mut second = show_prompt(&mut window);
399        let first_view = views.borrow()[0].clone();
400        let second_view = views.borrow()[1].clone();
401
402        app.update_entity(&first_view, |_, cx| cx.emit(PromptResponse(0)));
403        assert_eq!(first.try_recv().unwrap(), Some(0));
404        assert_eq!(second.try_recv().unwrap(), None);
405        window.update(|_, window, cx| {
406            assert!(window.has_active_prompt());
407            assert!(second_view.read(cx).focus.is_focused(window));
408        });
409
410        app.update_entity(&second_view, |_, cx| cx.emit(PromptResponse(0)));
411        assert_eq!(second.try_recv().unwrap(), Some(0));
412        window.update(|root, window, _| {
413            assert!(!window.has_active_prompt());
414            assert!(root.focus.is_focused(window));
415        });
416    }
417}