1use std::ops::Deref;
2use std::rc::Rc;
3
4use futures::channel::oneshot;
5
6use crate::{
7 AnyView, App, AppContext as _, Context, Entity, EventEmitter, FocusHandle, Focusable,
8 InteractiveElement, IntoElement, ParentElement, PromptButton, PromptLevel, Render,
9 StatefulInteractiveElement, Styled, div, opaque_grey, white,
10};
11
12use super::Window;
13
14pub struct PromptResponse(pub usize);
19
20pub trait Prompt: EventEmitter<PromptResponse> + Focusable {}
26
27impl<V: EventEmitter<PromptResponse> + Focusable> Prompt for V {}
28
29pub struct PromptHandle {
36 sender: oneshot::Sender<usize>,
37}
38
39impl PromptHandle {
40 pub(crate) fn new(sender: oneshot::Sender<usize>) -> Self {
41 Self { sender }
42 }
43
44 pub fn with_view<V: Prompt + Render>(
52 self,
53 view: Entity<V>,
54 window: &mut Window,
55 cx: &mut App,
56 ) -> RenderablePromptHandle {
57 let mut sender = Some(self.sender);
58 let previous_focus = window.prompt.as_ref().map_or_else(
60 || window.focused(cx),
61 |prompt| prompt.previous_focus.clone(),
62 );
63 let restore_focus = previous_focus.clone();
64 let token = Rc::new(());
65 let response_token = token.clone();
66 let window_handle = window.window_handle();
67 cx.subscribe(&view, move |_: Entity<V>, e: &PromptResponse, cx| {
68 if let Some(sender) = sender.take() {
69 sender.send(e.0).ok();
70 window_handle
71 .update(cx, |_, window, cx| {
72 if !window
74 .prompt
75 .as_ref()
76 .is_some_and(|prompt| Rc::ptr_eq(&prompt.token, &response_token))
77 {
78 return;
79 }
80 window.prompt.take();
81 if let Some(previous_focus) = &restore_focus {
82 window.focus(previous_focus, cx);
83 } else {
84 window.blur(cx);
85 }
86 window.refresh();
87 })
88 .ok();
89 }
90 })
91 .detach();
92
93 window.focus(&view.focus_handle(cx), cx);
94
95 RenderablePromptHandle {
96 view: Box::new(view),
97 token,
98 previous_focus,
99 }
100 }
101}
102
103pub struct RenderablePromptHandle {
108 pub(crate) view: Box<dyn PromptViewHandle>,
109 token: Rc<()>,
110 previous_focus: Option<FocusHandle>,
111}
112
113pub fn fallback_prompt_renderer(
141 level: PromptLevel,
142 message: &str,
143 detail: Option<&str>,
144 actions: &[PromptButton],
145 handle: PromptHandle,
146 window: &mut Window,
147 cx: &mut App,
148) -> RenderablePromptHandle {
149 let renderer = cx.new(|cx| FallbackPromptRenderer {
150 _level: level,
151 message: message.to_string(),
152 detail: detail.map(ToString::to_string),
153 actions: actions.to_vec(),
154 focus: cx.focus_handle(),
155 });
156
157 handle.with_view(renderer, window, cx)
158}
159
160pub struct FallbackPromptRenderer {
165 _level: PromptLevel,
166 message: String,
167 detail: Option<String>,
168 actions: Vec<PromptButton>,
169 focus: FocusHandle,
170}
171
172impl Render for FallbackPromptRenderer {
173 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
174 let prompt = div()
175 .cursor_default()
176 .track_focus(&self.focus)
177 .w_72()
178 .bg(white())
179 .rounded_lg()
180 .overflow_hidden()
181 .p_3()
182 .child(
183 div()
184 .w_full()
185 .flex()
186 .flex_row()
187 .justify_around()
188 .child(div().overflow_hidden().child(self.message.clone())),
189 )
190 .children(self.detail.clone().map(|detail| {
191 div()
192 .w_full()
193 .flex()
194 .flex_row()
195 .justify_around()
196 .text_sm()
197 .mb_2()
198 .child(div().child(detail))
199 }))
200 .children(self.actions.iter().enumerate().map(|(ix, action)| {
201 div()
202 .flex()
203 .flex_row()
204 .justify_around()
205 .border_1()
206 .border_color(opaque_grey(0.2, 0.5))
207 .mt_1()
208 .rounded_xs()
209 .cursor_pointer()
210 .text_sm()
211 .child(action.label().clone())
212 .id(ix)
213 .on_click(cx.listener(move |_, _, _, cx| {
214 cx.emit(PromptResponse(ix));
215 cx.stop_propagation();
216 }))
217 }));
218
219 div()
220 .occlude()
221 .size_full()
222 .child(
223 div()
224 .size_full()
225 .bg(opaque_grey(0.5, 0.6))
226 .absolute()
227 .top_0()
228 .left_0(),
229 )
230 .child(
231 div()
232 .size_full()
233 .absolute()
234 .top_0()
235 .left_0()
236 .flex()
237 .flex_col()
238 .justify_around()
239 .child(
240 div()
241 .w_full()
242 .flex()
243 .flex_row()
244 .justify_around()
245 .child(prompt),
246 ),
247 )
248 }
249}
250
251impl EventEmitter<PromptResponse> for FallbackPromptRenderer {}
252
253impl Focusable for FallbackPromptRenderer {
254 fn focus_handle(&self, _: &crate::App) -> FocusHandle {
255 self.focus.clone()
256 }
257}
258
259pub(crate) trait PromptViewHandle {
260 fn any_view(&self) -> AnyView;
261}
262
263impl<V: Prompt + Render> PromptViewHandle for Entity<V> {
264 fn any_view(&self) -> AnyView {
265 self.clone().into()
266 }
267}
268
269pub(crate) enum PromptBuilder {
270 Default,
271 Custom(
272 Box<
273 dyn Fn(
274 PromptLevel,
275 &str,
276 Option<&str>,
277 &[PromptButton],
278 PromptHandle,
279 &mut Window,
280 &mut App,
281 ) -> RenderablePromptHandle,
282 >,
283 ),
284}
285
286impl Deref for PromptBuilder {
287 type Target = dyn Fn(
288 PromptLevel,
289 &str,
290 Option<&str>,
291 &[PromptButton],
292 PromptHandle,
293 &mut Window,
294 &mut App,
295 ) -> RenderablePromptHandle;
296
297 fn deref(&self) -> &Self::Target {
298 match self {
299 Self::Default => &fallback_prompt_renderer,
300 Self::Custom(f) => f.as_ref(),
301 }
302 }
303}
304
305#[cfg(test)]
306mod tests {
307 use super::*;
308 use crate::{TestApp, TestAppWindow};
309 use std::cell::RefCell;
310
311 struct Root {
312 focus: FocusHandle,
313 }
314
315 impl Render for Root {
316 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
317 div().size_full().track_focus(&self.focus)
318 }
319 }
320
321 struct CustomPrompt {
322 focus: FocusHandle,
323 }
324
325 impl Render for CustomPrompt {
326 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
327 div().track_focus(&self.focus)
328 }
329 }
330
331 impl Focusable for CustomPrompt {
332 fn focus_handle(&self, _: &App) -> FocusHandle {
333 self.focus.clone()
334 }
335 }
336
337 impl EventEmitter<PromptResponse> for CustomPrompt {}
338
339 fn prompt_window() -> (
341 TestApp,
342 TestAppWindow<Root>,
343 Rc<RefCell<Vec<Entity<CustomPrompt>>>>,
344 ) {
345 let mut app = TestApp::new();
346 let views = Rc::new(RefCell::new(Vec::new()));
347 app.update({
348 let views = views.clone();
349 move |cx| {
350 cx.set_prompt_builder(move |_, _, _, _, handle, window, cx| {
351 let view = cx.new(|cx| CustomPrompt {
352 focus: cx.focus_handle(),
353 });
354 views.borrow_mut().push(view.clone());
355 handle.with_view(view, window, cx)
356 })
357 }
358 });
359 let window = app.open_window(|window, cx| {
360 let focus = cx.focus_handle();
361 window.focus(&focus, cx);
362 Root { focus }
363 });
364 (app, window, views)
365 }
366
367 fn show_prompt(window: &mut TestAppWindow<Root>) -> oneshot::Receiver<usize> {
368 window
369 .update(|_, window, cx| window.prompt(PromptLevel::Info, "Message", None, &["OK"], cx))
370 }
371
372 #[test]
373 fn showing_and_answering_a_prompt_requests_a_draw() {
374 let (mut app, mut window, views) = prompt_window();
375 window.draw();
376 assert!(!window.update(|_, window, _| window.invalidator.is_dirty()));
377
378 let mut response = window.update(|_, window, cx| {
380 let response = window.prompt(PromptLevel::Info, "Message", None, &["OK"], cx);
381 assert!(window.invalidator.is_dirty());
382 response
383 });
384
385 window.draw();
386 app.update_entity(&views.borrow()[0], |_, cx| cx.emit(PromptResponse(0)));
387 assert_eq!(response.try_recv().unwrap(), Some(0));
388 window.update(|root, window, _| {
389 assert!(!window.has_active_prompt());
390 assert!(root.focus.is_focused(window));
391 });
392 }
393
394 #[test]
395 fn superseded_response_cannot_close_new_prompt_or_restore_stale_focus() {
396 let (mut app, mut window, views) = prompt_window();
397 let mut first = show_prompt(&mut window);
398 let mut second = show_prompt(&mut window);
399 let first_view = views.borrow()[0].clone();
400 let second_view = views.borrow()[1].clone();
401
402 app.update_entity(&first_view, |_, cx| cx.emit(PromptResponse(0)));
403 assert_eq!(first.try_recv().unwrap(), Some(0));
404 assert_eq!(second.try_recv().unwrap(), None);
405 window.update(|_, window, cx| {
406 assert!(window.has_active_prompt());
407 assert!(second_view.read(cx).focus.is_focused(window));
408 });
409
410 app.update_entity(&second_view, |_, cx| cx.emit(PromptResponse(0)));
411 assert_eq!(second.try_recv().unwrap(), Some(0));
412 window.update(|root, window, _| {
413 assert!(!window.has_active_prompt());
414 assert!(root.focus.is_focused(window));
415 });
416 }
417}