use bevy::ecs::entity::Entities;
use bevy::input::ButtonState;
use bevy::input::keyboard::{KeyCode, KeyboardInput};
use bevy::input_focus::tab_navigation::TabIndex;
use bevy::input_focus::{FocusCause, FocusedInput, InputFocus, InputFocusVisible};
use bevy::picking::PickingSystems;
use bevy::picking::events::PointerPress;
use bevy::picking::pointer::{PointerButton, PointerId};
use bevy::prelude::*;
use bevy::text::EditableText;
use bevy::ui::InteractionDisabled;
use super::events::climb;
use crate::bridge::{FocusState, JsBridge, OutboundResource, ReactNode};
use crate::ext::VirtualPointers;
use crate::layer::pick3d::Transform3dPointer;
use crate::protocol::outbound::{Outbound, UiEvent};
use crate::protocol::props::Props;
#[derive(Resource, Default)]
pub(crate) struct FocusBeforePress(InputFocus);
pub(crate) fn add_press_rule(app: &mut App) {
app.init_resource::<FocusBeforePress>().add_systems(
PreUpdate,
(
snapshot_focus.before(PickingSystems::Hover),
resolve_pointer_focus.after(PickingSystems::Hover),
),
);
}
fn snapshot_focus(focus: Option<Res<InputFocus>>, mut before: ResMut<FocusBeforePress>) {
if let Some(focus) = focus {
before.0.clone_from(&focus);
}
}
#[allow(clippy::too_many_arguments)]
fn resolve_pointer_focus(
mut presses: MessageReader<PointerPress>,
before: Res<FocusBeforePress>,
focus: Option<ResMut<InputFocus>>,
react: Query<(), With<ReactNode>>,
focusable: Query<(), With<TabIndex>>,
child_of: Query<&ChildOf>,
virtual_pointers: Option<Res<VirtualPointers>>,
transform3d: Option<Res<Transform3dPointer>>,
) {
struct Press {
pointer: PointerId,
react: bool,
best: Option<(f32, Entity, PointerButton)>,
}
let mut pressed: Vec<Press> = Vec::new();
for ev in presses.read() {
let index = pressed
.iter()
.position(|p| p.pointer == ev.pointer.id)
.unwrap_or_else(|| {
pressed.push(Press {
pointer: ev.pointer.id,
react: false,
best: None,
});
pressed.len() - 1
});
let press = &mut pressed[index];
press.react |= react.contains(ev.entity);
if press.best.is_none_or(|(depth, ..)| ev.hit.depth < depth)
&& let Some(target) = climb(ev.entity, &child_of, |e| focusable.contains(e))
{
press.best = Some((ev.hit.depth, target, ev.button));
}
}
if !pressed.iter().any(|p| p.react) {
return;
}
let Some(mut focus) = focus else {
return;
};
pressed.sort_by_key(|p| {
if transform3d.as_ref().is_some_and(|t| t.id == p.pointer) {
0
} else if virtual_pointers
.as_ref()
.is_some_and(|v| v.contains(p.pointer))
{
2
} else {
1
}
});
let target = pressed.iter().find_map(|p| p.best);
if *focus != before.0 {
*focus = before.0.clone();
}
match target {
Some((_, entity, button)) if focus.get() != Some(entity) => focus.set(
entity,
if button == PointerButton::Primary {
FocusCause::Pressed
} else {
FocusCause::Navigated
},
),
None if focus.get().is_some() => focus.clear(),
_ => {}
}
}
#[allow(clippy::too_many_arguments)]
pub fn sync_focus(
focus: Option<ResMut<InputFocus>>,
indicator: Option<Res<InputFocusVisible>>,
out: Option<Res<OutboundResource>>,
bridge: Option<Res<JsBridge>>,
entities: &Entities,
nodes: Query<(&ReactNode, Has<InteractionDisabled>)>,
inputs: Query<(), With<EditableText>>,
mut states: Query<&mut FocusState>,
mut last: Local<Option<Entity>>,
) {
let Some(mut focus) = focus else {
return;
};
let mut current = focus.get();
if current.is_some_and(|entity| {
!entities.contains(entity) || nodes.get(entity).is_ok_and(|(_, disabled)| disabled)
}) {
focus.clear();
current = None;
}
if let Some(entity) = current
&& let Ok(mut state) = states.get_mut(entity)
{
let visible = indicator.is_some_and(|i| i.0) || inputs.contains(entity);
state.set_if_neq(FocusState {
focused: true,
visible,
});
}
let old = std::mem::replace(&mut *last, current);
if old == current {
return;
}
let send = |entity: Entity, event: &str, wanted: fn(&Props) -> bool| {
let (Ok((node, _)), Some(out), Some(bridge)) = (nodes.get(entity), &out, &bridge) else {
return;
};
if bridge.props_cache.get(&node.0).is_some_and(|p| wanted(p)) {
let _ = out.0.send(Outbound::ElementEvent {
id: node.0,
event: event.to_owned(),
payload: serde_json::Value::Null,
});
}
};
if let Some(old) = old {
if let Ok(mut state) = states.get_mut(old) {
state.set_if_neq(FocusState::default());
}
send(old, "blur", |props| props.on_blur);
}
if let Some(new) = current {
send(new, "focus", |props| props.on_focus);
}
}
pub(crate) fn click_focused_button(
key: On<FocusedInput<KeyboardInput>>,
buttons: Query<&ReactNode, (With<bevy::ui_widgets::Button>, Without<InteractionDisabled>)>,
out: Option<Res<OutboundResource>>,
) {
let input = &key.input;
if key.focused_entity != key.original_event_target()
|| input.repeat
|| input.state != ButtonState::Pressed
|| !matches!(input.key_code, KeyCode::Enter | KeyCode::Space)
{
return;
}
if let (Ok(node), Some(out)) = (buttons.get(key.focused_entity), out) {
let _ = out.0.send(Outbound::UiEvent {
event: UiEvent {
id: node.0,
kind: "click".to_owned(),
..default()
},
});
}
}
#[cfg(test)]
mod sync_tests;
#[cfg(test)]
mod tests;