use accesskit::Role;
use bevy_a11y::AccessibilityNode;
use bevy_app::{App, Plugin};
use bevy_ecs::event::EntityEvent;
use bevy_ecs::query::{Has, Without};
use bevy_ecs::system::{In, ResMut};
use bevy_ecs::{
component::Component,
observer::On,
system::{Commands, Query},
};
use bevy_input::keyboard::{KeyCode, KeyboardInput};
use bevy_input::ButtonState;
use bevy_input_focus::{FocusedInput, InputFocus, InputFocusVisible};
use bevy_picking::events::{Click, Pointer};
use bevy_ui::{Checkable, Checked, InteractionDisabled};
use crate::{Callback, Notify as _, ValueChange};
use bevy_ecs::entity::Entity;
#[derive(Component, Debug, Default)]
#[require(AccessibilityNode(accesskit::Node::new(Role::CheckBox)), Checkable)]
pub struct Checkbox {
pub on_change: Callback<In<ValueChange<bool>>>,
}
fn checkbox_on_key_input(
mut ev: On<FocusedInput<KeyboardInput>>,
q_checkbox: Query<(&Checkbox, Has<Checked>), Without<InteractionDisabled>>,
mut commands: Commands,
) {
if let Ok((checkbox, is_checked)) = q_checkbox.get(ev.focused_entity) {
let event = &ev.event().input;
if event.state == ButtonState::Pressed
&& !event.repeat
&& (event.key_code == KeyCode::Enter || event.key_code == KeyCode::Space)
{
ev.propagate(false);
set_checkbox_state(&mut commands, ev.focused_entity, checkbox, !is_checked);
}
}
}
fn checkbox_on_pointer_click(
mut click: On<Pointer<Click>>,
q_checkbox: Query<(&Checkbox, Has<Checked>, Has<InteractionDisabled>)>,
focus: Option<ResMut<InputFocus>>,
focus_visible: Option<ResMut<InputFocusVisible>>,
mut commands: Commands,
) {
if let Ok((checkbox, is_checked, disabled)) = q_checkbox.get(click.entity) {
if let Some(mut focus) = focus {
focus.0 = Some(click.entity);
}
if let Some(mut focus_visible) = focus_visible {
focus_visible.0 = false;
}
click.propagate(false);
if !disabled {
set_checkbox_state(&mut commands, click.entity, checkbox, !is_checked);
}
}
}
#[derive(EntityEvent)]
pub struct SetChecked {
pub entity: Entity,
pub checked: bool,
}
#[derive(EntityEvent)]
pub struct ToggleChecked {
pub entity: Entity,
}
fn checkbox_on_set_checked(
set_checked: On<SetChecked>,
q_checkbox: Query<(&Checkbox, Has<Checked>, Has<InteractionDisabled>)>,
mut commands: Commands,
) {
if let Ok((checkbox, is_checked, disabled)) = q_checkbox.get(set_checked.entity) {
if disabled {
return;
}
let will_be_checked = set_checked.checked;
if will_be_checked != is_checked {
set_checkbox_state(&mut commands, set_checked.entity, checkbox, will_be_checked);
}
}
}
fn checkbox_on_toggle_checked(
toggle_checked: On<ToggleChecked>,
q_checkbox: Query<(&Checkbox, Has<Checked>, Has<InteractionDisabled>)>,
mut commands: Commands,
) {
if let Ok((checkbox, is_checked, disabled)) = q_checkbox.get(toggle_checked.entity) {
if disabled {
return;
}
set_checkbox_state(&mut commands, toggle_checked.entity, checkbox, !is_checked);
}
}
fn set_checkbox_state(
commands: &mut Commands,
entity: impl Into<Entity>,
checkbox: &Checkbox,
new_state: bool,
) {
if !matches!(checkbox.on_change, Callback::Ignore) {
commands.notify_with(
&checkbox.on_change,
ValueChange {
source: entity.into(),
value: new_state,
},
);
} else if new_state {
commands.entity(entity.into()).insert(Checked);
} else {
commands.entity(entity.into()).remove::<Checked>();
}
}
pub struct CheckboxPlugin;
impl Plugin for CheckboxPlugin {
fn build(&self, app: &mut App) {
app.add_observer(checkbox_on_key_input)
.add_observer(checkbox_on_pointer_click)
.add_observer(checkbox_on_set_checked)
.add_observer(checkbox_on_toggle_checked);
}
}