zng_wgt_dialog/
lib.rs

1#![doc(html_favicon_url = "https://raw.githubusercontent.com/zng-ui/zng/main/examples/image/res/zng-logo-icon.png")]
2#![doc(html_logo_url = "https://raw.githubusercontent.com/zng-ui/zng/main/examples/image/res/zng-logo.png")]
3//!
4//! Dialog widget and service.
5//!
6//! # Crate
7//!
8#![doc = include_str!(concat!("../", std::env!("CARGO_PKG_README")))]
9#![warn(unused_extern_crates)]
10#![warn(missing_docs)]
11
12zng_wgt::enable_widget_macros!();
13
14use std::{fmt, ops, path::PathBuf, sync::Arc};
15
16use bitflags::bitflags;
17use parking_lot::Mutex;
18use zng_ext_l10n::l10n;
19use zng_ext_window::{WINDOW_CLOSE_REQUESTED_EVENT, WINDOWS, WindowCloseRequestedArgs};
20use zng_var::{ContextInitHandle, animation::easing};
21use zng_view_api::dialog as native_api;
22use zng_wgt::{prelude::*, *};
23use zng_wgt_container::Container;
24use zng_wgt_fill::background_color;
25use zng_wgt_filter::drop_shadow;
26use zng_wgt_input::focus::FocusableMix;
27use zng_wgt_layer::{
28    AnchorMode,
29    popup::{ContextCapture, POPUP, POPUP_CLOSE_REQUESTED_EVENT},
30};
31use zng_wgt_style::{Style, StyleMix, impl_style_fn, style_fn};
32use zng_wgt_text::Text;
33use zng_wgt_text_input::selectable::SelectableText;
34use zng_wgt_wrap::Wrap;
35
36pub mod backdrop;
37
38pub use zng_view_api::dialog::{FileDialogFilters, FileDialogResponse};
39
40/// A modal dialog overlay container.
41#[widget($crate::Dialog)]
42pub struct Dialog(FocusableMix<StyleMix<Container>>);
43impl Dialog {
44    fn widget_intrinsic(&mut self) {
45        self.style_intrinsic(STYLE_FN_VAR, property_id!(self::style_fn));
46
47        self.widget_builder()
48            .push_build_action(|b| b.push_intrinsic(NestGroup::EVENT, "dialog-closing", dialog_closing_node));
49
50        widget_set! {
51            self;
52            style_base_fn = style_fn!(|_| DefaultStyle!());
53
54            focus_on_init = true;
55            return_focus_on_deinit = true;
56
57            when *#is_close_delaying {
58                interactive = false;
59            }
60        }
61    }
62
63    widget_impl! {
64        /// If a respond close was requested for this dialog and it is just awaiting for the [`popup::close_delay`].
65        ///
66        /// The close delay is usually set on the backdrop widget style.
67        ///
68        /// [`popup::close_delay`]: fn@zng_wgt_layer::popup::close_delay
69        pub zng_wgt_layer::popup::is_close_delaying(state: impl IntoVar<bool>);
70
71        /// An attempt to close the dialog was made without setting the response.
72        ///
73        /// Dialogs must only close using [`DIALOG.respond`](DIALOG::respond).
74        pub on_dialog_close_canceled(args: impl WidgetHandler<DialogCloseCanceledArgs>);
75    }
76}
77impl_style_fn!(Dialog);
78
79fn dialog_closing_node(child: impl UiNode) -> impl UiNode {
80    match_node(child, move |_, op| {
81        match op {
82            UiNodeOp::Init => {
83                // layers receive events after window content, so we subscribe directly
84                let id = WIDGET.id();
85                let ctx = DIALOG_CTX.get();
86                let default_response = DEFAULT_RESPONSE_VAR.actual_var();
87                let responder = ctx.responder.clone();
88                let handle = WINDOW_CLOSE_REQUESTED_EVENT.on_pre_event(app_hn!(|args: &WindowCloseRequestedArgs, _| {
89                    // a window is closing
90                    if responder.get().is_waiting() {
91                        // dialog has no response
92
93                        let path = WINDOWS.widget_info(id).unwrap().path();
94                        if args.windows.contains(&path.window_id()) {
95                            // is closing dialog parent window
96
97                            if let Some(default) = default_response.get() {
98                                // has default response
99                                responder.respond(default);
100                                // in case the window close is canceled by other component
101                                zng_wgt_layer::popup::POPUP_CLOSE_CMD
102                                    .scoped(path.window_id())
103                                    .notify_param(path.widget_id());
104                            } else {
105                                // no default response, cancel close
106                                args.propagation().stop();
107                                DIALOG_CLOSE_CANCELED_EVENT.notify(DialogCloseCanceledArgs::now(path));
108                            }
109                        }
110                    }
111                }));
112                WIDGET.push_event_handle(handle);
113                WIDGET.sub_event(&POPUP_CLOSE_REQUESTED_EVENT);
114            }
115            UiNodeOp::Event { update } => {
116                if let Some(args) = POPUP_CLOSE_REQUESTED_EVENT.on(update) {
117                    // dialog is closing
118                    let ctx = DIALOG_CTX.get();
119                    if ctx.responder.get().is_waiting() {
120                        // dialog has no response
121                        if let Some(r) = DEFAULT_RESPONSE_VAR.get() {
122                            ctx.responder.respond(r);
123                        } else {
124                            args.propagation().stop();
125                            DIALOG_CLOSE_CANCELED_EVENT.notify(DialogCloseCanceledArgs::now(WIDGET.info().path()));
126                        }
127                    }
128                }
129            }
130            _ => (),
131        }
132    })
133}
134
135event_args! {
136    /// Arguments for [`DIALOG_CLOSE_CANCELED_EVENT`].
137    pub struct DialogCloseCanceledArgs {
138        /// Dialog widget.
139        pub target: WidgetPath,
140
141        ..
142
143        fn delivery_list(&self, list: &mut UpdateDeliveryList) {
144            list.insert_wgt(&self.target);
145        }
146    }
147}
148event! {
149    /// An attempt to close the dialog was made without setting the response.
150    ///
151    /// Dialogs must only close using [`DIALOG.respond`](DIALOG::respond).
152    pub static DIALOG_CLOSE_CANCELED_EVENT: DialogCloseCanceledArgs;
153}
154event_property! {
155    // An attempt to close the dialog was made without setting the response.
156    ///
157    /// Dialogs must only close using [`DIALOG.respond`](DIALOG::respond).
158    pub fn dialog_close_canceled {
159        event: DIALOG_CLOSE_CANCELED_EVENT,
160        args: DialogCloseCanceledArgs,
161    }
162}
163
164/// Dialog default style.
165#[widget($crate::DefaultStyle)]
166pub struct DefaultStyle(Style);
167impl DefaultStyle {
168    fn widget_intrinsic(&mut self) {
169        let highlight_color = var(colors::BLACK.transparent());
170        widget_set! {
171            self;
172
173            replace = true;
174
175            background_color = light_dark(rgb(0.7, 0.7, 0.7), rgb(0.3, 0.3, 0.3));
176            drop_shadow = {
177                offset: 4,
178                blur_radius: 6,
179                color: colors::BLACK.with_alpha(50.pct()),
180            };
181
182            corner_radius = 8;
183            clip_to_bounds = true;
184
185            margin = 10;
186            zng_wgt_container::padding = 15;
187
188            align = Align::CENTER;
189
190            zng_wgt_container::child_out_top = Container! {
191                corner_radius = 0;
192                background_color = light_dark(rgb(0.85, 0.85, 0.85), rgb(0.15, 0.15, 0.15));
193                child = presenter((), TITLE_VAR);
194                child_align = Align::START;
195                padding = (4, 8);
196                zng_wgt_text::font_weight = zng_ext_font::FontWeight::BOLD;
197            }, 0;
198
199            zng_wgt_container::child_out_bottom = presenter(RESPONSES_VAR, wgt_fn!(|responses: Responses| {
200                Wrap! {
201                    corner_radius = 0;
202                    background_color = light_dark(rgb(0.85, 0.85, 0.85), rgb(0.15, 0.15, 0.15));
203                    children_align = Align::END;
204                    zng_wgt_container::padding = 3;
205                    spacing = 3;
206                    children = {
207                        let last = responses.len().saturating_sub(1);
208                        responses.0
209                            .into_iter()
210                            .enumerate()
211                            .map(|(i, r)| presenter(
212                                DialogButtonArgs { response: r, is_last: i == last },
213                                BUTTON_FN_VAR
214                            ).boxed())
215                            .collect::<UiVec>()
216                    };
217                }
218            })), 0;
219
220            zng_wgt_container::child_out_left = Container! {
221                child = presenter((), ICON_VAR);
222                child_align = Align::TOP;
223            }, 0;
224
225            zng_wgt_container::child = presenter((), CONTENT_VAR);
226
227            #[easing(250.ms())]
228            zng_wgt_filter::opacity = 30.pct();
229            #[easing(250.ms())]
230            zng_wgt_transform::transform = Transform::new_translate_y(-10).scale(98.pct());
231            when *#is_inited && !*#zng_wgt_layer::popup::is_close_delaying {
232                zng_wgt_filter::opacity = 100.pct();
233                zng_wgt_transform::transform = Transform::identity();
234            }
235
236            zng_wgt_fill::foreground_highlight = {
237                offsets: 0,
238                widths: 2,
239                sides: highlight_color.map_into(),
240            };
241            on_dialog_close_canceled = hn!(highlight_color, |_| {
242                let c = colors::ACCENT_COLOR_VAR.rgba().get();
243                let mut repeats = 0;
244                highlight_color.sequence(move |cv| {
245                    repeats += 1;
246                    if repeats <= 2 {
247                        cv.set_ease(c, c.with_alpha(0.pct()), 120.ms(), easing::linear)
248                    } else {
249                        zng_var::animation::AnimationHandle::dummy()
250                    }
251                }).perm();
252            });
253        }
254    }
255}
256
257context_var! {
258    /// Title widget, usually placed as `child_out_top`.
259    pub static TITLE_VAR: WidgetFn<()> = WidgetFn::nil();
260    /// Icon widget, usually placed as `child_out_start`.
261    pub static ICON_VAR: WidgetFn<()> = WidgetFn::nil();
262    /// Content widget, usually the dialog child.
263    pub static CONTENT_VAR: WidgetFn<()> = WidgetFn::nil();
264    /// Dialog response button generator, usually placed as `child_out_bottom`.
265    pub static BUTTON_FN_VAR: WidgetFn<DialogButtonArgs> = WidgetFn::new(default_button_fn);
266    /// Dialog responses.
267    pub static RESPONSES_VAR: Responses = Responses::ok();
268    /// Dialog response when closed without setting a response.
269    pub static DEFAULT_RESPONSE_VAR: Option<Response> = None;
270    /// Defines what native dialogs are used on a context.
271    pub static NATIVE_DIALOGS_VAR: DialogKind = DIALOG.native_dialogs();
272}
273
274/// Default value of [`button_fn`](fn@button_fn)
275pub fn default_button_fn(args: DialogButtonArgs) -> impl UiNode {
276    zng_wgt_button::Button! {
277        child = Text!(args.response.label.clone());
278        on_click = hn_once!(|a: &zng_wgt_input::gesture::ClickArgs| {
279            a.propagation().stop();
280            DIALOG.respond(args.response);
281        });
282        focus_on_init = args.is_last;
283        when args.is_last {
284            style_fn = zng_wgt_button::PrimaryStyle!();
285        }
286    }
287}
288
289/// Arguments for [`button_fn`].
290///
291/// [`button_fn`]: fn@button_fn
292#[derive(Debug, Clone, PartialEq)]
293pub struct DialogButtonArgs {
294    /// The response that must be represented by the button.
295    pub response: Response,
296    /// If the button is the last entry on the responses list.
297    pub is_last: bool,
298}
299
300/// Dialog title widget.
301///
302/// Note that this takes in an widget, you can use `Text!("title")` to set to a text.
303#[property(CONTEXT, default(NilUiNode), widget_impl(Dialog))]
304pub fn title(child: impl UiNode, title: impl UiNode) -> impl UiNode {
305    with_context_var(child, TITLE_VAR, WidgetFn::singleton(title))
306}
307
308/// Dialog icon widget.
309///
310/// Note that this takes in an widget, you can use the `ICONS` service to get an icon widget.
311#[property(CONTEXT, default(NilUiNode), widget_impl(Dialog))]
312pub fn icon(child: impl UiNode, icon: impl UiNode) -> impl UiNode {
313    with_context_var(child, ICON_VAR, WidgetFn::singleton(icon))
314}
315
316/// Dialog content widget.
317///
318/// Note that this takes in an widget, you can use `SelectableText!("message")` for the message.
319#[property(CONTEXT, default(FillUiNode), widget_impl(Dialog))]
320pub fn content(child: impl UiNode, content: impl UiNode) -> impl UiNode {
321    with_context_var(child, CONTENT_VAR, WidgetFn::singleton(content))
322}
323
324/// Dialog button generator.
325#[property(CONTEXT, default(BUTTON_FN_VAR), widget_impl(Dialog))]
326pub fn button_fn(child: impl UiNode, button: impl IntoVar<WidgetFn<DialogButtonArgs>>) -> impl UiNode {
327    with_context_var(child, BUTTON_FN_VAR, button)
328}
329
330/// Dialog responses.
331#[property(CONTEXT, default(RESPONSES_VAR), widget_impl(Dialog))]
332pub fn responses(child: impl UiNode, responses: impl IntoVar<Responses>) -> impl UiNode {
333    with_context_var(child, RESPONSES_VAR, responses)
334}
335
336/// Dialog response when closed without setting a response.
337#[property(CONTEXT, default(DEFAULT_RESPONSE_VAR), widget_impl(Dialog))]
338pub fn default_response(child: impl UiNode, response: impl IntoVar<Option<Response>>) -> impl UiNode {
339    with_context_var(child, DEFAULT_RESPONSE_VAR, response)
340}
341
342/// Defines what native dialogs are used by dialogs opened on the context.
343///
344/// Sets [`NATIVE_DIALOGS_VAR`].
345#[property(CONTEXT, default(NATIVE_DIALOGS_VAR))]
346pub fn native_dialogs(child: impl UiNode, dialogs: impl IntoVar<DialogKind>) -> impl UiNode {
347    with_context_var(child, NATIVE_DIALOGS_VAR, dialogs)
348}
349
350/// Dialog info style.
351///
352/// Sets the info icon and a single "Ok" response.
353#[widget($crate::InfoStyle)]
354pub struct InfoStyle(DefaultStyle);
355impl InfoStyle {
356    fn widget_intrinsic(&mut self) {
357        widget_set! {
358            self;
359            icon = Container! {
360                child = ICONS.req(["dialog-info", "info"]);
361                zng_wgt_size_offset::size = 48;
362                zng_wgt_text::font_color = colors::AZURE;
363                padding = 5;
364            };
365            default_response = Response::ok();
366        }
367    }
368}
369
370/// Dialog warn style.
371///
372/// Sets the warn icon and a single "Ok" response.
373#[widget($crate::WarnStyle)]
374pub struct WarnStyle(DefaultStyle);
375impl WarnStyle {
376    fn widget_intrinsic(&mut self) {
377        widget_set! {
378            self;
379            icon = Container! {
380                child = ICONS.req(["dialog-warn", "warning"]);
381                zng_wgt_size_offset::size = 48;
382                zng_wgt_text::font_color = colors::ORANGE;
383                padding = 5;
384            };
385        }
386    }
387}
388
389/// Dialog error style.
390///
391/// Sets the error icon and a single "Ok" response.
392#[widget($crate::ErrorStyle)]
393pub struct ErrorStyle(DefaultStyle);
394impl ErrorStyle {
395    fn widget_intrinsic(&mut self) {
396        widget_set! {
397            self;
398            icon = Container! {
399                child = ICONS.req(["dialog-error", "error"]);
400                zng_wgt_size_offset::size = 48;
401                zng_wgt_text::font_color = rgb(209, 29, 29);
402                padding = 5;
403            };
404        }
405    }
406}
407
408/// Question style.
409///
410/// Sets the question icon and two "No" and "Yes" responses.
411#[widget($crate::AskStyle)]
412pub struct AskStyle(DefaultStyle);
413impl AskStyle {
414    fn widget_intrinsic(&mut self) {
415        widget_set! {
416            self;
417            icon = Container! {
418                child = ICONS.req(["dialog-question", "question-mark"]);
419                zng_wgt_size_offset::size = 48;
420                zng_wgt_text::font_color = colors::AZURE;
421                padding = 5;
422            };
423            responses = Responses::no_yes();
424        }
425    }
426}
427
428/// Confirmation style.
429///
430/// Sets the question icon and two "Cancel" and "Ok" responses.
431#[widget($crate::ConfirmStyle)]
432pub struct ConfirmStyle(DefaultStyle);
433impl ConfirmStyle {
434    fn widget_intrinsic(&mut self) {
435        widget_set! {
436            self;
437            icon = Container! {
438                child = ICONS.req(["dialog-confirm", "question-mark"]);
439                zng_wgt_size_offset::size = 48;
440                zng_wgt_text::font_color = colors::ORANGE;
441                padding = 5;
442            };
443            responses = Responses::cancel_ok();
444        }
445    }
446}
447
448/// Dialog response.
449#[derive(Clone)]
450pub struct Response {
451    /// Response identifying name.
452    pub name: Txt,
453    /// Response button label.
454    pub label: BoxedVar<Txt>,
455}
456impl fmt::Debug for Response {
457    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
458        write!(f, "{:?}", self.name)
459    }
460}
461impl PartialEq for Response {
462    fn eq(&self, other: &Self) -> bool {
463        self.name == other.name
464    }
465}
466impl Response {
467    /// New from name and label.
468    pub fn new(name: impl Into<Txt>, label: impl IntoVar<Txt>) -> Self {
469        Self {
470            name: name.into(),
471            label: label.into_var().boxed(),
472        }
473    }
474
475    /// "ok"
476    pub fn ok() -> Self {
477        Self::new("Ok", l10n!("response-ok", "Ok"))
478    }
479
480    /// "cancel"
481    pub fn cancel() -> Self {
482        Self::new("cancel", l10n!("response-cancel", "Cancel"))
483    }
484
485    /// "yes"
486    pub fn yes() -> Self {
487        Self::new("yes", l10n!("response-yes", "Yes"))
488    }
489    /// "no"
490    pub fn no() -> Self {
491        Self::new("no", l10n!("response-no", "No"))
492    }
493
494    /// "close"
495    pub fn close() -> Self {
496        Self::new("close", l10n!("response-close", "Close"))
497    }
498}
499impl_from_and_into_var! {
500    fn from(native: native_api::MsgDialogResponse) -> Response {
501        match native {
502            native_api::MsgDialogResponse::Ok => Response::ok(),
503            native_api::MsgDialogResponse::Yes => Response::yes(),
504            native_api::MsgDialogResponse::No => Response::no(),
505            native_api::MsgDialogResponse::Cancel => Response::cancel(),
506            native_api::MsgDialogResponse::Error(e) => Response {
507                name: Txt::from_static("native-error"),
508                label: LocalVar(e).boxed(),
509            },
510        }
511    }
512    fn from(response: Response) -> Option<Response>;
513}
514
515/// Response labels.
516#[derive(Clone, PartialEq, Debug)]
517pub struct Responses(pub Vec<Response>);
518impl Responses {
519    /// new with first response.
520    pub fn new(r: impl Into<Response>) -> Self {
521        Self(vec![r.into()])
522    }
523
524    /// With response.
525    pub fn with(mut self, response: impl Into<Response>) -> Self {
526        self.push(response.into());
527        self
528    }
529
530    /// "Ok"
531    pub fn ok() -> Self {
532        Response::ok().into()
533    }
534
535    /// "Close"
536    pub fn close() -> Self {
537        Response::close().into()
538    }
539
540    /// "No", "Yes"
541    pub fn no_yes() -> Self {
542        vec![Response::no(), Response::yes()].into()
543    }
544
545    /// "Cancel", "Ok"
546    pub fn cancel_ok() -> Self {
547        vec![Response::cancel(), Response::ok()].into()
548    }
549}
550impl ops::Deref for Responses {
551    type Target = Vec<Response>;
552
553    fn deref(&self) -> &Self::Target {
554        &self.0
555    }
556}
557impl ops::DerefMut for Responses {
558    fn deref_mut(&mut self) -> &mut Self::Target {
559        &mut self.0
560    }
561}
562impl_from_and_into_var! {
563    fn from(response: Response) -> Responses {
564        Responses::new(response)
565    }
566    fn from(responses: Vec<Response>) -> Responses {
567        Responses(responses)
568    }
569}
570
571/// Dialog service.
572///
573/// The non-custom dialog methods can be configured to open as native dialogs instead of the custom overlay dialogs.
574///
575/// # Panics
576///
577/// All dialog methods panic is not called inside a window.
578pub struct DIALOG;
579impl DIALOG {
580    /// Show an info dialog with "Ok" button.
581    pub fn info(&self, title: impl IntoVar<Txt>, msg: impl IntoVar<Txt>) -> ResponseVar<()> {
582        self.message(
583            msg.into_var().boxed(),
584            title.into_var().boxed(),
585            DialogKind::INFO,
586            &|| InfoStyle!(),
587            native_api::MsgDialogIcon::Info,
588            native_api::MsgDialogButtons::Ok,
589        )
590        .map_response(|_| ())
591    }
592
593    /// Show a warning dialog with "Ok" button.
594    pub fn warn(&self, title: impl IntoVar<Txt>, msg: impl IntoVar<Txt>) -> ResponseVar<()> {
595        self.message(
596            msg.into_var().boxed(),
597            title.into_var().boxed(),
598            DialogKind::WARN,
599            &|| WarnStyle!(),
600            native_api::MsgDialogIcon::Warn,
601            native_api::MsgDialogButtons::Ok,
602        )
603        .map_response(|_| ())
604    }
605
606    /// Show an error dialog with "Ok" button.
607    pub fn error(&self, title: impl IntoVar<Txt>, msg: impl IntoVar<Txt>) -> ResponseVar<()> {
608        self.message(
609            msg.into_var().boxed(),
610            title.into_var().boxed(),
611            DialogKind::ERROR,
612            &|| ErrorStyle!(),
613            native_api::MsgDialogIcon::Error,
614            native_api::MsgDialogButtons::Ok,
615        )
616        .map_response(|_| ())
617    }
618
619    /// Shows a question dialog with "No" and "Yes" buttons. Returns `true` for "Yes".
620    pub fn ask(&self, title: impl IntoVar<Txt>, question: impl IntoVar<Txt>) -> ResponseVar<bool> {
621        self.message(
622            question.into_var().boxed(),
623            title.into_var().boxed(),
624            DialogKind::ASK,
625            &|| AskStyle!(),
626            native_api::MsgDialogIcon::Info,
627            native_api::MsgDialogButtons::YesNo,
628        )
629        .map_response(|r| r.name == "yes")
630    }
631
632    /// Shows a question dialog with "Cancel" and "Ok" buttons. Returns `true` for "Ok".
633    pub fn confirm(&self, title: impl IntoVar<Txt>, question: impl IntoVar<Txt>) -> ResponseVar<bool> {
634        self.message(
635            question.into_var().boxed(),
636            title.into_var().boxed(),
637            DialogKind::CONFIRM,
638            &|| ConfirmStyle!(),
639            native_api::MsgDialogIcon::Warn,
640            native_api::MsgDialogButtons::OkCancel,
641        )
642        .map_response(|r| r.name == "ok")
643    }
644
645    /// Shows a native file picker dialog configured to select one existing file.
646    pub fn open_file(
647        &self,
648        title: impl IntoVar<Txt>,
649        starting_dir: impl Into<PathBuf>,
650        starting_name: impl IntoVar<Txt>,
651        filters: impl Into<FileDialogFilters>,
652    ) -> ResponseVar<FileDialogResponse> {
653        WINDOWS.native_file_dialog(
654            WINDOW.id(),
655            native_api::FileDialog {
656                title: title.into_var().get(),
657                starting_dir: starting_dir.into(),
658                starting_name: starting_name.into_var().get(),
659                filters: filters.into().build(),
660                kind: native_api::FileDialogKind::OpenFile,
661            },
662        )
663    }
664
665    /// Shows a native file picker dialog configured to select one or more existing files.
666    pub fn open_files(
667        &self,
668        title: impl IntoVar<Txt>,
669        starting_dir: impl Into<PathBuf>,
670        starting_name: impl IntoVar<Txt>,
671        filters: impl Into<FileDialogFilters>,
672    ) -> ResponseVar<FileDialogResponse> {
673        WINDOWS.native_file_dialog(
674            WINDOW.id(),
675            native_api::FileDialog {
676                title: title.into_var().get(),
677                starting_dir: starting_dir.into(),
678                starting_name: starting_name.into_var().get(),
679                filters: filters.into().build(),
680                kind: native_api::FileDialogKind::OpenFiles,
681            },
682        )
683    }
684
685    /// Shows a native file picker dialog configured to select one file path that does not exist yet.
686    pub fn save_file(
687        &self,
688        title: impl IntoVar<Txt>,
689        starting_dir: impl Into<PathBuf>,
690        starting_name: impl IntoVar<Txt>,
691        filters: impl Into<FileDialogFilters>,
692    ) -> ResponseVar<FileDialogResponse> {
693        WINDOWS.native_file_dialog(
694            WINDOW.id(),
695            native_api::FileDialog {
696                title: title.into_var().get(),
697                starting_dir: starting_dir.into(),
698                starting_name: starting_name.into_var().get(),
699                filters: filters.into().build(),
700                kind: native_api::FileDialogKind::SaveFile,
701            },
702        )
703    }
704
705    /// Shows a native file picker dialog configured to select one existing directory.
706    pub fn select_folder(
707        &self,
708        title: impl IntoVar<Txt>,
709        starting_dir: impl Into<PathBuf>,
710        starting_name: impl IntoVar<Txt>,
711    ) -> ResponseVar<FileDialogResponse> {
712        WINDOWS.native_file_dialog(
713            WINDOW.id(),
714            native_api::FileDialog {
715                title: title.into_var().get(),
716                starting_dir: starting_dir.into(),
717                starting_name: starting_name.into_var().get(),
718                filters: "".into(),
719                kind: native_api::FileDialogKind::SelectFolder,
720            },
721        )
722    }
723
724    /// Shows a native file picker dialog configured to select one or more existing directories.
725    pub fn select_folders(
726        &self,
727        title: impl IntoVar<Txt>,
728        starting_dir: impl Into<PathBuf>,
729        starting_name: impl IntoVar<Txt>,
730    ) -> ResponseVar<FileDialogResponse> {
731        WINDOWS.native_file_dialog(
732            WINDOW.id(),
733            native_api::FileDialog {
734                title: title.into_var().get(),
735                starting_dir: starting_dir.into(),
736                starting_name: starting_name.into_var().get(),
737                filters: "".into(),
738                kind: native_api::FileDialogKind::SelectFolders,
739            },
740        )
741    }
742
743    /// Open the custom `dialog`.
744    ///
745    /// Returns the selected response or [`close`] if the dialog is closed without response.
746    ///
747    /// [`close`]: Response::close
748    pub fn custom(&self, dialog: impl UiNode) -> ResponseVar<Response> {
749        self.show_impl(dialog.boxed())
750    }
751}
752
753impl DIALOG {
754    /// Variable that defines what native dialogs are used when the dialog methods are called in window contexts.
755    ///
756    /// The [`native_dialogs`](fn@native_dialogs) context property can also be used to override the config just for some widgets.
757    ///
758    /// Note that some dialogs only have the native implementation as of this release.
759    pub fn native_dialogs(&self) -> ArcVar<DialogKind> {
760        DIALOG_SV.read().native_dialogs.clone()
761    }
762}
763bitflags! {
764    /// Dialog kind options.
765    #[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
766    pub struct DialogKind: u32 {
767        /// [`DIALOG.info`](DIALOG::info)
768        const INFO =    0b0000_0000_0000_0001;
769        /// [`DIALOG.warn`](DIALOG::warn)
770        const WARN =    0b0000_0000_0000_0010;
771        /// [`DIALOG.error`](DIALOG::error)
772        const ERROR =   0b0000_0000_0000_0100;
773        /// [`DIALOG.ask`](DIALOG::ask)
774        const ASK =     0b0000_0000_0000_1000;
775        /// [`DIALOG.confirm`](DIALOG::confirm)
776        const CONFIRM = 0b0000_0000_0001_0000;
777
778        /// [`DIALOG.open_file`](DIALOG::open_file)
779        const OPEN_FILE =  0b1000_0000_0000_0000;
780        /// [`DIALOG.open_files`](DIALOG::open_files)
781        const OPEN_FILES = 0b0100_0000_0000_0000;
782        /// [`DIALOG.save_file`](DIALOG::save_file)
783        const SAVE_FILE =  0b0010_0000_0000_0000;
784
785        /// [`DIALOG.select_folder`](DIALOG::select_folder)
786        const SELECT_FOLDER =  0b0001_0000_0000_0000;
787        /// [`DIALOG.select_folders`](DIALOG::select_folders)
788        const SELECT_FOLDERS = 0b0000_1000_0000_0000;
789
790        /// All message dialogs.
791        const MESSAGE = Self::INFO.bits() | Self::WARN.bits() | Self::ERROR.bits()  | Self::ASK.bits()  | Self::CONFIRM.bits();
792        /// All file system dialogs.
793        const FILE = Self::OPEN_FILE.bits() | Self::OPEN_FILES.bits() | Self::SAVE_FILE.bits()  | Self::SELECT_FOLDER.bits() | Self::SELECT_FOLDERS.bits();
794    }
795}
796impl_from_and_into_var! {
797    fn from(empty_or_all: bool) -> DialogKind {
798        if empty_or_all {
799            DialogKind::all()
800        } else {
801            DialogKind::empty()
802        }
803    }
804}
805
806impl DIALOG {
807    /// Close the contextual dialog with the `response``.
808    pub fn respond(&self, response: Response) {
809        let ctx = DIALOG_CTX.get();
810        let id = *ctx.dialog_id.lock();
811        if let Some(id) = id {
812            ctx.responder.respond(response);
813            POPUP.close_id(id);
814        } else {
815            tracing::error!("DIALOG.respond called outside of a dialog");
816        }
817    }
818
819    /// Try to close the contextual dialog without directly setting a response.
820    ///
821    /// If the dialog has no [`default_response`](fn@default_response) the
822    /// [`on_dialog_close_canceled`](fn@on_dialog_close_canceled) event notifies instead of closing.
823    pub fn respond_default(&self) {
824        let ctx = DIALOG_CTX.get();
825        let id = *ctx.dialog_id.lock();
826        if let Some(id) = id {
827            POPUP.close_id(id);
828        } else {
829            tracing::error!("DIALOG.respond called outside of a dialog");
830        }
831    }
832
833    fn message(
834        &self,
835        msg: BoxedVar<Txt>,
836        title: BoxedVar<Txt>,
837        kind: DialogKind,
838        style: &dyn Fn() -> zng_wgt_style::StyleBuilder,
839        native_icon: native_api::MsgDialogIcon,
840        native_buttons: native_api::MsgDialogButtons,
841    ) -> ResponseVar<Response> {
842        if NATIVE_DIALOGS_VAR.get().contains(kind) {
843            WINDOWS
844                .native_message_dialog(
845                    WINDOW.id(),
846                    native_api::MsgDialog {
847                        title: title.get(),
848                        message: msg.get(),
849                        icon: native_icon,
850                        buttons: native_buttons,
851                    },
852                )
853                .map_response(|r| r.clone().into())
854        } else {
855            self.custom(Dialog! {
856                style_fn = style();
857                title = Text! {
858                    visibility = title.map(|t| Visibility::from(!t.is_empty()));
859                    txt = title;
860                };
861                content = SelectableText!(msg);
862            })
863        }
864    }
865
866    fn show_impl(&self, dialog: BoxedUiNode) -> ResponseVar<Response> {
867        let (responder, response) = response_var();
868
869        let mut ctx = Some(Arc::new(DialogCtx {
870            dialog_id: Mutex::new(None),
871            responder,
872        }));
873
874        let dialog = backdrop::DialogBackdrop!(dialog);
875
876        let dialog = match_widget(
877            dialog,
878            clmv!(|c, op| {
879                match &op {
880                    UiNodeOp::Init => {
881                        *ctx.as_ref().unwrap().dialog_id.lock() = c.with_context(WidgetUpdateMode::Ignore, || WIDGET.id());
882                        DIALOG_CTX.with_context(&mut ctx, || c.op(op));
883                        // in case a non-standard dialog widget is used
884                        *ctx.as_ref().unwrap().dialog_id.lock() = c.with_context(WidgetUpdateMode::Ignore, || WIDGET.id());
885                    }
886                    UiNodeOp::Deinit => {}
887                    _ => {
888                        DIALOG_CTX.with_context(&mut ctx, || c.op(op));
889                    }
890                }
891            }),
892        );
893
894        zng_wgt_layer::popup::CLOSE_ON_FOCUS_LEAVE_VAR.with_context_var(ContextInitHandle::new(), false, || {
895            POPUP.open_config(dialog, AnchorMode::window(), ContextCapture::NoCapture)
896        });
897
898        response
899    }
900}
901
902struct DialogCtx {
903    dialog_id: Mutex<Option<WidgetId>>,
904    responder: ResponderVar<Response>,
905}
906context_local! {
907    static DIALOG_CTX: DialogCtx = DialogCtx {
908        dialog_id: Mutex::new(None),
909        responder: response_var().0,
910    };
911}
912
913struct DialogService {
914    native_dialogs: ArcVar<DialogKind>,
915}
916app_local! {
917    static DIALOG_SV: DialogService = DialogService {
918        native_dialogs: var(DialogKind::FILE),
919    };
920}