use accesskit::Role;
use bevy_a11y::AccessibilityNode;
use bevy_app::{App, Plugin};
use bevy_ecs::{
component::Component,
entity::Entity,
hierarchy::{ChildOf, Children},
observer::On,
query::{Has, With},
reflect::ReflectComponent,
system::{Commands, Query, ResMut},
};
use bevy_input::keyboard::{KeyCode, KeyboardInput};
use bevy_input::ButtonState;
use bevy_input_focus::{FocusGained, FocusLost, FocusedInput, InputFocusVisible};
use bevy_picking::events::{Click, Pointer};
use bevy_reflect::Reflect;
use bevy_ui::{InteractionDisabled, Selectable, Selected};
use crate::{ScrollIntoView, ValueChange};
#[derive(Component, Debug, Clone, Default)]
#[require(
AccessibilityNode(accesskit::Node::new(Role::ListBox)),
ActiveDescendant
)]
pub struct ListBox;
#[derive(Component, Debug, Clone, Default)]
pub struct ListBoxMultiSelect;
#[derive(Component, Debug, Clone, Default)]
#[require(AccessibilityNode(accesskit::Node::new(Role::ListItem)), Selectable)]
#[derive(Reflect)]
#[reflect(Component)]
pub struct ListItem;
#[derive(Component, Debug, Clone, Default, Reflect)]
#[reflect(Component)]
#[component(immutable)]
pub struct ActiveDescendant(pub Option<Entity>);
fn listbox_on_key_input(
mut ev: On<FocusedInput<KeyboardInput>>,
q_listbox: Query<&ActiveDescendant, With<ListBox>>,
q_listitems: Query<(Has<Selected>, Has<InteractionDisabled>), With<ListItem>>,
q_children: Query<&Children>,
mut commands: Commands,
mut focus_visible: ResMut<InputFocusVisible>,
) {
if q_listbox.contains(ev.focused_entity) {
let listbox = ev.focused_entity;
let Ok(active_descendant) = q_listbox.get(listbox) else {
return;
};
let event = &ev.event().input;
if event.state == ButtonState::Pressed
&& !event.repeat
&& matches!(
event.key_code,
KeyCode::ArrowUp
| KeyCode::ArrowDown
| KeyCode::ArrowLeft
| KeyCode::ArrowRight
| KeyCode::Home
| KeyCode::End
| KeyCode::Space
| KeyCode::Enter
)
{
let key_code = event.key_code;
ev.propagate(false);
let list_items = q_children
.iter_descendants(listbox)
.filter_map(|child_id| match q_listitems.get(child_id) {
Ok((selected, disabled)) => Some((child_id, selected, disabled)),
Err(_) => None,
})
.collect::<Vec<_>>();
if list_items.is_empty() {
return; }
let prev_active = list_items
.iter()
.position(|(id, _, _)| Some(*id) == active_descendant.0)
.or_else(|| {
list_items.iter().position(|(_, selected, _)| *selected)
})
.unwrap_or(usize::MAX);
let next_active = match key_code {
KeyCode::ArrowUp | KeyCode::ArrowLeft => {
if prev_active == 0 || prev_active >= list_items.len() {
list_items.len() - 1
} else {
prev_active - 1
}
}
KeyCode::ArrowDown | KeyCode::ArrowRight => {
if prev_active >= list_items.len() - 1 {
0
} else {
prev_active + 1
}
}
KeyCode::Home => {
0
}
KeyCode::End => {
list_items.len() - 1
}
KeyCode::Space | KeyCode::Enter => {
if prev_active < list_items.len() {
let (active_id, selected, disabled) = list_items[prev_active];
if !selected && !disabled {
commands.trigger(ValueChange::<Entity> {
source: listbox,
value: active_id,
is_final: true,
});
}
}
return;
}
_ => {
return;
}
};
let (next_id, _, _) = list_items[next_active];
if prev_active != next_active {
focus_visible.0 = true;
commands
.entity(listbox)
.insert(ActiveDescendant(Some(next_id)));
}
commands.trigger(ScrollIntoView { entity: next_id });
}
}
}
fn listbox_on_row_click(
mut ev: On<Pointer<Click>>,
q_listbox: Query<(), With<ListBox>>,
q_listitems: Query<(Has<Selected>, Has<InteractionDisabled>), With<ListItem>>,
q_parents: Query<&ChildOf>,
q_children: Query<&Children>,
mut commands: Commands,
) {
if q_listbox.contains(ev.entity) {
let row_id = if q_listitems.contains(ev.original_event_target()) {
ev.original_event_target()
} else {
let mut found_row = None;
for ancestor in q_parents.iter_ancestors(ev.original_event_target()) {
if q_listbox.contains(ancestor) {
return;
}
if q_listitems.contains(ancestor) {
found_row = Some(ancestor);
break;
}
}
match found_row {
Some(row) => row,
None => return, }
};
commands
.entity(ev.entity)
.insert(ActiveDescendant(Some(row_id)));
if let (_, disabled) = q_listitems.get(row_id).unwrap()
&& disabled
{
return;
}
let all_rows = q_children
.iter_descendants(ev.entity)
.filter_map(|child_id| match q_listitems.get(child_id) {
Ok((selected, false)) => Some((child_id, selected)),
Ok((_, true)) | Err(_) => None,
})
.collect::<Vec<_>>();
if all_rows.is_empty() {
return; }
ev.propagate(false);
let current_row = all_rows
.iter()
.find(|(_, checked)| *checked)
.map(|(id, _)| *id);
if current_row == Some(row_id) {
return;
}
commands.trigger(ValueChange::<Entity> {
source: ev.entity,
value: row_id,
is_final: true,
});
}
}
fn listbox_focus_gained(
focus: On<FocusGained>,
q_listbox: Query<(Entity, &ActiveDescendant), With<ListBox>>,
q_listitems: Query<(Has<Selected>, Has<InteractionDisabled>), With<ListItem>>,
q_children: Query<&Children>,
mut commands: Commands,
) {
if let Ok((listbox, active_descendant)) = q_listbox.get(focus.entity) {
if active_descendant.0.is_none() {
let list_items = q_children
.iter_descendants(listbox)
.filter_map(|child_id| match q_listitems.get(child_id) {
Ok((selected, false)) => Some((child_id, selected)),
Ok((_, true)) | Err(_) => None,
})
.collect::<Vec<_>>();
if list_items.is_empty() {
return; }
let first_selected = list_items
.iter()
.position(|(_, selected)| *selected)
.unwrap_or(0);
commands
.entity(listbox)
.insert(ActiveDescendant(Some(list_items[first_selected].0)));
}
}
}
fn listbox_focus_lost(
focus: On<FocusLost>,
q_listbox: Query<Entity, With<ListBox>>,
mut commands: Commands,
) {
if let Ok(listbox) = q_listbox.get(focus.entity) {
commands.entity(listbox).insert(ActiveDescendant::default());
}
}
pub struct ListBoxPlugin;
impl Plugin for ListBoxPlugin {
fn build(&self, app: &mut App) {
app.add_observer(listbox_on_key_input)
.add_observer(listbox_on_row_click)
.add_observer(listbox_focus_gained)
.add_observer(listbox_focus_lost);
}
}
pub fn listbox_update_selection(
value_change: On<ValueChange<Entity>>,
q_listbox: Query<(), With<ListBox>>,
q_listitems: Query<(Has<Selected>, Has<InteractionDisabled>), With<ListItem>>,
q_parents: Query<&ChildOf>,
q_children: Query<&Children>,
mut commands: Commands,
) {
let change = value_change.event();
let row = change.value;
let listbox = if q_listbox.contains(change.source) {
change.source
} else {
let mut found = None;
for ancestor in q_parents.iter_ancestors(row) {
if q_listbox.contains(ancestor) {
found = Some(ancestor);
break;
}
}
match found {
Some(lb) => lb,
None => return, }
};
for child in q_children.iter_descendants(listbox) {
let Ok((selected, interaction_disabled)) = q_listitems.get(child) else {
continue;
};
if interaction_disabled {
continue;
}
if child == row {
if !selected {
commands.entity(child).insert(Selected);
}
} else {
if selected {
commands.entity(child).remove::<Selected>();
}
}
}
}