prompts.rs

  1use std::ops::Deref;
  2
  3use futures::channel::oneshot;
  4
  5use crate::{
  6    div, opaque_grey, white, AnyElement, AnyView, ElementContext, EventEmitter, FocusHandle,
  7    FocusableView, InteractiveElement, IntoElement, ParentElement, PromptLevel, Render,
  8    StatefulInteractiveElement, Styled, View, 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    view: Box<dyn PromptViewHandle>,
 61}
 62
 63impl RenderablePromptHandle {
 64    pub(crate) fn paint(&mut self, _: &mut ElementContext) -> AnyElement {
 65        self.view.any_view().into_any_element()
 66    }
 67}
 68
 69/// Use this function in conjunction with [AppContext::set_prompt_renderer] to force
 70/// GPUI to always use the fallback prompt renderer.
 71pub fn fallback_prompt_renderer(
 72    level: PromptLevel,
 73    message: &str,
 74    detail: Option<&str>,
 75    actions: &[&str],
 76    handle: PromptHandle,
 77    cx: &mut WindowContext,
 78) -> RenderablePromptHandle {
 79    let renderer = cx.new_view({
 80        |cx| FallbackPromptRenderer {
 81            _level: level,
 82            message: message.to_string(),
 83            detail: detail.map(ToString::to_string),
 84            actions: actions.iter().map(ToString::to_string).collect(),
 85            focus: cx.focus_handle(),
 86        }
 87    });
 88
 89    handle.with_view(renderer, cx)
 90}
 91
 92/// The default GPUI fallback for rendering prompts, when the platform doesn't support it.
 93pub struct FallbackPromptRenderer {
 94    _level: PromptLevel,
 95    message: String,
 96    detail: Option<String>,
 97    actions: Vec<String>,
 98    focus: FocusHandle,
 99}
100
101impl Render for FallbackPromptRenderer {
102    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
103        let prompt = div()
104            .cursor_default()
105            .track_focus(&self.focus)
106            .w_72()
107            .bg(white())
108            .rounded_lg()
109            .overflow_hidden()
110            .p_3()
111            .child(
112                div()
113                    .w_full()
114                    .flex()
115                    .flex_row()
116                    .justify_around()
117                    .child(div().overflow_hidden().child(self.message.clone())),
118            )
119            .children(self.detail.clone().map(|detail| {
120                div()
121                    .w_full()
122                    .flex()
123                    .flex_row()
124                    .justify_around()
125                    .text_sm()
126                    .mb_2()
127                    .child(div().child(detail))
128            }))
129            .children(self.actions.iter().enumerate().map(|(ix, action)| {
130                div()
131                    .flex()
132                    .flex_row()
133                    .justify_around()
134                    .border_1()
135                    .border_color(opaque_grey(0.2, 0.5))
136                    .mt_1()
137                    .rounded_sm()
138                    .cursor_pointer()
139                    .text_sm()
140                    .child(action.clone())
141                    .id(ix)
142                    .on_click(cx.listener(move |_, _, cx| {
143                        cx.emit(PromptResponse(ix));
144                    }))
145            }));
146
147        div()
148            .size_full()
149            .z_index(u16::MAX)
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 FocusableView for FallbackPromptRenderer {
182    fn focus_handle(&self, _: &crate::AppContext) -> FocusHandle {
183        self.focus.clone()
184    }
185}
186
187trait PromptViewHandle {
188    fn any_view(&self) -> AnyView;
189}
190
191impl<V: Prompt> PromptViewHandle for View<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                &[&str],
206                PromptHandle,
207                &mut WindowContext,
208            ) -> RenderablePromptHandle,
209        >,
210    ),
211}
212
213impl Deref for PromptBuilder {
214    type Target = dyn Fn(
215        PromptLevel,
216        &str,
217        Option<&str>,
218        &[&str],
219        PromptHandle,
220        &mut WindowContext,
221    ) -> RenderablePromptHandle;
222
223    fn deref(&self) -> &Self::Target {
224        match self {
225            Self::Default => &fallback_prompt_renderer,
226            Self::Custom(f) => f.as_ref(),
227        }
228    }
229}