#[cfg(feature = "gamepad")]
use crate::ecs::ui::picking::is_descendant_of;
#[cfg(feature = "gamepad")]
use crate::ecs::ui::resources::NavDirection;
#[cfg(feature = "gamepad")]
use crate::ecs::ui::types::Rect;
use crate::ecs::world::World;
#[cfg(feature = "gamepad")]
pub fn ui_gamepad_navigation_system(world: &mut World) {
if !world.resources.retained_ui.enabled {
return;
}
if !world.resources.retained_ui.gamepad_nav.enabled {
return;
}
if world.resources.input.gamepad.gilrs.is_none() {
return;
}
let now = world.resources.retained_ui.timing.current_time;
let deadzone = world.resources.retained_ui.gamepad_nav.deadzone;
let initial_delay = world.resources.retained_ui.gamepad_nav.initial_repeat_delay;
let repeat_interval = world.resources.retained_ui.gamepad_nav.repeat_interval;
let direction = read_direction(world, deadzone);
let activate = read_button_just_pressed(world, gilrs::Button::South);
let cancel = read_button_just_pressed(world, gilrs::Button::East);
let pause = read_button_just_pressed(world, gilrs::Button::Start);
let prev_tab = read_button_just_pressed(world, gilrs::Button::LeftTrigger)
|| read_button_just_pressed(world, gilrs::Button::LeftTrigger2);
let next_tab = read_button_just_pressed(world, gilrs::Button::RightTrigger)
|| read_button_just_pressed(world, gilrs::Button::RightTrigger2);
let nav_state = &mut world.resources.retained_ui.gamepad_nav;
let should_navigate = match direction {
Some(dir) => {
if nav_state.held_direction == Some(dir) {
if now >= nav_state.next_repeat_at {
nav_state.next_repeat_at = now + repeat_interval;
true
} else {
false
}
} else {
nav_state.held_direction = Some(dir);
nav_state.next_repeat_at = now + initial_delay;
true
}
}
None => {
nav_state.held_direction = None;
false
}
};
if should_navigate && let Some(dir) = direction {
focus_in_direction(world, dir);
}
if next_tab {
cycle_tab_focus(world, false);
}
if prev_tab {
cycle_tab_focus(world, true);
}
if activate {
if world
.resources
.retained_ui
.interaction
.focused_entity
.is_none()
{
let focusable = collect_focusable(world);
if let Some((entity, _, _)) = focusable.first() {
world
.resources
.retained_ui
.interaction_for_active_mut()
.focused_entity = Some(*entity);
world.resources.retained_ui.overlays.focus_ring_visible = true;
}
}
if let Some(focused) = world
.resources
.retained_ui
.interaction_for_active_mut()
.focused_entity
&& let Some(interaction) = world.ui.get_ui_node_interaction_mut(focused)
&& !interaction.disabled
{
interaction.clicked = true;
}
}
if cancel || pause {
world
.resources
.input
.keyboard
.just_pressed_keys
.insert(winit::keyboard::KeyCode::Escape);
world.resources.input.keyboard.keystates.insert(
winit::keyboard::KeyCode::Escape,
winit::event::ElementState::Pressed,
);
world
.resources
.input
.keyboard
.frame_keys
.push((winit::keyboard::KeyCode::Escape, true));
}
}
#[cfg(not(feature = "gamepad"))]
pub fn ui_gamepad_navigation_system(_world: &mut World) {}
#[cfg(feature = "gamepad")]
fn read_direction(world: &World, deadzone: f32) -> Option<NavDirection> {
let gamepad = active_gamepad(world)?;
let stick_x = gamepad.value(gilrs::Axis::LeftStickX);
let stick_y = gamepad.value(gilrs::Axis::LeftStickY);
let dpad_up = gamepad.is_pressed(gilrs::Button::DPadUp);
let dpad_down = gamepad.is_pressed(gilrs::Button::DPadDown);
let dpad_left = gamepad.is_pressed(gilrs::Button::DPadLeft);
let dpad_right = gamepad.is_pressed(gilrs::Button::DPadRight);
let vertical = if dpad_up || stick_y > deadzone {
Some(NavDirection::Up)
} else if dpad_down || stick_y < -deadzone {
Some(NavDirection::Down)
} else {
None
};
let horizontal = if dpad_right || stick_x > deadzone {
Some(NavDirection::Right)
} else if dpad_left || stick_x < -deadzone {
Some(NavDirection::Left)
} else {
None
};
if vertical.is_some() && horizontal.is_some() {
if stick_y.abs() >= stick_x.abs() {
vertical
} else {
horizontal
}
} else {
vertical.or(horizontal)
}
}
#[cfg(feature = "gamepad")]
fn read_button_just_pressed(world: &World, button: gilrs::Button) -> bool {
world
.resources
.input
.gamepad
.just_pressed_buttons
.contains(&button)
}
#[cfg(feature = "gamepad")]
fn active_gamepad(world: &World) -> Option<gilrs::Gamepad<'_>> {
let gilrs = world.resources.input.gamepad.gilrs.as_ref()?;
let id = world.resources.input.gamepad.gamepad?;
Some(gilrs.gamepad(id))
}
#[cfg(feature = "gamepad")]
fn collect_focusable(world: &World) -> Vec<(freecs::Entity, Rect, Option<i32>)> {
let mut focusable = Vec::new();
for entity in world
.ui
.query_entities(crate::ecs::world::UI_NODE_INTERACTION)
{
let Some(interaction) = world.ui.get_ui_node_interaction(entity) else {
continue;
};
if interaction.disabled {
continue;
}
let Some(node) = world.ui.get_ui_layout_node(entity) else {
continue;
};
if !node.visible {
continue;
}
if !crate::ecs::ui::widgets::world_layout::ui_node_effectively_visible(world, entity) {
continue;
}
let rect = node.computed_rect;
if rect.width() <= 0.0 || rect.height() <= 0.0 {
continue;
}
focusable.push((entity, rect, interaction.tab_index));
}
scope_to_active_overlay(world, &mut focusable);
focusable
}
#[cfg(feature = "gamepad")]
fn scope_to_active_overlay(
world: &World,
focusable: &mut Vec<(freecs::Entity, Rect, Option<i32>)>,
) {
if let Some(modal) = world.resources.retained_ui.overlays.active_modal {
focusable.retain(|(entity, _, _)| is_descendant_of(world, *entity, modal));
} else if !world
.resources
.retained_ui
.overlays
.popup_entities
.is_empty()
{
let popups = world.resources.retained_ui.overlays.popup_entities.clone();
focusable.retain(|(entity, _, _)| {
popups
.iter()
.any(|popup| is_descendant_of(world, *entity, *popup))
});
}
}
#[cfg(feature = "gamepad")]
fn focus_in_direction(world: &mut World, direction: NavDirection) {
let focusable = collect_focusable(world);
if focusable.is_empty() {
return;
}
let current = world
.resources
.retained_ui
.interaction_for_active_mut()
.focused_entity;
let next = match current {
Some(entity) => match focusable.iter().find(|(e, _, _)| *e == entity) {
Some((_, rect, _)) => find_neighbor(&focusable, entity, *rect, direction)
.or_else(|| fallback_endpoint(&focusable, direction)),
None => fallback_endpoint(&focusable, direction),
},
None => fallback_endpoint(&focusable, direction),
};
if let Some(entity) = next {
world
.resources
.retained_ui
.interaction_for_active_mut()
.focused_entity = Some(entity);
world.resources.retained_ui.overlays.focus_ring_visible = true;
}
}
#[cfg(feature = "gamepad")]
fn find_neighbor(
focusable: &[(freecs::Entity, Rect, Option<i32>)],
current: freecs::Entity,
current_rect: Rect,
direction: NavDirection,
) -> Option<freecs::Entity> {
let current_center = current_rect.center();
let mut best: Option<(freecs::Entity, f32)> = None;
for (entity, rect, _) in focusable {
if *entity == current {
continue;
}
let center = rect.center();
let delta = center - current_center;
let in_direction = match direction {
NavDirection::Up => delta.y < -1.0,
NavDirection::Down => delta.y > 1.0,
NavDirection::Left => delta.x < -1.0,
NavDirection::Right => delta.x > 1.0,
};
if !in_direction {
continue;
}
let (primary, secondary) = match direction {
NavDirection::Up | NavDirection::Down => (delta.y.abs(), delta.x.abs()),
NavDirection::Left | NavDirection::Right => (delta.x.abs(), delta.y.abs()),
};
let score = primary + secondary * 2.5;
match best {
Some((_, current_score)) if score >= current_score => {}
_ => best = Some((*entity, score)),
}
}
best.map(|(entity, _)| entity)
}
#[cfg(feature = "gamepad")]
fn fallback_endpoint(
focusable: &[(freecs::Entity, Rect, Option<i32>)],
direction: NavDirection,
) -> Option<freecs::Entity> {
focusable
.iter()
.min_by(|a, b| {
let key_a = endpoint_key(direction, a.1.center());
let key_b = endpoint_key(direction, b.1.center());
key_a
.partial_cmp(&key_b)
.unwrap_or(std::cmp::Ordering::Equal)
})
.map(|(entity, _, _)| *entity)
}
#[cfg(feature = "gamepad")]
fn endpoint_key(direction: NavDirection, center: nalgebra_glm::Vec2) -> f32 {
match direction {
NavDirection::Up => -center.y,
NavDirection::Down => center.y,
NavDirection::Left => -center.x,
NavDirection::Right => center.x,
}
}
#[cfg(feature = "gamepad")]
fn cycle_tab_focus(world: &mut World, reverse: bool) {
let mut tabbable: Vec<(freecs::Entity, i32)> = collect_focusable(world)
.into_iter()
.filter_map(|(entity, _, tab_index)| tab_index.map(|index| (entity, index)))
.collect();
tabbable.sort_by_key(|(_, index)| *index);
if tabbable.is_empty() {
return;
}
let current = world
.resources
.retained_ui
.interaction_for_active_mut()
.focused_entity;
let current_index =
current.and_then(|entity| tabbable.iter().position(|(stored, _)| *stored == entity));
let next_index = if reverse {
match current_index {
Some(0) => tabbable.len() - 1,
Some(index) => index - 1,
None => tabbable.len() - 1,
}
} else {
match current_index {
Some(index) => (index + 1) % tabbable.len(),
None => 0,
}
};
let focused = tabbable[next_index].0;
world
.resources
.retained_ui
.interaction_for_active_mut()
.focused_entity = Some(focused);
world.resources.retained_ui.overlays.focus_ring_visible = true;
}