use accesskit::Role;
use bevy_a11y::AccessibilityNode;
use bevy_app::{App, Plugin};
use bevy_ecs::{
component::Component,
hierarchy::{ChildOf, Children},
observer::On,
query::{Has, With},
reflect::ReflectComponent,
system::{Commands, In, Query},
};
use bevy_input::keyboard::{KeyCode, KeyboardInput};
use bevy_input::ButtonState;
use bevy_input_focus::FocusedInput;
use bevy_picking::events::{Click, Pointer};
use bevy_reflect::Reflect;
use bevy_ui::{Checkable, Checked, InteractionDisabled};
use crate::{Activate, Callback, Notify};
#[derive(Component, Debug)]
#[require(AccessibilityNode(accesskit::Node::new(Role::RadioGroup)))]
pub struct RadioGroup {
pub on_change: Callback<In<Activate>>,
}
#[derive(Component, Debug)]
#[require(AccessibilityNode(accesskit::Node::new(Role::RadioButton)), Checkable)]
#[derive(Reflect)]
#[reflect(Component)]
pub struct RadioButton;
fn radio_group_on_key_input(
mut ev: On<FocusedInput<KeyboardInput>>,
q_group: Query<&RadioGroup>,
q_radio: Query<(Has<Checked>, Has<InteractionDisabled>), With<RadioButton>>,
q_children: Query<&Children>,
mut commands: Commands,
) {
if let Ok(RadioGroup { on_change }) = q_group.get(ev.focused_entity) {
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
)
{
let key_code = event.key_code;
ev.propagate(false);
let radio_buttons = q_children
.iter_descendants(ev.focused_entity)
.filter_map(|child_id| match q_radio.get(child_id) {
Ok((checked, false)) => Some((child_id, checked)),
Ok((_, true)) | Err(_) => None,
})
.collect::<Vec<_>>();
if radio_buttons.is_empty() {
return;
}
let current_index = radio_buttons
.iter()
.position(|(_, checked)| *checked)
.unwrap_or(usize::MAX);
let next_index = match key_code {
KeyCode::ArrowUp | KeyCode::ArrowLeft => {
if current_index == 0 || current_index >= radio_buttons.len() {
radio_buttons.len() - 1
} else {
current_index - 1
}
}
KeyCode::ArrowDown | KeyCode::ArrowRight => {
if current_index >= radio_buttons.len() - 1 {
0
} else {
current_index + 1
}
}
KeyCode::Home => {
0
}
KeyCode::End => {
radio_buttons.len() - 1
}
_ => {
return;
}
};
if current_index == next_index {
return;
}
let (next_id, _) = radio_buttons[next_index];
commands.notify_with(on_change, Activate(next_id));
}
}
}
fn radio_group_on_button_click(
mut ev: On<Pointer<Click>>,
q_group: Query<&RadioGroup>,
q_radio: Query<(Has<Checked>, Has<InteractionDisabled>), With<RadioButton>>,
q_parents: Query<&ChildOf>,
q_children: Query<&Children>,
mut commands: Commands,
) {
if let Ok(RadioGroup { on_change }) = q_group.get(ev.entity) {
let radio_id = if q_radio.contains(ev.original_event_target()) {
ev.original_event_target()
} else {
let mut found_radio = None;
for ancestor in q_parents.iter_ancestors(ev.original_event_target()) {
if q_group.contains(ancestor) {
return;
}
if q_radio.contains(ancestor) {
found_radio = Some(ancestor);
break;
}
}
match found_radio {
Some(radio) => radio,
None => return,
}
};
if q_radio.get(radio_id).unwrap().1 {
return;
}
let radio_buttons = q_children
.iter_descendants(ev.entity)
.filter_map(|child_id| match q_radio.get(child_id) {
Ok((checked, false)) => Some((child_id, checked)),
Ok((_, true)) | Err(_) => None,
})
.collect::<Vec<_>>();
if radio_buttons.is_empty() {
return;
}
ev.propagate(false);
let current_radio = radio_buttons
.iter()
.find(|(_, checked)| *checked)
.map(|(id, _)| *id);
if current_radio == Some(radio_id) {
return;
}
commands.notify_with(on_change, Activate(radio_id));
}
}
pub struct RadioGroupPlugin;
impl Plugin for RadioGroupPlugin {
fn build(&self, app: &mut App) {
app.add_observer(radio_group_on_key_input)
.add_observer(radio_group_on_button_click);
}
}