Skip to main content

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, cx);
51                        }
52                    })
53                    .ok();
54            }
55        })
56        .detach();
57
58        window.focus(&view.focus_handle(cx), 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            .occlude()
151            .size_full()
152            .child(
153                div()
154                    .size_full()
155                    .bg(opaque_grey(0.5, 0.6))
156                    .absolute()
157                    .top_0()
158                    .left_0(),
159            )
160            .child(
161                div()
162                    .size_full()
163                    .absolute()
164                    .top_0()
165                    .left_0()
166                    .flex()
167                    .flex_col()
168                    .justify_around()
169                    .child(
170                        div()
171                            .w_full()
172                            .flex()
173                            .flex_row()
174                            .justify_around()
175                            .child(prompt),
176                    ),
177            )
178    }
179}
180
181impl EventEmitter<PromptResponse> for FallbackPromptRenderer {}
182
183impl Focusable for FallbackPromptRenderer {
184    fn focus_handle(&self, _: &crate::App) -> FocusHandle {
185        self.focus.clone()
186    }
187}
188
189pub(crate) trait PromptViewHandle {
190    fn any_view(&self) -> AnyView;
191}
192
193impl<V: Prompt + Render> PromptViewHandle for Entity<V> {
194    fn any_view(&self) -> AnyView {
195        self.clone().into()
196    }
197}
198
199pub(crate) enum PromptBuilder {
200    Default,
201    Custom(
202        Box<
203            dyn Fn(
204                PromptLevel,
205                &str,
206                Option<&str>,
207                &[PromptButton],
208                PromptHandle,
209                &mut Window,
210                &mut App,
211            ) -> RenderablePromptHandle,
212        >,
213    ),
214}
215
216impl Deref for PromptBuilder {
217    type Target = dyn Fn(
218        PromptLevel,
219        &str,
220        Option<&str>,
221        &[PromptButton],
222        PromptHandle,
223        &mut Window,
224        &mut App,
225    ) -> RenderablePromptHandle;
226
227    fn deref(&self) -> &Self::Target {
228        match self {
229            Self::Default => &fallback_prompt_renderer,
230            Self::Custom(f) => f.as_ref(),
231        }
232    }
233}