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