use kui_core::{Core, InputEvent, Key, NodeSpec, Overscroll, ScrollAxes, Size, UiEvent, Vec2};
const VIEW: Size = Size { w: 300.0, h: 200.0 };
fn gesture(core: &mut Core, dx: f32, dy: f32, begins: bool) -> Vec<UiEvent> {
core.handle_input(InputEvent::ScrollGesture {
delta: Vec2::new(dx, dy),
begins,
})
}
fn scrolls(out: &[UiEvent], key: Key) -> Vec<(f64, f64)> {
out.iter()
.filter(|e| e.key == key && e.kind() == Some("scroll"))
.map(|e| {
(
e.payload.get_float("dx").unwrap(),
e.payload.get_float("dy").unwrap(),
)
})
.collect()
}
fn strip(core: &mut Core, axes: ScrollAxes) {
let mut ui = core.frame(VIEW, 1.0);
ui.with_keyed(
"strip",
NodeSpec::row().size(300.0, 200.0).scroll_x(),
|ui| {
ui.with_keyed(
"list",
NodeSpec::column().size(150.0, 200.0).scroll_y(),
|ui| {
ui.leaf(NodeSpec::column().size(150.0, 800.0));
},
);
ui.with_keyed(
"term",
NodeSpec::column()
.size(150.0, 200.0)
.on_scroll("term")
.scroll_axes(axes),
|_| {},
);
ui.leaf(NodeSpec::column().size(600.0, 200.0));
},
);
ui.finish();
}
fn keys(core: &mut Core) -> (Key, Key, Key) {
(
core.key_of("strip").unwrap(),
core.key_of("list").unwrap(),
core.key_of("term").unwrap(),
)
}
#[test]
fn a_sideways_swipe_keeps_the_strip_when_a_terminal_comes_under_the_pointer() {
let mut core = Core::new();
strip(&mut core, ScrollAxes::Both);
strip(&mut core, ScrollAxes::Both);
let (strip_k, _, term) = keys(&mut core);
core.handle_input(InputEvent::CursorMoved(Vec2::new(130.0, 50.0)));
gesture(&mut core, -30.0, 0.0, true);
strip(&mut core, ScrollAxes::Both);
assert_eq!(core.scroll_offset(strip_k), Vec2::new(30.0, 0.0));
for _ in 0..3 {
let out = gesture(&mut core, -30.0, 0.0, false);
assert!(
scrolls(&out, term).is_empty(),
"latched: not the terminal's"
);
strip(&mut core, ScrollAxes::Both);
}
assert_eq!(core.scroll_offset(strip_k), Vec2::new(120.0, 0.0));
let out = gesture(&mut core, -30.0, 0.0, true);
assert_eq!(scrolls(&out, term), vec![(-30.0, 0.0)]);
strip(&mut core, ScrollAxes::Both);
assert_eq!(core.scroll_offset(strip_k), Vec2::new(120.0, 0.0));
}
#[test]
fn a_y_only_handler_passes_a_sideways_gesture_to_the_strip() {
let mut core = Core::new();
strip(&mut core, ScrollAxes::Y);
strip(&mut core, ScrollAxes::Y);
let (strip_k, _, term) = keys(&mut core);
core.handle_input(InputEvent::CursorMoved(Vec2::new(200.0, 50.0)));
let out = gesture(&mut core, -25.0, 0.0, true);
assert!(scrolls(&out, term).is_empty());
strip(&mut core, ScrollAxes::Y);
assert_eq!(core.scroll_offset(strip_k), Vec2::new(25.0, 0.0));
let out = gesture(&mut core, 0.0, -12.0, true);
assert_eq!(scrolls(&out, term), vec![(0.0, -12.0)]);
strip(&mut core, ScrollAxes::Y);
assert_eq!(core.scroll_offset(strip_k), Vec2::new(25.0, 0.0));
let out = core.handle_input(InputEvent::Scroll(Vec2::new(-5.0, -6.0)));
assert_eq!(scrolls(&out, term), vec![(0.0, -6.0)]);
strip(&mut core, ScrollAxes::Y);
assert_eq!(core.scroll_offset(strip_k), Vec2::new(30.0, 0.0));
}
#[test]
fn a_gesture_on_the_other_axis_goes_to_the_ancestor() {
let mut core = Core::new();
strip(&mut core, ScrollAxes::Both);
strip(&mut core, ScrollAxes::Both);
let (strip_k, list, _) = keys(&mut core);
core.handle_input(InputEvent::CursorMoved(Vec2::new(50.0, 50.0)));
gesture(&mut core, 0.0, -40.0, true);
strip(&mut core, ScrollAxes::Both);
assert_eq!(core.scroll_offset(list), Vec2::new(0.0, 40.0));
assert_eq!(core.scroll_offset(strip_k), Vec2::ZERO);
gesture(&mut core, -20.0, 0.0, false);
strip(&mut core, ScrollAxes::Both);
assert_eq!(core.scroll_offset(strip_k), Vec2::new(20.0, 0.0));
gesture(&mut core, 0.0, -10.0, false);
strip(&mut core, ScrollAxes::Both);
assert_eq!(core.scroll_offset(list), Vec2::new(0.0, 50.0));
gesture(&mut core, -10.0, 0.0, true);
strip(&mut core, ScrollAxes::Both);
assert_eq!(core.scroll_offset(strip_k), Vec2::new(30.0, 0.0));
assert_eq!(core.scroll_offset(list), Vec2::new(0.0, 50.0));
}
fn nested(core: &mut Core, contain: bool) {
let mut ui = core.frame(VIEW, 1.0);
ui.with_keyed(
"page",
NodeSpec::column().size(300.0, 200.0).scroll_y(),
|ui| {
ui.leaf(NodeSpec::column().size(300.0, 40.0));
let mut inner = NodeSpec::column().size(300.0, 100.0).scroll_y();
if contain {
inner = inner.overscroll(Overscroll::Contain);
}
ui.with_keyed("inner", inner, |ui| {
ui.leaf(NodeSpec::column().size(300.0, 300.0));
});
ui.leaf(NodeSpec::column().size(300.0, 600.0));
},
);
ui.finish();
}
fn nested_keys(core: &mut Core) -> (Key, Key) {
(core.key_of("page").unwrap(), core.key_of("inner").unwrap())
}
#[test]
fn a_gesture_starting_at_a_limit_chains_to_the_scroller_around() {
let mut core = Core::new();
nested(&mut core, false);
nested(&mut core, false);
let (page, inner) = nested_keys(&mut core);
core.set_scroll(inner, Vec2::new(0.0, 1e9));
nested(&mut core, false);
assert_eq!(core.scroll_offset(inner), Vec2::new(0.0, 200.0));
core.handle_input(InputEvent::CursorMoved(Vec2::new(50.0, 80.0)));
gesture(&mut core, 0.0, -30.0, true);
nested(&mut core, false);
assert_eq!(core.scroll_offset(page), Vec2::new(0.0, 30.0));
assert_eq!(core.scroll_offset(inner), Vec2::new(0.0, 200.0));
gesture(&mut core, 0.0, 50.0, true);
nested(&mut core, false);
assert_eq!(core.scroll_offset(inner), Vec2::new(0.0, 150.0));
assert_eq!(core.scroll_offset(page), Vec2::new(0.0, 30.0));
core.set_scroll(inner, Vec2::ZERO);
nested(&mut core, false);
gesture(&mut core, 0.0, 10.0, true);
nested(&mut core, false);
assert_eq!(core.scroll_offset(page), Vec2::new(0.0, 20.0));
core.set_scroll(inner, Vec2::new(0.0, 1e9));
nested(&mut core, false);
core.handle_input(InputEvent::Scroll(Vec2::new(0.0, -5.0)));
nested(&mut core, false);
assert_eq!(core.scroll_offset(page), Vec2::new(0.0, 25.0));
}
#[test]
fn a_gesture_reaching_a_limit_midway_stops_there() {
let mut core = Core::new();
nested(&mut core, false);
nested(&mut core, false);
let (page, inner) = nested_keys(&mut core);
core.set_scroll(inner, Vec2::new(0.0, 180.0));
nested(&mut core, false);
core.handle_input(InputEvent::CursorMoved(Vec2::new(50.0, 80.0)));
gesture(&mut core, 0.0, -15.0, true);
nested(&mut core, false);
assert_eq!(core.scroll_offset(inner), Vec2::new(0.0, 195.0));
for _ in 0..4 {
gesture(&mut core, 0.0, -15.0, false);
nested(&mut core, false);
}
assert_eq!(core.scroll_offset(inner), Vec2::new(0.0, 200.0));
assert_eq!(core.scroll_offset(page), Vec2::ZERO, "no chaining midway");
gesture(&mut core, 0.0, 20.0, false);
nested(&mut core, false);
assert_eq!(core.scroll_offset(inner), Vec2::new(0.0, 180.0));
core.set_scroll(inner, Vec2::new(0.0, 1e9));
nested(&mut core, false);
gesture(&mut core, 0.0, -10.0, true);
nested(&mut core, false);
assert_eq!(core.scroll_offset(page), Vec2::new(0.0, 10.0));
}
#[test]
fn contain_keeps_a_gesture_at_the_limit() {
let mut core = Core::new();
nested(&mut core, true);
nested(&mut core, true);
let (page, inner) = nested_keys(&mut core);
core.set_scroll(inner, Vec2::new(0.0, 1e9));
nested(&mut core, true);
core.handle_input(InputEvent::CursorMoved(Vec2::new(50.0, 80.0)));
gesture(&mut core, 0.0, -30.0, true);
nested(&mut core, true);
assert_eq!(core.scroll_offset(page), Vec2::ZERO, "contained");
assert_eq!(core.scroll_offset(inner), Vec2::new(0.0, 200.0));
core.handle_input(InputEvent::Scroll(Vec2::new(0.0, -30.0)));
nested(&mut core, true);
assert_eq!(core.scroll_offset(page), Vec2::ZERO, "a notch too");
gesture(&mut core, 0.0, 30.0, false);
nested(&mut core, true);
assert_eq!(core.scroll_offset(inner), Vec2::new(0.0, 170.0));
core.handle_input(InputEvent::CursorMoved(Vec2::new(50.0, 180.0)));
gesture(&mut core, 0.0, -30.0, true);
nested(&mut core, true);
assert_eq!(core.scroll_offset(page), Vec2::new(0.0, 30.0));
}
#[test]
fn contain_holds_only_on_the_axes_a_scroller_scrolls() {
let mut core = Core::new();
let build = |core: &mut Core| {
let mut ui = core.frame(VIEW, 1.0);
ui.with_keyed(
"strip",
NodeSpec::row().size(300.0, 200.0).scroll_x(),
|ui| {
ui.with_keyed(
"list",
NodeSpec::column()
.size(150.0, 200.0)
.scroll_y()
.overscroll(Overscroll::Contain),
|ui| {
ui.leaf(NodeSpec::column().size(150.0, 800.0));
},
);
ui.leaf(NodeSpec::column().size(600.0, 200.0));
},
);
ui.finish();
};
build(&mut core);
build(&mut core);
let strip_k = core.key_of("strip").unwrap();
core.handle_input(InputEvent::CursorMoved(Vec2::new(50.0, 50.0)));
gesture(&mut core, -30.0, 0.0, true);
build(&mut core);
assert_eq!(core.scroll_offset(strip_k), Vec2::new(30.0, 0.0));
}
#[test]
fn a_latched_target_that_goes_is_picked_again() {
let mut core = Core::new();
let build = |core: &mut Core, name: &str| {
let mut ui = core.frame(VIEW, 1.0);
ui.with_keyed(
name,
NodeSpec::column().size(300.0, 200.0).scroll_y(),
|ui| {
ui.leaf(NodeSpec::column().size(300.0, 800.0));
},
);
ui.finish();
};
build(&mut core, "a");
build(&mut core, "a");
let a = core.key_of("a").unwrap();
core.handle_input(InputEvent::CursorMoved(Vec2::new(50.0, 50.0)));
gesture(&mut core, 0.0, -10.0, true);
build(&mut core, "a");
assert_eq!(core.scroll_offset(a), Vec2::new(0.0, 10.0));
build(&mut core, "b");
build(&mut core, "b");
let b = core.key_of("b").unwrap();
gesture(&mut core, 0.0, -10.0, false);
build(&mut core, "b");
assert_eq!(core.scroll_offset(b), Vec2::new(0.0, 10.0));
}
#[test]
fn a_bare_notch_is_a_gesture_of_its_own() {
let mut core = Core::new();
strip(&mut core, ScrollAxes::Both);
strip(&mut core, ScrollAxes::Both);
let (strip_k, _, term) = keys(&mut core);
core.handle_input(InputEvent::CursorMoved(Vec2::new(130.0, 50.0)));
core.handle_input(InputEvent::Scroll(Vec2::new(-30.0, 0.0)));
strip(&mut core, ScrollAxes::Both);
assert_eq!(core.scroll_offset(strip_k), Vec2::new(30.0, 0.0));
let out = core.handle_input(InputEvent::Scroll(Vec2::new(-30.0, 0.0)));
assert_eq!(scrolls(&out, term), vec![(-30.0, 0.0)]);
strip(&mut core, ScrollAxes::Both);
assert_eq!(core.scroll_offset(strip_k), Vec2::new(30.0, 0.0));
}
#[test]
fn a_latched_gesture_follows_its_target_not_the_pointer() {
let mut core = Core::new();
strip(&mut core, ScrollAxes::Both);
strip(&mut core, ScrollAxes::Both);
let (_, list, term) = keys(&mut core);
core.handle_input(InputEvent::CursorMoved(Vec2::new(50.0, 50.0)));
gesture(&mut core, 0.0, -10.0, true);
core.handle_input(InputEvent::CursorMoved(Vec2::new(200.0, 50.0)));
let out = gesture(&mut core, 0.0, -10.0, false);
assert!(scrolls(&out, term).is_empty());
strip(&mut core, ScrollAxes::Both);
assert_eq!(core.scroll_offset(list), Vec2::new(0.0, 20.0));
}
fn page_and_popover(core: &mut Core, modal: bool) {
use kui_core::{Align, FloatConfig};
let mut ui = core.frame(VIEW, 1.0);
ui.with_keyed(
"page",
NodeSpec::column().size(300.0, 200.0).scroll_y(),
|ui| {
ui.leaf(NodeSpec::column().size(300.0, 800.0));
},
);
let pop = NodeSpec::column()
.float(
FloatConfig::viewport()
.inside(Align::Start, Align::Start)
.offset(50.0, 50.0),
)
.size(100.0, 100.0)
.scroll_y();
ui.with_keyed("pop", if modal { pop.modal("m") } else { pop }, |ui| {
ui.leaf(NodeSpec::column().size(100.0, 400.0));
});
ui.finish();
}
#[test]
fn a_popover_list_at_its_end_does_not_chain_to_the_page_it_floats_over() {
let mut core = Core::new();
page_and_popover(&mut core, false);
page_and_popover(&mut core, false);
let page = core.key_of("page").unwrap();
let pop = core.key_of("pop").unwrap();
core.set_scroll(pop, Vec2::new(0.0, 1e9));
page_and_popover(&mut core, false);
core.handle_input(InputEvent::CursorMoved(Vec2::new(100.0, 100.0)));
core.handle_input(InputEvent::Scroll(Vec2::new(0.0, -30.0)));
page_and_popover(&mut core, false);
assert_eq!(core.scroll_offset(page), Vec2::ZERO);
core.handle_input(InputEvent::CursorMoved(Vec2::new(250.0, 180.0)));
core.handle_input(InputEvent::Scroll(Vec2::new(0.0, -30.0)));
page_and_popover(&mut core, false);
assert_eq!(core.scroll_offset(page), Vec2::new(0.0, 30.0));
}
#[test]
fn a_wheel_during_a_build_routes_by_the_last_frame() {
let mut core = Core::new();
page_and_popover(&mut core, false);
page_and_popover(&mut core, false);
let page = core.key_of("page").unwrap();
core.handle_input(InputEvent::CursorMoved(Vec2::new(250.0, 180.0)));
core.begin_frame(VIEW, 1.0);
core.handle_input(InputEvent::Scroll(Vec2::new(0.0, -10.0)));
assert_eq!(core.scroll_offset(page), Vec2::new(0.0, 10.0));
}
#[test]
fn a_latched_target_behind_a_new_modal_is_picked_again() {
use kui_core::{Align, FloatConfig};
let frame = |core: &mut Core, modal: bool| {
let mut ui = core.frame(VIEW, 1.0);
ui.with_keyed(
"page",
NodeSpec::column().size(300.0, 200.0).scroll_y(),
|ui| {
ui.leaf(NodeSpec::column().size(300.0, 800.0));
},
);
if modal {
ui.with_keyed(
"sheet",
NodeSpec::column()
.float(FloatConfig::viewport().inside(Align::Start, Align::Start))
.size(300.0, 200.0)
.scroll_y()
.modal("m"),
|ui| {
ui.leaf(NodeSpec::column().size(300.0, 800.0));
},
);
}
ui.finish();
};
let mut core = Core::new();
frame(&mut core, false);
frame(&mut core, false);
let page = core.key_of("page").unwrap();
core.handle_input(InputEvent::CursorMoved(Vec2::new(100.0, 100.0)));
gesture(&mut core, 0.0, -10.0, true);
frame(&mut core, true);
frame(&mut core, true);
let sheet = core.key_of("sheet").unwrap();
gesture(&mut core, 0.0, -10.0, false);
frame(&mut core, true);
assert_eq!(core.scroll_offset(page), Vec2::new(0.0, 10.0));
assert_eq!(core.scroll_offset(sheet), Vec2::new(0.0, 10.0));
}
fn code_in_strip(core: &mut Core) {
let mut ui = core.frame(VIEW, 1.0);
ui.with_keyed(
"strip",
NodeSpec::row().size(300.0, 200.0).scroll_x(),
|ui| {
ui.with_keyed(
"code",
NodeSpec::column()
.size(300.0, 200.0)
.scroll_x()
.on_scroll("code"),
|ui| {
ui.leaf(NodeSpec::column().size(600.0, 200.0));
},
);
ui.leaf(NodeSpec::column().size(600.0, 200.0));
},
);
ui.finish();
}
#[test]
fn a_handler_that_scrolls_an_axis_passes_it_at_its_limit() {
let mut core = Core::new();
code_in_strip(&mut core);
code_in_strip(&mut core);
let strip_k = core.key_of("strip").unwrap();
let code = core.key_of("code").unwrap();
core.handle_input(InputEvent::CursorMoved(Vec2::new(150.0, 50.0)));
let out = gesture(&mut core, -20.0, 0.0, true);
assert_eq!(scrolls(&out, code), vec![(-20.0, 0.0)]);
code_in_strip(&mut core);
assert_eq!(core.scroll_offset(strip_k), Vec2::ZERO);
core.set_scroll(code, Vec2::new(1e9, 0.0));
code_in_strip(&mut core);
assert_eq!(core.scroll_offset(code), Vec2::new(300.0, 0.0));
for _ in 0..3 {
let out = gesture(&mut core, -20.0, 0.0, false);
assert_eq!(scrolls(&out, code), vec![(-20.0, 0.0)], "latched");
code_in_strip(&mut core);
}
assert_eq!(
core.scroll_offset(strip_k),
Vec2::ZERO,
"no chaining partway"
);
let out = gesture(&mut core, -20.0, 0.0, true);
assert!(scrolls(&out, code).is_empty(), "passed by: {out:?}");
code_in_strip(&mut core);
assert_eq!(core.scroll_offset(strip_k), Vec2::new(20.0, 0.0));
let out = gesture(&mut core, 15.0, 0.0, true);
assert_eq!(scrolls(&out, code), vec![(15.0, 0.0)]);
code_in_strip(&mut core);
assert_eq!(core.scroll_offset(strip_k), Vec2::new(20.0, 0.0));
let out = gesture(&mut core, 0.0, -10.0, true);
assert_eq!(scrolls(&out, code), vec![(0.0, -10.0)]);
}