use bevy::input_focus::tab_navigation::TabIndex;
use bevy::input_focus::{FocusCause, InputFocus};
use bevy::prelude::*;
use bevy::ui::{ComputedNode, OverflowAxis, ScrollPosition, UiGlobalTransform};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PfFocusDir {
Up,
Down,
Left,
Right,
}
impl PfFocusDir {
fn axis(self) -> Vec2 {
match self {
PfFocusDir::Up => Vec2::new(0.0, -1.0),
PfFocusDir::Down => Vec2::new(0.0, 1.0),
PfFocusDir::Left => Vec2::new(-1.0, 0.0),
PfFocusDir::Right => Vec2::new(1.0, 0.0),
}
}
}
#[derive(Message, Debug, Clone, Copy)]
pub enum PfFocusNav {
Move(PfFocusDir),
Activate,
}
#[derive(Resource, Debug, Default, Clone, Copy)]
pub struct PfFocusScope(pub Option<Entity>);
pub fn activate_control(world: &mut World, control: Entity) {
let mut windows = world.query_filtered::<Entity, With<bevy::window::Window>>();
let Some(window) = windows.iter(world).next() else {
return;
};
let Some(target) =
bevy::camera::RenderTarget::Window(bevy::window::WindowRef::Entity(window)).normalize(None)
else {
return;
};
crate::plugin::synthetic_click(world, target, control);
}
fn hidden(
entity: Entity,
visibilities: &Query<&Visibility>,
parents: &Query<&ChildOf>,
) -> bool {
let mut cursor = entity;
loop {
if visibilities
.get(cursor)
.is_ok_and(|v| *v == Visibility::Hidden)
{
return true;
}
match parents.get(cursor) {
Ok(parent) => cursor = parent.parent(),
Err(_) => return false,
}
}
}
fn in_subtree(entity: Entity, root: Entity, parents: &Query<&ChildOf>) -> bool {
let mut cursor = entity;
loop {
if cursor == root {
return true;
}
match parents.get(cursor) {
Ok(parent) => cursor = parent.parent(),
Err(_) => return false,
}
}
}
pub(crate) fn focus_nav(
mut messages: MessageReader<PfFocusNav>,
scope: Res<PfFocusScope>,
mut focus: ResMut<InputFocus>,
mut visible: Option<ResMut<bevy::input_focus::InputFocusVisible>>,
stops: Query<(Entity, &ComputedNode, &UiGlobalTransform), With<TabIndex>>,
visibilities: Query<&Visibility>,
parents: Query<&ChildOf>,
windows: Query<Entity, With<bevy::window::Window>>,
mut commands: Commands,
) {
for message in messages.read() {
match *message {
PfFocusNav::Activate => {
let Some(entity) = focus.get() else { continue };
let Some(window) = windows.iter().next() else {
continue;
};
let Some(target) =
bevy::camera::RenderTarget::Window(bevy::window::WindowRef::Entity(window))
.normalize(None)
else {
continue;
};
commands.queue(move |world: &mut World| {
crate::plugin::synthetic_click(world, target, entity);
});
}
PfFocusNav::Move(dir) => {
let mut candidates: Vec<(Entity, Vec2)> = Vec::new();
for (entity, node, transform) in &stops {
if let Some(root) = scope.0
&& !in_subtree(entity, root, &parents)
{
continue;
}
if hidden(entity, &visibilities, &parents) {
continue;
}
let scale = node.inverse_scale_factor();
if scale <= 0.0 {
continue;
}
candidates.push((entity, transform.translation * scale));
}
if candidates.is_empty() {
continue;
}
candidates.sort_by_key(|(e, _)| *e);
let current = focus.get().and_then(|f| {
candidates
.iter()
.find(|(c, _)| *c == f || in_subtree(f, *c, &parents))
.copied()
});
let next = match current {
None => {
candidates
.iter()
.min_by(|(_, a), (_, b)| {
(a.y, a.x)
.partial_cmp(&(b.y, b.x))
.unwrap_or(std::cmp::Ordering::Equal)
})
.map(|(e, _)| *e)
}
Some((cur, cur_center)) => {
let axis = dir.axis();
candidates
.iter()
.filter(|(e, _)| *e != cur)
.filter_map(|(e, center)| {
let delta = *center - cur_center;
let ahead = delta.dot(axis);
if ahead <= 0.5 {
return None;
}
let off_axis = (delta - axis * ahead).length();
Some((*e, ahead + off_axis * 2.0))
})
.min_by(|(_, a), (_, b)| {
a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)
})
.map(|(e, _)| e)
}
};
if let Some(next) = next
&& focus.get() != Some(next)
{
focus.set(next, FocusCause::Navigated);
if let Some(v) = visible.as_mut() {
v.0 = true;
}
}
}
}
}
}
pub(crate) fn scroll_focus_into_view(
focus: Res<InputFocus>,
geometry: Query<(&ComputedNode, &UiGlobalTransform)>,
nodes: Query<&Node>,
parents: Query<&ChildOf>,
mut scrollables: Query<&mut ScrollPosition>,
) {
if !focus.is_changed() {
return;
}
let Some(entity) = focus.get() else { return };
let Ok((node, transform)) = geometry.get(entity) else {
return;
};
let scale = node.inverse_scale_factor();
if scale <= 0.0 {
return;
}
let center = transform.translation * scale;
let half = node.size() * scale / 2.0;
let mut cursor = entity;
let container = loop {
match parents.get(cursor) {
Ok(parent) => {
cursor = parent.parent();
let scrolls_here = nodes.get(cursor).is_ok_and(|n| {
n.overflow.x == OverflowAxis::Scroll || n.overflow.y == OverflowAxis::Scroll
}) && scrollables.get(cursor).is_ok();
if scrolls_here {
break cursor;
}
}
Err(_) => return,
}
};
let Ok((c_node, c_transform)) = geometry.get(container) else {
return;
};
let c_scale = c_node.inverse_scale_factor();
if c_scale <= 0.0 {
return;
}
let c_center = c_transform.translation * c_scale;
let c_half = c_node.size() * c_scale / 2.0;
const MARGIN: f32 = 4.0;
let axes = nodes
.get(container)
.map(|n| (n.overflow.x == OverflowAxis::Scroll, n.overflow.y == OverflowAxis::Scroll))
.unwrap_or((false, true));
let Ok(mut position) = scrollables.get_mut(container) else {
return;
};
if axes.1 {
let above = (c_center.y - c_half.y) - (center.y - half.y - MARGIN);
let below = (center.y + half.y + MARGIN) - (c_center.y + c_half.y);
if above > 0.0 {
position.y -= above;
} else if below > 0.0 {
position.y += below;
}
}
if axes.0 {
let left = (c_center.x - c_half.x) - (center.x - half.x - MARGIN);
let right = (center.x + half.x + MARGIN) - (c_center.x + c_half.x);
if left > 0.0 {
position.x -= left;
} else if right > 0.0 {
position.x += right;
}
}
}