#![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")]
#![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};
#[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! {
pub zng_wgt_layer::popup::is_close_delaying(state: impl IntoVar<bool>);
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 => {
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, _| {
if responder.get().is_waiting() {
let path = WINDOWS.widget_info(id).unwrap().path();
if args.windows.contains(&path.window_id()) {
if let Some(default) = default_response.get() {
responder.respond(default);
zng_wgt_layer::popup::POPUP_CLOSE_CMD
.scoped(path.window_id())
.notify_param(path.widget_id());
} else {
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) {
let ctx = DIALOG_CTX.get();
if ctx.responder.get().is_waiting() {
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! {
pub struct DialogCloseCanceledArgs {
pub target: WidgetPath,
..
fn delivery_list(&self, list: &mut UpdateDeliveryList) {
list.insert_wgt(&self.target);
}
}
}
event! {
pub static DIALOG_CLOSE_CANCELED_EVENT: DialogCloseCanceledArgs;
}
event_property! {
pub fn dialog_close_canceled {
event: DIALOG_CLOSE_CANCELED_EVENT,
args: DialogCloseCanceledArgs,
}
}
#[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::<UiVec>()
};
}
})), 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! {
pub static TITLE_VAR: WidgetFn<()> = WidgetFn::nil();
pub static ICON_VAR: WidgetFn<()> = WidgetFn::nil();
pub static CONTENT_VAR: WidgetFn<()> = WidgetFn::nil();
pub static BUTTON_FN_VAR: WidgetFn<DialogButtonArgs> = WidgetFn::new(default_button_fn);
pub static RESPONSES_VAR: Responses = Responses::ok();
pub static DEFAULT_RESPONSE_VAR: Option<Response> = None;
pub static NATIVE_DIALOGS_VAR: DialogKind = DIALOG.native_dialogs();
}
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!();
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct DialogButtonArgs {
pub response: Response,
pub is_last: bool,
}
#[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))
}
#[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))
}
#[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))
}
#[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)
}
#[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)
}
#[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)
}
#[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)
}
#[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();
}
}
}
#[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;
};
}
}
}
#[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;
};
}
}
}
#[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();
}
}
}
#[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();
}
}
}
#[derive(Clone)]
pub struct Response {
pub name: Txt,
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 {
pub fn new(name: impl Into<Txt>, label: impl IntoVar<Txt>) -> Self {
Self {
name: name.into(),
label: label.into_var().boxed(),
}
}
pub fn ok() -> Self {
Self::new("Ok", l10n!("response-ok", "Ok"))
}
pub fn cancel() -> Self {
Self::new("cancel", l10n!("response-cancel", "Cancel"))
}
pub fn yes() -> Self {
Self::new("yes", l10n!("response-yes", "Yes"))
}
pub fn no() -> Self {
Self::new("no", l10n!("response-no", "No"))
}
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>;
}
#[derive(Clone, PartialEq, Debug)]
pub struct Responses(pub Vec<Response>);
impl Responses {
pub fn new(r: impl Into<Response>) -> Self {
Self(vec![r.into()])
}
pub fn with(mut self, response: impl Into<Response>) -> Self {
self.push(response.into());
self
}
pub fn ok() -> Self {
Response::ok().into()
}
pub fn close() -> Self {
Response::close().into()
}
pub fn no_yes() -> Self {
vec![Response::no(), Response::yes()].into()
}
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)
}
}
pub struct DIALOG;
impl DIALOG {
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(|_| ())
}
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(|_| ())
}
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(|_| ())
}
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")
}
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")
}
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,
},
)
}
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,
},
)
}
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,
},
)
}
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,
},
)
}
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,
},
)
}
pub fn custom(&self, dialog: impl UiNode) -> ResponseVar<Response> {
self.show_impl(dialog.boxed())
}
}
impl DIALOG {
pub fn native_dialogs(&self) -> ArcVar<DialogKind> {
DIALOG_SV.read().native_dialogs.clone()
}
}
bitflags! {
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct DialogKind: u32 {
const INFO = 0b0000_0000_0000_0001;
const WARN = 0b0000_0000_0000_0010;
const ERROR = 0b0000_0000_0000_0100;
const ASK = 0b0000_0000_0000_1000;
const CONFIRM = 0b0000_0000_0001_0000;
const OPEN_FILE = 0b1000_0000_0000_0000;
const OPEN_FILES = 0b0100_0000_0000_0000;
const SAVE_FILE = 0b0010_0000_0000_0000;
const SELECT_FOLDER = 0b0001_0000_0000_0000;
const SELECT_FOLDERS = 0b0000_1000_0000_0000;
const MESSAGE = Self::INFO.bits() | Self::WARN.bits() | Self::ERROR.bits() | Self::ASK.bits() | Self::CONFIRM.bits();
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 {
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");
}
}
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));
*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),
};
}