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