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_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#[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 pub zng_wgt_layer::popup::is_close_delaying(state: impl IntoVar<bool>);
69
70 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 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 if responder.get().is_waiting() {
90 let path = WINDOWS.widget_info(id).unwrap().path();
93 if args.windows.contains(&path.window_id()) {
94 if let Some(default) = default_response.get() {
97 responder.respond(default);
99 zng_wgt_layer::popup::POPUP_CLOSE_CMD
101 .scoped(path.window_id())
102 .notify_param(path.widget_id());
103 } else {
104 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 let ctx = DIALOG_CTX.get();
118 if ctx.responder.get().is_waiting() {
119 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 pub struct DialogCloseCanceledArgs {
137 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 pub static DIALOG_CLOSE_CANCELED_EVENT: DialogCloseCanceledArgs;
152}
153event_property! {
154 pub fn dialog_close_canceled {
158 event: DIALOG_CLOSE_CANCELED_EVENT,
159 args: DialogCloseCanceledArgs,
160 }
161}
162
163#[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 pub static TITLE_VAR: WidgetFn<()> = WidgetFn::nil();
262 pub static ICON_VAR: WidgetFn<()> = WidgetFn::nil();
264 pub static CONTENT_VAR: WidgetFn<()> = WidgetFn::nil();
266 pub static BUTTON_FN_VAR: WidgetFn<DialogButtonArgs> = WidgetFn::new(default_button_fn);
268 pub static RESPONSES_VAR: Responses = Responses::ok();
270 pub static DEFAULT_RESPONSE_VAR: Option<Response> = None;
272 pub static NATIVE_DIALOGS_VAR: DialogKind = DIALOG.native_dialogs();
274}
275
276pub 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#[derive(Debug, Clone, PartialEq)]
295#[non_exhaustive]
296pub struct DialogButtonArgs {
297 pub response: Response,
299 pub is_last: bool,
301}
302impl DialogButtonArgs {
303 pub fn new(response: Response, is_last: bool) -> Self {
305 Self { response, is_last }
306 }
307}
308
309#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[derive(Clone)]
469#[non_exhaustive]
470pub struct Response {
471 pub name: Txt,
473 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 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 pub fn ok() -> Self {
497 Self::new("ok", l10n!("response-ok", "Ok"))
498 }
499
500 pub fn cancel() -> Self {
502 Self::new("cancel", l10n!("response-cancel", "Cancel"))
503 }
504
505 pub fn yes() -> Self {
507 Self::new("yes", l10n!("response-yes", "Yes"))
508 }
509 pub fn no() -> Self {
511 Self::new("no", l10n!("response-no", "No"))
512 }
513
514 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#[derive(Clone, PartialEq, Debug)]
538pub struct Responses(pub Vec<Response>);
539impl Responses {
540 pub fn new(r: impl Into<Response>) -> Self {
542 Self(vec![r.into()])
543 }
544
545 pub fn with(mut self, response: impl Into<Response>) -> Self {
547 self.push(response.into());
548 self
549 }
550
551 pub fn ok() -> Self {
553 Response::ok().into()
554 }
555
556 pub fn close() -> Self {
558 Response::close().into()
559 }
560
561 pub fn no_yes() -> Self {
563 vec![Response::no(), Response::yes()].into()
564 }
565
566 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
592pub struct DIALOG;
600impl DIALOG {
601 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 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 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 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 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 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 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 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 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 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 pub fn custom(&self, dialog: impl IntoUiNode) -> ResponseVar<Response> {
770 self.show_impl(dialog.into_node())
771 }
772}
773
774impl DIALOG {
775 pub fn native_dialogs(&self) -> Var<DialogKind> {
781 DIALOG_SV.read().native_dialogs.clone()
782 }
783}
784bitflags! {
785 #[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
787 pub struct DialogKind: u32 {
788 const INFO = 0b0000_0000_0000_0001;
790 const WARN = 0b0000_0000_0000_0010;
792 const ERROR = 0b0000_0000_0000_0100;
794 const ASK = 0b0000_0000_0000_1000;
796 const CONFIRM = 0b0000_0000_0001_0000;
798
799 const OPEN_FILE = 0b1000_0000_0000_0000;
801 const OPEN_FILES = 0b0100_0000_0000_0000;
803 const SAVE_FILE = 0b0010_0000_0000_0000;
805
806 const SELECT_FOLDER = 0b0001_0000_0000_0000;
808 const SELECT_FOLDERS = 0b0000_1000_0000_0000;
810
811 const MESSAGE = Self::INFO.bits() | Self::WARN.bits() | Self::ERROR.bits() | Self::ASK.bits() | Self::CONFIRM.bits();
813 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 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 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 *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}