zng_wgt_dialog/
lib.rs

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