use crate::CurrentWidgetState;
use crate::widgets::{BindToID, IgnoreParentState, UIGenID, UIWidgetState};
use bevy::prelude::*;
use std::collections::HashMap;
#[derive(Resource, Default)]
pub struct BoundStateIndex {
by_widget: HashMap<usize, Vec<Entity>>,
by_entity: HashMap<Entity, usize>,
}
pub struct StateService;
impl Plugin for StateService {
fn build(&self, app: &mut App) {
app.init_resource::<BoundStateIndex>();
app.register_type::<Pickable>();
app.add_systems(
PostUpdate,
(refresh_bound_state_index, update_widget_states).chain(),
);
app.add_systems(
Update,
(
internal_state_check.run_if(resource_changed::<CurrentWidgetState>),
handle_tab_focus,
unfocus_disabled,
),
);
}
}
fn remove_from_bound_state_index(index: &mut BoundStateIndex, entity: Entity, widget_id: usize) {
let should_remove = if let Some(entries) = index.by_widget.get_mut(&widget_id) {
entries.retain(|current| *current != entity);
entries.is_empty()
} else {
false
};
if should_remove {
index.by_widget.remove(&widget_id);
}
}
fn refresh_bound_state_index(
mut index: ResMut<BoundStateIndex>,
query: Query<(Entity, &BindToID), Or<(Added<BindToID>, Changed<BindToID>)>>,
mut removed: RemovedComponents<BindToID>,
) {
for entity in removed.read() {
if let Some(previous_id) = index.by_entity.remove(&entity) {
remove_from_bound_state_index(&mut index, entity, previous_id);
}
}
for (entity, bind_to) in query.iter() {
let widget_id = bind_to.0;
if let Some(previous_id) = index.by_entity.insert(entity, widget_id) {
if previous_id != widget_id {
remove_from_bound_state_index(&mut index, entity, previous_id);
}
}
let entries = index.by_widget.entry(widget_id).or_default();
if !entries.contains(&entity) {
entries.push(entity);
}
}
}
pub fn update_widget_states(
main_query: Query<(&UIGenID, &UIWidgetState), (Changed<UIWidgetState>, With<UIGenID>)>,
index: Option<Res<BoundStateIndex>>,
mut inner_query: Query<
(Entity, &BindToID, &mut UIWidgetState),
(Without<UIGenID>, Without<IgnoreParentState>),
>,
) {
for (id, state) in main_query.iter() {
if let Some(index) = index.as_ref() {
if let Some(targets) = index.by_widget.get(&id.get()) {
for entity in targets {
let Ok((_, _, mut inner_state)) = inner_query.get_mut(*entity) else {
continue;
};
inner_state.hovered = state.hovered;
inner_state.focused = state.focused;
inner_state.readonly = state.readonly;
inner_state.disabled = state.disabled;
inner_state.checked = state.checked;
}
continue;
}
}
for (_, bind_to, mut inner_state) in inner_query.iter_mut() {
if bind_to.0 != id.get() {
continue;
}
inner_state.hovered = state.hovered;
inner_state.focused = state.focused;
inner_state.readonly = state.readonly;
inner_state.disabled = state.disabled;
inner_state.checked = state.checked;
}
}
}
fn internal_state_check(
current_state_element: Res<CurrentWidgetState>,
mut query: Query<(&mut UIWidgetState, &UIGenID), With<UIGenID>>,
) {
for (mut state, gen_id) in query.iter_mut() {
if gen_id.get() == current_state_element.widget_id {
continue;
}
state.focused = false;
}
}
fn handle_tab_focus(
mut query: Query<(Entity, &mut UIWidgetState, &UIGenID)>,
keyboard: Res<ButtonInput<KeyCode>>,
mut current_state: ResMut<CurrentWidgetState>,
) {
if !keyboard.just_pressed(KeyCode::Tab) {
return;
}
let reverse = keyboard.pressed(KeyCode::ShiftLeft) || keyboard.pressed(KeyCode::ShiftRight);
let mut elems: Vec<_> = query
.iter_mut()
.filter(|(_, state, _)| !state.disabled)
.collect();
elems.sort_by_key(|(_, _, id)| id.get());
let len = elems.len();
if len == 0 {
return;
}
let mut focused_idx = None;
for (i, (_, state, _)) in elems.iter().enumerate() {
if state.focused {
focused_idx = Some(i);
break;
}
}
match focused_idx {
Some(i) => {
elems[i].1.focused = false;
let next = if reverse {
(i + len - 1) % len
} else {
(i + 1) % len
};
elems[next].1.focused = true;
current_state.widget_id = elems[next].2.get();
}
None => {
let next = if reverse { len - 1 } else { 0 };
elems[next].1.focused = true;
current_state.widget_id = elems[next].2.get();
}
}
}
fn unfocus_disabled(mut q: Query<&mut UIWidgetState, Changed<UIWidgetState>>) {
for mut s in &mut q {
if s.disabled && s.focused {
s.focused = false;
}
}
}