Repository: zng-ui/zng (language: rust)

Issue:
Implement modal dialog widget
We have duplicate implementation of modal dialogs in multiple examples, should be a widget.


Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c12d4bac9..feba0f5dd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,10 @@
 # Unreleased
 
+* Fix `SelectableText!` shorthand syntax.
+* Fix layer `AnchorSize::Window` not filling the window space by default.
+* Fix `ContextCapture::NoCapture` excluding popup config.
+* Add `ResponseVar::map_response`.
+* Add `Dialog!` widget, `DIALOG` service and related types.
 
 # 0.10.4
 
diff --git a/crates/README.md b/crates/README.md
index 00fd47d06..cbe4c7f01 100644
--- a/crates/README.md
+++ b/crates/README.md
@@ -96,6 +96,7 @@ or for custom properties that deeply integrate with a widget.
 - `zng-wgt-fill`
 - `zng-wgt-checkerboard`
 - `zng-wgt-layer`
+- `zng-wgt-dialog`
 - `zng-wgt-undo-history`
 - `zng-wgt-image`
 - `zng-wgt-text`
diff --git a/crates/zng-ext-window/src/service.rs b/crates/zng-ext-window/src/service.rs
index 0c32bb288..2210ce6b6 100644
--- a/crates/zng-ext-window/src/service.rs
+++ b/crates/zng-ext-window/src/service.rs
@@ -1340,6 +1340,8 @@ impl WINDOWS {
     ///
     /// The dialog can be modal in the view-process, in the app-process it is always async, the
     /// response var will update once when the user responds to the dialog.
+    ///
+    /// Consider using the `DIALOG` service instead of the method directly.
     pub fn native_message_dialog(
         &self,
         window_id: WindowId,
@@ -1363,6 +1365,8 @@ impl WINDOWS {
     ///
     /// The dialog can be modal in the view-process, in the app-process it is always async, the
     /// response var will update once when the user responds to the dialog.
+    ///
+    /// Consider using the `DIALOG` service instead of the method directly.
     pub fn native_file_dialog(
         &self,
         window_id: WindowId,
diff --git a/crates/zng-task/src/http.rs b/crates/zng-task/src/http.rs
index 1c75ce164..7bf3cb8e4 100644
--- a/crates/zng-task/src/http.rs
+++ b/crates/zng-task/src/http.rs
@@ -820,10 +820,18 @@ pub async fn get(uri: impl TryUri) -> Result<Response, Error> {
 /// Send a GET request to the `uri` and read the response as a string.
 ///
 /// The [`default_client`] is used to send the request.
+#[deprecated = "use get_txt"]
 pub async fn get_text(uri: impl TryUri) -> Result<Txt, Error> {
     default_client().get_text(uri).await
 }
 
+/// Send a GET request to the `uri` and read the response as a string.
+///
+/// The [`default_client`] is used to send the request.
+pub async fn get_txt(uri: impl TryUri) -> Result<Txt, Error> {
+    default_client().get_text(uri).await
+}
+
 /// Send a GET request to the `uri` and read the response as raw bytes.
 ///
 /// The [`default_client`] is used to send the request.
diff --git a/crates/zng-var/src/lib.rs b/crates/zng-var/src/lib.rs
index d08c2d5e2..42dd27f1e 100644
--- a/crates/zng-var/src/lib.rs
+++ b/crates/zng-var/src/lib.rs
@@ -1069,7 +1069,7 @@ pub trait Var<T: VarValue>: IntoVar<T, Var = Self> + AnyVar + Clone {
     /// Output of [`Var::map_bidi`].
     type MapBidi<O: VarValue>: Var<O>;
 
-    /// Output of [`Var::map_bidi`].
+    /// Output of [`Var::flat_map`].
     type FlatMap<O: VarValue, V: Var<O>>: Var<O>;
 
     /// Output of [`Var::filter_map`].
@@ -1199,6 +1199,7 @@ pub trait Var<T: VarValue>: IntoVar<T, Var = Self> + AnyVar + Clone {
     /// Gets the value as a display [`Txt`].
     ///
     /// [`Txt`]: Txt
+    #[deprecated = "use get_txt"]
     fn get_text(&self) -> Txt
     where
         T: fmt::Display,
@@ -1206,6 +1207,16 @@ pub trait Var<T: VarValue>: IntoVar<T, Var = Self> + AnyVar + Clone {
         self.with(ToTxt::to_txt)
     }
 
+    /// Gets the value as a display [`Txt`].
+    ///
+    /// [`Txt`]: Txt
+    fn get_txt(&self) -> Txt
+    where
+        T: fmt::Display,
+    {
+        self.with(ToTxt::to_txt)
+    }
+
     /// Gets the value as a display [`String`].
     fn get_string(&self) -> String
     where
diff --git a/crates/zng-var/src/response.rs b/crates/zng-var/src/response.rs
index c8af40dd2..8204f3c10 100644
--- a/crates/zng-var/src/response.rs
+++ b/crates/zng-var/src/response.rs
@@ -159,6 +159,18 @@ impl<T: VarValue> ResponseVar<T> {
             waiting_value,
         )
     }
+
+    /// Map to another response variable.
+    pub fn map_response<O, M>(&self, mut map: M) -> ResponseVar<O>
+    where
+        O: VarValue,
+        M: FnMut(&T) -> O + Send + 'static,
+    {
+        self.map(move |r| match r {
+            Response::Waiting => Response::Waiting,
+            Response::Done(t) => Response::Done(map(t)),
+        })
+    }
 }
 
 impl<T: VarValue> ResponderVar<T> {
diff --git a/crates/zng-view-api/src/dialog.rs b/crates/zng-view-api/src/dialog.rs
index a0473715d..24a44f75f 100644
--- a/crates/zng-view-api/src/dialog.rs
+++ b/crates/zng-view-api/src/dialog.rs
@@ -1,6 +1,6 @@
 //! Native dialog types.
 
-use std::path::PathBuf;
+use std::{mem, path::PathBuf};
 
 use zng_txt::Txt;
 
@@ -80,37 +80,31 @@ pub enum MsgDialogResponse {
     Error(Txt),
 }
 
-/// Defines a native file dialog.
-#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
-pub struct FileDialog {
-    /// Dialog window title.
-    pub title: Txt,
-    /// Selected directory when the dialog opens.
-    pub starting_dir: PathBuf,
-    /// Starting file name.
-    pub starting_name: Txt,
-    /// File extension filters.
-    ///
-    /// Syntax:
-    ///
-    /// ```txt
-    /// Display Name|ext1;ext2|All Files|*
-    /// ```
-    ///
-    /// You can use the [`push_filter`] method to create filters. Note that the extensions are
-    /// not glob patterns, they must be an extension (without the dot prefix) or `*` for all files.
-    ///
-    /// [`push_filter`]: Self::push_filter
-    pub filters: Txt,
+/// File dialog filters builder.
+///
+/// # Syntax
+///
+/// ```txt
+/// Display Name|ext1;ext2|All Files|*
+/// ```
+///
+/// You can use the [`push_filter`] method to create filters. Note that the extensions are
+/// not glob patterns, they must be an extension (without the dot prefix) or `*` for all files.
+///
+/// [`push_filter`]: FileDialogFilters::push_filter
+#[derive(Debug, Default, PartialEq, Eq, Hash, Clone, serde::Serialize, serde::Deserialize)]
+#[serde(transparent)]
+pub struct FileDialogFilters(Txt);
+impl FileDialogFilters {
+    /// New default (empty).
+    pub fn new() -> Self {
+        Self::default()
+    }
 
-    /// Defines the file dialog looks and what kind of result is expected.
-    pub kind: FileDialogKind,
-}
-impl FileDialog {
     /// Push a filter entry.
     pub fn push_filter<S: AsRef<str>>(&mut self, display_name: &str, extensions: &[S]) -> &mut Self {
-        if !self.filters.is_empty() && !self.filters.ends_with('|') {
-            self.filters.push('|');
+        if !self.0.is_empty() && !self.0.ends_with('|') {
+            self.0.push('|');
         }
 
         let mut extensions: Vec<_> = extensions
@@ -125,27 +119,27 @@ impl FileDialog {
         let display_name = display_name.replace('|', " ");
         let display_name = display_name.trim();
         if !display_name.is_empty() {
-            self.filters.push_str(display_name);
-            self.filters.push_str(" (");
+            self.0.push_str(display_name);
+            self.0.push_str(" (");
         }
         let mut prefix = "";
         for pat in &extensions {
-            self.filters.push_str(prefix);
+            self.0.push_str(prefix);
             prefix = ", ";
-            self.filters.push_str("*.");
-            self.filters.push_str(pat);
+            self.0.push_str("*.");
+            self.0.push_str(pat);
         }
         if !display_name.is_empty() {
-            self.filters.push(')');
+            self.0.push(')');
         }
 
-        self.filters.push('|');
+        self.0.push('|');
 
         prefix = "";
         for pat in extensions {
-            self.filters.push_str(prefix);
+            self.0.push_str(prefix);
             prefix = ";";
-            self.filters.push_str(pat);
+            self.0.push_str(pat);
         }
 
         self
@@ -153,6 +147,9 @@ impl FileDialog {
 
     /// Iterate over filter entries and patterns.
     pub fn iter_filters(&self) -> impl Iterator<Item = (&str, impl Iterator<Item = &str>)> {
+        Self::iter_filters_str(self.0.as_str())
+    }
+    fn iter_filters_str(filters: &str) -> impl Iterator<Item = (&str, impl Iterator<Item = &str>)> {
         struct Iter<'a> {
             filters: &'a str,
         }
@@ -208,9 +205,66 @@ impl FileDialog {
             }
         }
         Iter {
-            filters: self.filters.trim_start().trim_start_matches('|'),
+            filters: filters.trim_start().trim_start_matches('|'),
         }
     }
+
+    /// Gets the filter text.
+    pub fn build(mut self) -> Txt {
+        self.0.end_mut();
+        self.0
+    }
+}
+#[cfg(feature = "var")]
+zng_var::impl_from_and_into_var! {
+    fn from(filter: Txt) -> FileDialogFilters {
+        FileDialogFilters(filter)
+    }
+
+    fn from(filter: &'static str) -> FileDialogFilters {
+        FileDialogFilters(filter.into())
+    }
+}
+
+/// Defines a native file dialog.
+#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
+pub struct FileDialog {
+    /// Dialog window title.
+    pub title: Txt,
+    /// Selected directory when the dialog opens.
+    pub starting_dir: PathBuf,
+    /// Starting file name.
+    pub starting_name: Txt,
+    /// File extension filters.
+    ///
+    /// Syntax:
+    ///
+    /// ```txt
+    /// Display Name|ext1;ext2|All Files|*
+    /// ```
+    ///
+    /// You can use the [`push_filter`] method to create filters. Note that the extensions are
+    /// not glob patterns, they must be an extension (without the dot prefix) or `*` for all files.
+    ///
+    /// [`push_filter`]: Self::push_filter
+    pub filters: Txt,
+
+    /// Defines the file dialog looks and what kind of result is expected.
+    pub kind: FileDialogKind,
+}
+impl FileDialog {
+    /// Push a filter entry.
+    pub fn push_filter<S: AsRef<str>>(&mut self, display_name: &str, extensions: &[S]) -> &mut Self {
+        let mut f = FileDialogFilters(mem::take(&mut self.filters));
+        f.push_filter(display_name, extensions);
+        self.filters = f.build();
+        self
+    }
+
+    /// Iterate over filter entries and patterns.
+    pub fn iter_filters(&self) -> impl Iterator<Item = (&str, impl Iterator<Item = &str>)> {
+        FileDialogFilters::iter_filters_str(&self.filters)
+    }
 }
 impl Default for FileDialog {
     fn default() -> Self {
@@ -254,6 +308,21 @@ pub enum FileDialogResponse {
     /// are not available for the given window ID at the current view-process instance.
     Error(Txt),
 }
+impl FileDialogResponse {
+    /// Gets the selected paths, or empty for cancel.
+    pub fn into_paths(self) -> Result<Vec<PathBuf>, Txt> {
+        match self {
+            FileDialogResponse::Selected(s) => Ok(s),
+            FileDialogResponse::Cancel => Ok(vec![]),
+            FileDialogResponse::Error(e) => Err(e),
+        }
+    }
+
+    /// Gets the last selected path, or `None` for cancel.
+    pub fn into_path(self) -> Result<Option<PathBuf>, Txt> {
+        self.into_paths().map(|mut p| p.pop())
+    }
+}
 
 #[cfg(test)]
 mod tests {
diff --git a/crates/zng-view-prebuilt/build.rs b/crates/zng-view-prebuilt/build.rs
index a1e6d2036..de7bb9ba4 100644
--- a/crates/zng-view-prebuilt/build.rs
+++ b/crates/zng-view-prebuilt/build.rs
@@ -1,5 +1,5 @@
 use std::{
-    env, fs,
+    env,
     hash::Hasher,
     path::{Path, PathBuf},
 };
@@ -24,9 +24,6 @@ fn main() {
     //
     // if version updates
     println!("cargo:rerun-if-changed={}", manifest_dir.join("Cargo.toml").display());
-    // in case of intermittent download error
-    let rerun_request = Path::new(&env::var("OUT_DIR").unwrap()).join("rerun");
-    println!("cargo:rerun-if-changed={}", rerun_request.display());
 
     #[allow(unused_variables)]
     let file = "";
@@ -42,8 +39,6 @@ fn main() {
 
     let is_docs_rs = env::var("DOCS_RS").is_ok();
 
-    let mut rerun = false;
-
     if !lib.exists() && !is_docs_rs {
         let version = env::var("CARGO_PKG_VERSION").unwrap();
         let out_dir = Path::new(&env::var("OUT_DIR").unwrap()).join(format!("v{version}"));
@@ -77,19 +72,16 @@ fn main() {
                                 if s.success() {
                                     if !lib.exists() {
                                         println!("cargo:warning=view prebuilt not embedded, unexpected missing {}", lib.display());
-                                        rerun = true;
                                     }
                                 } else {
                                     println!(
                                         "cargo:warning=view prebuilt not embedded, failed extract, tar exit code: {:?}",
                                         s.code()
                                     );
-                                    rerun = true;
                                 }
                             }
                             Err(e) => {
                                 println!("cargo:warning=view prebuilt not embedded, failed extract, {e}");
-                                rerun = true;
                             }
                         }
                     } else {
@@ -97,12 +89,10 @@ fn main() {
                             "cargo:warning=view prebuilt not embedded, failed download, curl exit code: {:?}",
                             s.code()
                         );
-                        rerun = true;
                     }
                 }
                 Err(e) => {
                     println!("cargo:warning=view prebuilt not embedded, failed download, {e}");
-                    rerun = true;
                 }
             }
         }
@@ -128,9 +118,6 @@ fn main() {
         );
     } else if is_docs_rs || PathBuf::from("../../tools/cargo-do").exists() {
         println!("cargo:warning=view prebuilt not embedded, missing '{file}', call `do prebuild`");
-        if rerun {
-            fs::write(rerun_request, "rerun").unwrap();
-        }
     } else {
         // exit with error also flags rerun
         panic!("view prebuilt not embedded, missing '{file}'");
diff --git a/crates/zng-wgt-dialog/Cargo.toml b/crates/zng-wgt-dialog/Cargo.toml
new file mode 100644
index 000000000..2eecb4729
--- /dev/null
+++ b/crates/zng-wgt-dialog/Cargo.toml
@@ -0,0 +1,38 @@
+[package]
+name = "zng-wgt-dialog"
+version = "0.1.0"
+authors = ["The Zng Project Developers"]
+edition = "2021"
+license = "Apache-2.0 OR MIT"
+readme = "README.md"
+description = "Part of the zng project."
+documentation = "https://zng-ui.github.io/doc/zng_wgt_dialog"
+repository = "https://github.com/zng-ui/zng"
+categories = ["gui"]
+keywords = ["gui", "ui", "user-interface", "zng"]
+
+[dependencies]
+zng-wgt = { path = "../zng-wgt", version = "0.4.7" }
+zng-wgt-container = { path = "../zng-wgt-container", version = "0.3.4" }
+zng-wgt-style = { path = "../zng-wgt-style", version = "0.3.14" }
+zng-wgt-input = { path = "../zng-wgt-input", version = "0.2.24" }
+zng-wgt-fill = { path = "../zng-wgt-fill", version = "0.2.24" }
+zng-wgt-filter = { path = "../zng-wgt-filter", version = "0.2.24" }
+zng-ext-window = { path = "../zng-ext-window", version = "0.3.14" }
+zng-app = { path = "../zng-app", version = "0.9.4" }
+zng-var = { path = "../zng-var", version = "0.4.8" }
+zng-ext-l10n = { path = "../zng-ext-l10n", version = "0.5.4" }
+zng-wgt-text = { path = "../zng-wgt-text", version = "0.4.4" }
+zng-wgt-text-input = { path = "../zng-wgt-text-input", version = "0.4.4" }
+zng-wgt-wrap = { path = "../zng-wgt-wrap", version = "0.3.14" }
+zng-wgt-layer = { path = "../zng-wgt-layer", version = "0.3.14" }
+zng-wgt-button = { path = "../zng-wgt-button", version = "0.4.4" }
+zng-ext-font = { path = "../zng-ext-font", version = "0.3.20" }
+zng-wgt-size-offset = { path = "../zng-wgt-size-offset", version = "0.2.24" }
+zng-wgt-transform = { path = "../zng-wgt-transform", version = "0.2.24" }
+zng-view-api = { path = "../zng-view-api", version = "0.6.3" }
+
+tracing = "0.1"
+parking_lot = "0.12"
+bitflags = { version = "2.5", features = ["serde"] }
+serde = { version = "1.0", features = ["derive"] }
diff --git a/crates/zng-wgt-dialog/README.md b/crates/zng-wgt-dialog/README.md
new file mode 100644
index 000000000..bf784cf50
--- /dev/null
+++ b/crates/zng-wgt-dialog/README.md
@@ -0,0 +1,7 @@
+<!--do doc --readme header-->
+This crate is part of the [`zng`](https://github.com/zng-ui/zng?tab=readme-ov-file#crates) project.
+
+
+<!--do doc --readme features-->
+
+
diff --git a/crates/zng-wgt-dialog/l10n/en-US/_.ftl b/crates/zng-wgt-dialog/l10n/en-US/_.ftl
new file mode 100644
index 000000000..ff4c43168
--- /dev/null
+++ b/crates/zng-wgt-dialog/l10n/en-US/_.ftl
@@ -0,0 +1,9 @@
+response-cancel = Cancel
+
+response-close = Close
+
+response-no = No
+
+response-ok = Ok
+
+response-yes = Yes
diff --git a/crates/zng-wgt-dialog/l10n/pt-BR/_.ftl b/crates/zng-wgt-dialog/l10n/pt-BR/_.ftl
new file mode 100644
index 000000000..58dc739c8
--- /dev/null
+++ b/crates/zng-wgt-dialog/l10n/pt-BR/_.ftl
@@ -0,0 +1,9 @@
+response-cancel = Cancelar
+
+response-close = Fechar
+
+response-no = Não
+
+response-ok = Ok
+
+response-yes = Sim
diff --git a/crates/zng-wgt-dialog/l10n/template/_.ftl b/crates/zng-wgt-dialog/l10n/template/_.ftl
new file mode 100644
index 000000000..ff4c43168
--- /dev/null
+++ b/crates/zng-wgt-dialog/l10n/template/_.ftl
@@ -0,0 +1,9 @@
+response-cancel = Cancel
+
+response-close = Close
+
+response-no = No
+
+response-ok = Ok
+
+response-yes = Yes
diff --git a/crates/zng-wgt-dialog/src/backdrop.rs b/crates/zng-wgt-dialog/src/backdrop.rs
new file mode 100644
index 000000000..e6a64b2e1
--- /dev/null
+++ b/crates/zng-wgt-dialog/src/backdrop.rs
@@ -0,0 +1,84 @@
+//! Modal backdrop widget.
+
+use zng_wgt::{prelude::*, *};
+use zng_wgt_container::Container;
+use zng_wgt_fill::background_color;
+use zng_wgt_input::gesture::{on_click, ClickArgs};
+use zng_wgt_layer::popup::POPUP_CLOSE_REQUESTED_EVENT;
+use zng_wgt_style::{impl_style_fn, style_fn, Style, StyleMix};
+
+use crate::DIALOG;
+
+/// Modal dialog parent widget that fills the window.
+///
+/// This widget is instantiated by [`DIALOG`] automatically, you can only customize it by setting the [`style_fn`](fn@style_fn)
+/// contextual property. Note that the [`popup::close_delay`] and [`popup::is_close_delaying`] properties work with this widget.
+///
+/// [`popup::close_delay`]: fn@zng_wgt_layer::popup::close_delay
+/// [`popup::is_close_delaying`]: fn@zng_wgt_layer::popup::is_close_delaying
+#[widget($crate::backdrop::DialogBackdrop {
+    ($child:expr) => {
+        child = $child;
+    }
+})]
+pub struct DialogBackdrop(StyleMix<Container>);
+impl DialogBackdrop {
+    fn widget_intrinsic(&mut self) {
+        self.style_intrinsic(STYLE_FN_VAR, property_id!(self::style_fn));
+
+        self.widget_builder()
+            .push_build_action(|b| b.push_intrinsic(NestGroup::EVENT, "popup-pump", backdrop_node));
+
+        widget_set! {
+            self;
+            style_base_fn = style_fn!(|_| DefaultStyle!());
+            modal = true;
+
+            on_click = hn!(|args: &ClickArgs| {
+                args.propagation().stop();
+                DIALOG.respond_default();
+            });
+        }
+    }
+}
+impl_style_fn!(DialogBackdrop);
+
+/// Share popup events with the dialog child.
+fn backdrop_node(child: impl UiNode) -> impl UiNode {
+    match_node(child, |c, op| match op {
+        UiNodeOp::Init => {
+            WIDGET.sub_event(&POPUP_CLOSE_REQUESTED_EVENT);
+        }
+        UiNodeOp::Event { update } => {
+            if let Some(args) = POPUP_CLOSE_REQUESTED_EVENT.on(update) {
+                for child in WIDGET.info().descendants() {
+                    if POPUP_CLOSE_REQUESTED_EVENT.is_subscriber(child.id()) {
+                        let mut delivery = UpdateDeliveryList::new_any();
+                        delivery.insert_wgt(&child);
+                        let update = POPUP_CLOSE_REQUESTED_EVENT.new_update_custom(args.clone(), delivery);
+                        c.event(&update);
+                    }
+                }
+            }
+        }
+        _ => {}
+    })
+}
+
+/// Dialog backdrop default style.
+#[widget($crate::backdrop::DefaultStyle)]
+pub struct DefaultStyle(Style);
+impl DefaultStyle {
+    fn widget_intrinsic(&mut self) {
+        widget_set! {
+            self;
+
+            #[easing(250.ms())]
+            background_color = colors::BLACK.transparent();
+            zng_wgt_layer::popup::close_delay = 250.ms();
+            when *#is_inited && !*#zng_wgt_layer::popup::is_close_delaying {
+                background_color = colors::BLACK.with_alpha(20.pct());
+            }
+        }
+    }
+}
diff --git a/crates/zng-wgt-dialog/src/lib.rs b/crates/zng-wgt-dialog/src/lib.rs
new file mode 100644
index 000000000..0e22b77ba
--- /dev/null
+++ b/crates/zng-wgt-dialog/src/lib.rs
@@ -0,0 +1,922 @@
+#![doc(html_favicon_url = "https://raw.githubusercontent.com/zng-ui/zng/main/examples/image/res/zng-logo-icon.png")]
+#![doc(html_logo_url = "https://raw.githubusercontent.com/zng-ui/zng/main/examples/image/res/zng-logo.png")]
+//!
+//! Dialog widget and service.
+//!
+//! # Crate
+//!
+#![doc = include_str!(concat!("../", std::env!("CARGO_PKG_README")))]
+#![warn(unused_extern_crates)]
+#![warn(missing_docs)]
+
+zng_wgt::enable_widget_macros!();
+
+use std::{fmt, ops, path::PathBuf, sync::Arc};
+
+use bitflags::bitflags;
+use parking_lot::Mutex;
+use zng_ext_l10n::l10n;
+use zng_ext_window::{WindowCloseRequestedArgs, WINDOWS, WINDOW_CLOSE_REQUESTED_EVENT};
+use zng_var::{animation::easing, ContextInitHandle};
+use zng_view_api::dialog as native_api;
+use zng_wgt::{prelude::*, *};
+use zng_wgt_container::Container;
+use zng_wgt_fill::background_color;
+use zng_wgt_filter::drop_shadow;
+use zng_wgt_input::focus::FocusableMix;
+use zng_wgt_layer::{
+    popup::{ContextCapture, POPUP, POPUP_CLOSE_REQUESTED_EVENT},
+    AnchorMode,
+};
+use zng_wgt_style::{impl_style_fn, style_fn, Style, StyleMix};
+use zng_wgt_text::Text;
+use zng_wgt_text_input::selectable::SelectableText;
+use zng_wgt_wrap::Wrap;
+
+pub mod backdrop;
+
+pub use zng_view_api::dialog::{FileDialogFilters, FileDialogResponse};
+
+/// A modal dialog overlay container.
+#[widget($crate::Dialog)]
+pub struct Dialog(FocusableMix<StyleMix<Container>>);
+impl Dialog {
+    fn widget_intrinsic(&mut self) {
+        self.style_intrinsic(STYLE_FN_VAR, property_id!(self::style_fn));
+
+        self.widget_builder()
+            .push_build_action(|b| b.push_intrinsic(NestGroup::EVENT, "dialog-closing", dialog_closing_node));
+
+        widget_set! {
+            self;
+            style_base_fn = style_fn!(|_| DefaultStyle!());
+
+            focus_on_init = true;
+            return_focus_on_deinit = true;
+
+            when *#is_close_delaying {
+                interactive = false;
+            }
+        }
+    }
+
+    widget_impl! {
+        /// If a respond close was requested for this dialog and it is just awaiting for the [`popup::close_delay`].
+        ///
+        /// The close delay is usually set on the backdrop widget style.
+        ///
+        /// [`popup::close_delay`]: fn@zng_wgt_layer::popup::close_delay
+        pub zng_wgt_layer::popup::is_close_delaying(state: impl IntoVar<bool>);
+
+        /// An attempt to close the dialog was made without setting the response.
+        ///
+        /// Dialogs must only close using [`DIALOG.respond`](DIALOG::respond).
+        pub on_dialog_close_canceled(args: impl WidgetHandler<DialogCloseCanceledArgs>);
+    }
+}
+impl_style_fn!(Dialog);
+
+fn dialog_closing_node(child: impl UiNode) -> impl UiNode {
+    match_node(child, move |_, op| {
+        match op {
+            UiNodeOp::Init => {
+                // layers receive events after window content, so we subscribe directly
+                let id = WIDGET.id();
+                let ctx = DIALOG_CTX.get();
+                let default_response = DEFAULT_RESPONSE_VAR.actual_var();
+                let responder = ctx.responder.clone();
+                let handle = WINDOW_CLOSE_REQUESTED_EVENT.on_pre_event(app_hn!(|args: &WindowCloseRequestedArgs, _| {
+                    // a window is closing
+                    if responder.get().is_waiting() {
+                        // dialog has no response
+
+                        let path = WINDOWS.widget_info(id).unwrap().path();
+                        if args.windows.contains(&path.window_id()) {
+                            // is closing dialog parent window
+
+                            if let Some(default) = default_response.get() {
+                                // has default response
+                                responder.respond(default);
+                                // in case the window close is canceled by other component
+                                zng_wgt_layer::popup::POPUP_CLOSE_CMD
+                                    .scoped(path.window_id())
+                                    .notify_param(path.widget_id());
+                            } else {
+                                // no default response, cancel close
+                                args.propagation().stop();
+                                DIALOG_CLOSE_CANCELED_EVENT.notify(DialogCloseCanceledArgs::now(path));
+                            }
+                        }
+                    }
+                }));
+                WIDGET.push_event_handle(handle);
+                WIDGET.sub_event(&POPUP_CLOSE_REQUESTED_EVENT);
+            }
+            UiNodeOp::Event { update } => {
+                if let Some(args) = POPUP_CLOSE_REQUESTED_EVENT.on(update) {
+                    // dialog is closing
+                    let ctx = DIALOG_CTX.get();
+                    if ctx.responder.get().is_waiting() {
+                        // dialog has no response
+                        if let Some(r) = DEFAULT_RESPONSE_VAR.get() {
+                            ctx.responder.respond(r);
+                        } else {
+                            args.propagation().stop();
+                            DIALOG_CLOSE_CANCELED_EVENT.notify(DialogCloseCanceledArgs::now(WIDGET.info().path()));
+                        }
+                    }
+                }
+            }
+            _ => (),
+        }
+    })
+}
+
+event_args! {
+    /// Arguments for [`DIALOG_CLOSE_CANCELED_EVENT`].
+    pub struct DialogCloseCanceledArgs {
+        /// Dialog widget.
+        pub target: WidgetPath,
+
+        ..
+
+        fn delivery_list(&self, list: &mut UpdateDeliveryList) {
+            list.insert_wgt(&self.target);
+        }
+    }
+}
+event! {
+    /// An attempt to close the dialog was made without setting the response.
+    ///
+    /// Dialogs must only close using [`DIALOG.respond`](DIALOG::respond).
+    pub static DIALOG_CLOSE_CANCELED_EVENT: DialogCloseCanceledArgs;
+}
+event_property! {
+    // An attempt to close the dialog was made without setting the response.
+    ///
+    /// Dialogs must only close using [`DIALOG.respond`](DIALOG::respond).
+    pub fn dialog_close_canceled {
+        event: DIALOG_CLOSE_CANCELED_EVENT,
+        args: DialogCloseCanceledArgs,
+    }
+}
+
+/// Dialog default style.
+#[widget($crate::DefaultStyle)]
+pub struct DefaultStyle(Style);
+impl DefaultStyle {
+    fn widget_intrinsic(&mut self) {
+        let highlight_color = var(colors::BLACK.transparent());
+        widget_set! {
+            self;
+
+            replace = true;
+
+            background_color = light_dark(rgb(0.7, 0.7, 0.7), rgb(0.3, 0.3, 0.3));
+            drop_shadow = {
+                offset: 4,
+                blur_radius: 6,
+                color: colors::BLACK.with_alpha(50.pct()),
+            };
+
+            corner_radius = 8;
+            clip_to_bounds = true;
+
+            margin = 10;
+            zng_wgt_container::padding = 15;
+
+            align = Align::CENTER;
+
+            zng_wgt_container::child_out_top = Container! {
+                corner_radius = 0;
+                background_color = light_dark(rgb(0.85, 0.85, 0.85), rgb(0.15, 0.15, 0.15));
+                child = presenter((), TITLE_VAR);
+                child_align = Align::START;
+                padding = (4, 8);
+                zng_wgt_text::font_weight = zng_ext_font::FontWeight::BOLD;
+            }, 0;
+
+            zng_wgt_container::child_out_bottom = presenter(RESPONSES_VAR, wgt_fn!(|responses: Responses| {
+                Wrap! {
+                    corner_radius = 0;
+                    background_color = light_dark(rgb(0.85, 0.85, 0.85), rgb(0.15, 0.15, 0.15));
+                    children_align = Align::END;
+                    zng_wgt_container::padding = 3;
+                    spacing = 3;
+                    children = {
+                        let last = responses.len().saturating_sub(1);
+                        responses.0
+                            .into_iter()
+                            .enumerate()
+                            .map(|(i, r)| presenter(
+                                DialogButtonArgs { response: r, is_last: i == last },
+                                BUTTON_FN_VAR
+                            ).boxed())
+                            .collect::<UiNodeVec>()
+                    };
+                }
+            })), 0;
+
+            zng_wgt_container::child_out_left = Container! {
+                child = presenter((), ICON_VAR);
+                child_align = Align::TOP;
+            }, 0;
+
+            zng_wgt_container::child = presenter((), CONTENT_VAR);
+
+            #[easing(250.ms())]
+            zng_wgt_filter::opacity = 30.pct();
+            #[easing(250.ms())]
+            zng_wgt_transform::transform = Transform::new_translate_y(-10).scale(98.pct());
+            when *#is_inited && !*#zng_wgt_layer::popup::is_close_delaying {
+                zng_wgt_filter::opacity = 100.pct();
+                zng_wgt_transform::transform = Transform::identity();
+            }
+
+            zng_wgt_fill::foreground_highlight = {
+                offsets: 0,
+                widths: 2,
+                sides: highlight_color.map_into(),
+            };
+            on_dialog_close_canceled = hn!(highlight_color, |_| {
+                let c = colors::ACCENT_COLOR_VAR.rgba().get();
+                let mut repeats = 0;
+                highlight_color.sequence(move |cv| {
+                    repeats += 1;
+                    if repeats <= 2 {
+                        cv.set_ease(c, c.with_alpha(0.pct()), 120.ms(), easing::linear)
+                    } else {
+                        zng_var::animation::AnimationHandle::dummy()
+                    }
+                }).perm();
+            });
+        }
+    }
+}
+
+context_var! {
+    /// Title widget, usually placed as `child_out_top`.
+    pub static TITLE_VAR: WidgetFn<()> = WidgetFn::nil();
+    /// Icon widget, usually placed as `child_out_start`.
+    pub static ICON_VAR: WidgetFn<()> = WidgetFn::nil();
+    /// Content widget, usually the dialog child.
+    pub static CONTENT_VAR: WidgetFn<()> = WidgetFn::nil();
+    /// Dialog response button generator, usually placed as `child_out_bottom`.
+    pub static BUTTON_FN_VAR: WidgetFn<DialogButtonArgs> = WidgetFn::new(default_button_fn);
+    /// Dialog responses.
+    pub static RESPONSES_VAR: Responses = Responses::ok();
+    /// Dialog response when closed without setting a response.
+    pub static DEFAULT_RESPONSE_VAR: Option<Response> = None;
+    /// Defines what native dialogs are used on a context.
+    pub static NATIVE_DIALOGS_VAR: DialogKind = DIALOG.native_dialogs();
+}
+
+/// Default value of [`button_fn`](fn@button_fn)
+pub fn default_button_fn(args: DialogButtonArgs) -> impl UiNode {
+    zng_wgt_button::Button! {
+        child = Text!(args.response.label.clone());
+        on_click = hn_once!(|a: &zng_wgt_input::gesture::ClickArgs| {
+            a.propagation().stop();
+            DIALOG.respond(args.response);
+        });
+        focus_on_init = args.is_last;
+        when args.is_last {
+            style_fn = zng_wgt_button::PrimaryStyle!();
+        }
+    }
+}
+
+/// Arguments for [`button_fn`].
+///
+/// [`button_fn`]: fn@button_fn
+#[derive(Debug, Clone, PartialEq)]
+pub struct DialogButtonArgs {
+    /// The response that must be represented by the button.
+    pub response: Response,
+    /// If the button is the last entry on the responses list.
+    pub is_last: bool,
+}
+
+/// Dialog title widget.
+///
+/// Note that this takes in an widget, you can use `Text!("title")` to set to a text.
+#[property(CONTEXT, default(NilUiNode), widget_impl(Dialog))]
+pub fn title(child: impl UiNode, title: impl UiNode) -> impl UiNode {
+    with_context_var(child, TITLE_VAR, WidgetFn::singleton(title))
+}
+
+/// Dialog icon widget.
+///
+/// Note that this takes in an widget, you can use the `ICONS` service to get an icon widget.
+#[property(CONTEXT, default(NilUiNode), widget_impl(Dialog))]
+pub fn icon(child: impl UiNode, icon: impl UiNode) -> impl UiNode {
+    with_context_var(child, ICON_VAR, WidgetFn::singleton(icon))
+}
+
+/// Dialog content widget.
+///
+/// Note that this takes in an widget, you can use `SelectableText!("message")` for the message.
+#[property(CONTEXT, default(FillUiNode), widget_impl(Dialog))]
+pub fn content(child: impl UiNode, content: impl UiNode) -> impl UiNode {
+    with_context_var(child, CONTENT_VAR, WidgetFn::singleton(content))
+}
+
+/// Dialog button generator.
+#[property(CONTEXT, default(BUTTON_FN_VAR), widget_impl(Dialog))]
+pub fn button_fn(child: impl UiNode, button: impl IntoVar<WidgetFn<DialogButtonArgs>>) -> impl UiNode {
+    with_context_var(child, BUTTON_FN_VAR, button)
+}
+
+/// Dialog responses.
+#[property(CONTEXT, default(RESPONSES_VAR), widget_impl(Dialog))]
+pub fn responses(child: impl UiNode, responses: impl IntoVar<Responses>) -> impl UiNode {
+    with_context_var(child, RESPONSES_VAR, responses)
+}
+
+/// Dialog response when closed without setting a response.
+#[property(CONTEXT, default(DEFAULT_RESPONSE_VAR), widget_impl(Dialog))]
+pub fn default_response(child: impl UiNode, response: impl IntoVar<Option<Response>>) -> impl UiNode {
+    with_context_var(child, DEFAULT_RESPONSE_VAR, response)
+}
+
+/// Defines what native dialogs are used by dialogs opened on the context.
+///
+/// Sets [`NATIVE_DIALOGS_VAR`].
+#[property(CONTEXT, default(NATIVE_DIALOGS_VAR))]
+pub fn native_dialogs(child: impl UiNode, dialogs: impl IntoVar<DialogKind>) -> impl UiNode {
+    with_context_var(child, NATIVE_DIALOGS_VAR, dialogs)
+}
+
+/// Dialog info style.
+///
+/// Sets the info icon and a single "Ok" response.
+#[widget($crate::InfoStyle)]
+pub struct InfoStyle(DefaultStyle);
+impl InfoStyle {
+    fn widget_intrinsic(&mut self) {
+        widget_set! {
+            self;
+            icon = Container! {
+                child = ICONS.req(["dialog-info", "info"]);
+                zng_wgt_size_offset::size = 48;
+                zng_wgt_text::font_color = colors::AZURE;
+                padding = 5;
+            };
+            default_response = Response::ok();
+        }
+    }
+}
+
+/// Dialog warn style.
+///
+/// Sets the warn icon and a single "Ok" response.
+#[widget($crate::WarnStyle)]
+pub struct WarnStyle(DefaultStyle);
+impl WarnStyle {
+    fn widget_intrinsic(&mut self) {
+        widget_set! {
+            self;
+            icon = Container! {
+                child = ICONS.req(["dialog-warn", "warning"]);
+                zng_wgt_size_offset::size = 48;
+                zng_wgt_text::font_color = colors::ORANGE;
+                padding = 5;
+            };
+
+        }
+    }
+}
+
+/// Dialog error style.
+///
+/// Sets the error icon and a single "Ok" response.
+#[widget($crate::ErrorStyle)]
+pub struct ErrorStyle(DefaultStyle);
+impl ErrorStyle {
+    fn widget_intrinsic(&mut self) {
+        widget_set! {
+            self;
+            icon = Container! {
+                child = ICONS.req(["dialog-error", "error"]);
+                zng_wgt_size_offset::size = 48;
+                zng_wgt_text::font_color = rgb(209, 29, 29);
+                padding = 5;
+            };
+
+        }
+    }
+}
+
+/// Question style.
+///
+/// Sets the question icon and two "No" and "Yes" responses.
+#[widget($crate::AskStyle)]
+pub struct AskStyle(DefaultStyle);
+impl AskStyle {
+    fn widget_intrinsic(&mut self) {
+        widget_set! {
+            self;
+            icon = Container! {
+                child = ICONS.req(["dialog-question", "question-mark"]);
+                zng_wgt_size_offset::size = 48;
+                zng_wgt_text::font_color = colors::AZURE;
+                padding = 5;
+            };
+            responses = Responses::no_yes();
+        }
+    }
+}
+
+/// Confirmation style.
+///
+/// Sets the question icon and two "Cancel" and "Ok" responses.
+#[widget($crate::ConfirmStyle)]
+pub struct ConfirmStyle(DefaultStyle);
+impl ConfirmStyle {
+    fn widget_intrinsic(&mut self) {
+        widget_set! {
+            self;
+            icon = Container! {
+                child = ICONS.req(["dialog-confirm", "question-mark"]);
+                zng_wgt_size_offset::size = 48;
+                zng_wgt_text::font_color = colors::ORANGE;
+                padding = 5;
+            };
+            responses = Responses::cancel_ok();
+        }
+    }
+}
+
+/// Dialog response.
+#[derive(Clone)]
+pub struct Response {
+    /// Response identifying name.
+    pub name: Txt,
+    /// Response button label.
+    pub label: BoxedVar<Txt>,
+}
+impl fmt::Debug for Response {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{:?}", self.name)
+    }
+}
+impl PartialEq for Response {
+    fn eq(&self, other: &Self) -> bool {
+        self.name == other.name
+    }
+}
+impl Response {
+    /// New from name and label.
+    pub fn new(name: impl Into<Txt>, label: impl IntoVar<Txt>) -> Self {
+        Self {
+            name: name.into(),
+            label: label.into_var().boxed(),
+        }
+    }
+
+    /// "ok"
+    pub fn ok() -> Self {
+        Self::new("Ok", l10n!("response-ok", "Ok"))
+    }
+
+    /// "cancel"
+    pub fn cancel() -> Self {
+        Self::new("cancel", l10n!("response-cancel", "Cancel"))
+    }
+
+    /// "yes"
+    pub fn yes() -> Self {
+        Self::new("yes", l10n!("response-yes", "Yes"))
+    }
+    /// "no"
+    pub fn no() -> Self {
+        Self::new("no", l10n!("response-no", "No"))
+    }
+
+    /// "close"
+    pub fn close() -> Self {
+        Self::new("close", l10n!("response-close", "Close"))
+    }
+}
+impl_from_and_into_var! {
+    fn from(native: native_api::MsgDialogResponse) -> Response {
+        match native {
+            native_api::MsgDialogResponse::Ok => Response::ok(),
+            native_api::MsgDialogResponse::Yes => Response::yes(),
+            native_api::MsgDialogResponse::No => Response::no(),
+            native_api::MsgDialogResponse::Cancel => Response::cancel(),
+            native_api::MsgDialogResponse::Error(e) => Response {
+                name: Txt::from_static("native-error"),
+                label: LocalVar(e).boxed(),
+            },
+        }
+    }
+    fn from(response: Response) -> Option<Response>;
+}
+
+/// Response labels.
+#[derive(Clone, PartialEq, Debug)]
+pub struct Responses(pub Vec<Response>);
+impl Responses {
+    /// new with first response.
+    pub fn new(r: impl Into<Response>) -> Self {
+        Self(vec![r.into()])
+    }
+
+    /// With response.
+    pub fn with(mut self, response: impl Into<Response>) -> Self {
+        self.push(response.into());
+        self
+    }
+
+    /// "Ok"
+    pub fn ok() -> Self {
+        Response::ok().into()
+    }
+
+    /// "Close"
+    pub fn close() -> Self {
+        Response::close().into()
+    }
+
+    /// "No", "Yes"
+    pub fn no_yes() -> Self {
+        vec![Response::no(), Response::yes()].into()
+    }
+
+    /// "Cancel", "Ok"
+    pub fn cancel_ok() -> Self {
+        vec![Response::cancel(), Response::ok()].into()
+    }
+}
+impl ops::Deref for Responses {
+    type Target = Vec<Response>;
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+impl ops::DerefMut for Responses {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.0
+    }
+}
+impl_from_and_into_var! {
+    fn from(response: Response) -> Responses {
+        Responses::new(response)
+    }
+    fn from(responses: Vec<Response>) -> Responses {
+        Responses(responses)
+    }
+}
+
+/// Dialog service.
+///
+/// The non-custom dialog methods can be configured to open as native dialogs instead of the custom overlay dialogs.
+///
+/// # Panics
+///
+/// All dialog methods panic is not called inside a window.
+pub struct DIALOG;
+impl DIALOG {
+    /// Show an info dialog with "Ok" button.
+    pub fn info(&self, title: impl IntoVar<Txt>, msg: impl IntoVar<Txt>) -> ResponseVar<()> {
+        self.message(
+            msg.into_var().boxed(),
+            title.into_var().boxed(),
+            DialogKind::INFO,
+            &|| InfoStyle!(),
+            native_api::MsgDialogIcon::Info,
+            native_api::MsgDialogButtons::Ok,
+        )
+        .map_response(|_| ())
+    }
+
+    /// Show a warning dialog with "Ok" button.
+    pub fn warn(&self, title: impl IntoVar<Txt>, msg: impl IntoVar<Txt>) -> ResponseVar<()> {
+        self.message(
+            msg.into_var().boxed(),
+            title.into_var().boxed(),
+            DialogKind::WARN,
+            &|| WarnStyle!(),
+            native_api::MsgDialogIcon::Warn,
+            native_api::MsgDialogButtons::Ok,
+        )
+        .map_response(|_| ())
+    }
+
+    /// Show an error dialog with "Ok" button.
+    pub fn error(&self, title: impl IntoVar<Txt>, msg: impl IntoVar<Txt>) -> ResponseVar<()> {
+        self.message(
+            msg.into_var().boxed(),
+            title.into_var().boxed(),
+            DialogKind::ERROR,
+            &|| ErrorStyle!(),
+            native_api::MsgDialogIcon::Error,
+            native_api::MsgDialogButtons::Ok,
+        )
+        .map_response(|_| ())
+    }
+
+    /// Shows a question dialog with "No" and "Yes" buttons. Returns `true` for "Yes".
+    pub fn ask(&self, title: impl IntoVar<Txt>, question: impl IntoVar<Txt>) -> ResponseVar<bool> {
+        self.message(
+            question.into_var().boxed(),
+            title.into_var().boxed(),
+            DialogKind::ASK,
+            &|| AskStyle!(),
+            native_api::MsgDialogIcon::Info,
+            native_api::MsgDialogButtons::YesNo,
+        )
+        .map_response(|r| r.name == "yes")
+    }
+
+    /// Shows a question dialog with "Cancel" and "Ok" buttons. Returns `true` for "Ok".
+    pub fn confirm(&self, title: impl IntoVar<Txt>, question: impl IntoVar<Txt>) -> ResponseVar<bool> {
+        self.message(
+            question.into_var().boxed(),
+            title.into_var().boxed(),
+            DialogKind::CONFIRM,
+            &|| ConfirmStyle!(),
+            native_api::MsgDialogIcon::Warn,
+            native_api::MsgDialogButtons::OkCancel,
+        )
+        .map_response(|r| r.name == "ok")
+    }
+
+    /// Shows a native file picker dialog configured to select one existing file.
+    pub fn open_file(
+        &self,
+        title: impl IntoVar<Txt>,
+        starting_dir: impl Into<PathBuf>,
+        starting_name: impl IntoVar<Txt>,
+        filters: impl Into<FileDialogFilters>,
+    ) -> ResponseVar<FileDialogResponse> {
+        WINDOWS.native_file_dialog(
+            WINDOW.id(),
+            native_api::FileDialog {
+                title: title.into_var().get(),
+                starting_dir: starting_dir.into(),
+                starting_name: starting_name.into_var().get(),
+                filters: filters.into().build(),
+                kind: native_api::FileDialogKind::OpenFile,
+            },
+        )
+    }
+
+    /// Shows a native file picker dialog configured to select one or more existing files.
+    pub fn open_files(
+        &self,
+        title: impl IntoVar<Txt>,
+        starting_dir: impl Into<PathBuf>,
+        starting_name: impl IntoVar<Txt>,
+        filters: impl Into<FileDialogFilters>,
+    ) -> ResponseVar<FileDialogResponse> {
+        WINDOWS.native_file_dialog(
+            WINDOW.id(),
+            native_api::FileDialog {
+                title: title.into_var().get(),
+                starting_dir: starting_dir.into(),
+                starting_name: starting_name.into_var().get(),
+                filters: filters.into().build(),
+                kind: native_api::FileDialogKind::OpenFiles,
+            },
+        )
+    }
+
+    /// Shows a native file picker dialog configured to select one file path that does not exist yet.
+    pub fn save_file(
+        &self,
+        title: impl IntoVar<Txt>,
+        starting_dir: impl Into<PathBuf>,
+        starting_name: impl IntoVar<Txt>,
+        filters: impl Into<FileDialogFilters>,
+    ) -> ResponseVar<FileDialogResponse> {
+        WINDOWS.native_file_dialog(
+            WINDOW.id(),
+            native_api::FileDialog {
+                title: title.into_var().get(),
+                starting_dir: starting_dir.into(),
+                starting_name: starting_name.into_var().get(),
+                filters: filters.into().build(),
+                kind: native_api::FileDialogKind::SaveFile,
+            },
+        )
+    }
+
+    /// Shows a native file picker dialog configured to select one existing directory.
+    pub fn select_folder(
+        &self,
+        title: impl IntoVar<Txt>,
+        starting_dir: impl Into<PathBuf>,
+        starting_name: impl IntoVar<Txt>,
+    ) -> ResponseVar<FileDialogResponse> {
+        WINDOWS.native_file_dialog(
+            WINDOW.id(),
+            native_api::FileDialog {
+                title: title.into_var().get(),
+                starting_dir: starting_dir.into(),
+                starting_name: starting_name.into_var().get(),
+                filters: "".into(),
+                kind: native_api::FileDialogKind::SelectFolder,
+            },
+        )
+    }
+
+    /// Shows a native file picker dialog configured to select one or more existing directories.
+    pub fn select_folders(
+        &self,
+        title: impl IntoVar<Txt>,
+        starting_dir: impl Into<PathBuf>,
+        starting_name: impl IntoVar<Txt>,
+    ) -> ResponseVar<FileDialogResponse> {
+        WINDOWS.native_file_dialog(
+            WINDOW.id(),
+            native_api::FileDialog {
+                title: title.into_var().get(),
+                starting_dir: starting_dir.into(),
+                starting_name: starting_name.into_var().get(),
+                filters: "".into(),
+                kind: native_api::FileDialogKind::SelectFolders,
+            },
+        )
+    }
+
+    /// Open the custom `dialog`.
+    ///
+    /// Returns the selected response or [`close`] if the dialog is closed without response.
+    ///
+    /// [`close`]: Response::close
+    pub fn custom(&self, dialog: impl UiNode) -> ResponseVar<Response> {
+        self.show_impl(dialog.boxed())
+    }
+}
+
+impl DIALOG {
+    /// Variable that defines what native dialogs are used when the dialog methods are called in window contexts.
+    ///
+    /// The [`native_dialogs`](fn@native_dialogs) context property can also be used to override the config just for some widgets.
+    ///
+    /// Note that some dialogs only have the native implementation as of this release.
+    pub fn native_dialogs(&self) -> ArcVar<DialogKind> {
+        DIALOG_SV.read().native_dialogs.clone()
+    }
+}
+bitflags! {
+    /// Dialog kind options.
+    #[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
+    pub struct DialogKind: u32 {
+        /// [`DIALOG.info`](DIALOG::info)
+        const INFO =    0b0000_0000_0000_0001;
+        /// [`DIALOG.warn`](DIALOG::warn)
+        const WARN =    0b0000_0000_0000_0010;
+        /// [`DIALOG.error`](DIALOG::error)
+        const ERROR =   0b0000_0000_0000_0100;
+        /// [`DIALOG.ask`](DIALOG::ask)
+        const ASK =     0b0000_0000_0000_1000;
+        /// [`DIALOG.confirm`](DIALOG::confirm)
+        const CONFIRM = 0b0000_0000_0001_0000;
+
+        /// [`DIALOG.open_file`](DIALOG::open_file)
+        const OPEN_FILE =  0b1000_0000_0000_0000;
+        /// [`DIALOG.open_files`](DIALOG::open_files)
+        const OPEN_FILES = 0b0100_0000_0000_0000;
+        /// [`DIALOG.save_file`](DIALOG::save_file)
+        const SAVE_FILE =  0b0010_0000_0000_0000;
+
+        /// [`DIALOG.select_folder`](DIALOG::select_folder)
+        const SELECT_FOLDER =  0b0001_0000_0000_0000;
+        /// [`DIALOG.select_folders`](DIALOG::select_folders)
+        const SELECT_FOLDERS = 0b0000_1000_0000_0000;
+
+        /// All message dialogs.
+        const MESSAGE = Self::INFO.bits() | Self::WARN.bits() | Self::ERROR.bits()  | Self::ASK.bits()  | Self::CONFIRM.bits();
+        /// All file system dialogs.
+        const FILE = Self::OPEN_FILE.bits() | Self::OPEN_FILES.bits() | Self::SAVE_FILE.bits()  | Self::SELECT_FOLDER.bits() | Self::SELECT_FOLDERS.bits();
+    }
+}
+impl_from_and_into_var! {
+    fn from(empty_or_all: bool) -> DialogKind {
+        if empty_or_all {
+            DialogKind::all()
+        } else {
+            DialogKind::empty()
+        }
+    }
+}
+
+impl DIALOG {
+    /// Close the contextual dialog with the `response``.
+    pub fn respond(&self, response: Response) {
+        let ctx = DIALOG_CTX.get();
+        let id = *ctx.dialog_id.lock();
+        if let Some(id) = id {
+            ctx.responder.respond(response);
+            POPUP.close_id(id);
+        } else {
+            tracing::error!("DIALOG.respond called outside of a dialog");
+        }
+    }
+
+    /// Try to close the contextual dialog without directly setting a response.
+    ///
+    /// If the dialog has no [`default_response`](fn@default_response) the
+    /// [`on_dialog_close_canceled`](fn@on_dialog_close_canceled) event notifies instead of closing.
+    pub fn respond_default(&self) {
+        let ctx = DIALOG_CTX.get();
+        let id = *ctx.dialog_id.lock();
+        if let Some(id) = id {
+            POPUP.close_id(id);
+        } else {
+            tracing::error!("DIALOG.respond called outside of a dialog");
+        }
+    }
+
+    fn message(
+        &self,
+        msg: BoxedVar<Txt>,
+        title: BoxedVar<Txt>,
+        kind: DialogKind,
+        style: &dyn Fn() -> zng_wgt_style::StyleBuilder,
+        native_icon: native_api::MsgDialogIcon,
+        native_buttons: native_api::MsgDialogButtons,
+    ) -> ResponseVar<Response> {
+        if NATIVE_DIALOGS_VAR.get().contains(kind) {
+            WINDOWS
+                .native_message_dialog(
+                    WINDOW.id(),
+                    native_api::MsgDialog {
+                        title: title.get(),
+                        message: msg.get(),
+                        icon: native_icon,
+                        buttons: native_buttons,
+                    },
+                )
+                .map_response(|r| r.clone().into())
+        } else {
+            self.custom(Dialog! {
+                style_fn = style();
+                title = Text! {
+                    visibility = title.map(|t| Visibility::from(!t.is_empty()));
+                    txt = title;
+                };
+                content = SelectableText!(msg);
+            })
+        }
+    }
+
+    fn show_impl(&self, dialog: BoxedUiNode) -> ResponseVar<Response> {
+        let (responder, response) = response_var();
+
+        let mut ctx = Some(Arc::new(DialogCtx {
+            dialog_id: Mutex::new(None),
+            responder,
+        }));
+
+        let dialog = backdrop::DialogBackdrop!(dialog);
+
+        let dialog = match_widget(
+            dialog,
+            clmv!(|c, op| {
+                match &op {
+                    UiNodeOp::Init => {
+                        *ctx.as_ref().unwrap().dialog_id.lock() = c.with_context(WidgetUpdateMode::Ignore, || WIDGET.id());
+                        DIALOG_CTX.with_context(&mut ctx, || c.op(op));
+                        // in case a non-standard dialog widget is used
+                        *ctx.as_ref().unwrap().dialog_id.lock() = c.with_context(WidgetUpdateMode::Ignore, || WIDGET.id());
+                    }
+                    UiNodeOp::Deinit => {}
+                    _ => {
+                        DIALOG_CTX.with_context(&mut ctx, || c.op(op));
+                    }
+                }
+            }),
+        );
+
+        zng_wgt_layer::popup::CLOSE_ON_FOCUS_LEAVE_VAR.with_context_var(ContextInitHandle::new(), false, || {
+            POPUP.open_config(dialog, AnchorMode::window(), ContextCapture::NoCapture)
+        });
+
+        response
+    }
+}
+
+struct DialogCtx {
+    dialog_id: Mutex<Option<WidgetId>>,
+    responder: ResponderVar<Response>,
+}
+context_local! {
+    static DIALOG_CTX: DialogCtx = DialogCtx {
+        dialog_id: Mutex::new(None),
+        responder: response_var().0,
+    };
+}
+
+struct DialogService {
+    native_dialogs: ArcVar<DialogKind>,
+}
+app_local! {
+    static DIALOG_SV: DialogService = DialogService {
+        native_dialogs: var(DialogKind::FILE),
+    };
+}
diff --git a/crates/zng-wgt-input/src/focus.rs b/crates/zng-wgt-input/src/focus.rs
index 85fe8f823..4887554ce 100644
--- a/crates/zng-wgt-input/src/focus.rs
+++ b/crates/zng-wgt-input/src/focus.rs
@@ -494,7 +494,10 @@ pub fn focus_on_init(child: impl UiNode, enabled: impl IntoVar<bool>) -> impl Ui
 /// This can be used with the [`modal`] property to declare *modal dialogs* that return the focus
 /// to the widget that opens the dialog.
 ///
+/// Consider using [`focus_click_behavior`] if the widget is also an ALT focus scope.
+///
 /// [`modal`]: fn@zng_wgt::modal
+/// [`focus_click_behavior`]: fn@focus_click_behavior
 #[property(CONTEXT, default(false), widget_impl(FocusableMix<P>))]
 pub fn return_focus_on_deinit(child: impl UiNode, enabled: impl IntoVar<bool>) -> impl UiNode {
     let enabled = enabled.into_var();
diff --git a/crates/zng-wgt-layer/src/lib.rs b/crates/zng-wgt-layer/src/lib.rs
index 79ca19cd5..3db9172f0 100644
--- a/crates/zng-wgt-layer/src/lib.rs
+++ b/crates/zng-wgt-layer/src/lib.rs
@@ -25,6 +25,7 @@ use std::sync::Arc;
 use std::{fmt, mem, ops};
 
 pub mod popup;
+
 struct LayersCtx {
     items: EditableUiNodeListRef,
 }
@@ -280,9 +281,9 @@ impl LAYERS {
                         let mut constraints = match mode.min_size {
                             AnchorSize::Unbounded => PxConstraints2d::new_unbounded(),
                             AnchorSize::Window => LAYOUT.constraints(),
-                            AnchorSize::InnerSize => PxConstraints2d::new_exact_size(bounds.inner_size()),
-                            AnchorSize::InnerBorder => PxConstraints2d::new_exact_size(border.inner_size(bounds)),
-                            AnchorSize::OuterSize => PxConstraints2d::new_exact_size(bounds.outer_size()),
+                            AnchorSize::InnerSize => PxConstraints2d::new_exact_size(bounds.inner_size()).with_fill(false, false),
+                            AnchorSize::InnerBorder => PxConstraints2d::new_exact_size(border.inner_size(bounds)).with_fill(false, false),
+                            AnchorSize::OuterSize => PxConstraints2d::new_exact_size(bounds.outer_size()).with_fill(false, false),
                         };
                         if mode.max_size != mode.min_size {
                             constraints = match mode.max_size {
@@ -296,7 +297,6 @@ impl LAYERS {
                                 AnchorSize::OuterSize => constraints.with_new_max_size(bounds.outer_size()),
                             };
                         }
-                        constraints = constraints.with_fill(false, false);
 
                         *desired_size = LAYOUT.with_constraints(constraints, || widget.measure(wm));
                     }
@@ -314,9 +314,9 @@ impl LAYERS {
                         let mut constraints = match mode.min_size {
                             AnchorSize::Unbounded => PxConstraints2d::new_unbounded(),
                             AnchorSize::Window => LAYOUT.constraints(),
-                            AnchorSize::InnerSize => PxConstraints2d::new_exact_size(bounds.inner_size()),
-                            AnchorSize::InnerBorder => PxConstraints2d::new_exact_size(border.inner_size(bounds)),
-                            AnchorSize::OuterSize => PxConstraints2d::new_exact_size(bounds.outer_size()),
+                            AnchorSize::InnerSize => PxConstraints2d::new_exact_size(bounds.inner_size()).with_fill(false, false),
+                            AnchorSize::InnerBorder => PxConstraints2d::new_exact_size(border.inner_size(bounds)).with_fill(false, false),
+                            AnchorSize::OuterSize => PxConstraints2d::new_exact_size(bounds.outer_size()).with_fill(false, false),
                         };
                         if mode.max_size != mode.min_size {
                             constraints = match mode.max_size {
@@ -330,7 +330,6 @@ impl LAYERS {
                                 AnchorSize::OuterSize => constraints.with_new_max_size(bounds.outer_size()),
                             };
                         }
-                        constraints = constraints.with_fill(false, false);
 
                         let layer_size = LAYOUT.with_constraints(constraints, || {
                             if mode.corner_radius {
diff --git a/crates/zng-wgt-layer/src/popup.rs b/crates/zng-wgt-layer/src/popup.rs
index 60823c4bc..a79d3df27 100644
--- a/crates/zng-wgt-layer/src/popup.rs
+++ b/crates/zng-wgt-layer/src/popup.rs
@@ -1,7 +1,5 @@
 //! Popup widget.
 
-zng_wgt::enable_widget_macros!();
-
 use std::time::Duration;
 
 use zng_ext_input::focus::{DirectionalNav, TabNav, FOCUS_CHANGED_EVENT};
@@ -230,10 +228,21 @@ impl POPUP {
         )
         .boxed();
 
-        if let ContextCapture::CaptureBlend { filter, over } = context_capture {
-            if filter != CaptureFilter::None {
-                popup = with_context_blend(LocalContext::capture_filtered(filter), over, popup).boxed();
+        let (filter, over) = match context_capture {
+            ContextCapture::NoCapture => {
+                let filter = CaptureFilter::Include({
+                    let mut set = ContextValueSet::new();
+                    set.insert(&CLOSE_ON_FOCUS_LEAVE_VAR);
+                    set.insert(&ANCHOR_MODE_VAR);
+                    set.insert(&CONTEXT_CAPTURE_VAR);
+                    set
+                });
+                (filter, false)
             }
+            ContextCapture::CaptureBlend { filter, over } => (filter, over),
+        };
+        if filter != CaptureFilter::None {
+            popup = with_context_blend(LocalContext::capture_filtered(filter), over, popup).boxed();
         }
         LAYERS.insert_anchored(LayerIndex::TOP_MOST, anchor_id, anchor_mode, popup);
 
@@ -337,8 +346,11 @@ impl DefaultStyle {
 /// [`with_context_blend`]: zng_wgt::prelude::with_context_blend
 #[derive(Clone, PartialEq, Eq, Debug)]
 pub enum ContextCapture {
-    /// No context capture or blending, the popup will have
-    /// the context it is inited in, like any other widget.
+    /// No context capture except the popup configuration context.
+    ///
+    /// The popup will only have the window context as it is open as a layer on the window root.
+    ///
+    /// Note to filter out even the popup config use [`CaptureFilter::None`] instead.
     NoCapture,
     /// Build/instantiation context is captured and blended with the node context during all [`UiNodeOp`].
     ///
@@ -463,7 +475,7 @@ pub fn close_delay(child: impl UiNode, delay: impl IntoVar<Duration>) -> impl Ui
     let delay = delay.into_var();
     let mut timer = None::<DeadlineHandle>;
 
-    match_node(child, move |_, op| match op {
+    let child = match_node(child, move |c, op| match op {
         UiNodeOp::Init => {
             WIDGET.sub_event(&POPUP_CLOSE_REQUESTED_EVENT);
         }
@@ -471,6 +483,7 @@ pub fn close_delay(child: impl UiNode, delay: impl IntoVar<Duration>) -> impl Ui
             timer = None;
         }
         UiNodeOp::Event { update } => {
+            c.event(update);
             if let Some(args) = POPUP_CLOSE_REQUESTED_EVENT.on_unhandled(update) {
                 if args.popup != WIDGET.id() {
                     return;
@@ -503,16 +516,16 @@ pub fn close_delay(child: impl UiNode, delay: impl IntoVar<Duration>) -> impl Ui
             }
         }
         _ => {}
-    })
+    });
+    with_context_var(child, IS_CLOSE_DELAYED_VAR, var(false))
 }
 
 /// If close was requested for this layered widget and it is just awaiting for the [`close_delay`].
 ///
 /// [`close_delay`]: fn@close_delay
-#[property(CONTEXT, widget_impl(Popup))]
+#[property(EVENT+1, widget_impl(Popup))]
 pub fn is_close_delaying(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
-    // reverse context var, is set by `close_delay`.
-    with_context_var(child, IS_CLOSE_DELAYED_VAR, state)
+    bind_state(child, IS_CLOSE_DELAYED_VAR, state)
 }
 
 context_var! {
diff --git a/crates/zng-wgt-markdown/src/resolvers.rs b/crates/zng-wgt-markdown/src/resolvers.rs
index 24b145163..f4eb286e1 100644
--- a/crates/zng-wgt-markdown/src/resolvers.rs
+++ b/crates/zng-wgt-markdown/src/resolvers.rs
@@ -412,9 +412,7 @@ pub fn try_open_link(args: &LinkArgs) -> bool {
     LAYERS.insert_anchored(
         LayerIndex::ADORNER,
         args.link.widget_id(),
-        AnchorMode::window()
-            .with_transform(AnchorOffset::out_bottom())
-            .with_viewport_bound(true),
+        AnchorMode::popup(AnchorOffset::out_bottom()),
         popup,
     );
 
diff --git a/crates/zng-wgt-text-input/src/selectable.rs b/crates/zng-wgt-text-input/src/selectable.rs
index 9edbe783b..0d50a3c89 100644
--- a/crates/zng-wgt-text-input/src/selectable.rs
+++ b/crates/zng-wgt-text-input/src/selectable.rs
@@ -11,6 +11,9 @@ use zng_wgt_menu::{
 use zng_wgt_style::{impl_style_fn, style_fn, Style, StyleMix};
 use zng_wgt_text::{self as text, *};
 
+#[doc(hidden)]
+pub use zng_wgt::prelude::formatx as __formatx;
+
 /// Styleable read-only text widget that can be selected and copied to clipboard.
 ///
 /// # Shorthand
@@ -18,13 +21,13 @@ use zng_wgt_text::{self as text, *};
 /// The same [`Text!`](struct@zng_wgt_text::Text#shorthand) shorthand can be used in this macro.
 #[widget($crate::selectable::SelectableText {
     ($txt:literal) => {
-        txt = $crate::__formatx!($txt);
+        txt = $crate::selectable::__formatx!($txt);
     };
     ($txt:expr) => {
         txt = $txt;
     };
     ($txt:tt, $($format:tt)*) => {
-        txt = $crate::__formatx!($txt, $($format)*);
+        txt = $crate::selectable::__formatx!($txt, $($format)*);
     };
 })]
 pub struct SelectableText(FocusableMix<StyleMix<zng_wgt_text::Text>>);
diff --git a/crates/zng/Cargo.toml b/crates/zng/Cargo.toml
index 221c2cd1e..7e6068a3e 100644
--- a/crates/zng/Cargo.toml
+++ b/crates/zng/Cargo.toml
@@ -235,6 +235,7 @@ zng-wgt-tooltip = { path = "../zng-wgt-tooltip", version = "0.4.4" }
 zng-wgt-markdown = { path = "../zng-wgt-markdown", version = "0.3.14" }
 zng-wgt-inspector = { path = "../zng-wgt-inspector", version = "0.2.25" }
 zng-wgt-settings = { path = "../zng-wgt-settings", version = "0.1.5" }
+zng-wgt-dialog = { path = "../zng-wgt-dialog", version = "0.1.0" }
 
 zng-wgt-material-icons = { path = "../zng-wgt-material-icons", version = "0.3.4", default-features = false, optional = true }
 zng-ext-single-instance = { path = "../zng-ext-single-instance", version = "0.3.9", optional = true }
diff --git a/crates/zng/src/dialog.rs b/crates/zng/src/dialog.rs
new file mode 100644
index 000000000..c2a28fafd
--- /dev/null
+++ b/crates/zng/src/dialog.rs
@@ -0,0 +1,119 @@
+//! Modal dialog overlay widget and service.
+//!
+//! The [`DIALOG`] service provides custom and modal native dialogs.
+//!
+//! ```
+//! use zng::prelude::*;
+//!
+//! # let _scope = APP.defaults();
+//! # let _ =
+//! Button! {
+//!     child = Text!("Info, Warn, Error");
+//!     on_click = async_hn!(|_| {
+//!         DIALOG.info("Info", "Information message.").wait_rsp().await;
+//!         DIALOG.warn("Warn", "Warning message.").wait_rsp().await;
+//!         DIALOG.error("Error", "Error message.").wait_rsp().await;
+//!     });
+//!     // dialog::native_dialogs = true;
+//! }
+//! # ;
+//! ```
+//!
+//! The example above shows 3 custom dialogs in sequence, info, warn and error. If `dialog::native_dialogs = true` is uncommented
+//! the example shows 3 native dialogs.
+//!
+//! Custom dialogs modal widgets, rendered in the window content, instantiated using the [`Dialog!`] widget.
+//!
+//! ```
+//! use zng::prelude::*;
+//!
+//! # async fn _demo() {
+//! let r = DIALOG
+//!     .custom(dialog::Dialog! {
+//!         style_fn = dialog::WarnStyle!();
+//!         title = Text!(l10n!("save-dlg.title", "Save File?"));
+//!         content = SelectableText!(l10n!("save-dlg.msg", "Save file? All unsaved changes will be lost."));
+//!         responses = vec![
+//!             dialog::Response::cancel(),
+//!             dialog::Response::new("discard", l10n!("save-dlg.discard", "Discard")),
+//!             dialog::Response::new("save", l10n!("save-dlg.save", "Save")),
+//!         ]
+//!     })
+//!     .wait_rsp()
+//!     .await;
+//! if r.name == "save" {
+//!     // save
+//! }
+//! # }
+//! ```
+//!
+//! The example above creates a custom dialog based on the warning dialog (`WarnStyle!`), it uses custom responses that are
+//! identified by name.
+//!
+//! Some of the dialogs provided are native by default (and only native on this release), the example below shows a native save file dialog:
+//!
+//! ```
+//! use zng::prelude::*;
+//!
+//! # async fn _demo() {
+//! let mut f = dialog::FileDialogFilters::default();
+//! f.push_filter("Text Files", &["txt", "md"]);
+//! f.push_filter("Text File", &["txt"]);
+//! f.push_filter("Markdown File", &["md"]);
+//! f.push_filter("All Files", &["*"]);
+//! let filters = f;
+//!
+//! let r = DIALOG
+//!     .save_file("Save Text", "last/save/dir", "last-name.txt", filters)
+//!     .wait_rsp()
+//!     .await
+//!     .into_path();
+//!
+//! if let Ok(Some(path)) = r {
+//!     std::fs::write(path, "contents".as_bytes()).unwrap();
+//! }
+//! # }
+//! ```
+//!
+//! [`Dialog!`]: struct@Dialog
+//!
+//! # Full API
+//!
+//! See [`zng_wgt_dialog`] for the full view API.
+
+pub use zng_wgt_dialog::{
+    native_dialogs, AskStyle, ConfirmStyle, DefaultStyle, Dialog, DialogButtonArgs, DialogKind, ErrorStyle, FileDialogFilters,
+    FileDialogResponse, InfoStyle, Response, Responses, WarnStyle, DIALOG,
+};
+
+/// Modal dialog parent widget that fills the window.
+///
+/// Note that the actual [`DialogBackdrop!`] widget is not included in this module because it is instantiated by the [`DIALOG`] service.
+/// The backdrop can be customized by setting the [`backdrop::style_fn`].
+///
+/// ```
+/// use zng::prelude::*;
+///
+/// # let _scope = APP.defaults();
+/// # let _ =
+/// Window! {
+///     dialog::backdrop::style_fn = Style! {
+///         replace = true;
+///         color::filter::backdrop_blur = 2;
+///     };
+/// }
+/// # ;
+/// ```
+///
+/// The example above configures the backdrop to blur the window content when any dialog is open.
+///
+/// [`DialogBackdrop!`]: struct@zng_wgt_dialog::backdrop::DialogBackdrop
+/// [`backdrop::style_fn`]: fn@crate::dialog::backdrop::style_fn
+/// [`DIALOG`]: crate::dialog::DIALOG
+///
+/// # Full API
+///
+/// See [`zng_wgt_dialog::backdrop`] for the fill view API.
+pub mod backdrop {
+    pub use zng_wgt_dialog::backdrop::{style_fn, DefaultStyle};
+}
diff --git a/crates/zng/src/lib.rs b/crates/zng/src/lib.rs
index b5cd3be32..74193980d 100644
--- a/crates/zng/src/lib.rs
+++ b/crates/zng/src/lib.rs
@@ -491,6 +491,7 @@ pub mod config;
 pub mod container;
 pub mod data_context;
 pub mod data_view;
+pub mod dialog;
 pub mod env;
 pub mod event;
 pub mod focus;
@@ -681,6 +682,9 @@ mod __prelude {
     pub use zng_wgt_data_view::{DataView, DataViewArgs};
 
     pub use zng_wgt_settings::SettingBuilderEditorExt as _;
+
+    pub use crate::dialog;
+    pub use zng_wgt_dialog::DIALOG;
 }
 
 /// Prelude for declaring new properties and widgets.
diff --git a/crates/zng/src/window.rs b/crates/zng/src/window.rs
index 0503601a5..453efad7f 100644
--- a/crates/zng/src/window.rs
+++ b/crates/zng/src/window.rs
@@ -127,13 +127,8 @@ pub use zng_wgt_window::events::{
     on_window_restored, on_window_state_changed, on_window_unmaximized, on_window_unminimized,
 };
 
-/// Native dialog types.
-///
-/// Types in this module can be used with [`WINDOWS.native_message_dialog`] and [`WINDOWS.native_file_dialog`]
-/// to show system provided dialog windows.
-///
-/// [`WINDOWS.native_message_dialog`]: WINDOWS::native_message_dialog
-/// [`WINDOWS.native_file_dialog`]: WINDOWS::native_file_dialog
+#[doc(hidden)]
+#[deprecated = "use `zng::dialog`"]
 pub mod native_dialog {
     pub use zng_view_api::dialog::{
         FileDialog, FileDialogKind, FileDialogResponse, MsgDialog, MsgDialogButtons, MsgDialogIcon, MsgDialogResponse,
diff --git a/examples/image/src/main.rs b/examples/image/src/main.rs
index f42cfc330..e7a609124 100644
--- a/examples/image/src/main.rs
+++ b/examples/image/src/main.rs
@@ -514,25 +514,20 @@ fn open_or_paste_image() -> impl UiNode {
 }
 
 async fn open_dialog() -> Option<PathBuf> {
-    use window::native_dialog::*;
-
-    let mut dlg = FileDialog {
-        title: "Open Image".into(),
-        kind: FileDialogKind::OpenFile,
-        ..Default::default()
-    };
-    dlg.push_filter("Image Files", &IMAGES.available_decoders())
-        .push_filter("All Files", &["*"]);
-
-    let r = WINDOWS.native_file_dialog(WINDOW.id(), dlg).wait_rsp().await;
-    match r {
-        FileDialogResponse::Selected(mut s) => s.pop(),
-        FileDialogResponse::Cancel => None,
-        FileDialogResponse::Error(e) => {
-            tracing::error!("{e:?}");
+    DIALOG
+        .open_file("Open Image", std::env::current_dir().unwrap_or_default(), "", {
+            let mut f = dialog::FileDialogFilters::default();
+            f.push_filter("Image Files", &IMAGES.available_decoders());
+            f.push_filter("All Files", &["*"]);
+            f
+        })
+        .wait_rsp()
+        .await
+        .into_path()
+        .unwrap_or_else(|e| {
+            tracing::error!("open file dialog error, {e}");
             None
-        }
-    }
+        })
 }
 
 fn img_cache_mode(req: &task::http::Request) -> http::CacheMode {
diff --git a/examples/text/src/editor.rs b/examples/text/src/editor.rs
index 18d729f43..482c3cc00 100644
--- a/examples/text/src/editor.rs
+++ b/examples/text/src/editor.rs
@@ -1,4 +1,4 @@
-use std::sync::Arc;
+use std::{path::PathBuf, sync::Arc};
 
 use zng::{
     app::{NEW_CMD, OPEN_CMD, SAVE_AS_CMD, SAVE_CMD},
@@ -15,7 +15,7 @@ use zng::{
     undo::UNDO_CMD,
     var::ArcVar,
     widget::{corner_radius, enabled, visibility, Visibility},
-    window::{native_dialog, WindowRoot},
+    window::WindowRoot,
 };
 
 pub fn text_editor() -> impl UiNode {
@@ -49,7 +49,6 @@ fn text_editor_window(is_open: ArcVar<bool>) -> WindowRoot {
         on_close = hn!(is_open, |_| {
             is_open.set(false);
         });
-        enabled = editor.enabled();
         on_close_requested = async_hn!(editor, |args: WindowCloseRequestedArgs| {
             editor.on_close_requested(args).await;
         });
@@ -61,6 +60,7 @@ fn text_editor_window(is_open: ArcVar<bool>) -> WindowRoot {
             mode = ScrollMode::VERTICAL;
             child_align = Align::FILL;
             scroll_to_focused_mode = None;
+            enabled = editor.enabled();
 
             // line numbers
             child_start = Text! {
@@ -159,6 +159,7 @@ fn text_editor_menu(editor: Arc<TextEditor>) -> impl UiNode {
         spacing = 4;
         direction = StackDirection::left_to_right();
         padding = 4;
+        enabled = editor.enabled();
         layout::actual_width = menu_width;
         button::style_fn = Style! {
             padding = (2, 4);
@@ -292,16 +293,14 @@ impl TextEditor {
             return;
         }
 
-        let mut dlg = native_dialog::FileDialog {
-            title: "Open Text".into(),
-            kind: native_dialog::FileDialogKind::OpenFile,
-            ..Default::default()
-        };
-        dlg.push_filter("Text Files", &["txt", "md"]).push_filter("All Files", &["*"]);
-        let r = WINDOWS.native_file_dialog(WINDOW.id(), dlg).wait_rsp().await;
+        let (init_dir, init_name, filters) = self.file_dialog_data();
+        let r = DIALOG
+            .open_file("Open Text", init_dir, init_name, filters)
+            .wait_rsp()
+            .await
+            .into_path();
         match r {
-            native_dialog::FileDialogResponse::Selected(mut s) => {
-                let file = s.remove(0);
+            Ok(Some(file)) => {
                 let r = task::wait(clmv!(file, || std::fs::read_to_string(file))).await;
                 match r {
                     Ok(t) => {
@@ -314,10 +313,10 @@ impl TextEditor {
                     }
                 }
             }
-            native_dialog::FileDialogResponse::Cancel => {}
-            native_dialog::FileDialogResponse::Error(e) => {
+            Err(e) => {
                 self.handle_error("opening file", e).await;
             }
+            _ => {}
         }
     }
 
@@ -335,30 +334,25 @@ impl TextEditor {
     pub async fn save_as(&self) -> bool {
         let _busy = self.enter_busy();
 
-        let mut dlg = native_dialog::FileDialog {
-            title: "Save Text".into(),
-            kind: native_dialog::FileDialogKind::SaveFile,
-            ..Default::default()
-        };
-        dlg.push_filter("Text", &["txt"])
-            .push_filter("Markdown", &["md"])
-            .push_filter("All Files", &["*"]);
-        let r = WINDOWS.native_file_dialog(WINDOW.id(), dlg).wait_rsp().await;
+        let (init_dir, init_name, filters) = self.file_dialog_data();
+        let r = DIALOG
+            .save_file("Save Text", init_dir, init_name, filters)
+            .wait_rsp()
+            .await
+            .into_path();
         match r {
-            native_dialog::FileDialogResponse::Selected(mut s) => {
-                if let Some(file) = s.pop() {
-                    let ok = self.write(file.clone()).await;
-                    self.txt_touched.set(!ok);
-                    if ok {
-                        self.file.set(Some(file));
-                    }
-                    return ok;
+            Ok(Some(file)) => {
+                let ok = self.write(file.clone()).await;
+                self.txt_touched.set(!ok);
+                if ok {
+                    self.file.set(Some(file));
                 }
+                return ok;
             }
-            native_dialog::FileDialogResponse::Cancel => {}
-            native_dialog::FileDialogResponse::Error(e) => {
+            Err(e) => {
                 self.handle_error("saving file", e.to_txt()).await;
             }
+            _ => {}
         }
 
         false // cancel
@@ -374,6 +368,27 @@ impl TextEditor {
         }
     }
 
+    fn file_dialog_data(&self) -> (PathBuf, Txt, dialog::FileDialogFilters) {
+        let mut dlg_dir = std::env::current_dir().unwrap_or_default();
+        let mut dlg_name = Txt::from("text.md");
+        if let Some(p) = self.file.get() {
+            if let Some(n) = p.file_name() {
+                dlg_name = n.to_string_lossy().to_txt();
+            }
+            if let Some(p) = p.parent() {
+                p.clone_into(&mut dlg_dir);
+            }
+        }
+
+        let mut f = dialog::FileDialogFilters::default();
+        f.push_filter("Text Files", &["txt", "md"]);
+        f.push_filter("Text File", &["txt"]);
+        f.push_filter("Markdown File", &["md"]);
+        f.push_filter("All Files", &["*"]);
+
+        (dlg_dir, dlg_name, f)
+    }
+
     async fn write(&self, file: std::path::PathBuf) -> bool {
         let txt = self.txt.clone();
         let r = task::wait(move || txt.with(move |txt| std::fs::write(file, txt.as_bytes()))).await;
@@ -391,30 +406,29 @@ impl TextEditor {
             return true;
         }
 
-        let dlg = native_dialog::MsgDialog {
-            title: "Save File?".into(),
-            message: "Save file? All unsaved changes will be lost.".into(),
-            icon: native_dialog::MsgDialogIcon::Warn,
-            buttons: native_dialog::MsgDialogButtons::YesNo,
-        };
-        let r = WINDOWS.native_message_dialog(WINDOW.id(), dlg).wait_rsp().await;
-        match r {
-            native_dialog::MsgDialogResponse::Yes => self.save().await,
-            native_dialog::MsgDialogResponse::No => true,
+        let r = DIALOG
+            .custom(dialog::Dialog! {
+                style_fn = dialog::WarnStyle!();
+                title = Text!("Save File?");
+                content = SelectableText!("Save file? All unsaved changes will be lost.");
+                responses = vec![
+                    dialog::Response::cancel(),
+                    dialog::Response::new("Discard", "Discard"),
+                    dialog::Response::new("Save", "Save"),
+                ]
+            })
+            .wait_rsp()
+            .await;
+        match r.name.as_str() {
+            "Discard" => true,
+            "Save" => self.save().await,
             _ => false,
         }
     }
 
     async fn handle_error(&self, context: &'static str, e: Txt) {
         tracing::error!("error {context}, {e}");
-
-        let dlg = native_dialog::MsgDialog {
-            title: "Error".into(),
-            message: formatx!("Error {context}.\n\n{e}"),
-            icon: native_dialog::MsgDialogIcon::Error,
-            buttons: native_dialog::MsgDialogButtons::Ok,
-        };
-        let _ = WINDOWS.native_message_dialog(WINDOW.id(), dlg).wait_rsp().await;
+        DIALOG.error("Error", formatx!("Error {context}.\n\n{e}")).wait_rsp().await;
     }
 
     fn enter_busy(&self) -> impl Drop {
diff --git a/examples/window/Cargo.toml b/examples/window/Cargo.toml
index 5b8ffcc37..8ad5d2d18 100644
--- a/examples/window/Cargo.toml
+++ b/examples/window/Cargo.toml
@@ -5,5 +5,5 @@ publish = false
 edition = "2021"
 
 [dependencies]
-zng = { path = "../../crates/zng", features = ["view_prebuilt", "inspector"] }
+zng = { path = "../../crates/zng", features = ["view_prebuilt", "inspector", "material_icons"] }
 tracing = "0.1"
\ No newline at end of file
diff --git a/examples/window/src/main.rs b/examples/window/src/main.rs
index 66015803d..61ac16fd5 100644
--- a/examples/window/src/main.rs
+++ b/examples/window/src/main.rs
@@ -1,23 +1,16 @@
 //! Demonstrates the window widget, service, state and commands.
 
 use zng::{
-    access::ACCESS,
     app::EXIT_CMD,
-    button,
-    color::{
-        filter::{backdrop_blur, drop_shadow, opacity},
-        Rgba,
-    },
+    color::Rgba,
     event::Command,
-    focus::{directional_nav, focus_scope, tab_nav, DirectionalNav, TabNav},
     handler::WidgetHandler,
     image::ImageDataFormat,
     layout::*,
     prelude::*,
     scroll::ScrollMode,
-    var::ArcVar,
     widget::{background_color, corner_radius, enabled, visibility, LineStyle},
-    window::{native_dialog, FocusIndicator, FrameCaptureMode, FrameImageReadyArgs, WindowChangedArgs, WindowState},
+    window::{FocusIndicator, FrameCaptureMode, FrameImageReadyArgs, WindowChangedArgs, WindowState},
 };
 
 fn main() {
@@ -588,97 +581,71 @@ fn native() -> impl UiNode {
             Button! {
                 child = Text!("Messages");
                 tooltip = Tip!(Text!(r#"Shows a "Yes/No" message, then an "Ok" message dialogs."#));
+                dialog::native_dialogs = true;
                 on_click = async_hn!(|_| {
-                    let rsp = WINDOWS.native_message_dialog(WINDOW.id(), native_dialog::MsgDialog {
-                        title: Txt::from_static("Question?"),
-                        message: Txt::from_static("Example message. Yes -> Warn, No -> Error."),
-                        icon: native_dialog::MsgDialogIcon::Info,
-                        buttons: native_dialog::MsgDialogButtons::YesNo,
-                    }).wait_rsp().await;
-                    let icon = match rsp {
-                        native_dialog::MsgDialogResponse::Yes => {
-                            native_dialog::MsgDialogIcon::Warn
-                        },
-                        native_dialog::MsgDialogResponse::No => {
-                            native_dialog::MsgDialogIcon::Error
-                        }
-                        e => {
-                            tracing::error!("unexpected message response {e:?}");
-                            return;
-                        },
-                    };
-                    WINDOWS.native_message_dialog(WINDOW.id(), native_dialog::MsgDialog {
-                        title: Txt::from_static("Title"),
-                        message: Txt::from_static("Message"),
-                        icon,
-                        buttons: native_dialog::MsgDialogButtons::Ok,
-                    });
+                    let rsp = DIALOG.ask("Question?", "Example message. Yes -> Warn, No -> Error.").wait_rsp().await;
+                    if rsp {
+                        DIALOG.warn("Title", "Yes -> Warn Message").wait_done().await;
+                    } else {
+                        DIALOG.error("Title", "No -> Error message").wait_done().await;
+                    }
                 });
             },
             Button! {
                 child = Text!("File Picker");
                 tooltip = Tip!(Text!(r#"Shows a "Directory Picker", then an "Open Many Files", then a "Save File" dialogs."#));
+                dialog::native_dialogs = true;
                 on_click = async_hn!(|_| {
-                    let res = WINDOWS.native_file_dialog(WINDOW.id(), native_dialog::FileDialog {
-                        title: "Select Dir".into(),
-                        starting_dir: "".into(),
-                        starting_name: "".into(),
-                        filters: "".into(),
-                        kind: native_dialog::FileDialogKind::SelectFolder,
-                    }).wait_rsp().await;
+                    let res = DIALOG.select_folder("Select Dir", "", "").wait_rsp().await;
                     let dir = match res {
-                        native_dialog::FileDialogResponse::Selected(mut s) => {
+                        dialog::FileDialogResponse::Selected(mut s) => {
                             s.remove(0)
                         }
-                        native_dialog::FileDialogResponse::Cancel => {
+                        dialog::FileDialogResponse::Cancel => {
                             tracing::info!("canceled");
                             return;
                         }
-                        native_dialog::FileDialogResponse::Error(e) => {
+                        dialog::FileDialogResponse::Error(e) => {
                             tracing::error!("unexpected select dir response {e:?}");
                             return;
                         }
                     };
 
-                    let mut dlg = native_dialog::FileDialog {
-                        title: "Open Files".into(),
-                        starting_dir: dir,
-                        starting_name: "".into(),
-                        filters: "".into(),
-                        kind: native_dialog::FileDialogKind::OpenFiles,
-                    };
-                    dlg.push_filter("Text", &["*.txt", "*.md"]);
-                    dlg.push_filter("All", &["*.*"]);
-                    let res = WINDOWS.native_file_dialog(WINDOW.id(), dlg.clone()).wait_rsp().await;
+                    let mut filters = dialog::FileDialogFilters::new();
+                    filters.push_filter("Text", &["*.txt", "*.md"]);
+                    filters.push_filter("All", &["*.*"]);
+
+                    let res = DIALOG.open_files("Open Files", dir, "", filters.clone()).wait_rsp().await;
                     let first_file = match res {
-                        native_dialog::FileDialogResponse::Selected(mut s) => {
+                        dialog::FileDialogResponse::Selected(mut s) => {
                             tracing::info!("selected {} file(s)", s.len());
                             s.remove(0)
                         }
-                        native_dialog::FileDialogResponse::Cancel => {
+                        dialog::FileDialogResponse::Cancel => {
                             tracing::info!("canceled");
                             return;
                         }
-                        native_dialog::FileDialogResponse::Error(e) => {
+                        dialog::FileDialogResponse::Error(e) => {
                             tracing::error!("unexpected open files response {e:?}");
                             return;
                         }
                     };
 
-                    dlg.title = "Save File".into();
-                    dlg.kind = native_dialog::FileDialogKind::SaveFile;
-                    dlg.starting_dir = first_file.parent().map(|p| p.to_owned()).unwrap_or_default();
-                    dlg.starting_name = first_file.file_name().map(|p| Txt::from_str(&p.to_string_lossy())).unwrap_or_default();
-                    let res = WINDOWS.native_file_dialog(WINDOW.id(), dlg.clone()).wait_rsp().await;
+                    let res = DIALOG.save_file(
+                        "Save File",
+                        first_file.parent().map(|p| p.to_owned()).unwrap_or_default(),
+                        first_file.file_name().map(|p| Txt::from_str(&p.to_string_lossy())).unwrap_or_default(),
+                        filters
+                    ).wait_rsp().await;
                     let save_file = match res {
-                        native_dialog::FileDialogResponse::Selected(mut s) => {
+                        dialog::FileDialogResponse::Selected(mut s) => {
                             s.remove(0)
                         }
-                        native_dialog::FileDialogResponse::Cancel => {
+                        dialog::FileDialogResponse::Cancel => {
                             tracing::info!("canceled");
                             return;
                         }
-                        native_dialog::FileDialogResponse::Error(e) => {
+                        dialog::FileDialogResponse::Error(e) => {
                             tracing::error!("unexpected save file response {e:?}");
                             return;
                         }
@@ -698,14 +665,42 @@ enum CloseState {
 }
 fn confirm_close() -> impl WidgetHandler<WindowCloseRequestedArgs> {
     let state = var(CloseState::Ask);
-    hn!(|args: &WindowCloseRequestedArgs| {
+    async_hn!(state, |args: WindowCloseRequestedArgs| {
         match state.get() {
             CloseState::Ask => {
                 args.propagation().stop();
                 state.set(CloseState::Asking);
 
-                let dlg = close_dialog(args.headed().collect(), state.clone());
-                LAYERS.insert(LayerIndex::TOP_MOST, dlg)
+                let dlg = if args.windows.len() == 1 {
+                    dialog::Dialog! {
+                        style_fn = dialog::AskStyle!();
+                        title = Text!("Close?");
+                        content = SelectableText!("Close the window?");
+                        responses = vec![
+                            dialog::Response::cancel(),
+                            dialog::Response::close(),
+                        ]
+                    }
+                } else {
+                    dialog::Dialog! {
+                        style_fn = dialog::AskStyle!();
+                        title = Text!("Close all?");
+                        content = SelectableText!("Close {} windows?", args.windows.len());
+                        responses = vec![
+                            dialog::Response::cancel(),
+                            dialog::Response::new("close", "Close All"),
+                        ]
+                    }
+                };
+                let r = DIALOG.custom(dlg).wait_rsp().await;
+                if r.name == "close" {
+                    state.set(CloseState::Close);
+                    let mut windows = args.windows;
+                    windows.retain(|w| WINDOWS.is_open(*w));
+                    let _ = WINDOWS.close_together(windows);
+                } else {
+                    state.set(CloseState::Ask);
+                }
             }
             CloseState::Asking => args.propagation().stop(),
             CloseState::Close => {}
@@ -713,81 +708,6 @@ fn confirm_close() -> impl WidgetHandler<WindowCloseRequestedArgs> {
     })
 }
 
-fn close_dialog(mut windows: Vec<WindowId>, state: ArcVar<CloseState>) -> impl UiNode {
-    let opacity = var(0.fct());
-    opacity.ease(1.fct(), 300.ms(), easing::linear).perm();
-    Container! {
-        opacity = opacity.clone();
-
-        id = "close-dialog";
-        widget::modal = true;
-        zng::focus::return_focus_on_deinit = true;
-        backdrop_blur = 2;
-        background_color = light_dark(colors::BLACK.with_alpha(10.pct()), colors::WHITE.with_alpha(10.pct()));
-        child_align = Align::CENTER;
-        gesture::on_click = hn!(|args: &gesture::ClickArgs| {
-            if WIDGET.id() == args.target.widget_id() {
-                args.propagation().stop();
-                ACCESS.click(WINDOW.id(), "cancel-btn", true);
-            }
-        });
-        child = Container! {
-            background_color = light_dark(colors::WHITE.with_alpha(90.pct()), colors::BLACK.with_alpha(90.pct()));
-            focus_scope = true;
-            tab_nav = TabNav::Cycle;
-            directional_nav = DirectionalNav::Cycle;
-            drop_shadow = (0, 0), 4, colors::BLACK;
-            padding = 4;
-
-            button::style_fn = Style! {
-                padding = 4;
-                corner_radius = unset!;
-            };
-
-            child = Stack! {
-                direction = StackDirection::top_to_bottom();
-                children_align = Align::RIGHT;
-                children = ui_vec![
-                    SelectableText! {
-                        txt = match windows.len() {
-                            1 => "Close Confirmation\n\nClose 1 window?".to_txt(),
-                            n => formatx!("Close Confirmation\n\nClose {n} windows?")
-                        };
-                        margin = 15;
-                    },
-                    Stack! {
-                        direction = StackDirection::left_to_right();
-                        spacing = 4;
-                        children = ui_vec![
-                            Button! {
-                                style_fn = button::PrimaryStyle!();
-                                focus_on_init = true;
-                                child = Text!("Close");
-                                on_click = hn_once!(state, |_| {
-                                    state.set(CloseState::Close);
-                                    windows.retain(|w| WINDOWS.is_open(*w));
-                                    let _ = WINDOWS.close_together(windows);
-                                })
-                            },
-                            Button! {
-                                id = "cancel-btn";
-                                child = Text!("Cancel");
-                                on_click = async_hn!(opacity, state, |_| {
-                                    opacity.ease(0.fct(), 150.ms(), easing::linear).perm();
-                                    opacity.wait_animation().await;
-
-                                    state.set(CloseState::Ask);
-                                    LAYERS.remove("close-dialog");
-                                });
-                            },
-                        ]
-                    }
-                ]
-            }
-        }
-    }
-}
-
 fn cmd_btn(cmd: Command) -> impl UiNode {
     Button! {
         child = Text!(cmd.name_with_shortcut());


Test patch: