use kui_core::testing::{click_at, tab};
use kui_core::{
AccessAction, AccessRequest, Align, Color, Core, EditKey, EditOptions, FloatConfig, InputEvent,
Key, Mods, NodeSpec, Role, Size, TextStyle, UiEvent, Value, Vec2, WindowButton,
};
const H: f32 = 20.0;
struct Keys {
go: Key,
save: Key,
mute: Key,
vol: Key,
off: Key,
hidden: Key,
row: Key,
name: Key,
pane: Key,
styled: Key,
}
const FOCUS_BG: Color = Color {
r: 1.0,
g: 0.0,
b: 1.0,
a: 1.0,
};
fn frame(core: &mut Core, declare_pane: bool, autofocus_edit: bool) -> Keys {
let mut ui = core.frame(Size::new(200.0, 300.0), 1.0);
ui.window_title("Focus");
ui.configure_root(NodeSpec::column().fill());
let row = || NodeSpec::row().size(100.0, H);
ui.with_keyed("titlebar", row().window_drag(), |ui| {
ui.leaf_keyed(
"close",
NodeSpec::row()
.size(H, H)
.window_button(WindowButton::Close),
);
});
ui.text_in_keyed(
"heading",
row().role(Role::Heading),
"Controls",
TextStyle::new(12.0),
);
let go = ui.text_in_keyed("go", row().on_click("go"), "Go", TextStyle::new(12.0));
let save = ui.leaf_keyed("save", row().on_click("save").label("Save"));
let mute = ui.leaf_keyed(
"mute",
row()
.role(Role::Switch)
.checked(true)
.on_click("mute")
.label("Mute"),
);
let vol = ui.leaf_keyed(
"vol",
row()
.role(Role::Slider)
.on_drag("vol")
.label("Volume")
.value_now(3.0)
.value_min(0.0)
.value_max(10.0),
);
let off = ui.leaf_keyed(
"off",
row()
.on_click("nope")
.disabled(true)
.hover_bg(Color::WHITE)
.label("Off"),
);
let mut hidden = Key::ROOT;
ui.with_keyed("deco", row().role(Role::None), |ui| {
hidden = ui.leaf_keyed("hidden", NodeSpec::row().size(H, H).on_click("hidden"));
});
ui.leaf_keyed("plain", row());
let row_key = ui.leaf_keyed("row", row().focusable().label("Row"));
let name = ui.text_edit(
"name",
"hi",
&EditOptions {
autofocus: autofocus_edit,
..Default::default()
},
row().label("Name"),
);
let pane = ui.leaf_keyed("pane", row().on_key("pane"));
if declare_pane {
ui.take_key_focus(pane);
}
let styled = ui.leaf_keyed(
"styled",
row().on_click("styled").bg(Color::BLACK).focus_bg(FOCUS_BG),
);
ui.finish();
Keys {
go,
save,
mute,
vol,
off,
hidden,
row: row_key,
name,
pane,
styled,
}
}
fn key(core: &mut Core, k: EditKey) -> Vec<UiEvent> {
core.handle_input(InputEvent::Key(k, Mods::default()))
}
fn payloads(evs: &[UiEvent]) -> Vec<String> {
evs.iter()
.map(|e| match e.payload.as_str() {
Some(s) => s.to_string(),
None => format!(
"{}:{}",
e.kind().unwrap_or("?"),
e.payload.get_str("action").unwrap_or("")
),
})
.collect()
}
fn ring_count(core: &mut Core) -> usize {
core.output()
.0
.quads
.iter()
.filter(|q| q.color.a == 0.0 && q.border_w == 2.0 && q.border_color.a > 0.0)
.count()
}
#[test]
fn tab_walks_every_control_in_tree_order_and_wraps() {
let mut core = Core::new();
let k = frame(&mut core, false, false);
assert_eq!(core.focus(), None);
let ring = [k.go, k.save, k.mute, k.vol, k.row, k.name, k.pane];
let mut seen = Vec::new();
for _ in 0..ring.len() {
tab(&mut core, false);
seen.push(core.focus().unwrap());
}
assert_eq!(
seen, ring,
"chrome, the heading, the disabled button, decoration and plain boxes are not stops"
);
assert!(core.focus_visible(), "keyboard focus shows");
assert!(!seen.contains(&k.off) && !seen.contains(&k.hidden));
tab(&mut core, false);
assert_eq!(core.focus(), Some(k.pane), "a key sink keeps Tab");
core.focus_next(true);
assert_eq!(core.focus(), Some(k.styled));
assert!(core.is_focused(k.styled) && core.key_focus() == Some(k.styled));
tab(&mut core, false);
assert_eq!(core.focus(), Some(k.go));
tab(&mut core, true);
assert_eq!(core.focus(), Some(k.styled));
core.set_focus(Some(k.name));
assert_eq!(core.edit.focused(), Some(k.name));
tab(&mut core, true);
assert_eq!(core.focus(), Some(k.row));
assert_eq!(core.edit.focused(), None);
}
#[test]
fn a_view_steps_focus_onto_the_frame_it_is_declaring() {
let build = |core: &mut Core, step: Option<bool>| {
let mut ui = core.frame(Size::new(200.0, 200.0), 1.0);
match step {
Some(true) => ui.focus_next(),
Some(false) => ui.focus_prev(),
None => {}
}
for name in ["a", "b", "c"] {
ui.leaf_keyed(name, NodeSpec::row().focusable().size(100.0, H));
}
ui.finish();
};
let root = Key::ROOT;
let (a, c) = (root.str("a"), root.str("c"));
let mut immediate = Core::new();
{
let mut ui = immediate.frame(Size::new(200.0, 200.0), 1.0);
ui.core().focus_next(true);
for name in ["a", "b", "c"] {
ui.leaf_keyed(name, NodeSpec::row().focusable().size(100.0, H));
}
ui.finish();
}
assert_eq!(immediate.focus(), None, "no ring to walk mid-build");
let mut core = Core::new();
build(&mut core, Some(true));
assert_eq!(core.focus(), Some(a), "the deferred step found the ring");
{
let mut ui = core.frame(Size::new(200.0, 200.0), 1.0);
ui.focus(a);
ui.focus_prev();
for name in ["a", "b", "c"] {
ui.leaf_keyed(name, NodeSpec::row().focusable().size(100.0, H));
}
ui.finish();
}
assert_eq!(core.focus(), Some(c), "stepped back from a, wrapping");
build(&mut core, None);
assert_eq!(core.focus(), Some(c), "no step, no move");
}
#[test]
fn enter_and_space_press_the_focused_control() {
let mut core = Core::new();
let k = frame(&mut core, false, false);
core.set_focus(Some(k.go));
assert_eq!(payloads(&key(&mut core, EditKey::Enter)), ["go"]);
assert_eq!(
payloads(&core.handle_input(InputEvent::Text(" ".into()))),
["go"]
);
core.set_focus(Some(k.mute));
assert_eq!(
payloads(&core.handle_input(InputEvent::Text(" ".into()))),
["mute"]
);
core.set_focus(Some(k.row));
assert!(key(&mut core, EditKey::Enter).is_empty());
core.set_focus(Some(k.name));
assert!(
key(&mut core, EditKey::Enter)
.iter()
.all(|e| e.payload.get_str("kind") == Some("submit"))
);
core.set_focus(Some(k.pane));
assert!(key(&mut core, EditKey::Enter).is_empty());
assert!(core.handle_input(InputEvent::Text(" ".into())).is_empty());
core.set_focus(Some(k.go));
key(&mut core, EditKey::Escape);
assert_eq!(core.focus(), None);
}
#[test]
fn arrows_nudge_a_focused_slider_like_assistive_technology_does() {
let mut core = Core::new();
let k = frame(&mut core, false, false);
core.set_focus(Some(k.vol));
let up = key(&mut core, EditKey::Right);
assert_eq!(payloads(&up), ["access:increment"]);
assert_eq!(up[0].key, k.vol);
assert_eq!(up[0].payload.get_str("tag"), Some("vol"));
assert_eq!(payloads(&key(&mut core, EditKey::Up)), ["access:increment"]);
assert_eq!(
payloads(&key(&mut core, EditKey::Left)),
["access:decrement"]
);
assert_eq!(
payloads(&key(&mut core, EditKey::Down)),
["access:decrement"]
);
let req = core.handle_input(InputEvent::Access(AccessRequest::new(
k.vol,
AccessAction::Increment,
)));
assert_eq!(payloads(&req), ["access:increment"]);
core.set_focus(Some(k.go));
assert!(key(&mut core, EditKey::Right).is_empty());
}
#[test]
fn keyboard_focus_draws_a_ring_and_pointer_focus_does_not() {
let mut core = Core::new();
let k = frame(&mut core, false, false);
assert_eq!(ring_count(&mut core), 0);
tab(&mut core, false);
assert_eq!(core.focus(), Some(k.go));
frame(&mut core, false, false);
assert_eq!(
ring_count(&mut core),
1,
"Tab onto the button draws the ring"
);
let ring = core
.output()
.0
.quads
.iter()
.find(|q| q.color.a == 0.0 && q.border_w == 2.0)
.copied()
.unwrap();
assert_eq!(
(ring.rect.x, ring.rect.y, ring.rect.w, ring.rect.h),
(-2.0, 2.0 * H - 2.0, 104.0, H + 4.0)
);
click_at(&mut core, 50.0, 2.0 * H + 10.0);
assert_eq!(core.focus(), Some(k.go));
assert!(!core.focus_visible());
frame(&mut core, false, false);
assert_eq!(ring_count(&mut core), 0);
core.set_focus(Some(k.name));
tab(&mut core, false);
assert_eq!(core.focus(), Some(k.pane));
frame(&mut core, false, false);
assert_eq!(ring_count(&mut core), 0);
core.focus_next(false);
assert_eq!(core.focus(), Some(k.name));
frame(&mut core, false, false);
assert_eq!(ring_count(&mut core), 0);
}
#[test]
fn focus_bg_swaps_the_background_instead_of_a_ring() {
let mut core = Core::new();
let k = frame(&mut core, false, false);
let styled_quad = |core: &mut Core| {
core.output()
.0
.quads
.iter()
.find(|q| q.rect.y == 12.0 * H && q.rect.h == H)
.copied()
.unwrap()
};
assert_eq!(styled_quad(&mut core).color, Color::BLACK);
tab(&mut core, true);
assert_eq!(core.focus(), Some(k.styled));
frame(&mut core, false, false);
assert_eq!(styled_quad(&mut core).color, FOCUS_BG);
assert_eq!(ring_count(&mut core), 0, "a focus_bg node gets no ring");
click_at(&mut core, 50.0, 12.0 * H + 10.0);
frame(&mut core, false, false);
assert_eq!(styled_quad(&mut core).color, Color::BLACK);
}
#[test]
fn disabled_nodes_are_inert_and_say_so() {
let mut core = Core::new();
let k = frame(&mut core, false, false);
assert!(click_at(&mut core, 50.0, 6.0 * H + 10.0).is_empty());
assert_eq!(
core.focus(),
None,
"a press on a disabled node takes no focus"
);
core.set_focus(Some(k.off));
assert!(key(&mut core, EditKey::Enter).is_empty());
assert!(core.handle_input(InputEvent::Text(" ".into())).is_empty());
assert!(
core.handle_input(InputEvent::Access(AccessRequest::new(
k.off,
AccessAction::Click
)))
.is_empty()
);
core.handle_input(InputEvent::CursorMoved(Vec2::new(50.0, 6.0 * H + 10.0)));
assert!(core.is_hovered(k.off));
frame(&mut core, false, false);
let off_quad = core
.output()
.0
.quads
.iter()
.find(|q| q.rect.y == 6.0 * H && q.rect.h == H)
.copied();
assert!(
off_quad.is_none(),
"no hover_bg swap: the node stays unpainted"
);
let tree = core.access_tree();
let off = tree.get(k.off).unwrap();
assert!(off.disabled);
assert!(!off.supports(AccessAction::Click));
assert!(!off.supports(AccessAction::Focus));
let go = tree.get(k.go).unwrap();
assert!(!go.disabled);
assert!(go.supports(AccessAction::Click) && go.supports(AccessAction::Focus));
}
#[test]
fn assistive_technology_focuses_what_tab_would() {
let mut core = Core::new();
let k = frame(&mut core, false, false);
let tree = core.access_tree().clone();
for key in [k.go, k.save, k.mute, k.vol, k.row, k.name, k.pane, k.styled] {
let n = tree.get(key).unwrap();
assert!(
n.supports(AccessAction::Focus) && n.supports(AccessAction::Blur),
"{:?} advertises focus",
n.role
);
assert!(!n.focused);
}
assert_eq!(tree.focus, None);
let close = tree.get(Key::ROOT.str("titlebar").str("close")).unwrap();
assert!(
!close.supports(AccessAction::Focus),
"chrome is not a Tab stop"
);
core.handle_input(InputEvent::Access(AccessRequest::new(
k.mute,
AccessAction::Focus,
)));
assert_eq!(core.focus(), Some(k.mute));
assert!(core.focus_visible(), "a reader's focus shows like Tab's");
let tree = core.access_tree();
assert_eq!(tree.focus, Some(k.mute));
assert!(tree.get(k.mute).unwrap().focused);
tab(&mut core, false);
assert_eq!(core.access_tree().focus, Some(k.vol));
core.handle_input(InputEvent::Access(AccessRequest::new(
k.go,
AccessAction::Blur,
)));
assert_eq!(core.focus(), Some(k.vol));
core.handle_input(InputEvent::Access(AccessRequest::new(
k.hidden,
AccessAction::Focus,
)));
assert_eq!(core.focus(), Some(k.vol));
core.handle_input(InputEvent::Access(AccessRequest::new(
k.vol,
AccessAction::Blur,
)));
assert_eq!(core.focus(), None);
assert_eq!(core.access_tree().focus, None);
}
#[test]
fn a_repeated_declaration_takes_focus_once() {
let mut core = Core::new();
let k = frame(&mut core, true, false);
assert_eq!(core.focus(), Some(k.pane), "declared: focused");
assert!(!core.focus_visible(), "programmatic focus shows no ring");
core.focus_next(true);
assert_eq!(core.focus(), Some(k.styled));
frame(&mut core, true, false);
assert_eq!(core.focus(), Some(k.styled));
frame(&mut core, false, false);
assert_eq!(core.focus(), Some(k.styled));
frame(&mut core, true, false);
assert_eq!(core.focus(), Some(k.pane));
core.set_key_focus(None);
assert_eq!(core.focus(), None);
}
#[test]
fn autofocus_is_an_edge_that_takes_the_keyboard_only_while_nothing_holds_it() {
let mut core = Core::new();
let k = frame(&mut core, false, true);
assert_eq!(core.focus(), Some(k.name));
assert_eq!(core.edit.focused(), Some(k.name));
core.set_focus(Some(k.go));
frame(&mut core, false, true);
assert_eq!(core.focus(), Some(k.go), "a focused control keeps focus");
assert_eq!(core.edit.focused(), None);
core.set_focus(None);
frame(&mut core, false, true);
assert_eq!(core.focus(), None, "a blur stands: the flag asked once");
frame(&mut core, false, false);
frame(&mut core, false, true);
assert_eq!(core.focus(), Some(k.name), "an edge: autofocus takes it");
core.set_focus(Some(k.go));
frame(&mut core, false, false);
frame(&mut core, false, true);
assert_eq!(core.focus(), Some(k.go));
}
#[test]
fn a_press_moves_focus_to_what_it_hits() {
let mut core = Core::new();
let k = frame(&mut core, false, false);
assert_eq!(payloads(&click_at(&mut core, 50.0, 2.0 * H + 10.0)), ["go"]);
assert_eq!(core.focus(), Some(k.go));
assert!(click_at(&mut core, 50.0, 9.0 * H + 10.0).is_empty());
assert_eq!(core.focus(), Some(k.row));
click_at(&mut core, 50.0, 8.0 * H + 10.0);
assert_eq!(core.focus(), None);
click_at(&mut core, 50.0, 10.0 * H + 10.0);
assert_eq!(core.focus(), Some(k.name));
assert_eq!(core.edit.focused(), Some(k.name));
tab(&mut core, false);
assert_eq!(core.focus(), Some(k.pane));
assert_eq!(core.edit.focused(), None);
}
struct Modal {
open: Key,
behind: Key,
list: Key,
note: Key,
dialog: Key,
ok: Key,
reason: Key,
}
fn modal_frame(core: &mut Core, dialog: bool) -> Modal {
let mut ui = core.frame(Size::new(200.0, 300.0), 1.0);
ui.window_title("Modal");
ui.configure_root(NodeSpec::column().fill());
let row = || NodeSpec::row().size(100.0, H);
ui.with_keyed("titlebar", row().window_drag(), |ui| {
ui.leaf_keyed(
"close",
NodeSpec::row()
.size(H, H)
.window_button(WindowButton::Close),
);
});
let open = ui.leaf_keyed("open", row().on_click("open").label("Open"));
let list = ui.with_keyed(
"list",
NodeSpec::column().size(100.0, 2.0 * H).scroll_y(),
|ui| {
for n in 0..4 {
ui.leaf_keyed(["a", "b", "c", "d"][n], row().on_click("row").label("Row"));
}
},
);
let behind = Key::ROOT.str("list").str("a");
let note = ui.text_edit("note", "hi", &EditOptions::default(), row().label("Note"));
let mut ok = Key::ROOT;
let mut reason = Key::ROOT;
let mut dlg = Key::ROOT;
if dialog {
dlg = ui.with_keyed(
"dialog",
NodeSpec::column()
.size(100.0, 3.0 * H)
.float(FloatConfig::viewport().inside(Align::End, Align::End))
.modal("dlg")
.label("Settings"),
|ui| {
ok = ui.leaf_keyed("ok", row().on_click("ok").label("OK"));
reason = ui.text_edit("reason", "", &EditOptions::default(), row().label("Reason"));
},
);
}
ui.finish();
Modal {
open,
behind,
list,
note,
dialog: dlg,
ok,
reason,
}
}
const IN_MODAL: (f32, f32) = (150.0, 300.0 - 3.0 * H + H / 2.0);
const MODAL_BG: (f32, f32) = (150.0, 300.0 - H / 2.0);
const BEHIND: (f32, f32) = (50.0, H + H / 2.0);
#[test]
fn everything_outside_the_modal_is_inert() {
let mut core = Core::new();
let m = modal_frame(&mut core, true);
assert_eq!(core.modal(), Some(m.dialog));
let out = click_at(&mut core, BEHIND.0, BEHIND.1);
assert_eq!(payloads(&out), ["dismiss:"], "no `open`, one dismiss");
assert_eq!(out[0].payload.get_str("reason"), Some("outside"));
assert_eq!(out[0].key, m.dialog);
assert_eq!(core.focus(), Some(m.ok), "focus stays in the modal");
core.handle_input(InputEvent::CursorMoved(Vec2::new(BEHIND.0, BEHIND.1)));
assert!(!core.is_hovered(m.open));
core.set_focus(Some(m.open));
assert!(key(&mut core, EditKey::Enter).is_empty());
assert!(core.handle_input(InputEvent::Text(" ".into())).is_empty());
let out = core.handle_input(InputEvent::Access(AccessRequest::new(
m.behind,
AccessAction::Click,
)));
assert_eq!(payloads(&out), ["dismiss:"]);
assert_eq!(out[0].key, m.dialog);
let before = core.scroll.offset(m.list);
core.handle_input(InputEvent::Scroll(Vec2::new(0.0, -30.0)));
assert_eq!(core.scroll.offset(m.list), before, "the list stays put");
assert_eq!(
payloads(&click_at(&mut core, IN_MODAL.0, IN_MODAL.1)),
["ok"]
);
assert_eq!(core.focus(), Some(m.ok));
let m = modal_frame(&mut core, false);
assert_eq!(core.modal(), None);
assert_eq!(payloads(&click_at(&mut core, BEHIND.0, BEHIND.1)), ["open"]);
assert_eq!(core.focus(), Some(m.open));
core.handle_input(InputEvent::CursorMoved(Vec2::new(50.0, 3.0 * H)));
core.handle_input(InputEvent::Scroll(Vec2::new(0.0, -30.0)));
assert!(core.scroll.offset(m.list).y > 0.0);
}
#[test]
fn window_chrome_stays_live_behind_a_modal() {
let mut core = Core::new();
let m = modal_frame(&mut core, true);
let out = click_at(&mut core, H / 2.0, H / 2.0);
assert!(payloads(&out).is_empty(), "chrome emits no app event");
assert_eq!(core.take_window_commands().len(), 1, "the window closes");
assert_eq!(core.focus(), Some(m.ok), "and focus stays in the modal");
}
#[test]
fn escape_asks_the_modal_to_go_away_and_does_nothing_else() {
let mut core = Core::new();
let m = modal_frame(&mut core, true);
let out = key(&mut core, EditKey::Escape);
assert_eq!(payloads(&out), ["dismiss:"]);
assert_eq!(out[0].key, m.dialog);
assert_eq!(out[0].payload.get_str("reason"), Some("escape"));
assert_eq!(
out[0].payload.get_str("tag"),
Some("dlg"),
"the modal's tag rides along"
);
assert_eq!(core.focus(), Some(m.ok), "Escape does not let go");
core.set_focus(Some(m.reason));
let out = key(&mut core, EditKey::Escape);
assert_eq!(payloads(&out), ["dismiss:"]);
assert_eq!(core.focus(), Some(m.reason));
assert_eq!(core.edit.focused(), Some(m.reason));
let m = modal_frame(&mut core, false);
core.set_focus(Some(m.note));
assert!(payloads(&key(&mut core, EditKey::Escape)).is_empty());
assert_eq!(core.focus(), None);
}
#[test]
fn a_press_on_the_modal_itself_is_not_outside_it() {
let mut core = Core::new();
let m = modal_frame(&mut core, true);
let out = click_at(&mut core, MODAL_BG.0, MODAL_BG.1);
assert!(payloads(&out).is_empty(), "{:?}", payloads(&out));
assert_eq!(core.modal(), Some(m.dialog));
}
#[test]
fn a_modal_without_a_tag_still_dismisses() {
let mut core = Core::new();
let mut ui = core.frame(Size::new(200.0, 300.0), 1.0);
ui.configure_root(NodeSpec::column().fill());
let menu = ui.leaf_keyed(
"menu",
NodeSpec::column()
.size(100.0, H)
.float(FloatConfig::viewport())
.modal(Value::Null)
.label("Menu"),
);
ui.finish();
let out = key(&mut core, EditKey::Escape);
assert_eq!(out.len(), 1);
assert_eq!(out[0].key, menu);
assert_eq!(out[0].kind(), Some("dismiss"));
assert_eq!(out[0].payload.get("tag"), None, "a null tag carries none");
}
#[test]
fn the_access_tree_reports_the_modal_and_derives_a_dialog() {
let mut core = Core::new();
let m = modal_frame(&mut core, true);
let tree = core.access_tree().clone();
let dlg = tree.get(m.dialog).unwrap();
assert_eq!(dlg.role, Role::Dialog, "a modal box is a dialog");
assert_eq!(dlg.name.as_deref(), Some("Settings"));
assert!(dlg.modal, "aria-modal");
assert_eq!(tree.focus, Some(m.ok), "the reader's cursor is inside it");
assert!(tree.get(m.open).is_some());
assert_eq!(tree.nodes.iter().filter(|n| n.modal).count(), 1);
let hash = tree.hash;
modal_frame(&mut core, false);
let tree = core.access_tree();
assert!(!tree.nodes.iter().any(|n| n.modal));
assert_ne!(tree.hash, hash);
}
#[test]
fn a_modal_without_a_name_warns() {
let dialog = |label: Option<&'static str>| {
let mut core = Core::new();
let mut ui = core.frame(Size::new(200.0, 300.0), 1.0);
ui.configure_root(NodeSpec::column().fill());
let mut spec = NodeSpec::column()
.size(100.0, 2.0 * H)
.float(FloatConfig::viewport())
.modal("dlg");
if let Some(label) = label {
spec = spec.label(label);
}
let key = ui.with_keyed("dialog", spec, |ui| {
ui.text("Delete everything?", TextStyle::default());
});
ui.finish();
(core.take_warnings(), key)
};
let (ws, key) = dialog(None);
let named: Vec<_> = ws
.iter()
.filter(|w| w.code == kui_core::diag::MODAL_WITHOUT_NAME)
.collect();
assert_eq!(named.len(), 1);
assert_eq!(named[0].key, key);
assert!(named[0].message.contains("`label`"), "{}", named[0].message);
let (ws, _) = dialog(Some("Confirm"));
assert!(
!ws.iter()
.any(|w| w.code == kui_core::diag::MODAL_WITHOUT_NAME),
"a labelled dialog says nothing: {ws:?}"
);
}
#[test]
fn a_modal_that_is_not_floated_warns() {
let mut core = Core::new();
let mut ui = core.frame(Size::new(200.0, 300.0), 1.0);
ui.configure_root(NodeSpec::column().fill());
let sheet = ui.leaf_keyed(
"sheet",
NodeSpec::column()
.size(100.0, H)
.modal(Value::Null)
.label("Sheet"),
);
ui.leaf_keyed("after", NodeSpec::row().height(H));
ui.finish();
let ws = core.take_warnings();
let modal: Vec<_> = ws
.iter()
.filter(|w| w.code == kui_core::diag::MODAL_BEHIND_CONTENT)
.collect();
assert_eq!(modal.len(), 1);
assert_eq!(modal[0].key, sheet);
let mut core = Core::new();
modal_frame(&mut core, true);
assert!(
!core
.take_warnings()
.iter()
.any(|w| w.code == kui_core::diag::MODAL_BEHIND_CONTENT)
);
}
#[test]
fn a_scrollbar_behind_a_modal_is_drawn_but_not_grabbable() {
let mut core = Core::new();
let m = modal_frame(&mut core, true);
let track = (99.0, 4.0 * H - 4.0);
let out = click_at(&mut core, track.0, track.1);
assert_eq!(
core.scroll.offset(m.list).y,
0.0,
"the track press did nothing"
);
assert_eq!(
payloads(&out),
["dismiss:"],
"an inert bar is not a hit region: the press is outside the modal"
);
assert_eq!(core.focus(), Some(m.ok));
let m = modal_frame(&mut core, false);
let out = click_at(&mut core, track.0, track.1);
assert!(
payloads(&out).is_empty(),
"a scrollbar press is not a click"
);
assert!(
core.scroll.offset(m.list).y > 0.0,
"the track press scrolled"
);
}
#[test]
fn a_float_declared_before_a_modal_paints_over_it_too() {
let mut core = Core::new();
let mut ui = core.frame(Size::new(200.0, 300.0), 1.0);
ui.configure_root(NodeSpec::column().fill());
ui.leaf_keyed(
"hud",
NodeSpec::row().size(50.0, H).float(FloatConfig::viewport()),
);
let sheet = ui.leaf_keyed(
"sheet",
NodeSpec::column()
.size(100.0, H)
.modal(Value::Null)
.label("Sheet"),
);
ui.finish();
let ws = core.take_warnings();
let modal: Vec<_> = ws
.iter()
.filter(|w| w.code == kui_core::diag::MODAL_BEHIND_CONTENT)
.collect();
assert_eq!(modal.len(), 1);
assert_eq!(modal[0].key, sheet);
let mut core = Core::new();
let mut ui = core.frame(Size::new(200.0, 300.0), 1.0);
ui.configure_root(NodeSpec::column().fill());
ui.with_keyed(
"sheet",
NodeSpec::column()
.size(100.0, H)
.modal(Value::Null)
.label("Sheet"),
|ui| {
ui.leaf_keyed(
"menu",
NodeSpec::row().size(50.0, H).float(FloatConfig::viewport()),
);
},
);
ui.finish();
assert!(
!core
.take_warnings()
.iter()
.any(|w| w.code == kui_core::diag::MODAL_BEHIND_CONTENT)
);
}
fn drag_frame(core: &mut Core, dialog: bool) -> (Key, Key) {
let mut ui = core.frame(Size::new(200.0, 300.0), 1.0);
ui.configure_root(NodeSpec::column().fill());
let row = || NodeSpec::row().size(100.0, H);
let handle = ui.leaf_keyed("handle", row().on_drag("split"));
let button = ui.leaf_keyed("button", row().on_click("go"));
if dialog {
ui.with_keyed(
"dialog",
NodeSpec::column()
.size(100.0, H)
.float(FloatConfig::viewport().inside(Align::End, Align::End))
.modal(Value::Null)
.label("Wait"),
|ui| {
ui.leaf_keyed("ok", row().on_click("ok"));
},
);
}
ui.finish();
(handle, button)
}
fn drag_phases(evs: &[UiEvent]) -> Vec<String> {
evs.iter()
.filter(|e| e.kind() == Some("drag"))
.map(|e| e.payload.get_str("phase").unwrap_or("?").to_string())
.collect()
}
#[test]
fn a_drag_in_flight_when_a_modal_appears_still_ends() {
let mut core = Core::new();
drag_frame(&mut core, false);
let mut all = Vec::new();
all.extend(core.handle_input(InputEvent::CursorMoved(Vec2::new(50.0, H / 2.0))));
all.extend(core.handle_input(InputEvent::mouse_down(1)));
all.extend(core.handle_input(InputEvent::CursorMoved(Vec2::new(60.0, H / 2.0))));
assert_eq!(drag_phases(&all), ["start", "move"]);
drag_frame(&mut core, true);
let mut after = Vec::new();
after.extend(core.handle_input(InputEvent::CursorMoved(Vec2::new(70.0, H / 2.0))));
after.extend(core.handle_input(InputEvent::mouse_up()));
assert_eq!(
drag_phases(&after),
["move", "end"],
"the capture runs to release"
);
assert!(
!after.iter().any(|e| e.payload.as_str().is_some()),
"and a release after a drag is not a click"
);
let out = click_at(&mut core, 50.0, H / 2.0);
assert!(drag_phases(&out).is_empty());
assert_eq!(payloads(&out), ["dismiss:"]);
}
#[test]
fn a_press_behind_released_after_a_modal_appears_is_not_a_click() {
let mut core = Core::new();
let (_, button) = drag_frame(&mut core, false);
core.handle_input(InputEvent::CursorMoved(Vec2::new(50.0, H + H / 2.0)));
core.handle_input(InputEvent::mouse_down(1));
assert!(core.is_pressed(button));
drag_frame(&mut core, true);
assert!(!core.is_hovered(button));
let out = core.handle_input(InputEvent::mouse_up());
assert!(payloads(&out).is_empty(), "{:?}", payloads(&out));
}
fn rename_frame(
core: &mut Core,
nodes: &[&str],
editing: bool,
focus_on: Option<&str>,
) -> Vec<Key> {
let mut ui = core.frame(Size::new(200.0, 300.0), 1.0);
ui.configure_root(NodeSpec::column().fill());
let row = || NodeSpec::row().size(100.0, H);
let keys: Vec<Key> = nodes
.iter()
.map(|n| ui.leaf_keyed(n, row().focusable().label(*n)))
.collect();
if let Some(name) = focus_on {
let i = nodes
.iter()
.position(|n| n == &name)
.expect("a declared node");
ui.take_key_focus(keys[i]);
}
if editing {
ui.with_keyed(
"rename",
NodeSpec::column()
.size(100.0, 2.0 * H)
.float(FloatConfig::viewport().inside(Align::End, Align::End))
.modal("rename")
.label("Rename"),
|ui| {
ui.text_edit("field", "", &EditOptions::default(), row().label("Name"));
},
);
}
ui.finish();
keys
}
#[test]
fn a_key_focus_edge_beats_the_focus_the_modal_gives_back() {
let mut core = Core::new();
let k = rename_frame(&mut core, &["a"], false, Some("a"));
let a = k[0];
assert_eq!(core.focus(), Some(a));
let k = rename_frame(&mut core, &["a", "b"], true, None);
let b = k[1];
assert_eq!(core.focus(), Some(Key::ROOT.str("rename").str("field")));
rename_frame(&mut core, &["a", "b"], false, Some("b"));
assert_eq!(
core.focus(),
Some(b),
"the edge stands, the restore is dropped"
);
assert_ne!(core.focus(), Some(a));
}
#[test]
fn a_modal_closing_over_no_edge_still_gives_the_focus_back() {
let mut core = Core::new();
let k = rename_frame(&mut core, &["a", "b"], false, Some("a"));
let a = k[0];
core.set_focus(Some(k[1]));
rename_frame(&mut core, &["a", "b"], true, Some("a"));
assert_eq!(core.focus(), Some(Key::ROOT.str("rename").str("field")));
rename_frame(&mut core, &["a", "b"], false, Some("a"));
assert_eq!(core.focus(), Some(k[1]), "back where the editor found it");
core.set_focus(Some(a));
rename_frame(&mut core, &["a", "b"], true, None);
rename_frame(&mut core, &["a", "b"], false, None);
assert_eq!(core.focus(), Some(a));
}
#[test]
fn an_imperative_focus_beats_the_focus_the_modal_gives_back() {
let mut core = Core::new();
let k = rename_frame(&mut core, &["a", "b"], false, Some("a"));
let (a, b) = (k[0], k[1]);
rename_frame(&mut core, &["a", "b"], true, None);
assert_eq!(core.focus(), Some(Key::ROOT.str("rename").str("field")));
core.set_focus(Some(b));
rename_frame(&mut core, &["a", "b"], false, None);
assert_eq!(
core.focus(),
Some(b),
"the app's move stands over the restore"
);
core.set_focus(Some(a));
rename_frame(&mut core, &["a", "b"], true, None);
let mut ui = core.frame(Size::new(200.0, 300.0), 1.0);
ui.configure_root(NodeSpec::column().fill());
let row = || NodeSpec::row().size(100.0, H);
ui.leaf_keyed("a", row().focusable().label("a"));
ui.leaf_keyed("b", row().focusable().label("b"));
ui.focus(b);
ui.finish();
assert_eq!(core.focus(), Some(b));
core.set_focus(Some(a));
rename_frame(&mut core, &["a", "b"], true, None);
let field = Key::ROOT.str("rename").str("field");
assert_eq!(core.focus(), Some(field));
rename_frame(&mut core, &["a", "b"], true, None);
rename_frame(&mut core, &["a", "b"], false, None);
assert_eq!(core.focus(), Some(a), "no app move since: the restore");
}
#[test]
fn a_key_acting_on_pointer_focus_shows_it() {
let mut core = Core::new();
let k = frame(&mut core, false, false);
click_at(&mut core, 50.0, 2.0 * H + 10.0);
assert_eq!(core.focus(), Some(k.go));
assert!(!core.focus_visible(), "the click alone shows nothing");
assert_eq!(
payloads(&core.handle_input(InputEvent::Text(" ".into()))),
["go"]
);
assert!(core.focus_visible(), "Space pressed it: say which one");
frame(&mut core, false, false);
assert_eq!(ring_count(&mut core), 1);
click_at(&mut core, 50.0, 4.0 * H + 10.0);
assert_eq!(core.focus(), Some(k.mute));
assert!(!core.focus_visible());
assert_eq!(payloads(&key(&mut core, EditKey::Enter)), ["mute"]);
assert!(core.focus_visible(), "Enter pressed it");
click_at(&mut core, 50.0, 5.0 * H + 10.0);
assert_eq!(core.focus(), Some(k.vol));
assert!(!core.focus_visible());
assert_eq!(
payloads(&key(&mut core, EditKey::Right)),
["access:increment"]
);
assert!(core.focus_visible(), "the slider moved: say which one");
click_at(&mut core, 50.0, 2.0 * H + 10.0);
assert_eq!(core.focus(), Some(k.go));
assert!(key(&mut core, EditKey::Right).is_empty());
assert!(!core.focus_visible(), "nothing acted: nothing to show");
key(&mut core, EditKey::Escape);
assert_eq!(core.focus(), None);
frame(&mut core, false, false);
assert_eq!(ring_count(&mut core), 0);
}
#[test]
fn the_root_is_never_a_tab_stop() {
let mut core = Core::new();
let mut go = Key::ROOT;
let build = |core: &mut Core, go: &mut Key| {
let mut ui = core.frame(Size::new(200.0, 100.0), 1.0);
ui.configure_root(NodeSpec::column().fill().on_key("root"));
*go = ui.leaf_keyed("go", NodeSpec::row().height(H).on_click("go"));
ui.finish();
};
build(&mut core, &mut go);
tab(&mut core, false);
assert_eq!(core.focus(), Some(go));
tab(&mut core, false);
assert_eq!(
core.focus(),
Some(go),
"one stop: the root is not the other"
);
core.set_focus(Some(Key::ROOT));
build(&mut core, &mut go);
let evs = core.handle_input(InputEvent::KeyDown(kui_core::KeyPress::new(
kui_core::KeyCode::Char('x'),
Default::default(),
)));
assert_eq!(
evs[0].payload.get_str("tag"),
Some("root"),
"a focused root sink keeps the keyboard"
);
}
#[test]
fn a_stroke_declared_before_an_in_flow_modal_is_not_content_over_it() {
let build = |core: &mut Core, viewport: bool| {
let mut ui = core.frame(Size::new(200.0, 300.0), 1.0);
ui.configure_root(NodeSpec::column().fill());
ui.with_keyed("bar", NodeSpec::row().size(200.0, H), |ui| {
let mut spec = NodeSpec::row();
if viewport {
spec = spec.float(kui_core::FloatConfig::viewport());
}
ui.line(
Vec2::new(4.0, 4.0),
Vec2::new(16.0, 16.0),
kui_core::Stroke::new(2.0, Color::WHITE),
spec,
);
});
ui.leaf_keyed(
"sheet",
NodeSpec::column()
.size(100.0, H)
.modal(Value::Null)
.label("Sheet"),
);
ui.finish();
core.take_warnings()
.iter()
.filter(|w| w.code == kui_core::diag::MODAL_BEHIND_CONTENT)
.count()
};
let mut core = Core::new();
assert_eq!(
build(&mut core, false),
0,
"a stroke in the bar's layer is under the modal"
);
let mut core = Core::new();
assert_eq!(
build(&mut core, true),
1,
"a viewport-anchored stroke is a layer over it"
);
}