use bevy::ecs::system::SystemParam;
use bevy::input_focus::directional_navigation::{
AutoNavigationConfig, DirectionalNavigation, DirectionalNavigationError, FocusableArea,
};
use bevy::input_focus::navigator::find_best_candidate;
use bevy::input_focus::tab_navigation::{NavAction, TabGroup, TabIndex, TabNavigation};
use bevy::input_focus::{AutoFocus, FocusCause, InputFocus, InputFocusVisible};
use bevy::math::CompassOctant;
use bevy::prelude::*;
use bevy::ui::auto_directional_navigation::AutoDirectionalNavigation;
use bevy::ui::{
ComputedNode, ComputedStackIndex, ComputedUiTargetCamera, InteractionDisabled,
UiGlobalTransform,
};
use bevy::ui_widgets::{
Activate, Checkbox, SetSliderValue, Slider, SliderValueChange, Tab, TextInput, ToggleChecked,
};
use crate::choice::{ChoiceCount, Move, MoveChoice};
use crate::color::{AreaStepBy, ColorArea};
use crate::dock::{DockTab, moves_tabs};
use crate::list::{ListView, span};
use crate::menu::{MenuRow, MenuTitle};
use crate::model::{PAD_REPEAT_DELAY, PAD_REPEAT_RATE, repeat_steps};
use crate::number::{NumberInput, NumberStepBy};
use crate::popover::PopoverPanel;
use crate::radial::RadialCount;
use crate::region::{ArrowPolicy, FocusRegion};
use crate::skin::Role;
use crate::split::{SplitAxis, SplitDivider};
use crate::table::{TableCache, TableCursor, TableHeaderCell};
use crate::tabs::{StopQuery, TabsCount, TabsValue, first_stop};
use crate::text::AreaField;
use crate::tree::TreeView;
type TakesSideways = (
Or<(
With<Slider>,
With<ChoiceCount>,
With<NumberInput>,
With<ColorArea>,
)>,
Without<InteractionDisabled>,
);
type KeepsSideways = (
Or<(
With<Slider>,
With<ChoiceCount>,
With<Tab>,
With<TreeView>,
With<NumberInput>,
With<ColorArea>,
)>,
Without<InteractionDisabled>,
);
type Steppable = (
Or<(With<NumberInput>, With<ColorArea>)>,
Without<InteractionDisabled>,
);
type Adjustable = (Has<Slider>, Has<NumberInput>, Has<ColorArea>);
type TypesText = (With<TextInput>, Without<InteractionDisabled>);
type TurnableTabs = (With<TabsValue>, Without<InteractionDisabled>);
#[derive(Component, Debug, Default, Clone, Reflect)]
#[reflect(Component, Default, Debug, Clone)]
pub struct Dialog {
opener: Option<Entity>,
entered: bool,
}
type Navigable = (
Entity,
&'static ComputedUiTargetCamera,
&'static ComputedNode,
&'static UiGlobalTransform,
&'static InheritedVisibility,
);
type StartPoint = (
Entity,
&'static InheritedVisibility,
&'static ComputedStackIndex,
);
type StartMark = (With<AutoFocus>, With<TabIndex>);
#[derive(SystemParam)]
pub struct FocusScopes<'w, 's> {
groups: Query<'w, 's, &'static TabGroup>,
parents: Query<'w, 's, &'static ChildOf>,
tabs: TabNavigation<'w, 's>,
layers: Query<
'w,
's,
(
Entity,
&'static TabGroup,
&'static InheritedVisibility,
&'static ComputedStackIndex,
),
>,
popovers: Query<
'w,
's,
(
Entity,
&'static PopoverPanel,
&'static InheritedVisibility,
&'static ComputedStackIndex,
),
>,
starts: Query<'w, 's, StartPoint, StartMark>,
stops: Query<'w, 's, Option<&'static InheritedVisibility>, With<TabIndex>>,
sideways: Query<'w, 's, (), KeepsSideways>,
dividers: Query<'w, 's, &'static SplitDivider, Without<InteractionDisabled>>,
dock_tabs: Query<'w, 's, (), With<DockTab>>,
tables:
Query<'w, 's, (&'static TableCursor, &'static TableCache), Without<InteractionDisabled>>,
table_headers: Query<'w, 's, (), With<TableHeaderCell>>,
menu_rows: Query<'w, 's, (), MenuRow>,
menu_titles: Query<'w, 's, (), With<MenuTitle>>,
numbers: Query<'w, 's, (), Steppable>,
radials: Query<'w, 's, (), (With<RadialCount>, Without<InteractionDisabled>)>,
typing: Query<'w, 's, (), TypesText>,
areas: Query<'w, 's, (), (With<AreaField>, Without<InteractionDisabled>)>,
lists: Query<'w, 's, (&'static ComputedNode, &'static UiGlobalTransform), With<ListView>>,
tab_stops: Query<'w, 's, &'static ChildOf, With<Tab>>,
tab_roots: Query<'w, 's, (Entity, &'static InheritedVisibility), TurnableTabs>,
tab_values: Query<'w, 's, (&'static TabsValue, &'static TabsCount)>,
kids: Query<'w, 's, &'static Children>,
roles: Query<'w, 's, &'static Role>,
regions: Query<'w, 's, (Entity, &'static FocusRegion, &'static InheritedVisibility)>,
}
impl FocusScopes<'_, '_> {
fn hosts_arrows(&self, entity: Entity) -> bool {
std::iter::once(entity)
.chain(self.parents.iter_ancestors(entity))
.find_map(|e| self.regions.get(e).ok())
.is_some_and(|(_, region, _)| region.arrows == ArrowPolicy::Host)
}
pub(crate) fn region_stop(
&self,
from: Option<Entity>,
backwards: bool,
stops: &Query<StopQuery>,
) -> Option<Entity> {
let scope = self.active();
let mut found: Vec<(Vec<usize>, Entity, Entity)> = self
.regions
.iter()
.filter(|&(entity, _, visible)| visible.get() && self.of(entity) == scope)
.filter_map(|(entity, ..)| {
Some((self.tree_path(entity), entity, first_stop(entity, stops)?))
})
.collect();
found.sort_by(|a, b| a.0.cmp(&b.0));
let here = from.and_then(|f| {
std::iter::once(f)
.chain(self.parents.iter_ancestors(f))
.find_map(|e| found.iter().position(|(_, region, _)| *region == e))
});
let at = match (here, backwards) {
(Some(i), false) => (i + 1) % found.len(),
(Some(i), true) => (i + found.len() - 1) % found.len(),
(None, false) => 0,
(None, true) => found.len().checked_sub(1)?,
};
found.get(at).map(|&(_, _, stop)| stop)
}
fn tree_path(&self, entity: Entity) -> Vec<usize> {
let mut path = Vec::new();
let mut current = entity;
while let Ok(parent) = self.parents.get(current) {
let index = self
.kids
.get(parent.parent())
.ok()
.and_then(|kids| kids.iter().position(|kid| kid == current))
.unwrap_or(0);
path.push(index);
current = parent.parent();
}
path.push(current.index_u32() as usize);
path.reverse();
path
}
fn of(&self, entity: Entity) -> Option<Entity> {
self.parents
.iter_ancestors(entity)
.find_map(|a| self.groups.get(a).ok().map(|g| (a, g.modal)))
.and_then(|(a, modal)| modal.then_some(a))
}
fn holds(&self, entity: Entity) -> bool {
self.stops
.get(entity)
.is_ok_and(|visible| visible.is_none_or(|v| v.get()))
}
fn keeps_sideways(&self, entity: Entity) -> bool {
self.menu_rows.contains(entity) || self.keeps_own_sideways(entity)
}
fn keeps_own_sideways(&self, entity: Entity) -> bool {
self.sideways.contains(entity)
}
fn types_text(&self, entity: Entity) -> bool {
self.typing.contains(entity)
}
fn types_lines(&self, entity: Entity) -> bool {
self.areas.contains(entity)
}
pub(crate) fn is_tab(&self, entity: Entity) -> bool {
self.tab_stops.contains(entity)
}
pub(crate) fn tabs_for(&self, from: Entity) -> Option<Entity> {
if let Some(root) = self
.parents
.iter_ancestors(from)
.find(|&a| self.tab_roots.contains(a))
{
return Some(root);
}
if !self.holds(from) {
return None;
}
let scope = self.of(from);
let mut here = self
.tab_roots
.iter()
.filter(|&(root, visible)| visible.get() && self.of(root) == scope)
.map(|(root, _)| root);
let only = here.next()?;
here.next().is_none().then_some(only)
}
pub(crate) fn tabs_state(&self, root: Entity) -> Option<(usize, usize)> {
let (value, count) = self.tab_values.get(root).ok()?;
Some((value.0.min(count.0.saturating_sub(1)), count.0))
}
pub(crate) fn tab_at(&self, root: Entity, index: usize) -> Option<Entity> {
let strip = self
.kids
.get(root)
.ok()?
.iter()
.find(|&kid| self.roles.get(kid) == Ok(&Role::TabList))?;
self.kids.get(strip).ok()?.get(index).copied()
}
fn landing(&self, from: Entity, to: Entity) -> Entity {
let Ok(strip) = self.tab_stops.get(to).map(ChildOf::parent) else {
return to;
};
if self
.tab_stops
.get(from)
.is_ok_and(|tab| tab.parent() == strip)
{
return to;
}
self.parents
.get(strip)
.ok()
.and_then(|root| {
let (selected, _) = self.tabs_state(root.parent())?;
self.tab_at(root.parent(), selected)
})
.unwrap_or(to)
}
fn list_of(&self, entity: Entity) -> Option<Entity> {
self.parents
.iter_ancestors(entity)
.find(|&a| self.lists.contains(a))
}
fn hides(
&self,
candidate: Entity,
node: &ComputedNode,
transform: &UiGlobalTransform,
from: Entity,
) -> bool {
let Some(list) = self.list_of(candidate) else {
return false;
};
if self.list_of(from) == Some(list) {
return false;
}
let Ok((view, view_transform)) = self.lists.get(list) else {
return false;
};
let (top, height) = span(node, transform);
let (view_top, view_height) = span(view, view_transform);
top >= view_top + view_height || top + height <= view_top
}
fn active(&self) -> Option<Entity> {
self.layers
.iter()
.filter(|(_, group, visible, _)| group.modal && visible.get())
.max_by_key(|(.., stack)| stack.0)
.map(|(entity, ..)| entity)
}
pub(crate) fn cancel_target(&self, focus: Option<Entity>) -> Option<Entity> {
let dialog = self.active();
focus
.filter(|&f| self.holds(f) && self.of(f) == dialog)
.map(|f| self.popover_of(f).unwrap_or(f))
.or(dialog)
}
fn popover_of(&self, focus: Entity) -> Option<Entity> {
self.popovers
.iter()
.filter(|(entity, panel, visible, _)| {
!panel.modal
&& visible.get()
&& !self.parents.iter_ancestors(focus).any(|a| a == *entity)
&& (focus == panel.anchor
|| self
.parents
.iter_ancestors(focus)
.any(|a| a == panel.anchor))
})
.max_by_key(|(.., stack)| stack.0)
.map(|(entity, ..)| entity)
}
fn start(&self, scope: Option<Entity>) -> Option<Entity> {
self.starts
.iter()
.filter(|&(entity, visible, _)| visible.get() && self.of(entity) == scope)
.max_by_key(|(_, _, stack)| stack.0)
.map(|(entity, ..)| entity)
.or_else(|| match scope {
Some(group) => self.tabs.initialize(group, NavAction::First).ok(),
None => self
.tabs
.navigate(&InputFocus::default(), NavAction::First)
.ok(),
})
}
}
pub(crate) fn show_focus(shown: &mut ResMut<InputFocusVisible>) {
if !shown.0 {
shown.0 = true;
}
}
pub fn navigate_by_input(
keys: Res<ButtonInput<KeyCode>>,
gamepads: Query<&Gamepad>,
mut nav: DirectionalNavigation,
mut shown: ResMut<InputFocusVisible>,
config: Res<AutoNavigationConfig>,
navigable: Query<Navigable, With<AutoDirectionalNavigation>>,
scopes: FocusScopes,
) {
let pressed = |key: KeyCode, pad: GamepadButton| {
keys.just_pressed(key) || gamepads.iter().any(|g| g.just_pressed(pad))
};
let pad = |button: GamepadButton| gamepads.iter().any(|g| g.just_pressed(button));
let axis = |plus: bool, minus: bool| plus as i8 as f32 - minus as i8 as f32;
let net_x = axis(
pressed(KeyCode::ArrowRight, GamepadButton::DPadRight),
pressed(KeyCode::ArrowLeft, GamepadButton::DPadLeft),
);
let net_y = axis(
pressed(KeyCode::ArrowUp, GamepadButton::DPadUp),
pressed(KeyCode::ArrowDown, GamepadButton::DPadDown),
);
let from = nav.focus.get().filter(|&f| scopes.holds(f));
let (net_x, net_y) = if from.is_some_and(|f| scopes.hosts_arrows(f) || scopes.types_lines(f)) {
(
axis(pad(GamepadButton::DPadRight), pad(GamepadButton::DPadLeft)),
axis(pad(GamepadButton::DPadUp), pad(GamepadButton::DPadDown)),
)
} else {
(net_x, net_y)
};
if net_x == 0.0 && net_y == 0.0 {
return;
}
show_focus(&mut shown);
if from.is_some_and(|f| scopes.radials.contains(f)) {
return;
}
if from.is_some_and(|f| scopes.dock_tabs.contains(f)) && moves_tabs(&keys) {
return;
}
let net_x = if from.is_some_and(|f| scopes.keeps_sideways(f)) {
0.0
} else if from.is_some_and(|f| scopes.types_text(f)) {
axis(pad(GamepadButton::DPadRight), pad(GamepadButton::DPadLeft))
} else {
net_x
};
let net_y = if from.is_some_and(|f| scopes.menu_titles.contains(f)) {
0.0
} else {
net_y
};
let net_y = if from.is_some_and(|f| scopes.numbers.contains(f)) {
axis(pad(GamepadButton::DPadUp), pad(GamepadButton::DPadDown))
} else {
net_y
};
let net_y = if from.is_some_and(|f| {
scopes
.tables
.get(f)
.is_ok_and(|(cursor, cache)| cache.takes(cursor.0, net_y > 0.0))
}) {
0.0
} else {
net_y
};
if net_x != 0.0
&& from.is_some_and(|f| scopes.table_headers.contains(f))
&& (keys.pressed(KeyCode::ControlLeft) || keys.pressed(KeyCode::ControlRight))
{
return;
}
let divider = from.and_then(|f| scopes.dividers.get(f).ok().map(SplitDivider::axis));
let net_x = if divider == Some(SplitAxis::Horizontal) {
0.0
} else {
net_x
};
let net_y = if divider == Some(SplitAxis::Vertical) {
0.0
} else {
net_y
};
let Some(direction) = Dir2::from_xy(net_x, net_y).ok().map(CompassOctant::from) else {
return;
};
let Some(from) = from else {
if let Some(start) = scopes.start(scopes.active()) {
nav.focus.set(start, FocusCause::Navigated);
}
return;
};
if !matches!(
nav.navigate(direction),
Err(DirectionalNavigationError::NoNeighborInDirection { .. })
) {
return;
}
let Some((camera, origin)) = navigable.get(from).ok().and_then(focusable_area) else {
return;
};
let scope = scopes.of(from);
let candidates: Vec<FocusableArea> = navigable
.iter()
.filter(|(entity, _, node, transform, visible)| {
!node.is_empty()
&& visible.get()
&& scopes.of(*entity) == scope
&& !scopes.hides(*entity, node, transform, from)
})
.filter_map(focusable_area)
.filter_map(|(c, area)| (c == camera).then_some(area))
.collect();
if let Some(list) = scopes.list_of(from)
&& matches!(direction, CompassOctant::North | CompassOctant::South)
{
let rows: Vec<FocusableArea> = candidates
.iter()
.filter(|area| scopes.list_of(area.entity) == Some(list))
.copied()
.collect();
if let Some(to) = find_best_candidate(&origin, direction, &rows, &config) {
nav.focus.set(to, FocusCause::Navigated);
return;
}
}
if let Some(to) = find_best_candidate(&origin, direction, &candidates, &config) {
nav.focus
.set(scopes.landing(from, to), FocusCause::Navigated);
}
}
fn focusable_area(
(entity, camera, node, transform, _): (
Entity,
&ComputedUiTargetCamera,
&ComputedNode,
&UiGlobalTransform,
&InheritedVisibility,
),
) -> Option<(Entity, FocusableArea)> {
let (scale, rotation, translation) = transform.to_scale_angle_translation();
let size = node.size() * node.inverse_scale_factor() * scale;
let (sin, cos) = (rotation.sin().abs(), rotation.cos().abs());
Some((
camera.get()?,
FocusableArea {
entity,
position: translation * node.inverse_scale_factor(),
size: Vec2::new(size.x * cos + size.y * sin, size.x * sin + size.y * cos),
},
))
}
pub(crate) fn remember_opener(
add: On<Add<Dialog>>,
focus: Option<Res<InputFocus>>,
mut dialogs: Query<&mut Dialog>,
) {
if let (Some(focus), Ok(mut dialog)) = (focus, dialogs.get_mut(add.entity)) {
dialog.opener = focus.get();
}
}
pub fn focus_opened_dialogs(
mut dialogs: Query<(Entity, &mut Dialog, &InheritedVisibility)>,
mut focus: ResMut<InputFocus>,
mut shown: ResMut<InputFocusVisible>,
tabs: TabNavigation,
parents: Query<&ChildOf>,
) {
for (entity, mut dialog, visible) in &mut dialogs {
if dialog.entered {
continue;
}
if !inside(focus.get(), entity, &parents)
&& let Ok(first) = tabs.initialize(entity, NavAction::First)
{
focus.set(first, FocusCause::Navigated);
}
let holds = inside(focus.get(), entity, &parents);
dialog.entered = visible.get() || holds;
if holds {
show_focus(&mut shown);
}
}
}
pub(crate) fn return_focus(
remove: On<Remove<Dialog>>,
dialogs: Query<&Dialog>,
mut focus: ResMut<InputFocus>,
parents: Query<&ChildOf>,
alive: Query<()>,
) {
let Some(opener) = dialogs
.get(remove.entity)
.ok()
.and_then(|d| d.opener)
.filter(|&o| alive.contains(o))
else {
return;
};
let current = focus.get();
if current.is_none_or(|f| !alive.contains(f)) || inside(current, remove.entity, &parents) {
focus.set(opener, FocusCause::Navigated);
}
}
fn inside(focus: Option<Entity>, root: Entity, parents: &Query<&ChildOf>) -> bool {
focus.is_some_and(|f| f == root || parents.iter_ancestors(f).any(|a| a == root))
}
pub fn gamepad_select(
focus: Res<InputFocus>,
mut shown: ResMut<InputFocusVisible>,
gamepads: Query<&Gamepad>,
buttons: Query<(), (With<bevy::ui_widgets::Button>, Without<InteractionDisabled>)>,
toggles: Query<(), (With<Checkbox>, Without<InteractionDisabled>)>,
mut commands: Commands,
) {
if !gamepads
.iter()
.any(|g| g.just_pressed(GamepadButton::South))
{
return;
}
show_focus(&mut shown);
if let Some(entity) = focus.get() {
if buttons.contains(entity) {
commands.trigger(Activate { entity });
} else if toggles.contains(entity) {
commands.trigger(ToggleChecked { entity });
}
}
}
pub fn gamepad_adjust(
time: Res<Time<Real>>,
focus: Res<InputFocus>,
mut shown: ResMut<InputFocusVisible>,
gamepads: Query<&Gamepad>,
controls: Query<Adjustable, TakesSideways>,
mut held: Local<(i8, f32)>,
mut commands: Commands,
) {
let down = |button| gamepads.iter().any(|g| g.pressed(button));
let direction = down(GamepadButton::DPadRight) as i8 - down(GamepadButton::DPadLeft) as i8;
let vertical = down(GamepadButton::DPadUp) || down(GamepadButton::DPadDown);
let Some((control, is_slider, is_number, is_area)) = focus
.get()
.filter(|_| direction != 0 && !vertical)
.and_then(|f| {
controls
.get(f)
.ok()
.map(|(slider, number, area)| (f, slider, number, area))
})
else {
*held = (0, 0.0);
return;
};
let steps = if held.0 == direction {
let before = held.1;
held.1 += time.delta_secs();
repeat_steps(before, held.1, PAD_REPEAT_DELAY, PAD_REPEAT_RATE)
} else {
*held = (direction, 0.0);
1
};
if steps > 0 {
show_focus(&mut shown);
if is_number {
commands.trigger(NumberStepBy {
entity: control,
steps: f32::from(direction) * steps as f32,
});
} else if is_area {
commands.trigger(AreaStepBy {
entity: control,
steps: f32::from(direction) * steps as f32,
});
} else if is_slider {
commands.trigger(SetSliderValue {
entity: control,
change: SliderValueChange::RelativeStep(f32::from(direction) * steps as f32),
});
} else {
commands.trigger(MoveChoice {
entity: control,
to: Move::Step(i32::from(direction) * steps as i32),
});
}
}
}