gpui/window/
prompts.rs

1use std::ops::Deref;
2
3use futures::channel::oneshot;
4
5use crate::{
6    AnyView, App, AppContext as _, Context, Entity, EventEmitter, FocusHandle, Focusable,
7    InteractiveElement, IntoElement, ParentElement, PromptButton, PromptLevel, Render,
8    StatefulInteractiveElement, Styled, div, opaque_grey, white,
9};
10
11use super::Window;
12
13/// The event emitted when a prompt's option is selected.
14/// The usize is the index of the selected option, from the actions
15/// passed to the prompt.
16pub struct PromptResponse(pub usize);
17
18/// A prompt that can be rendered in the window.
19pub trait Prompt: EventEmitter<PromptResponse> + Focusable {}
20
21impl<V: EventEmitter<PromptResponse> + Focusable> Prompt for V {}
22
23/// A handle to a prompt that can be used to interact with it.
24pub struct PromptHandle {
25    sender: oneshot::Sender<usize>,
26}
27
28impl PromptHandle {
29    pub(crate) fn new(sender: oneshot::Sender<usize>) -> Self {
30        Self { sender }
31    }
32
33    /// Construct a new prompt handle from a view of the appropriate types
34    pub fn with_view<V: Prompt + Render>(
35        self,
36        view: Entity<V>,
37        window: &mut Window,
38        cx: &mut App,
39    ) -> RenderablePromptHandle {
40        let mut sender = Some(self.sender);
41        let previous_focus = window.focused(cx);
42        let window_handle = window.window_handle();
43        cx.subscribe(&view, move |_: Entity<V>, e: &PromptResponse, cx| {
44            if let Some(sender) = sender.take() {
45                sender.send(e.0).ok();
46                window_handle
47                    .update(cx, |_, window, _cx| {
48                        window.prompt.take();
49                        if let Some(previous_focus) = &previous_focus {
50                            window.focus(previous_focus);
51                        }
52                    })
53                    .ok();
54            }
55        })
56        .detach();
57
58        window.focus(&view.focus_handle(cx));
59
60        RenderablePromptHandle {
61            view: Box::new(view),
62        }
63    }
64}
65
66/// A prompt handle capable of being rendered in a window.
67pub struct RenderablePromptHandle {
68    pub(crate) view: Box<dyn PromptViewHandle>,
69}
70
71/// Use this function in conjunction with [App::set_prompt_builder] to force
72/// GPUI to always use the fallback prompt renderer.
73pub fn fallback_prompt_renderer(
74    level: PromptLevel,
75    message: &str,
76    detail: Option<&str>,
77    actions: &[PromptButton],
78    handle: PromptHandle,
79    window: &mut Window,
80    cx: &mut App,
81) -> RenderablePromptHandle {
82    let renderer = cx.new(|cx| FallbackPromptRenderer {
83        _level: level,
84        message: message.to_string(),
85        detail: detail.map(ToString::to_string),
86        actions: actions.to_vec(),
87        focus: cx.focus_handle(),
88    });
89
90    handle.with_view(renderer, window, cx)
91}
92
93/// The default GPUI fallback for rendering prompts, when the platform doesn't support it.
94pub struct FallbackPromptRenderer {
95    _level: PromptLevel,
96    message: String,
97    detail: Option<String>,
98    actions: Vec<PromptButton>,
99    focus: FocusHandle,
100}
101
102impl Render for FallbackPromptRenderer {
103    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
104        let prompt = div()
105            .cursor_default()
106            .track_focus(&self.focus)
107            .w_72()
108            .bg(white())
109            .rounded_lg()
110            .overflow_hidden()
111            .p_3()
112            .child(
113                div()
114                    .w_full()
115                    .flex()
116                    .flex_row()
117                    .justify_around()
118                    .child(div().overflow_hidden().child(self.message.clone())),
119            )
120            .children(self.detail.clone().map(|detail| {
121                div()
122                    .w_full()
123                    .flex()
124                    .flex_row()
125                    .justify_around()
126                    .text_sm()
127                    .mb_2()
128                    .child(div().child(detail))
129            }))
130            .children(self.actions.iter().enumerate().map(|(ix, action)| {
131                div()
132                    .flex()
133                    .flex_row()
134                    .justify_around()
135                    .border_1()
136                    .border_color(opaque_grey(0.2, 0.5))
137                    .mt_1()
138                    .rounded_xs()
139                    .cursor_pointer()
140                    .text_sm()
141                    .child(action.label().clone())
142                    .id(ix)
143                    .on_click(cx.listener(move |_, _, _, cx| {
144                        cx.emit(PromptResponse(ix));
145                        cx.stop_propagation();
146                    }))
147            }));
148
149        div()
150            .size_full()
151            .child(
152                div()
153                    .size_full()
154                    .bg(opaque_grey(0.5, 0.6))
155                    .absolute()
156                    .top_0()
157                    .left_0(),
158            )
159            .child(
160                div()
161                    .size_full()
162                    .absolute()
163                    .top_0()
164                    .left_0()
165                    .flex()
166                    .flex_col()
167                    .justify_around()
168                    .child(
169                        div()
170                            .w_full()
171                            .flex()
172                            .flex_row()
173                            .justify_around()
174                            .child(prompt),
175                    ),
176            )
177    }
178}
179
180impl EventEmitter<PromptResponse> for FallbackPromptRenderer {}
181
182impl Focusable for FallbackPromptRenderer {
183    fn focus_handle(&self, _: &crate::App) -> FocusHandle {
184        self.focus.clone()
185    }
186}
187
188pub(crate) trait PromptViewHandle {
189    fn any_view(&self) -> AnyView;
190}
191
192impl<V: Prompt + Render> PromptViewHandle for Entity<V> {
193    fn any_view(&self) -> AnyView {
194        self.clone().into()
195    }
196}
197
198pub(crate) enum PromptBuilder {
199    Default,
200    Custom(
201        Box<
202            dyn Fn(
203                PromptLevel,
204                &str,
205                Option<&str>,
206                &[PromptButton],
207                PromptHandle,
208                &mut Window,
209                &mut App,
210            ) -> RenderablePromptHandle,
211        >,
212    ),
213}
214
215impl Deref for PromptBuilder {
216    type Target = dyn Fn(
217        PromptLevel,
218        &str,
219        Option<&str>,
220        &[PromptButton],
221        PromptHandle,
222        &mut Window,
223        &mut App,
224    ) -> RenderablePromptHandle;
225
226    fn deref(&self) -> &Self::Target {
227        match self {
228            Self::Default => &fallback_prompt_renderer,
229            Self::Custom(f) => f.as_ref(),
230        }
231    }
232}