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#![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#[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 pub zng_wgt_layer::popup::is_close_delaying(state: impl IntoVar<bool>);
72
73 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 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 if responder.get().is_waiting() {
95 let path = WINDOWS.widget_info(id).unwrap().path();
98 if args.windows.contains(&path.window_id()) {
99 if let Some(default) = default_response.get() {
102 responder.respond(default);
104 zng_wgt_layer::popup::POPUP_CLOSE_CMD
106 .scoped(path.window_id())
107 .notify_param(path.widget_id());
108 } else {
109 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 let ctx = DIALOG_CTX.get();
124 if ctx.responder.get().is_waiting() {
125 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 pub struct DialogCloseCanceledArgs {
143 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 pub static DIALOG_CLOSE_CANCELED_EVENT: DialogCloseCanceledArgs;
158}
159event_property! {
160 #[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#[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 pub static TITLE_VAR: WidgetFn<()> = WidgetFn::nil();
272 pub static ICON_VAR: WidgetFn<()> = WidgetFn::nil();
274 pub static CONTENT_VAR: WidgetFn<()> = WidgetFn::nil();
276 pub static BUTTON_FN_VAR: WidgetFn<DialogButtonArgs> = WidgetFn::new(default_button_fn);
278 pub static RESPONSES_VAR: Responses = Responses::ok();
280 pub static DEFAULT_RESPONSE_VAR: Option<Response> = None;
282 pub static NATIVE_DIALOGS_VAR: DialogKind = DIALOG.native_dialogs();
284}
285
286pub 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#[derive(Debug, Clone, PartialEq)]
305#[non_exhaustive]
306pub struct DialogButtonArgs {
307 pub response: Response,
309 pub is_last: bool,
311}
312impl DialogButtonArgs {
313 pub fn new(response: Response, is_last: bool) -> Self {
315 Self { response, is_last }
316 }
317}
318
319#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[derive(Clone)]
479#[non_exhaustive]
480pub struct Response {
481 pub name: Txt,
483 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 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 pub fn ok() -> Self {
507 Self::new("ok", l10n!("response-ok", "Ok"))
508 }
509
510 pub fn cancel() -> Self {
512 Self::new("cancel", l10n!("response-cancel", "Cancel"))
513 }
514
515 pub fn yes() -> Self {
517 Self::new("yes", l10n!("response-yes", "Yes"))
518 }
519 pub fn no() -> Self {
521 Self::new("no", l10n!("response-no", "No"))
522 }
523
524 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#[derive(Clone, PartialEq, Debug)]
548pub struct Responses(pub Vec<Response>);
549impl Responses {
550 pub fn new(r: impl Into<Response>) -> Self {
552 Self(vec![r.into()])
553 }
554
555 pub fn with(mut self, response: impl Into<Response>) -> Self {
557 self.push(response.into());
558 self
559 }
560
561 pub fn ok() -> Self {
563 Response::ok().into()
564 }
565
566 pub fn close() -> Self {
568 Response::close().into()
569 }
570
571 pub fn no_yes() -> Self {
573 vec![Response::no(), Response::yes()].into()
574 }
575
576 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
602pub struct DIALOG;
610impl DIALOG {
611 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 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 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 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 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 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 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 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 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 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 pub fn custom(&self, dialog: impl IntoUiNode) -> ResponseVar<Response> {
780 self.show_impl(dialog.into_node())
781 }
782
783 pub fn notification(&self, notification: impl IntoVar<Notification>) -> ResponseVar<NotificationResponse> {
790 self.notification_impl(notification.into_var())
791 }
792}
793
794impl DIALOG {
795 pub fn native_dialogs(&self) -> Var<DialogKind> {
801 DIALOG_SV.read().native_dialogs.clone()
802 }
803
804 pub fn available_native_dialogs(&self) -> NativeDialogCapacity {
806 VIEW_PROCESS.info().dialog
807 }
808}
809bitflags! {
810 #[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
812 pub struct DialogKind: u32 {
813 const INFO = 0b0000_0000_0000_0001;
815 const WARN = 0b0000_0000_0000_0010;
817 const ERROR = 0b0000_0000_0000_0100;
819 const ASK = 0b0000_0000_0000_1000;
821 const CONFIRM = 0b0000_0000_0001_0000;
823
824 const OPEN_FILE = 0b1000_0000_0000_0000;
826 const OPEN_FILES = 0b0100_0000_0000_0000;
828 const SAVE_FILE = 0b0010_0000_0000_0000;
830
831 const SELECT_FOLDER = 0b0001_0000_0000_0000;
833 const SELECT_FOLDERS = 0b0000_1000_0000_0000;
835
836 const MESSAGE = Self::INFO.bits() | Self::WARN.bits() | Self::ERROR.bits() | Self::ASK.bits() | Self::CONFIRM.bits();
838 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 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 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 *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}