use bevy::{ecs::system::SystemParam, input_focus::InputFocus, math::CompassQuadrant, prelude::*};
#[cfg(not(test))]
use bevy::input_focus::InputDispatchPlugin;
use std::fmt::Debug;
use std::sync::atomic::{AtomicUsize, Ordering};
#[derive(SystemParam)]
pub struct Focus<'w> {
focus: ResMut<'w, InputFocus>,
}
impl Focus<'_> {
pub fn is_focused(&self, id: Entity) -> bool {
self.focus.get() == Some(id)
}
pub fn focus_on(&mut self, id: Entity) {
self.focus.0 = Some(id);
}
}
#[derive(Resource, Default, Debug)]
pub struct KeyboardNav(bool);
static FOCUSABLE_COUNTER: AtomicUsize = AtomicUsize::new(0);
#[derive(Component, Clone, Reflect)]
pub struct Focusable {
version: usize,
block: bool,
pub created: usize,
}
impl Default for Focusable {
fn default() -> Self {
Self {
version: 0,
block: false,
created: FOCUSABLE_COUNTER.fetch_add(1, Ordering::SeqCst),
}
}
}
impl Focusable {
fn touch(&mut self) {
self.version += 1;
}
}
pub(crate) fn plugin(app: &mut App) {
#[cfg(not(test))]
app.add_plugins(InputDispatchPlugin);
app.register_type::<Focusable>()
.insert_resource(KeyboardNav(true))
.add_systems(PreUpdate, (sync_focus_to_focusable, focus_keys))
.add_systems(Update, reset_focus);
}
fn sync_focus_to_focusable(
input_focus: Res<InputFocus>,
mut focusables: Query<(Entity, &mut Focusable)>,
mut last_focus: Local<Option<Entity>>,
) {
let current_focus = input_focus.get();
if *last_focus != current_focus {
if let Some(old_id) = *last_focus
&& let Ok((_, mut focusable)) = focusables.get_mut(old_id)
{
focusable.touch();
}
if let Some(new_id) = current_focus
&& let Ok((_, mut focusable)) = focusables.get_mut(new_id)
{
focusable.touch();
}
*last_focus = current_focus;
}
}
#[derive(SystemParam)]
pub struct FocusParam<'w, 's> {
query: Query<'w, 's, (Entity, &'static mut Focusable)>,
focus: ResMut<'w, InputFocus>,
keyboard_nav: ResMut<'w, KeyboardNav>,
}
impl FocusParam<'_, '_> {
pub fn is_focused(&self, id: Entity) -> bool {
self.focus.get() == Some(id)
}
pub fn move_focus(&mut self, dir: CompassQuadrant) {
let old_created = if let Some(old_focus) = self.focus.0 {
if let Ok((_, focusable)) = self.query.get(old_focus) {
focusable.created
} else {
self.move_focus_from(None);
return;
}
} else {
self.move_focus_from(None);
return;
};
use CompassQuadrant::*;
let candidates: Vec<_> = self
.query
.iter()
.filter(|(id, focusable)| *id != self.focus.0.unwrap() && !focusable.block)
.map(|(id, focusable)| (id, focusable.created))
.collect();
let result = match dir {
North | West => {
candidates
.iter()
.filter(|(_, created)| *created < old_created)
.max_by_key(|(_, created)| *created)
.map(|(id, _)| *id)
}
South | East => {
candidates
.iter()
.filter(|(_, created)| *created > old_created)
.min_by_key(|(_, created)| *created)
.map(|(id, _)| *id)
}
};
let result = result.or_else(|| {
match dir {
North | West => {
candidates
.iter()
.max_by_key(|(_, created)| *created)
.map(|(id, _)| *id)
}
South | East => {
candidates
.iter()
.min_by_key(|(_, created)| *created)
.map(|(id, _)| *id)
}
}
});
if let Some(id) = result {
self.move_focus_to(id);
}
}
pub fn move_focus_to(&mut self, id: Entity) {
self.focus.0 = Some(id);
}
pub fn move_focus_from(&mut self, id_maybe: impl Into<Option<Entity>>) {
if let Some(focus_id) = id_maybe.into().or(self.focus.0) {
let current_created = self
.query
.get(focus_id)
.map(|(_, focusable)| focusable.created);
let mut candidates: Vec<_> = self
.query
.iter()
.filter(|(id, focusable)| *id != focus_id && !focusable.block)
.map(|(id, focusable)| (id, focusable.created))
.collect();
candidates.sort_by_key(|(_, created)| *created);
let result = candidates
.iter()
.find_map(|(id, created)| {
current_created
.map(|id| *created > id)
.unwrap_or(true)
.then_some(*id)
})
.or_else(|| candidates.first().map(|(id, _)| *id));
self.focus.0 = result;
} else {
let mut candidates: Vec<_> = self
.query
.iter()
.filter(|(_, focusable)| !focusable.block)
.map(|(id, focusable)| (id, focusable.created))
.collect();
candidates.sort_by_key(|(_, created)| *created);
self.focus.0 = candidates.first().map(|(id, _)| *id);
}
}
pub fn keyboard_nav(&self) -> bool {
self.keyboard_nav.0
}
pub fn set_keyboard_nav(&mut self, on: bool) {
self.keyboard_nav.0 = on;
}
pub fn block_and_move(&mut self, id_maybe: impl Into<Option<Entity>>) {
let id = id_maybe.into();
self.block(id);
self.move_focus_from(id);
}
pub fn is_blocked(&self, id: Entity) -> bool {
self.query
.get(id)
.map(|(_, focusable)| focusable.block)
.unwrap_or(true)
}
pub fn block(&mut self, id_maybe: impl Into<Option<Entity>>) {
if let Some(id) = id_maybe.into().or(self.focus.0) {
if let Ok((_, mut focus)) = self.query.get_mut(id) {
focus.block = true;
} else {
warn!("Cannot block entity {:?}: no Focusable component", id);
}
} else {
warn!("No id to block");
}
}
pub fn unblock(&mut self, id_maybe: impl Into<Option<Entity>>) {
if let Some(id) = id_maybe.into().or(self.focus.0) {
if let Ok((_, mut focus)) = self.query.get_mut(id) {
focus.block = false;
} else {
warn!("Cannot unblock entity {:?}: no Focusable component", id);
}
} else {
warn!("No id to unblock");
}
}
}
fn focus_keys(input: Res<ButtonInput<KeyCode>>, mut focus: FocusParam) {
if !focus.keyboard_nav()
|| !input.any_just_pressed([
KeyCode::ArrowUp,
KeyCode::ArrowDown,
KeyCode::ArrowLeft,
KeyCode::ArrowRight,
])
{
return;
}
if input.just_pressed(KeyCode::ArrowUp) {
focus.move_focus(CompassQuadrant::North);
} else if input.just_pressed(KeyCode::ArrowDown) {
focus.move_focus(CompassQuadrant::South);
} else if input.just_pressed(KeyCode::ArrowLeft) {
focus.move_focus(CompassQuadrant::West);
} else if input.just_pressed(KeyCode::ArrowRight) {
focus.move_focus(CompassQuadrant::East);
}
}
#[allow(dead_code)]
fn focus_on_tab(input: Res<ButtonInput<KeyCode>>, mut focus: FocusParam) {
if input.just_pressed(KeyCode::Tab) {
focus.move_focus_from(None);
}
}
fn reset_focus(mut focus: FocusParam) {
match focus.focus.0 {
None => focus.move_focus_from(None),
Some(id) => {
if focus.is_blocked(id) {
focus.move_focus_from(None)
}
}
}
}