prompts.rs

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