Core radio button and radio group (#19778)
# Objective Core Radio Button and Radio Group widgets. Part of #19236
This commit is contained in:
parent
596eb48d8a
commit
9f551bb1e2
213
crates/bevy_core_widgets/src/core_radio.rs
Normal file
213
crates/bevy_core_widgets/src/core_radio.rs
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
use accesskit::Role;
|
||||||
|
use bevy_a11y::AccessibilityNode;
|
||||||
|
use bevy_app::{App, Plugin};
|
||||||
|
use bevy_ecs::hierarchy::{ChildOf, Children};
|
||||||
|
use bevy_ecs::query::Has;
|
||||||
|
use bevy_ecs::system::In;
|
||||||
|
use bevy_ecs::{
|
||||||
|
component::Component,
|
||||||
|
entity::Entity,
|
||||||
|
observer::On,
|
||||||
|
query::With,
|
||||||
|
system::{Commands, Query, SystemId},
|
||||||
|
};
|
||||||
|
use bevy_input::keyboard::{KeyCode, KeyboardInput};
|
||||||
|
use bevy_input::ButtonState;
|
||||||
|
use bevy_input_focus::FocusedInput;
|
||||||
|
use bevy_picking::events::{Click, Pointer};
|
||||||
|
use bevy_ui::{Checked, InteractionDisabled};
|
||||||
|
|
||||||
|
/// Headless widget implementation for a "radio button group". This component is used to group
|
||||||
|
/// multiple [`CoreRadio`] components together, allowing them to behave as a single unit. It
|
||||||
|
/// implements the tab navigation logic and keyboard shortcuts for radio buttons.
|
||||||
|
///
|
||||||
|
/// The [`CoreRadioGroup`] component does not have any state itself, and makes no assumptions about
|
||||||
|
/// what, if any, value is associated with each radio button, or what Rust type that value might be.
|
||||||
|
/// Instead, the output of the group is the entity id of the selected button. The app can then
|
||||||
|
/// derive the selected value from this using app-specific means, such as accessing a component on
|
||||||
|
/// the individual buttons.
|
||||||
|
///
|
||||||
|
/// The [`CoreRadioGroup`] doesn't actually set the [`Checked`] states directly, that is presumed to
|
||||||
|
/// happen by the app or via some external data-binding scheme. Typically, each button would be
|
||||||
|
/// associated with a particular constant value, and would be checked whenever that value is equal
|
||||||
|
/// to the group's value. This also means that as long as each button's associated value is unique
|
||||||
|
/// within the group, it should never be the case that more than one button is selected at a time.
|
||||||
|
#[derive(Component, Debug)]
|
||||||
|
#[require(AccessibilityNode(accesskit::Node::new(Role::RadioGroup)))]
|
||||||
|
pub struct CoreRadioGroup {
|
||||||
|
/// Callback which is called when the selected radio button changes.
|
||||||
|
pub on_change: Option<SystemId<In<Entity>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Headless widget implementation for radio buttons. These should be enclosed within a
|
||||||
|
/// [`CoreRadioGroup`] widget, which is responsible for the mutual exclusion logic.
|
||||||
|
///
|
||||||
|
/// According to the WAI-ARIA best practices document, radio buttons should not be focusable,
|
||||||
|
/// but rather the enclosing group should be focusable.
|
||||||
|
/// See <https://www.w3.org/WAI/ARIA/apg/patterns/radio>/
|
||||||
|
#[derive(Component, Debug)]
|
||||||
|
#[require(AccessibilityNode(accesskit::Node::new(Role::RadioButton)), Checked)]
|
||||||
|
pub struct CoreRadio;
|
||||||
|
|
||||||
|
fn radio_group_on_key_input(
|
||||||
|
mut ev: On<FocusedInput<KeyboardInput>>,
|
||||||
|
q_group: Query<&CoreRadioGroup>,
|
||||||
|
q_radio: Query<(Has<Checked>, Has<InteractionDisabled>), With<CoreRadio>>,
|
||||||
|
q_children: Query<&Children>,
|
||||||
|
mut commands: Commands,
|
||||||
|
) {
|
||||||
|
if let Ok(CoreRadioGroup { on_change }) = q_group.get(ev.target()) {
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Find all radio descendants that are not disabled
|
||||||
|
let radio_buttons = q_children
|
||||||
|
.iter_descendants(ev.target())
|
||||||
|
.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; // No enabled radio buttons in the group
|
||||||
|
}
|
||||||
|
let current_index = radio_buttons
|
||||||
|
.iter()
|
||||||
|
.position(|(_, checked)| *checked)
|
||||||
|
.unwrap_or(usize::MAX); // Default to invalid index if none are checked
|
||||||
|
|
||||||
|
let next_index = match key_code {
|
||||||
|
KeyCode::ArrowUp | KeyCode::ArrowLeft => {
|
||||||
|
// Navigate to the previous radio button in the group
|
||||||
|
if current_index == 0 || current_index >= radio_buttons.len() {
|
||||||
|
// If we're at the first one, wrap around to the last
|
||||||
|
radio_buttons.len() - 1
|
||||||
|
} else {
|
||||||
|
// Move to the previous one
|
||||||
|
current_index - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::ArrowDown | KeyCode::ArrowRight => {
|
||||||
|
// Navigate to the next radio button in the group
|
||||||
|
if current_index >= radio_buttons.len() - 1 {
|
||||||
|
// If we're at the last one, wrap around to the first
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
// Move to the next one
|
||||||
|
current_index + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Home => {
|
||||||
|
// Navigate to the first radio button in the group
|
||||||
|
0
|
||||||
|
}
|
||||||
|
KeyCode::End => {
|
||||||
|
// Navigate to the last radio button in the group
|
||||||
|
radio_buttons.len() - 1
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if current_index == next_index {
|
||||||
|
// If the next index is the same as the current, do nothing
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (next_id, _) = radio_buttons[next_index];
|
||||||
|
|
||||||
|
// Trigger the on_change event for the newly checked radio button
|
||||||
|
if let Some(on_change) = on_change {
|
||||||
|
commands.run_system_with(*on_change, next_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn radio_group_on_button_click(
|
||||||
|
mut ev: On<Pointer<Click>>,
|
||||||
|
q_group: Query<&CoreRadioGroup>,
|
||||||
|
q_radio: Query<(Has<Checked>, Has<InteractionDisabled>), With<CoreRadio>>,
|
||||||
|
q_parents: Query<&ChildOf>,
|
||||||
|
q_children: Query<&Children>,
|
||||||
|
mut commands: Commands,
|
||||||
|
) {
|
||||||
|
if let Ok(CoreRadioGroup { on_change }) = q_group.get(ev.target()) {
|
||||||
|
// Starting with the original target, search upward for a radio button.
|
||||||
|
let radio_id = if q_radio.contains(ev.original_target()) {
|
||||||
|
ev.original_target()
|
||||||
|
} else {
|
||||||
|
// Search ancestors for the first radio button
|
||||||
|
let mut found_radio = None;
|
||||||
|
for ancestor in q_parents.iter_ancestors(ev.original_target()) {
|
||||||
|
if q_group.contains(ancestor) {
|
||||||
|
// We reached a radio group before finding a radio button, bail out
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if q_radio.contains(ancestor) {
|
||||||
|
found_radio = Some(ancestor);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match found_radio {
|
||||||
|
Some(radio) => radio,
|
||||||
|
None => return, // No radio button found in the ancestor chain
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Gather all the enabled radio group descendants for exclusion.
|
||||||
|
let radio_buttons = q_children
|
||||||
|
.iter_descendants(ev.target())
|
||||||
|
.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; // No enabled radio buttons in the group
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pick out the radio button that is currently checked.
|
||||||
|
ev.propagate(false);
|
||||||
|
let current_radio = radio_buttons
|
||||||
|
.iter()
|
||||||
|
.find(|(_, checked)| *checked)
|
||||||
|
.map(|(id, _)| *id);
|
||||||
|
|
||||||
|
if current_radio == Some(radio_id) {
|
||||||
|
// If they clicked the currently checked radio button, do nothing
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger the on_change event for the newly checked radio button
|
||||||
|
if let Some(on_change) = on_change {
|
||||||
|
commands.run_system_with(*on_change, radio_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Plugin that adds the observers for the [`CoreRadioGroup`] widget.
|
||||||
|
pub struct CoreRadioGroupPlugin;
|
||||||
|
|
||||||
|
impl Plugin for CoreRadioGroupPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.add_observer(radio_group_on_key_input)
|
||||||
|
.add_observer(radio_group_on_button_click);
|
||||||
|
}
|
||||||
|
}
|
@ -23,7 +23,7 @@ use bevy_picking::events::{Drag, DragEnd, DragStart, Pointer, Press};
|
|||||||
use bevy_ui::{ComputedNode, ComputedNodeTarget, InteractionDisabled, UiGlobalTransform, UiScale};
|
use bevy_ui::{ComputedNode, ComputedNodeTarget, InteractionDisabled, UiGlobalTransform, UiScale};
|
||||||
|
|
||||||
/// Defines how the slider should behave when you click on the track (not the thumb).
|
/// Defines how the slider should behave when you click on the track (not the thumb).
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default, PartialEq, Clone, Copy)]
|
||||||
pub enum TrackClick {
|
pub enum TrackClick {
|
||||||
/// Clicking on the track lets you drag to edit the value, just like clicking on the thumb.
|
/// Clicking on the track lets you drag to edit the value, just like clicking on the thumb.
|
||||||
#[default]
|
#[default]
|
||||||
|
@ -16,12 +16,14 @@
|
|||||||
|
|
||||||
mod core_button;
|
mod core_button;
|
||||||
mod core_checkbox;
|
mod core_checkbox;
|
||||||
|
mod core_radio;
|
||||||
mod core_slider;
|
mod core_slider;
|
||||||
|
|
||||||
use bevy_app::{App, Plugin};
|
use bevy_app::{App, Plugin};
|
||||||
|
|
||||||
pub use core_button::{CoreButton, CoreButtonPlugin};
|
pub use core_button::{CoreButton, CoreButtonPlugin};
|
||||||
pub use core_checkbox::{CoreCheckbox, CoreCheckboxPlugin, SetChecked, ToggleChecked};
|
pub use core_checkbox::{CoreCheckbox, CoreCheckboxPlugin, SetChecked, ToggleChecked};
|
||||||
|
pub use core_radio::{CoreRadio, CoreRadioGroup, CoreRadioGroupPlugin};
|
||||||
pub use core_slider::{
|
pub use core_slider::{
|
||||||
CoreSlider, CoreSliderDragState, CoreSliderPlugin, CoreSliderThumb, SetSliderValue,
|
CoreSlider, CoreSliderDragState, CoreSliderPlugin, CoreSliderThumb, SetSliderValue,
|
||||||
SliderRange, SliderStep, SliderValue, TrackClick,
|
SliderRange, SliderStep, SliderValue, TrackClick,
|
||||||
@ -33,6 +35,11 @@ pub struct CoreWidgetsPlugin;
|
|||||||
|
|
||||||
impl Plugin for CoreWidgetsPlugin {
|
impl Plugin for CoreWidgetsPlugin {
|
||||||
fn build(&self, app: &mut App) {
|
fn build(&self, app: &mut App) {
|
||||||
app.add_plugins((CoreButtonPlugin, CoreCheckboxPlugin, CoreSliderPlugin));
|
app.add_plugins((
|
||||||
|
CoreButtonPlugin,
|
||||||
|
CoreCheckboxPlugin,
|
||||||
|
CoreRadioGroupPlugin,
|
||||||
|
CoreSliderPlugin,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,8 +3,8 @@
|
|||||||
use bevy::{
|
use bevy::{
|
||||||
color::palettes::basic::*,
|
color::palettes::basic::*,
|
||||||
core_widgets::{
|
core_widgets::{
|
||||||
CoreButton, CoreCheckbox, CoreSlider, CoreSliderThumb, CoreWidgetsPlugin, SliderRange,
|
CoreButton, CoreCheckbox, CoreRadio, CoreRadioGroup, CoreSlider, CoreSliderThumb,
|
||||||
SliderValue, TrackClick,
|
CoreWidgetsPlugin, SliderRange, SliderValue, TrackClick,
|
||||||
},
|
},
|
||||||
ecs::system::SystemId,
|
ecs::system::SystemId,
|
||||||
input_focus::{
|
input_focus::{
|
||||||
@ -27,7 +27,10 @@ fn main() {
|
|||||||
))
|
))
|
||||||
// Only run the app when there is user input. This will significantly reduce CPU/GPU use.
|
// Only run the app when there is user input. This will significantly reduce CPU/GPU use.
|
||||||
.insert_resource(WinitSettings::desktop_app())
|
.insert_resource(WinitSettings::desktop_app())
|
||||||
.insert_resource(DemoWidgetStates { slider_value: 50.0 })
|
.insert_resource(DemoWidgetStates {
|
||||||
|
slider_value: 50.0,
|
||||||
|
slider_click: TrackClick::Snap,
|
||||||
|
})
|
||||||
.add_systems(Startup, setup)
|
.add_systems(Startup, setup)
|
||||||
.add_systems(
|
.add_systems(
|
||||||
Update,
|
Update,
|
||||||
@ -37,8 +40,8 @@ fn main() {
|
|||||||
update_button_style2,
|
update_button_style2,
|
||||||
update_slider_style.after(update_widget_values),
|
update_slider_style.after(update_widget_values),
|
||||||
update_slider_style2.after(update_widget_values),
|
update_slider_style2.after(update_widget_values),
|
||||||
update_checkbox_style.after(update_widget_values),
|
update_checkbox_or_radio_style.after(update_widget_values),
|
||||||
update_checkbox_style2.after(update_widget_values),
|
update_checkbox_or_radio_style2.after(update_widget_values),
|
||||||
toggle_disabled,
|
toggle_disabled,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -69,6 +72,11 @@ struct DemoSliderThumb;
|
|||||||
#[derive(Component, Default)]
|
#[derive(Component, Default)]
|
||||||
struct DemoCheckbox;
|
struct DemoCheckbox;
|
||||||
|
|
||||||
|
/// Marker which identifies a styled radio button. We'll use this to change the track click
|
||||||
|
/// behavior.
|
||||||
|
#[derive(Component, Default)]
|
||||||
|
struct DemoRadio(TrackClick);
|
||||||
|
|
||||||
/// A struct to hold the state of various widgets shown in the demo.
|
/// A struct to hold the state of various widgets shown in the demo.
|
||||||
///
|
///
|
||||||
/// While it is possible to use the widget's own state components as the source of truth,
|
/// While it is possible to use the widget's own state components as the source of truth,
|
||||||
@ -78,19 +86,33 @@ struct DemoCheckbox;
|
|||||||
#[derive(Resource)]
|
#[derive(Resource)]
|
||||||
struct DemoWidgetStates {
|
struct DemoWidgetStates {
|
||||||
slider_value: f32,
|
slider_value: f32,
|
||||||
|
slider_click: TrackClick,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update the widget states based on the changing resource.
|
/// Update the widget states based on the changing resource.
|
||||||
fn update_widget_values(
|
fn update_widget_values(
|
||||||
res: Res<DemoWidgetStates>,
|
res: Res<DemoWidgetStates>,
|
||||||
mut sliders: Query<Entity, With<DemoSlider>>,
|
mut sliders: Query<(Entity, &mut CoreSlider), With<DemoSlider>>,
|
||||||
|
radios: Query<(Entity, &DemoRadio, Has<Checked>)>,
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
) {
|
) {
|
||||||
if res.is_changed() {
|
if res.is_changed() {
|
||||||
for slider_ent in sliders.iter_mut() {
|
for (slider_ent, mut slider) in sliders.iter_mut() {
|
||||||
commands
|
commands
|
||||||
.entity(slider_ent)
|
.entity(slider_ent)
|
||||||
.insert(SliderValue(res.slider_value));
|
.insert(SliderValue(res.slider_value));
|
||||||
|
slider.track_click = res.slider_click;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (radio_id, radio_value, checked) in radios.iter() {
|
||||||
|
let will_be_checked = radio_value.0 == res.slider_click;
|
||||||
|
if will_be_checked != checked {
|
||||||
|
if will_be_checked {
|
||||||
|
commands.entity(radio_id).insert(Checked);
|
||||||
|
} else {
|
||||||
|
commands.entity(radio_id).remove::<Checked>();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -109,15 +131,32 @@ fn setup(mut commands: Commands, assets: Res<AssetServer>) {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// System to update a resource when the radio group changes.
|
||||||
|
let on_change_radio = commands.register_system(
|
||||||
|
|value: In<Entity>,
|
||||||
|
mut widget_states: ResMut<DemoWidgetStates>,
|
||||||
|
q_radios: Query<&DemoRadio>| {
|
||||||
|
if let Ok(radio) = q_radios.get(*value) {
|
||||||
|
widget_states.slider_click = radio.0;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// ui camera
|
// ui camera
|
||||||
commands.spawn(Camera2d);
|
commands.spawn(Camera2d);
|
||||||
commands.spawn(demo_root(&assets, on_click, on_change_value));
|
commands.spawn(demo_root(
|
||||||
|
&assets,
|
||||||
|
on_click,
|
||||||
|
on_change_value,
|
||||||
|
on_change_radio,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn demo_root(
|
fn demo_root(
|
||||||
asset_server: &AssetServer,
|
asset_server: &AssetServer,
|
||||||
on_click: SystemId,
|
on_click: SystemId,
|
||||||
on_change_value: SystemId<In<f32>>,
|
on_change_value: SystemId<In<f32>>,
|
||||||
|
on_change_radio: SystemId<In<Entity>>,
|
||||||
) -> impl Bundle {
|
) -> impl Bundle {
|
||||||
(
|
(
|
||||||
Node {
|
Node {
|
||||||
@ -135,6 +174,7 @@ fn demo_root(
|
|||||||
button(asset_server, on_click),
|
button(asset_server, on_click),
|
||||||
slider(0.0, 100.0, 50.0, Some(on_change_value)),
|
slider(0.0, 100.0, 50.0, Some(on_change_value)),
|
||||||
checkbox(asset_server, "Checkbox", None),
|
checkbox(asset_server, "Checkbox", None),
|
||||||
|
radio_group(asset_server, Some(on_change_radio)),
|
||||||
Text::new("Press 'D' to toggle widget disabled states"),
|
Text::new("Press 'D' to toggle widget disabled states"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@ -476,11 +516,11 @@ fn checkbox(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update the checkbox's styles.
|
// Update the checkbox's styles.
|
||||||
fn update_checkbox_style(
|
fn update_checkbox_or_radio_style(
|
||||||
mut q_checkbox: Query<
|
mut q_checkbox: Query<
|
||||||
(Has<Checked>, &Hovered, Has<InteractionDisabled>, &Children),
|
(Has<Checked>, &Hovered, Has<InteractionDisabled>, &Children),
|
||||||
(
|
(
|
||||||
With<DemoCheckbox>,
|
Or<(With<DemoCheckbox>, With<DemoRadio>)>,
|
||||||
Or<(
|
Or<(
|
||||||
Added<DemoCheckbox>,
|
Added<DemoCheckbox>,
|
||||||
Changed<Hovered>,
|
Changed<Hovered>,
|
||||||
@ -489,7 +529,10 @@ fn update_checkbox_style(
|
|||||||
)>,
|
)>,
|
||||||
),
|
),
|
||||||
>,
|
>,
|
||||||
mut q_border_color: Query<(&mut BorderColor, &mut Children), Without<DemoCheckbox>>,
|
mut q_border_color: Query<
|
||||||
|
(&mut BorderColor, &mut Children),
|
||||||
|
(Without<DemoCheckbox>, Without<DemoRadio>),
|
||||||
|
>,
|
||||||
mut q_bg_color: Query<&mut BackgroundColor, (Without<DemoCheckbox>, Without<Children>)>,
|
mut q_bg_color: Query<&mut BackgroundColor, (Without<DemoCheckbox>, Without<Children>)>,
|
||||||
) {
|
) {
|
||||||
for (checked, Hovered(is_hovering), is_disabled, children) in q_checkbox.iter_mut() {
|
for (checked, Hovered(is_hovering), is_disabled, children) in q_checkbox.iter_mut() {
|
||||||
@ -511,7 +554,7 @@ fn update_checkbox_style(
|
|||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
set_checkbox_style(
|
set_checkbox_or_radio_style(
|
||||||
is_disabled,
|
is_disabled,
|
||||||
*is_hovering,
|
*is_hovering,
|
||||||
checked,
|
checked,
|
||||||
@ -521,13 +564,19 @@ fn update_checkbox_style(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_checkbox_style2(
|
fn update_checkbox_or_radio_style2(
|
||||||
mut q_checkbox: Query<
|
mut q_checkbox: Query<
|
||||||
(Has<Checked>, &Hovered, Has<InteractionDisabled>, &Children),
|
(Has<Checked>, &Hovered, Has<InteractionDisabled>, &Children),
|
||||||
With<DemoCheckbox>,
|
Or<(With<DemoCheckbox>, With<DemoRadio>)>,
|
||||||
|
>,
|
||||||
|
mut q_border_color: Query<
|
||||||
|
(&mut BorderColor, &mut Children),
|
||||||
|
(Without<DemoCheckbox>, Without<DemoRadio>),
|
||||||
|
>,
|
||||||
|
mut q_bg_color: Query<
|
||||||
|
&mut BackgroundColor,
|
||||||
|
(Without<DemoCheckbox>, Without<DemoRadio>, Without<Children>),
|
||||||
>,
|
>,
|
||||||
mut q_border_color: Query<(&mut BorderColor, &mut Children), Without<DemoCheckbox>>,
|
|
||||||
mut q_bg_color: Query<&mut BackgroundColor, (Without<DemoCheckbox>, Without<Children>)>,
|
|
||||||
mut removed_checked: RemovedComponents<Checked>,
|
mut removed_checked: RemovedComponents<Checked>,
|
||||||
mut removed_disabled: RemovedComponents<InteractionDisabled>,
|
mut removed_disabled: RemovedComponents<InteractionDisabled>,
|
||||||
) {
|
) {
|
||||||
@ -557,7 +606,7 @@ fn update_checkbox_style2(
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
set_checkbox_style(
|
set_checkbox_or_radio_style(
|
||||||
is_disabled,
|
is_disabled,
|
||||||
*is_hovering,
|
*is_hovering,
|
||||||
checked,
|
checked,
|
||||||
@ -568,7 +617,7 @@ fn update_checkbox_style2(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_checkbox_style(
|
fn set_checkbox_or_radio_style(
|
||||||
disabled: bool,
|
disabled: bool,
|
||||||
hovering: bool,
|
hovering: bool,
|
||||||
checked: bool,
|
checked: bool,
|
||||||
@ -601,11 +650,94 @@ fn set_checkbox_style(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a demo radio group
|
||||||
|
fn radio_group(asset_server: &AssetServer, on_change: Option<SystemId<In<Entity>>>) -> impl Bundle {
|
||||||
|
(
|
||||||
|
Node {
|
||||||
|
display: Display::Flex,
|
||||||
|
flex_direction: FlexDirection::Column,
|
||||||
|
align_items: AlignItems::Start,
|
||||||
|
column_gap: Val::Px(4.0),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
Name::new("RadioGroup"),
|
||||||
|
CoreRadioGroup { on_change },
|
||||||
|
TabIndex::default(),
|
||||||
|
children![
|
||||||
|
(radio(asset_server, TrackClick::Drag, "Slider Drag"),),
|
||||||
|
(radio(asset_server, TrackClick::Step, "Slider Step"),),
|
||||||
|
(radio(asset_server, TrackClick::Snap, "Slider Snap"),)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a demo radio button
|
||||||
|
fn radio(asset_server: &AssetServer, value: TrackClick, caption: &str) -> impl Bundle {
|
||||||
|
(
|
||||||
|
Node {
|
||||||
|
display: Display::Flex,
|
||||||
|
flex_direction: FlexDirection::Row,
|
||||||
|
justify_content: JustifyContent::FlexStart,
|
||||||
|
align_items: AlignItems::Center,
|
||||||
|
align_content: AlignContent::Center,
|
||||||
|
column_gap: Val::Px(4.0),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
Name::new("RadioButton"),
|
||||||
|
Hovered::default(),
|
||||||
|
DemoRadio(value),
|
||||||
|
CoreRadio,
|
||||||
|
Children::spawn((
|
||||||
|
Spawn((
|
||||||
|
// Radio outer
|
||||||
|
Node {
|
||||||
|
display: Display::Flex,
|
||||||
|
width: Val::Px(16.0),
|
||||||
|
height: Val::Px(16.0),
|
||||||
|
border: UiRect::all(Val::Px(2.0)),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
BorderColor::all(CHECKBOX_OUTLINE), // Border color for the checkbox
|
||||||
|
BorderRadius::MAX,
|
||||||
|
children![
|
||||||
|
// Radio inner
|
||||||
|
(
|
||||||
|
Node {
|
||||||
|
display: Display::Flex,
|
||||||
|
width: Val::Px(8.0),
|
||||||
|
height: Val::Px(8.0),
|
||||||
|
position_type: PositionType::Absolute,
|
||||||
|
left: Val::Px(2.0),
|
||||||
|
top: Val::Px(2.0),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
BorderRadius::MAX,
|
||||||
|
BackgroundColor(CHECKBOX_CHECK),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)),
|
||||||
|
Spawn((
|
||||||
|
Text::new(caption),
|
||||||
|
TextFont {
|
||||||
|
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
|
||||||
|
font_size: 20.0,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn toggle_disabled(
|
fn toggle_disabled(
|
||||||
input: Res<ButtonInput<KeyCode>>,
|
input: Res<ButtonInput<KeyCode>>,
|
||||||
mut interaction_query: Query<
|
mut interaction_query: Query<
|
||||||
(Entity, Has<InteractionDisabled>),
|
(Entity, Has<InteractionDisabled>),
|
||||||
Or<(With<CoreButton>, With<CoreSlider>, With<CoreCheckbox>)>,
|
Or<(
|
||||||
|
With<CoreButton>,
|
||||||
|
With<CoreSlider>,
|
||||||
|
With<CoreCheckbox>,
|
||||||
|
With<CoreRadio>,
|
||||||
|
)>,
|
||||||
>,
|
>,
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
) {
|
) {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: Headless Widgets
|
title: Headless Widgets
|
||||||
authors: ["@viridia"]
|
authors: ["@viridia"]
|
||||||
pull_requests: [19366, 19584, 19665]
|
pull_requests: [19366, 19584, 19665, 19778]
|
||||||
---
|
---
|
||||||
|
|
||||||
Bevy's `Button` and `Interaction` components have been around for a long time. Unfortunately
|
Bevy's `Button` and `Interaction` components have been around for a long time. Unfortunately
|
||||||
|
Loading…
Reference in New Issue
Block a user