zng/examples/focus.rs

488 lines
16 KiB
Rust

//! Demonstrates the focus service, logical and directional navigation.
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use zng::{
button,
color::{color_scheme_map, filter::drop_shadow},
focus::{
alt_focus_scope, directional_nav, focus_click_behavior, focus_scope, focus_shortcut, focusable, is_focused, is_return_focus,
tab_index, tab_nav, DirectionalNav, FocusChangedArgs, FocusClickBehavior, FocusRequest, FocusTarget, TabIndex, TabNav,
FOCUS_CHANGED_EVENT,
},
font::FontName,
gesture::ClickArgs,
layout::{align, margin, padding},
prelude::*,
text::font_color,
var::ArcVar,
widget::{background_color, border, corner_radius, enabled, node::ArcNode},
window::FocusIndicator,
};
use zng::view_process::prebuilt as view_process;
fn main() {
examples_util::print_info();
view_process::init();
zng::app::crash_handler::init_debug();
// let rec = examples_util::record_profile("focus");
// view_process::run_same_process(app_main);
app_main();
// rec.finish();
}
fn app_main() {
APP.defaults().run_window(async {
WINDOW.id().set_name("main").unwrap();
trace_focus();
let window_enabled = var(true);
Window! {
title = "Focus Example";
enabled = window_enabled.clone();
widget::background = commands();
child = Stack! {
direction = StackDirection::top_to_bottom();
children = ui_vec![
alt_scope(),
Stack! {
direction = StackDirection::left_to_right();
margin = (50, 0, 0, 0);
align = Align::CENTER;
spacing = 10;
children = ui_vec![
tab_index_example(),
functions(window_enabled),
delayed_focus(),
]
}
];
};
// zng::window::inspector::show_center_points = true;
// zng::window::inspector::show_bounds = true;
// zng::window::inspector::show_hit_test = true;
// zng::window::inspector::show_directional_query = Some(zng::core::unit::Orientation2D::Below);
}
})
}
fn alt_scope() -> impl UiNode {
Stack! {
direction = StackDirection::left_to_right();
alt_focus_scope = true;
focus_click_behavior = FocusClickBehavior::Exit;
button::style_fn = Style! {
border = unset!;
corner_radius = unset!;
};
children = ui_vec![
button("alt", TabIndex::AUTO),
button("scope", TabIndex::AUTO),
];
}
}
fn tab_index_example() -> impl UiNode {
Stack! {
direction = StackDirection::top_to_bottom();
spacing = 5;
focus_shortcut = shortcut!('T');
children = ui_vec![
title("TabIndex (T)"),
button("Button A5", 5),
button("Button A4", 3),
button("Button A3", 2),
button("Button A1", 0),
button("Button A2", 0),
];
}
}
fn functions(window_enabled: ArcVar<bool>) -> impl UiNode {
Stack! {
direction = StackDirection::top_to_bottom();
spacing = 5;
focus_shortcut = shortcut!('F');
children = ui_vec![
title("Functions (F)"),
// New Window
Button! {
child = Text!("New Window");
on_click = hn!(|_| {
WINDOWS.open(async {
let _ = WINDOW.id().set_name("other");
Window! {
title = "Other Window";
focus_shortcut = shortcut!('W');
child = Stack! {
direction = StackDirection::top_to_bottom();
align = Align::CENTER;
spacing = 5;
children = ui_vec![
title("Other Window (W)"),
button("Button B5", 5),
button("Button B4", 3),
button("Button B3", 2),
button("Button B1", 0),
button("Button B2", 0),
]
};
}
});
});
},
// Detach Button
{
let detach_focused = ArcNode::new_cyclic(|wk| {
let btn = Button! {
child = Text!("Detach Button");
// focus_on_init = true;
on_click = hn!(|_| {
let wwk = wk.clone();
WINDOWS.open(async move {
Window! {
title = "Detached Button";
child_align = Align::CENTER;
child = wwk.upgrade().unwrap().take_on_init();
}
});
});
};
btn.boxed()
});
detach_focused.take_on_init().into_widget()
},
// Disable Window
disable_window(window_enabled.clone()),
// Overlay Scope
Button! {
child = Text!("Overlay Scope");
on_click = hn!(|_| {
LAYERS.insert(LayerIndex::TOP_MOST, overlay(window_enabled.clone()));
});
},
nested_focusables(),
]
}
}
fn disable_window(window_enabled: ArcVar<bool>) -> impl UiNode {
Button! {
child = Text!(window_enabled.map(|&e| if e { "Disable Window" } else { "Enabling in 1s..." }.into()));
layout::min_width = 140;
on_click = async_hn!(window_enabled, |_| {
window_enabled.set(false);
task::deadline(1.secs()).await;
window_enabled.set(true);
});
}
}
fn overlay(window_enabled: ArcVar<bool>) -> impl UiNode {
Container! {
id = "overlay";
widget::modal = true;
child_align = Align::CENTER;
child = Container! {
focus_scope = true;
tab_nav = TabNav::Cycle;
directional_nav = DirectionalNav::Cycle;
background_color = color_scheme_map(colors::BLACK.with_alpha(90.pct()), colors::WHITE.with_alpha(90.pct()));
drop_shadow = (0, 0), 4, colors::BLACK;
padding = 2;
child = Stack! {
direction = StackDirection::top_to_bottom();
children_align = Align::RIGHT;
children = ui_vec![
Text! {
txt = "Window scope is disabled so the overlay scope is the root scope.";
margin = 15;
},
Stack! {
direction = StackDirection::left_to_right();
spacing = 2;
children = ui_vec![
disable_window(window_enabled),
Button! {
child = Text!("Close");
on_click = hn!(|_| {
LAYERS.remove("overlay");
})
}
]
}
]
}
}
}
}
fn delayed_focus() -> impl UiNode {
Stack! {
direction = StackDirection::top_to_bottom();
spacing = 5;
focus_shortcut = shortcut!('D');
children = ui_vec![
title("Delayed 4s (D)"),
delayed_btn("Force Focus", || {
FOCUS.focus(FocusRequest {
target: FocusTarget::Direct { target: WidgetId::named("target") },
highlight: true,
force_window_focus: true,
window_indicator: None,
});
}),
delayed_btn("Info Indicator", || {
FOCUS.focus(FocusRequest {
target: FocusTarget::Direct { target: WidgetId::named("target") },
highlight: true,
force_window_focus: false,
window_indicator: Some(FocusIndicator::Info),
});
}),
delayed_btn("Critical Indicator", || {
FOCUS.focus(FocusRequest {
target: FocusTarget::Direct { target: WidgetId::named("target") },
highlight: true,
force_window_focus: false,
window_indicator: Some(FocusIndicator::Critical),
});
}),
Text! {
id = "target";
padding = (7, 15);
corner_radius = 4;
txt = "delayed target";
font_style = FontStyle::Italic;
txt_align = Align::CENTER;
background_color = color_scheme_map(rgb(30, 30, 30), rgb(225, 225, 225));
focusable = true;
when *#is_focused {
txt = "focused";
background_color = color_scheme_map(web_colors::DARK_GREEN, web_colors::LIGHT_GREEN);
}
},
]
}
}
fn delayed_btn(content: impl Into<Txt>, on_timeout: impl FnMut() + Send + 'static) -> impl UiNode {
use std::sync::Arc;
use task::parking_lot::Mutex;
let on_timeout = Arc::new(Mutex::new(Box::new(on_timeout)));
let enabled = var(true);
Button! {
child = Text!(content.into());
on_click = async_hn!(enabled, on_timeout, |_| {
enabled.set(false);
task::deadline(4.secs()).await;
let mut on_timeout = on_timeout.lock();
on_timeout();
enabled.set(true);
});
enabled;
}
}
fn title(txt: impl IntoVar<Txt>) -> impl UiNode {
Text! { txt; font_weight = FontWeight::BOLD; align = Align::CENTER; }
}
fn button(content: impl Into<Txt>, tab_index: impl Into<TabIndex>) -> impl UiNode {
let content = content.into();
let tab_index = tab_index.into();
Button! {
child = Text!(content.clone());
tab_index;
on_click = hn!(|_| {
tracing::info!("Clicked {content} {tab_index:?}")
});
}
}
fn commands() -> impl UiNode {
use zng::focus::cmd::*;
let cmds = [
FOCUS_NEXT_CMD,
FOCUS_PREV_CMD,
FOCUS_UP_CMD,
FOCUS_RIGHT_CMD,
FOCUS_DOWN_CMD,
FOCUS_LEFT_CMD,
FOCUS_ALT_CMD,
FOCUS_ENTER_CMD,
FOCUS_EXIT_CMD,
];
Stack! {
direction = StackDirection::top_to_bottom();
align = Align::BOTTOM_RIGHT;
padding = 10;
spacing = 8;
children_align = Align::RIGHT;
text::font_family = FontName::monospace();
font_color = colors::GRAY;
children = cmds.into_iter().map(|cmd| {
Text! {
txt = cmd.name_with_shortcut();
when *#{cmd.is_enabled()} {
font_color = color_scheme_map(colors::WHITE, colors::BLACK);
}
}.boxed()
}).collect::<Vec<_>>();
}
}
fn trace_focus() {
FOCUS_CHANGED_EVENT
.on_pre_event(app_hn!(|args: &FocusChangedArgs, _| {
if args.is_highlight_changed() {
tracing::info!("highlight: {}", args.highlight);
} else if args.is_widget_move() {
tracing::info!("focused {:?} moved", args.new_focus.as_ref().unwrap());
} else if args.is_enabled_change() {
tracing::info!("focused {:?} enabled/disabled", args.new_focus.as_ref().unwrap());
} else {
tracing::info!("{} -> {}", inspect::focus(&args.prev_focus), inspect::focus(&args.new_focus));
}
}))
.perm();
}
fn nested_focusables() -> impl UiNode {
Button! {
child = Text!("Nested Focusables");
on_click = hn!(|args: &ClickArgs| {
args.propagation().stop();
WINDOWS.focus_or_open("nested-focusables", async {
Window! {
title = "Focus Example - Nested Focusables";
color_scheme = color::ColorScheme::Dark;
background_color = web_colors::DIM_GRAY;
// zng::properties::inspector::show_center_points = true;
child_align = Align::CENTER;
child = Stack! {
direction = StackDirection::top_to_bottom();
spacing = 10;
children = ui_vec![
nested_focusables_group('a'),
nested_focusables_group('b'),
];
}
}
});
})
}
}
fn nested_focusables_group(g: char) -> impl UiNode {
Stack! {
direction = StackDirection::left_to_right();
align = Align::TOP;
spacing = 10;
children = (0..5).map(|n| nested_focusable(g, n, 0).boxed()).collect::<Vec<_>>()
}
}
fn nested_focusable(g: char, column: u8, row: u8) -> impl UiNode {
let nested = Text! {
txt = format!("Focusable {column}, {row}");
margin = 5;
};
Stack! {
id = formatx!("focusable-{g}-{column}-{row}");
padding = 2;
direction = StackDirection::top_to_bottom();
children = if row == 5 {
ui_vec![nested]
} else {
ui_vec![
nested,
nested_focusable(g, column, row + 1),
]
};
focusable = true;
corner_radius = 5;
border = 1, colors::RED.with_alpha(30.pct());
background_color = colors::RED.with_alpha(20.pct());
when *#is_focused {
background_color = web_colors::GREEN;
}
when *#is_return_focus {
border = 1, web_colors::LIME_GREEN;
}
}
}
#[cfg(debug_assertions)]
mod inspect {
use super::*;
pub fn focus(path: &Option<widget::info::InteractionPath>) -> String {
path.as_ref()
.map(|p| {
let frame = if let Ok(w) = WINDOWS.widget_tree(p.window_id()) {
w
} else {
return format!("<{p}>");
};
let widget = if let Some(w) = frame.get(p.widget_id()) {
w
} else {
return format!("<{p}>");
};
let wgt_mod = if let Some(b) = widget.inspector_info() {
b.builder.widget_type()
} else {
return format!("<{p}>");
};
if wgt_mod.path.ends_with("button") {
let txt = widget
.inspect_descendant("text")
.expect("expected text in button")
.inspect_property("txt")
.expect("expected txt property in text")
.live_debug(0)
.get();
format!("button({txt})")
} else if wgt_mod.path.ends_with("window") {
let title = widget.inspect_property("title").map(|p| p.live_debug(0).get()).unwrap_or_default();
format!("window({title})")
} else {
let focus_info = widget.into_focus_info(true, true);
if focus_info.is_alt_scope() {
format!("{}(is_alt_scope)", wgt_mod.name())
} else if focus_info.is_scope() {
format!("{}(is_scope)", wgt_mod.name())
} else {
format!("{}({})", wgt_mod.name(), p.widget_id())
}
}
})
.unwrap_or_else(|| "<none>".to_owned())
}
}
#[cfg(not(debug_assertions))]
mod inspect {
pub fn focus(path: &Option<zng::widget::info::InteractionPath>) -> String {
path.as_ref()
.map(|p| format!("{:?}", p.widget_id()))
.unwrap_or_else(|| "<none>".to_owned())
}
}