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