use bevy::picking::events::PointerClick;
use bevy::picking::pointer::{PointerButton, PointerId};
use bevy::platform::collections::HashMap;
use bevy::prelude::*;
use bevy::text::EditableText;
use bevy::ui::ScrollPosition;
use crate::bridge::{ClickOwner, JsBridge, ReactNode, ScrollListener};
use crate::protocol::{NodeId, outbound::Outbound, outbound::UiEvent};
pub(super) type ClickOwners = Or<(
With<ClickOwner>,
With<bevy::ui_widgets::Button>,
With<EditableText>,
)>;
#[allow(clippy::too_many_arguments)]
pub fn collect_ui_events(
bridge: Res<JsBridge>,
active_drag: Option<Res<super::ActiveDrag>>,
transform3d: Option<Res<crate::layer::pick3d::Transform3dPointer>>,
touch_scroll: Option<Res<crate::touch_scroll::TouchScrollState>>,
mut clicks: MessageReader<PointerClick>,
targets: Query<&ReactNode, ClickOwners>,
disabled: Query<(), With<bevy::ui::InteractionDisabled>>,
child_of: Query<&ChildOf>,
) {
let mut topmost: HashMap<PointerId, (f32, Entity)> = HashMap::new();
for ev in clicks.read() {
if ev.button != PointerButton::Primary {
continue;
}
if let PointerId::Touch(id) = ev.pointer.id
&& touch_scroll.as_ref().is_some_and(|s| s.is_suppressed(id))
{
continue;
}
if let Some(target) = climb(ev.entity, &child_of, |e| targets.contains(e)) {
let of = super::picking::device(ev.pointer.id, transform3d.as_deref());
if let Some(drag) = active_drag
.as_ref()
.and_then(|active| active.of_device(of, transform3d.as_deref()))
&& target != drag.entity
&& !drag.above.contains(&ev.entity)
&& !child_of.iter_ancestors(target).any(|a| a == drag.entity)
{
continue;
}
let candidate = (ev.hit.depth, target);
topmost
.entry(ev.pointer.id)
.and_modify(|best| {
if candidate.0 < best.0 {
*best = candidate;
}
})
.or_insert(candidate);
}
}
for (_, (_, target)) in topmost {
if disabled.contains(target) {
continue;
}
if let Ok(rnode) = targets.get(target) {
tracing::debug!("click -> reconciler node {}", rnode.0);
send_ui_event(&bridge, rnode.0, "click", None, None, None, None);
}
}
}
#[allow(clippy::type_complexity)]
pub fn collect_scroll_events(
mut bridge: ResMut<JsBridge>,
query: Query<(&ScrollPosition, &ReactNode), (With<ScrollListener>, Changed<ScrollPosition>)>,
) {
for (scroll, rnode) in &query {
let id = rnode.0;
if bridge.scroll_positions.get(&id) == Some(&scroll.0) {
continue;
}
bridge.scroll_positions.insert(id, scroll.0);
tracing::debug!("scroll -> reconciler node {id}");
let _ = bridge.outbound_tx.send(Outbound::UiEvent {
event: UiEvent {
id,
kind: "scroll".to_string(),
scroll_top: Some(scroll.0.y),
scroll_left: Some(scroll.0.x),
..default()
},
});
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq)]
pub(crate) struct Modifiers {
pub shift: bool,
pub ctrl: bool,
pub alt: bool,
pub meta: bool,
}
impl Modifiers {
pub(crate) fn read(keys: Option<&ButtonInput<KeyCode>>) -> Self {
let Some(keys) = keys else {
return Self::default();
};
Self {
shift: keys.any_pressed([KeyCode::ShiftLeft, KeyCode::ShiftRight]),
ctrl: keys.any_pressed([KeyCode::ControlLeft, KeyCode::ControlRight]),
alt: keys.any_pressed([KeyCode::AltLeft, KeyCode::AltRight]),
meta: keys.any_pressed([KeyCode::SuperLeft, KeyCode::SuperRight]),
}
}
pub(crate) fn stamp(self, event: &mut UiEvent) {
event.shift_key = Some(self.shift);
event.ctrl_key = Some(self.ctrl);
event.alt_key = Some(self.alt);
event.meta_key = Some(self.meta);
}
}
pub(super) fn send_ui_event(
bridge: &JsBridge,
id: NodeId,
kind: &str,
pos: Option<Vec2>,
abs: Option<Vec2>,
button: Option<u8>,
mods: Option<Modifiers>,
) {
let mut event = UiEvent {
id,
kind: kind.to_string(),
x: pos.map(|p| p.x),
y: pos.map(|p| p.y),
client_x: abs.map(|a| a.x),
client_y: abs.map(|a| a.y),
button,
..default()
};
if let Some(mods) = mods {
mods.stamp(&mut event);
}
let _ = bridge.outbound_tx.send(Outbound::UiEvent { event });
}
pub(super) fn dom_button(button: PointerButton) -> u8 {
match button {
PointerButton::Primary => 0,
PointerButton::Middle => 1,
PointerButton::Secondary => 2,
}
}
pub(crate) fn climb(
mut entity: Entity,
child_of: &Query<&ChildOf>,
is_target: impl Fn(Entity) -> bool,
) -> Option<Entity> {
loop {
if is_target(entity) {
return Some(entity);
}
entity = child_of.get(entity).ok()?.parent();
}
}
#[cfg(test)]
mod tests;