zng/examples/window.rs

889 lines
31 KiB
Rust

//! Demonstrates the window widget, service, state and commands.
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use zng::{
access::ACCESS,
app::EXIT_CMD,
button,
color::{
color_scheme_map,
filter::{backdrop_blur, drop_shadow, opacity},
Rgba,
},
event::Command,
focus::{directional_nav, focus_scope, tab_nav, DirectionalNav, TabNav},
handler::WidgetHandler,
image::ImageDataFormat,
layout::*,
prelude::*,
scroll::ScrollMode,
text::Strong,
var::ArcVar,
widget::{background_color, corner_radius, enabled, visibility, LineStyle},
window::{native_dialog, FocusIndicator, FrameCaptureMode, FrameImageReadyArgs, WindowChangedArgs, WindowState},
};
use zng::view_process::default as view_process;
fn main() {
examples_util::print_info();
// view_process::init();
zng::app::crash_handler::init_debug();
// let rec = examples_util::record_profile("window");
view_process::run_same_process(app_main);
// app_main();
// rec.finish();
}
fn app_main() {
APP.defaults().run_window(main_window());
}
async fn main_window() -> window::WindowRoot {
// WINDOWS.exit_on_last_close().set(false);
zng::image::IMAGES.limits().modify(|l| {
let l = l.to_mut();
l.allow_path = zng::image::PathFilter::allow_dir("examples/res");
});
let window_vars = WINDOW.vars();
let title = merge_var!(
window_vars.actual_position(),
window_vars.actual_size(),
window_vars.scale_factor(),
move |p: &DipPoint, s: &DipSize, f: &Factor| { formatx!("Window Example - position: {p:.0?}, size: {s:.0?}, factor: {f:?}") }
);
LAYERS.insert(LayerIndex::TOP_MOST, custom_chrome(title.clone()));
let background = var(colors::BLACK);
Window! {
background_color = background.easing(150.ms(), easing::linear);
clear_color = rgba(0, 0, 0, 0);
title;
on_state_changed = hn!(|args: &WindowChangedArgs| {
tracing::info!("state: {:?}", args.new_state().unwrap());
});
on_close_requested = confirm_close();
child_align = Align::CENTER;
child = Stack! {
direction = StackDirection::left_to_right();
spacing = 40;
children = ui_vec![
Stack! {
direction = StackDirection::top_to_bottom();
spacing = 20;
children = ui_vec![
state_commands(),
focus_control(),
]
},
Stack! {
direction = StackDirection::top_to_bottom();
spacing = 20;
children = ui_vec![
state(),
visibility_example(),
];
},
Stack! {
direction = StackDirection::top_to_bottom();
spacing = 20;
children = ui_vec![
icon_example(),
background_color_example(background),
];
},
Stack! {
direction = StackDirection::top_to_bottom();
spacing = 20;
children = ui_vec![
screenshot(),
misc(),
native(),
];
},
];
};
}
}
fn background_color_example(color: impl Var<Rgba>) -> impl UiNode {
fn color_btn(c: impl Var<Rgba>, select_on_init: bool) -> impl UiNode {
Toggle! {
value::<Rgba> = c.clone();
select_on_init;
child = Stack! {
direction = StackDirection::left_to_right();
spacing = 4;
children_align = Align::LEFT;
children = ui_vec![
Wgt! {
background_color = c.clone();
size = (16, 16);
},
Text!(c.map_to_txt()),
];
};
}
}
fn primary_color(c: Rgba) -> impl UiNode {
let c = c.desaturate(50.pct());
let c = color_scheme_map(rgba(0, 0, 0, 20.pct()).mix_normal(c), rgba(255, 255, 255, 20.pct()).mix_normal(c));
color_btn(c, false)
}
select(
"Background Color",
color,
ui_vec![
color_btn(color_scheme_map(rgb(0.1, 0.1, 0.1), rgb(0.9, 0.9, 0.9)), true),
primary_color(rgb(1.0, 0.0, 0.0)),
primary_color(rgb(0.0, 0.8, 0.0)),
primary_color(rgb(0.0, 0.0, 1.0)),
primary_color(rgba(0, 0, 240, 20.pct())),
],
)
}
fn screenshot() -> impl UiNode {
fn of_window() -> impl UiNode {
let enabled = var(true);
Button! {
child = Text!(enabled.map(|&enabled| {
if enabled {
"screenshot".to_txt()
} else {
"saving..".to_txt()
}
}));
on_click = async_hn!(enabled, |_| {
// disable button until screenshot is saved.
enabled.set(false);
tracing::info!("taking `screenshot.png`..");
let t = INSTANT.now();
let img = WINDOW.frame_image(None).get();
img.wait_done().await;
tracing::info!("taken in {:?}, saving..", t.elapsed());
let t = INSTANT.now();
match img.save("screenshot.png").await {
Ok(_) => {
tracing::info!("saved in {:?}", t.elapsed());
}
Err(e) => {
tracing::error!("error {e}")
}
}
enabled.set(true);
});
enabled;
}
}
fn of_headless_temp() -> impl UiNode {
let enabled = var(true);
Button! {
child = Text!(enabled.map(|&enabled| {
if enabled {
"headless".to_txt()
} else {
"saving..".to_txt()
}
}));
enabled = enabled.clone();
on_click = hn!(|_| {
enabled.set(false);
tracing::info!("taking `screenshot.png` using a new headless window ..");
let parent = WINDOW.id();
WINDOWS.open_headless(async_clmv!(enabled, {
Window! {
parent;
size = (500, 400);
background_color = web_colors::DARK_GREEN;
font_size = 72;
child_align = Align::CENTER;
child = Text!("No Head!");
frame_capture_mode = FrameCaptureMode::Next;
on_frame_image_ready = async_hn_once!(|args: FrameImageReadyArgs| {
tracing::info!("saving screenshot..");
match args.frame_image.unwrap().save("screenshot.png").await {
Ok(_) => tracing::info!("saved"),
Err(e) => tracing::error!("{e}")
}
debug_assert_eq!(WINDOW.id(), args.window_id);
WINDOW.close();
enabled.set(true);
});
}
}),
true
);
});
}
}
section("Screenshot", ui_vec![of_window(), of_headless_temp(),])
}
fn icon_example() -> impl UiNode {
let icon_btn = |label: &'static str, ico: WindowIcon| {
Toggle! {
child = Text!(label);
value = ico;
}
};
select(
"Icon",
WINDOW.vars().icon(),
ui_vec![
icon_btn("Default", WindowIcon::Default),
icon_btn("Png File", "examples/res/window/icon-file.png".into()),
icon_btn("Png Bytes", include_bytes!("res/window/icon-bytes.png").into()),
icon_btn("Raw BGRA", {
let color = [0, 0, 255, 255 / 2];
let size = PxSize::new(Px(32), Px(32));
let len = size.width.0 * size.height.0 * 4;
let bgra: Vec<u8> = color.iter().copied().cycle().take(len as usize).collect();
(bgra, ImageDataFormat::from(size)).into()
}),
icon_btn("Render", WindowIcon::render(logo_icon))
],
)
}
fn state_commands() -> impl UiNode {
use zng::window::cmd::*;
let window_id = WINDOW.id();
section(
"Commands",
ui_vec![
cmd_btn(MINIMIZE_CMD.scoped(window_id)),
separator(),
cmd_btn(RESTORE_CMD.scoped(window_id)),
cmd_btn(MAXIMIZE_CMD.scoped(window_id)),
separator(),
cmd_btn(FULLSCREEN_CMD.scoped(window_id)),
cmd_btn(EXCLUSIVE_FULLSCREEN_CMD.scoped(window_id)),
separator(),
cmd_btn(CLOSE_CMD.scoped(window_id)),
cmd_btn(EXIT_CMD),
],
)
}
fn focus_control() -> impl UiNode {
let enabled = var(true);
let focus_btn = Button! {
enabled = enabled.clone();
child = Text!("Focus in 5s");
on_click = async_hn!(enabled, |_| {
enabled.set(false);
task::deadline(5.secs()).await;
WINDOWS.focus(WINDOW.id()).unwrap();
enabled.set(true);
});
};
let enabled = var(true);
let critical_btn = Button! {
enabled = enabled.clone();
child = Text!("Critical Alert in 5s");
on_click = async_hn!(enabled, |_| {
enabled.set(false);
task::deadline(5.secs()).await;
WINDOW.vars().focus_indicator().set(FocusIndicator::Critical);
enabled.set(true);
});
};
let enabled = var(true);
let info_btn = Button! {
enabled = enabled.clone();
child = Text!("Info Alert in 5s");
on_click = async_hn!(enabled, |_| {
enabled.set(false);
task::deadline(5.secs()).await;
WINDOW.vars().focus_indicator().set(FocusIndicator::Info);
enabled.set(true);
});
};
section("Focus", ui_vec![focus_btn, critical_btn, info_btn,])
}
fn state() -> impl UiNode {
let state_btn = |s: WindowState| {
Toggle! {
child = Text!("{s:?}");
value = s;
}
};
select(
"State",
WINDOW.vars().state(),
ui_vec![
state_btn(WindowState::Minimized),
separator(),
state_btn(WindowState::Normal),
state_btn(WindowState::Maximized),
separator(),
state_btn(WindowState::Fullscreen),
Stack! {
direction = StackDirection::top_to_bottom();
children = ui_vec![
Toggle! {
child = Text!("Exclusive");
value = WindowState::Exclusive;
corner_radius = (4, 4, 0, 0);
},
exclusive_mode(),
]
}
],
)
}
fn exclusive_mode() -> impl UiNode {
Toggle! {
style_fn = toggle::ComboStyle!();
corner_radius = (0, 0, 4, 4);
tooltip = Tip!(Text!("Exclusive video mode"));
child = Text! {
txt = WINDOW.vars().video_mode().map_to_txt();
txt_align = Align::CENTER;
padding = 2;
};
checked_popup = wgt_fn!(|_| {
let vars = WINDOW.vars();
let selected_opt = vars.video_mode();
let default_opt = zng::window::VideoMode::MAX;
let opts = vars.video_modes().get();
popup::Popup! {
max_height = 80.vh_pct();
child = Scroll! {
mode = ScrollMode::VERTICAL;
child = Stack! {
toggle::selector = toggle::Selector::single(selected_opt);
direction = StackDirection::top_to_bottom();
children = [default_opt].into_iter().chain(opts).map(|o| Toggle! {
child = Text!(formatx!("{o}"));
value = o;
})
.collect::<UiNodeVec>();
}
};
}
});
}
}
fn visibility_example() -> impl UiNode {
let visible = WINDOW.vars().visible();
let btn = Button! {
enabled = visible.clone();
child = Text!("Hide for 1s");
on_click = async_hn!(visible, |_| {
visible.set(false);
tracing::info!("visible=false");
task::deadline(1.secs()).await;
visible.set(true);
tracing::info!("visible=true");
});
};
let chrome = Toggle! {
child = Text!("Chrome");
checked = WINDOW.vars().chrome();
};
section("Visibility", ui_vec![btn, chrome])
}
fn custom_chrome(title: impl Var<Txt>) -> impl UiNode {
let vars = WINDOW.vars();
let can_move = vars.state().map(|s| matches!(s, WindowState::Normal | WindowState::Maximized));
let title = Text! {
txt = title.clone();
align = Align::TOP;
background_color = color_scheme_map(colors::BLACK, colors::WHITE);
padding = 4;
corner_radius = (0, 0, 5, 5);
when *#{can_move.clone()} {
mouse::cursor = mouse::CursorIcon::Move;
}
mouse::on_mouse_down = hn!(|args: &mouse::MouseInputArgs| {
if args.is_primary() && can_move.get() {
window::cmd::DRAG_MOVE_RESIZE_CMD.scoped(WINDOW.id()).notify();
}
});
gesture::on_context_click = hn!(|args: &gesture::ClickArgs| {
if matches!(WINDOW.vars().state().get(), WindowState::Normal | WindowState::Maximized) {
if let Some(p) = args.position() {
window::cmd::OPEN_TITLE_BAR_CONTEXT_MENU_CMD.scoped(WINDOW.id()).notify_param(p);
}
}
});
};
use window::cmd::ResizeDirection as RD;
fn resize_direction(wgt_pos: PxPoint) -> Option<RD> {
let p = wgt_pos;
let s = WIDGET.bounds().inner_size();
let b = WIDGET.border().offsets();
let corner_b = b * FactorSideOffsets::from(3.fct());
if p.x <= b.left {
if p.y <= corner_b.top {
Some(RD::NorthWest)
} else if p.y >= s.height - corner_b.bottom {
Some(RD::SouthWest)
} else {
Some(RD::West)
}
} else if p.x >= s.width - b.right {
if p.y <= corner_b.top {
Some(RD::NorthEast)
} else if p.y >= s.height - corner_b.bottom {
Some(RD::SouthEast)
} else {
Some(RD::East)
}
} else if p.y <= b.top {
if p.x <= corner_b.left {
Some(RD::NorthWest)
} else if p.x >= s.width - corner_b.right {
Some(RD::NorthEast)
} else {
Some(RD::North)
}
} else if p.y >= s.height - b.bottom {
if p.x <= corner_b.left {
Some(RD::SouthWest)
} else if p.x >= s.width - corner_b.right {
Some(RD::SouthEast)
} else {
Some(RD::South)
}
} else {
None
}
}
let cursor = var(mouse::CursorSource::Hidden);
Container! {
visibility = expr_var!((#{vars.state()}.is_fullscreen() || !*#{vars.chrome()}).into());
widget::hit_test_mode = widget::HitTestMode::Detailed;
child = title;
when matches!(#{vars.state()}, WindowState::Normal) {
widget::border = 5, color_scheme_map(colors::BLACK, colors::WHITE);
mouse::cursor = cursor.clone();
mouse::on_mouse_move = hn!(|args: &mouse::MouseMoveArgs| {
cursor.set(match args.position_wgt().and_then(resize_direction) {
Some(d) => mouse::CursorIcon::from(d).into(),
None => mouse::CursorSource::Hidden,
});
});
mouse::on_mouse_down = hn!(|args: &mouse::MouseInputArgs| {
if args.is_primary() {
if let Some(d) = args.position_wgt().and_then(resize_direction) {
window::cmd::DRAG_MOVE_RESIZE_CMD.scoped(WINDOW.id()).notify_param(d);
}
}
});
}
}
}
fn misc() -> impl UiNode {
let window_vars = WINDOW.vars();
let window_id = WINDOW.id();
let can_open_windows = window_vars.state().map(|&s| s != WindowState::Exclusive);
section(
"Misc.",
ui_vec![
Toggle! {
child = Text!("Taskbar Visible");
checked = window_vars.taskbar_visible();
},
Toggle! {
child = Text!("Always on Top");
checked = window_vars.always_on_top();
},
separator(),
cmd_btn(zng::window::cmd::INSPECT_CMD.scoped(window_id)),
separator(),
{
let mut child_count = 0;
Button! {
child = Text!("Open Child Window");
enabled = can_open_windows.clone();
on_click = hn!(|_| {
child_count += 1;
let parent = WINDOW.id();
WINDOWS.open(async move {
Window! {
title = formatx!("Window Example - Child {child_count}");
size = (400, 300);
parent;
child_align = Align::CENTER;
start_position = window::StartPosition::CenterParent;
child = Text! {
txt = formatx!("Child {child_count}");
font_size = 20;
};
}
});
})
}
},
{
let mut other_count = 0;
Button! {
child = Text!("Open Other Window");
enabled = can_open_windows;
on_click = hn!(|_| {
other_count += 1;
WINDOWS.open(async move {
Window! {
title = formatx!("Window Example - Other {other_count}");
size = (400, 300);
child_align = Align::CENTER;
child = Text! {
txt = formatx!("Other {other_count}");
font_size = 20;
};
}
});
})
}
}
],
)
}
fn native() -> impl UiNode {
section(
"Native Dialogs",
ui_vec![
Button! {
child = Text!("Messages");
tooltip = Tip!(Text!(r#"Shows a "Yes/No" message, then an "Ok" message dialogs."#));
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,
});
});
},
Button! {
child = Text!("File Picker");
tooltip = Tip!(Text!(r#"Shows a "Directory Picker", then an "Open Many Files", then a "Save File" dialogs."#));
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 dir = match res {
native_dialog::FileDialogResponse::Selected(mut s) => {
s.remove(0)
}
native_dialog::FileDialogResponse::Cancel => {
tracing::info!("canceled");
return;
}
native_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 first_file = match res {
native_dialog::FileDialogResponse::Selected(mut s) => {
tracing::info!("selected {} file(s)", s.len());
s.remove(0)
}
native_dialog::FileDialogResponse::Cancel => {
tracing::info!("canceled");
return;
}
native_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 save_file = match res {
native_dialog::FileDialogResponse::Selected(mut s) => {
s.remove(0)
}
native_dialog::FileDialogResponse::Cancel => {
tracing::info!("canceled");
return;
}
native_dialog::FileDialogResponse::Error(e) => {
tracing::error!("unexpected save file response {e:?}");
return;
}
};
tracing::info!("save {}", save_file.display());
});
}
],
)
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum CloseState {
Ask,
Asking,
Close,
}
fn confirm_close() -> impl WidgetHandler<WindowCloseRequestedArgs> {
let state = var(CloseState::Ask);
hn!(|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)
}
CloseState::Asking => args.propagation().stop(),
CloseState::Close => {}
}
})
}
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;
backdrop_blur = 2;
background_color = color_scheme_map(colors::WHITE.with_alpha(10.pct()), colors::BLACK.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 = color_scheme_map(colors::BLACK.with_alpha(90.pct()), colors::WHITE.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! {
focus_on_init = true;
child = Strong!("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());
cmd;
}
}
fn section(header: &'static str, items: impl UiNodeList) -> impl UiNode {
Stack! {
direction = StackDirection::top_to_bottom();
spacing = 5;
children = ui_vec![Text! {
txt = header;
font_weight = FontWeight::BOLD;
margin = (0, 4);
}].chain(items);
}
}
fn select<T: VarValue + PartialEq>(header: &'static str, selection: impl Var<T>, items: impl UiNodeList) -> impl UiNode {
Stack! {
direction = StackDirection::top_to_bottom();
spacing = 5;
toggle::selector = toggle::Selector::single(selection);
children = ui_vec![Text! {
txt = header;
font_weight = FontWeight::BOLD;
margin = (0, 4);
}].chain(items);
}
}
fn separator() -> impl UiNode {
Hr! {
color = rgba(1.0, 1.0, 1.0, 0.2);
margin = (0, 8);
line_style = LineStyle::Dashed;
}
}
fn logo_icon() -> impl UiNode {
let logo = Container! {
layout::size = 50;
layout::perspective = 125;
child = Stack! {
layout::transform_style = layout::TransformStyle::Preserve3D;
text::font_size = 48;
text::font_family = "Arial";
text::font_weight = FontWeight::EXTRA_BOLD;
text::txt_align = Align::CENTER;
text::font_color = colors::WHITE;
layout::transform = layout::Transform::new_rotate_y((-45).deg()).rotate_x((-35).deg()).translate_z(-25);
children = ui_vec![
Text! {
txt = "Z";
layout::padding = (-13, 0, 0, 0);
layout::transform = layout::Transform::new_translate_z(25);
widget::background_color = colors::RED.darken(50.pct());
widget::border = (0, 0, 4, 4), colors::WHITE;
},
Text! {
txt = "Z";
layout::padding = (-12, 0, 0, 0);
layout::transform = layout::Transform::new_translate_z(25).rotate_x(90.deg());
widget::background_color = colors::GREEN.darken(60.pct());
widget::border = (4, 0, 0, 4), colors::WHITE;
},
Text! {
txt = "g";
layout::padding = (-23, 0, 0, 0);
layout::transform = layout::Transform::new_translate_z(25).rotate_y(90.deg());
widget::background_color = colors::BLUE.darken(50.pct());
widget::border = (0, 4, 4, 0), colors::WHITE;
},
];
}
};
Container! {
layout::size = 68;
child_align = Align::CENTER;
padding = (-6, 0, 0, 0);
child = logo;
}
}