use bevy::picking::events::Pointer;
use bevy::picking::pointer::PointerMap;
use bevy::ui::{ComputedNode, UiGlobalTransform};
use tokio::sync::mpsc::UnboundedReceiver;
use super::*;
use crate::PointerCapture;
use crate::protocol::{op::Op, outbound::Outbound, outbound::UiEvent};
use crate::reconcile::picking::test_pointers::{at, hit, move_to, spawn_pointer};
use crate::touch_scroll::TouchScrollState;
const PRIMARY: PointerButton = PointerButton::Primary;
const MOUSE: PointerId = PointerId::Mouse;
fn drag_app() -> (App, UnboundedReceiver<Outbound>) {
let mut app = App::new();
crate::schedule_shuffle::shuffle_schedules(&mut app);
app.add_plugins(MinimalPlugins);
let (out_tx, out_rx) = tokio::sync::mpsc::unbounded_channel::<Outbound>();
let (ops_tx, ops_rx) = crossbeam_channel::unbounded::<Vec<Op>>();
std::mem::forget(ops_tx); let root = app.world_mut().spawn_empty().id();
app.insert_resource(JsBridge::new(ops_rx, out_tx, root))
.init_resource::<PointerMap>()
.init_resource::<PointerCapture>()
.init_resource::<ActiveDrag>()
.init_resource::<TouchScrollState>()
.add_message::<PointerPress>()
.add_message::<PointerInput>()
.add_systems(Update, collect_pointer_events);
(app, out_rx)
}
const HANDLERS: PointerHandlers = PointerHandlers {
down: true,
moved: true,
up: true,
enter: false,
leave: true,
hover: false,
};
fn handler_node(app: &mut App, id: u32) -> Entity {
app.world_mut()
.spawn((
ReactNode(id),
HANDLERS,
ComputedNode {
size: Vec2::splat(200.0),
inverse_scale_factor: 1.0,
..default()
},
UiGlobalTransform::from_translation(Vec2::splat(100.0)),
))
.id()
}
fn press(
app: &mut App,
pointer: PointerId,
button: PointerButton,
position: Vec2,
entities: &[Entity],
) {
let world = app.world_mut();
world.write_message(PointerInput::new(
pointer,
at(position),
PointerAction::Press(button),
));
for (depth, &entity) in entities.iter().enumerate() {
world.write_message(PointerPress {
entity,
pointer: Pointer::new(pointer, at(position)),
button,
hit: hit(depth as f32 * 0.00001),
count: 1,
});
}
}
fn act(app: &mut App, pointer: PointerId, position: Vec2, action: PointerAction) {
app.world_mut()
.write_message(PointerInput::new(pointer, at(position), action));
}
fn release(app: &mut App, pointer: PointerId, button: PointerButton, position: Vec2) {
act(app, pointer, position, PointerAction::Release(button));
}
fn frame(app: &mut App, rx: &mut UnboundedReceiver<Outbound>) -> Vec<UiEvent> {
app.update();
std::iter::from_fn(|| rx.try_recv().ok())
.map(|o| match o {
Outbound::UiEvent { event } => event,
other => panic!("expected a UiEvent, got {other:?}"),
})
.collect()
}
fn kinds(events: &[UiEvent]) -> Vec<&str> {
events.iter().map(|e| e.kind.as_str()).collect()
}
fn dragging(app: &App) -> bool {
app.world().resource::<PointerCapture>().dragging
}
#[test]
fn a_drag_reports_down_move_up() {
let (mut app, mut rx) = drag_app();
let node = handler_node(&mut app, 1);
let mouse = spawn_pointer(app.world_mut(), MOUSE, Vec2::splat(100.0));
press(&mut app, MOUSE, PRIMARY, Vec2::splat(100.0), &[node]);
let down = frame(&mut app, &mut rx);
assert_eq!(kinds(&down), ["pointerDown"]);
assert_eq!((down[0].x, down[0].y), (Some(0.5), Some(0.5)));
assert_eq!((down[0].client_x, down[0].button), (Some(100.0), Some(0)));
assert!(dragging(&app));
assert!(frame(&mut app, &mut rx).is_empty(), "held but still");
move_to(app.world_mut(), mouse, Vec2::new(250.0, 100.0));
let moved = frame(&mut app, &mut rx);
assert_eq!(kinds(&moved), ["pointerMove"]);
assert_eq!((moved[0].x, moved[0].client_x), (Some(1.0), Some(250.0)));
release(&mut app, MOUSE, PRIMARY, Vec2::new(250.0, 100.0));
assert_eq!(kinds(&frame(&mut app, &mut rx)), ["pointerUp"]);
assert!(!dragging(&app));
assert!(frame(&mut app, &mut rx).is_empty());
}
#[test]
fn the_topmost_pressed_handler_gets_the_drag() {
let (mut app, mut rx) = drag_app();
let top = handler_node(&mut app, 1);
let beneath = handler_node(&mut app, 2);
spawn_pointer(app.world_mut(), MOUSE, Vec2::splat(100.0));
let plain = app.world_mut().spawn(ReactNode(3)).id();
let location = Pointer::new(MOUSE, at(Vec2::splat(100.0)));
for (entity, depth) in [(beneath, 0.00002), (plain, 0.0), (top, 0.00001)] {
app.world_mut().write_message(PointerPress {
entity,
pointer: location.clone(),
button: PRIMARY,
hit: hit(depth),
count: 1,
});
}
let down = frame(&mut app, &mut rx);
assert_eq!(kinds(&down), ["pointerDown"]);
assert_eq!(down[0].id, 1);
let active = app.world().resource::<ActiveDrag>();
assert_eq!(active.of_device(MOUSE, None).unwrap().above, [plain]);
}
#[test]
fn a_second_button_is_ignored_during_a_drag() {
let (mut app, mut rx) = drag_app();
let node = handler_node(&mut app, 1);
let other = handler_node(&mut app, 2);
spawn_pointer(app.world_mut(), MOUSE, Vec2::splat(100.0));
let here = Vec2::splat(100.0);
press(&mut app, MOUSE, PointerButton::Secondary, here, &[node]);
let down = frame(&mut app, &mut rx);
assert_eq!(
(kinds(&down), down[0].button),
(vec!["pointerDown"], Some(2))
);
press(&mut app, MOUSE, PRIMARY, here, &[other]);
assert!(frame(&mut app, &mut rx).is_empty());
release(&mut app, MOUSE, PRIMARY, here);
assert!(frame(&mut app, &mut rx).is_empty());
release(&mut app, MOUSE, PointerButton::Secondary, here);
let up = frame(&mut app, &mut rx);
assert_eq!(
(kinds(&up), up[0].id, up[0].button),
(vec!["pointerUp"], 1, Some(2))
);
}
#[test]
fn a_touch_drags_at_its_own_position() {
let (mut app, mut rx) = drag_app();
let node = handler_node(&mut app, 1);
spawn_pointer(app.world_mut(), MOUSE, Vec2::new(200.0, 50.0));
let finger = PointerId::Touch(7);
let finger_entity = spawn_pointer(app.world_mut(), finger, Vec2::splat(100.0));
press(&mut app, finger, PRIMARY, Vec2::splat(100.0), &[node]);
let down = frame(&mut app, &mut rx);
assert_eq!(
(kinds(&down), down[0].button),
(vec!["pointerDown"], Some(0))
);
assert_eq!(down[0].client_x, Some(100.0));
assert!(dragging(&app), "a touch drag claims the pointer");
move_to(app.world_mut(), finger_entity, Vec2::new(110.0, 100.0));
let moved = frame(&mut app, &mut rx);
assert_eq!(
(kinds(&moved), moved[0].client_x),
(vec!["pointerMove"], Some(110.0))
);
act(
&mut app,
finger,
Vec2::new(112.0, 100.0),
PointerAction::Cancel,
);
let up = frame(&mut app, &mut rx);
assert_eq!(
(kinds(&up), up[0].client_x),
(vec!["pointerUp"], Some(112.0))
);
assert!(!dragging(&app));
}
#[test]
fn a_stolen_touch_drag_ends_with_pointer_leave() {
let (mut app, mut rx) = drag_app();
let node = handler_node(&mut app, 1);
let finger = PointerId::Touch(7);
let finger_entity = spawn_pointer(app.world_mut(), finger, Vec2::splat(100.0));
press(&mut app, finger, PRIMARY, Vec2::splat(100.0), &[node]);
assert_eq!(kinds(&frame(&mut app, &mut rx)), ["pointerDown"]);
app.world_mut()
.resource_mut::<TouchScrollState>()
.steal_for_test(7);
move_to(app.world_mut(), finger_entity, Vec2::new(100.0, 60.0));
let leave = frame(&mut app, &mut rx);
assert_eq!(kinds(&leave), ["pointerLeave"]);
assert_eq!(leave[0].client_y, Some(60.0), "where the finger is");
assert!(!dragging(&app), "a stolen drag no longer owns the pointer");
move_to(app.world_mut(), finger_entity, Vec2::new(100.0, 40.0));
assert!(frame(&mut app, &mut rx).is_empty());
release(&mut app, finger, PRIMARY, Vec2::new(100.0, 40.0));
assert!(frame(&mut app, &mut rx).is_empty());
}
#[test]
fn a_despawned_node_holds_the_pointer_until_release() {
let (mut app, mut rx) = drag_app();
let node = handler_node(&mut app, 1);
let mouse = spawn_pointer(app.world_mut(), MOUSE, Vec2::splat(100.0));
press(&mut app, MOUSE, PRIMARY, Vec2::splat(100.0), &[node]);
frame(&mut app, &mut rx);
app.world_mut().despawn(node);
move_to(app.world_mut(), mouse, Vec2::splat(120.0));
assert!(frame(&mut app, &mut rx).is_empty());
assert!(dragging(&app));
release(&mut app, MOUSE, PRIMARY, Vec2::splat(120.0));
assert!(frame(&mut app, &mut rx).is_empty());
assert!(!dragging(&app));
}
#[test]
fn two_pointers_drag_two_nodes() {
let (mut app, mut rx) = drag_app();
let a = handler_node(&mut app, 1);
let b = handler_node(&mut app, 2);
spawn_pointer(app.world_mut(), MOUSE, Vec2::splat(100.0));
let finger = PointerId::Touch(1);
let finger_entity = spawn_pointer(app.world_mut(), finger, Vec2::splat(100.0));
press(&mut app, MOUSE, PRIMARY, Vec2::splat(100.0), &[a]);
press(&mut app, finger, PRIMARY, Vec2::splat(100.0), &[b]);
let mut ids: Vec<_> = frame(&mut app, &mut rx).iter().map(|e| e.id).collect();
ids.sort();
assert_eq!(ids, [1, 2], "both went down");
move_to(app.world_mut(), finger_entity, Vec2::splat(110.0));
let moved = frame(&mut app, &mut rx);
assert_eq!((kinds(&moved), moved[0].id), (vec!["pointerMove"], 2));
release(&mut app, MOUSE, PRIMARY, Vec2::splat(100.0));
let up = frame(&mut app, &mut rx);
assert_eq!((kinds(&up), up[0].id), (vec!["pointerUp"], 1));
assert!(dragging(&app), "the finger still drags");
}
#[test]
fn the_remapped_press_beats_the_mouse_press() {
use bevy::ecs::system::RunSystemOnce;
let (mut app, mut rx) = drag_app();
let on_quad = handler_node(&mut app, 1);
let beneath = handler_node(&mut app, 2);
app.world_mut()
.run_system_once(crate::layer::pick3d::init_transform3d_pointer)
.unwrap();
let remap = app.world().resource::<Transform3dPointer>().id;
let remap_entity = app.world().resource::<PointerMap>().get_entity(remap);
let on_plane = Vec2::new(150.0, 100.0);
move_to(app.world_mut(), remap_entity.unwrap(), on_plane);
spawn_pointer(app.world_mut(), MOUSE, Vec2::new(640.0, 400.0));
press(
&mut app,
MOUSE,
PRIMARY,
Vec2::new(640.0, 400.0),
&[beneath],
);
act(&mut app, remap, on_plane, PointerAction::Press(PRIMARY));
app.world_mut().write_message(PointerPress {
entity: on_quad,
pointer: Pointer::new(remap, at(on_plane)),
button: PRIMARY,
hit: hit(0.00002),
count: 1,
});
let down = frame(&mut app, &mut rx);
assert_eq!(kinds(&down), ["pointerDown"]);
assert_eq!(down[0].id, 1, "the node on the quad");
assert_eq!((down[0].x, down[0].client_x), (Some(0.75), Some(640.0)));
release(&mut app, MOUSE, PRIMARY, Vec2::new(640.0, 400.0));
assert!(frame(&mut app, &mut rx).is_empty());
release(&mut app, remap, PRIMARY, Vec2::new(150.0, 100.0));
assert_eq!(kinds(&frame(&mut app, &mut rx)), ["pointerUp"]);
}
#[test]
fn an_elements_own_position_overrides_xy() {
let (mut app, mut rx) = drag_app();
let shape = app
.world_mut()
.spawn((
ReactNode(1),
HANDLERS,
EventLocalPos(Some(Vec2::new(42.0, 17.0))),
))
.id();
let mouse = spawn_pointer(app.world_mut(), MOUSE, Vec2::splat(100.0));
press(&mut app, MOUSE, PRIMARY, Vec2::splat(100.0), &[shape]);
let down = frame(&mut app, &mut rx);
assert_eq!((down[0].x, down[0].y), (Some(42.0), Some(17.0)));
app.world_mut().get_mut::<EventLocalPos>(shape).unwrap().0 = None;
move_to(app.world_mut(), mouse, Vec2::splat(300.0));
let moved = frame(&mut app, &mut rx);
assert_eq!(kinds(&moved), ["pointerMove"]);
assert_eq!((moved[0].x, moved[0].y), (Some(42.0), Some(17.0)));
assert_eq!(moved[0].client_x, Some(300.0));
}
#[test]
fn a_virtual_pointer_drag_is_not_the_windows() {
let (mut app, mut rx) = drag_app();
let node = handler_node(&mut app, 1);
let pointer = PointerId::Custom(bevy::asset::uuid::Uuid::from_u128(0x5eed));
let entity = spawn_pointer(app.world_mut(), pointer, Vec2::splat(100.0));
app.world_mut()
.get_resource_or_init::<VirtualPointers>()
.register(pointer);
press(&mut app, pointer, PRIMARY, Vec2::splat(100.0), &[node]);
assert_eq!(kinds(&frame(&mut app, &mut rx)), ["pointerDown"]);
assert!(!dragging(&app));
release(&mut app, pointer, PRIMARY, Vec2::new(150.0, 100.0));
move_to(app.world_mut(), entity, Vec2::splat(-1.0));
let up = frame(&mut app, &mut rx);
assert_eq!(kinds(&up), ["pointerUp"]);
assert_eq!((up[0].x, up[0].client_x), (Some(0.75), Some(150.0)));
}
#[test]
fn a_vanished_pointer_ends_the_drag_where_it_was() {
let (mut app, mut rx) = drag_app();
let node = handler_node(&mut app, 1);
let finger = PointerId::Touch(3);
let entity = spawn_pointer(app.world_mut(), finger, Vec2::splat(100.0));
press(&mut app, finger, PRIMARY, Vec2::splat(100.0), &[node]);
frame(&mut app, &mut rx);
move_to(app.world_mut(), entity, Vec2::new(150.0, 100.0));
frame(&mut app, &mut rx);
app.world_mut().despawn(entity);
let up = frame(&mut app, &mut rx);
assert_eq!(kinds(&up), ["pointerUp"]);
assert_eq!((up[0].x, up[0].client_x), (Some(0.75), Some(150.0)));
assert!(!dragging(&app));
}
#[test]
fn inputs_within_one_frame_keep_their_order() {
let (mut app, mut rx) = drag_app();
let node = handler_node(&mut app, 1);
spawn_pointer(app.world_mut(), MOUSE, Vec2::splat(100.0));
let here = Vec2::splat(100.0);
press(&mut app, MOUSE, PRIMARY, here, &[node]);
release(&mut app, MOUSE, PRIMARY, here);
assert_eq!(
kinds(&frame(&mut app, &mut rx)),
["pointerDown", "pointerUp"]
);
assert!(!dragging(&app));
release(&mut app, MOUSE, PRIMARY, here);
press(&mut app, MOUSE, PRIMARY, here, &[node]);
assert_eq!(kinds(&frame(&mut app, &mut rx)), ["pointerDown"]);
assert!(dragging(&app));
}
#[test]
fn the_wheel_claim_resets_each_frame() {
let (mut app, _rx) = drag_app();
app.world_mut()
.resource_mut::<PointerCapture>()
.wheel_captured = true;
app.update();
assert!(!app.world().resource::<PointerCapture>().wheel_captured);
}
#[test]
fn an_ended_drag_is_known_for_the_release_frame() {
let (mut app, mut rx) = drag_app();
let node = handler_node(&mut app, 1);
spawn_pointer(app.world_mut(), MOUSE, Vec2::splat(100.0));
let dragged = |app: &App| {
app.world()
.resource::<ActiveDrag>()
.of_device(MOUSE, None)
.map(|drag| drag.entity)
};
press(&mut app, MOUSE, PRIMARY, Vec2::splat(100.0), &[node]);
frame(&mut app, &mut rx);
assert_eq!(dragged(&app), Some(node));
release(&mut app, MOUSE, PRIMARY, Vec2::splat(100.0));
frame(&mut app, &mut rx);
assert_eq!(dragged(&app), Some(node), "the release frame");
assert!(!dragging(&app), "but the pointer is free");
frame(&mut app, &mut rx);
assert_eq!(dragged(&app), None);
}