use bevy::picking::events::PointerPress;
use bevy::picking::pointer::{
Location, PointerAction, PointerButton, PointerId, PointerInput, PointerLocation,
};
use bevy::prelude::*;
use super::events::{Modifiers, dom_button, send_ui_event};
use super::picking::{Geometry, client_pos, device, event_pos, sample_pointer};
use crate::bridge::{JsBridge, PointerHandlers, ReactNode};
use crate::ext::{EventLocalPos, VirtualPointers};
use crate::layer::pick3d::Transform3dPointer;
#[derive(Clone)]
pub(crate) struct Drag {
pub entity: Entity,
pub pointer: PointerId,
pub button: PointerButton,
pub above: Vec<Entity>,
last_pos: Vec2,
last_abs: Vec2,
}
#[derive(Resource, Default)]
pub struct ActiveDrag {
drags: Vec<Drag>,
ended: Vec<Drag>,
}
impl ActiveDrag {
pub(crate) fn begin(
&mut self,
entity: Entity,
pointer: PointerId,
button: PointerButton,
pos: Vec2,
abs: Vec2,
) -> &mut Drag {
self.drags.push(Drag {
entity,
pointer,
button,
above: Vec::new(),
last_pos: pos,
last_abs: abs,
});
self.drags.last_mut().unwrap()
}
pub(crate) fn of_device(
&self,
of: PointerId,
transform3d: Option<&Transform3dPointer>,
) -> Option<&Drag> {
self.drags
.iter()
.chain(&self.ended)
.find(|drag| device(drag.pointer, transform3d) == of)
}
pub(crate) fn owns_touch(&self, id: u64) -> bool {
self.drags
.iter()
.any(|drag| drag.pointer == PointerId::Touch(id))
}
}
#[allow(clippy::type_complexity, clippy::too_many_arguments)]
pub fn collect_pointer_events(
bridge: Res<JsBridge>,
mut presses: MessageReader<PointerPress>,
mut inputs: MessageReader<PointerInput>,
pointers: Query<(&PointerId, &PointerLocation)>,
cameras: Query<&Camera>,
transform3d: Option<Res<Transform3dPointer>>,
virtual_pointers: Option<Res<VirtualPointers>>,
nodes: Query<(
&ReactNode,
&PointerHandlers,
// SVG shapes report x/y in user units instead (see [`event_pos`]).
Option<&EventLocalPos>,
Geometry,
)>,
mut capture: ResMut<crate::PointerCapture>,
mut active: ResMut<ActiveDrag>,
touch_scroll: Res<crate::touch_scroll::TouchScrollState>,
keys: Option<Res<ButtonInput<KeyCode>>>,
) {
let mods = Modifiers::read(keys.as_deref());
let transform3d = transform3d.as_deref();
let emit = |rnode: &ReactNode, kind: &str, pos: Vec2, abs: Vec2, button: PointerButton| {
send_ui_event(
&bridge,
rnode.0,
kind,
Some(pos),
Some(abs),
Some(dom_button(button)),
Some(mods),
);
};
active.ended.clear();
let live = active.drags.len();
let presses: Vec<&PointerPress> = presses.read().collect();
let mut begins: Vec<&PointerPress> = Vec::new();
for &press in &presses {
let of = device(press.pointer.id, transform3d);
if !nodes.contains(press.entity) || active.of_device(of, transform3d).is_some() {
continue;
}
let rank = |press: &PointerPress| (press.pointer.id == of, press.hit.depth);
match begins
.iter_mut()
.find(|best| device(best.pointer.id, transform3d) == of)
{
Some(best) if rank(press) < rank(best) => *best = press,
Some(_) => {}
None => begins.push(press),
}
}
for press in begins {
let Ok((rnode, handlers, user, geometry)) = nodes.get(press.entity) else {
continue;
};
let location = press.pointer.location();
let pos = event_pos(user, geometry, &cameras, &location).unwrap_or(Vec2::ZERO);
let abs = client_pos(press.pointer.id, &location, &pointers, transform3d);
let (pointer, button) = (press.pointer.id, press.button);
active.begin(press.entity, pointer, button, pos, abs).above = presses
.iter()
.filter(|other| other.pointer.id == pointer && other.button == button)
.filter(|other| other.hit.depth < press.hit.depth)
.map(|other| other.entity)
.collect();
if handlers.down {
emit(rnode, "pointerDown", pos, abs, press.button);
}
}
let inputs: Vec<&PointerInput> = inputs
.read()
.filter(|input| {
matches!(
input.action,
PointerAction::Press(_) | PointerAction::Release(_) | PointerAction::Cancel
)
})
.collect();
let mut index = 0;
let ActiveDrag { drags, ended } = &mut *active;
drags.retain_mut(|drag| {
let fresh = index >= live;
index += 1;
let node = nodes.get(drag.entity).ok();
let stolen = matches!(drag.pointer, PointerId::Touch(id) if touch_scroll.stole_drag(id));
let released = release_of(&inputs, drag, fresh);
let sample = sample_pointer(drag.pointer, &pointers, transform3d);
if !stolen
&& released.is_none()
&& let Some(sample) = &sample
{
if let Some((rnode, handlers, user, geometry)) = node {
let pos =
event_pos(user, geometry, &cameras, &sample.location).unwrap_or(drag.last_pos);
let moved = sample.client != drag.last_abs;
drag.last_pos = pos;
drag.last_abs = sample.client;
if moved && handlers.moved {
emit(rnode, "pointerMove", pos, sample.client, drag.button);
}
}
return true;
}
let location = released.or(sample.as_ref().map(|sample| &sample.location));
if let Some((rnode, handlers, user, geometry)) = node {
let (pos, abs) = match location {
Some(location) => (
event_pos(user, geometry, &cameras, location).unwrap_or(drag.last_pos),
client_pos(drag.pointer, location, &pointers, transform3d),
),
None => (drag.last_pos, drag.last_abs),
};
if stolen {
if handlers.leave {
emit(rnode, "pointerLeave", pos, abs, drag.button);
}
} else if handlers.up {
emit(rnode, "pointerUp", pos, abs, drag.button);
}
}
ended.push(drag.clone());
false
});
capture.dragging = active.drags.iter().any(|drag| {
!virtual_pointers
.as_ref()
.is_some_and(|v| v.contains(drag.pointer))
});
capture.wheel_captured = false;
}
fn release_of<'a>(inputs: &[&'a PointerInput], drag: &Drag, fresh: bool) -> Option<&'a Location> {
let mine = |input: &&&PointerInput| input.pointer_id == drag.pointer;
let from = if fresh {
inputs
.iter()
.rposition(|input| {
mine(&input) && matches!(input.action, PointerAction::Press(b) if b == drag.button)
})
.map_or(0, |press| press + 1)
} else {
0
};
inputs[from..]
.iter()
.filter(mine)
.find(|input| match input.action {
PointerAction::Release(button) => button == drag.button,
PointerAction::Cancel => true,
_ => false,
})
.map(|input| &input.location)
}
#[cfg(test)]
mod tests;