diff --git a/crates/bevy_feathers/src/controls/radio.rs b/crates/bevy_feathers/src/controls/radio.rs index c1ac867e8962b..eaf4c31ed2be2 100644 --- a/crates/bevy_feathers/src/controls/radio.rs +++ b/crates/bevy_feathers/src/controls/radio.rs @@ -20,9 +20,9 @@ use bevy_scene::prelude::*; use bevy_text::FontWeight; use bevy_ui::{ AlignItems, BorderRadius, Checked, Display, FlexDirection, InteractionDisabled, JustifyContent, - Node, UiRect, Val, + Node, Pressed, UiRect, Val, }; -use bevy_ui_widgets::RadioButton; +use bevy_ui_widgets::{ActivateOnPress, RadioButton}; use crate::{ constants::{fonts, size}, @@ -184,12 +184,19 @@ fn update_radio_styles( Entity, Has, Has, + Has, + Has, &Hovered, &ThemeFontColor, ), ( With, - Or<(Changed, Added, Added)>, + Or<( + Changed, + Added, + Added, + Added, + )>, ), >, q_children: Query<&Children>, @@ -197,7 +204,9 @@ fn update_radio_styles( mut q_mark: Query<&ThemeBackgroundColor, With>, mut commands: Commands, ) { - for (radio_ent, disabled, checked, hovered, font_color) in q_radioes.iter() { + for (radio_ent, disabled, checked, pressed, activate_on_press, hovered, font_color) in + q_radioes.iter() + { let Some(outline_ent) = q_children .iter_descendants(radio_ent) .find(|en| q_outline.contains(*en)) @@ -218,7 +227,9 @@ fn update_radio_styles( mark_ent, disabled, checked, + pressed, hovered.0, + activate_on_press, outline_border, mark_color, font_color, @@ -233,6 +244,8 @@ fn update_radio_styles_remove( Entity, Has, Has, + Has, + Has, &Hovered, &ThemeFontColor, ), @@ -243,13 +256,26 @@ fn update_radio_styles_remove( mut q_mark: Query<&ThemeBackgroundColor, With>, mut removed_disabled: RemovedComponents, mut removed_checked: RemovedComponents, + mut remove_pressed: RemovedComponents, + mut remove_activate_on_press: RemovedComponents, mut commands: Commands, ) { removed_disabled .read() .chain(removed_checked.read()) + .chain(remove_pressed.read()) + .chain(remove_activate_on_press.read()) .for_each(|ent| { - if let Ok((radio_ent, disabled, checked, hovered, font_color)) = q_radioes.get(ent) { + if let Ok(( + radio_ent, + disabled, + checked, + pressed, + activate_on_press, + hovered, + font_color, + )) = q_radioes.get(ent) + { let Some(outline_ent) = q_children .iter_descendants(radio_ent) .find(|en| q_outline.contains(*en)) @@ -270,7 +296,9 @@ fn update_radio_styles_remove( mark_ent, disabled, checked, + pressed, hovered.0, + activate_on_press, outline_border, mark_color, font_color, @@ -286,21 +314,44 @@ fn set_radio_styles( mark_ent: Entity, disabled: bool, checked: bool, + pressed: bool, hovered: bool, + activate_on_press: bool, outline_border: &ThemeBorderColor, mark_color: &ThemeBackgroundColor, font_color: &ThemeFontColor, commands: &mut Commands, ) { - let outline_border_token = match (disabled, hovered) { - (true, _) => tokens::RADIO_BORDER_DISABLED, - (false, true) => tokens::RADIO_BORDER_HOVER, - _ => tokens::RADIO_BORDER, + let outline_border_token = if checked { + if disabled { + tokens::RADIO_BORDER_CHECKED_DISABLED + } else if pressed && !activate_on_press { + tokens::RADIO_BORDER_CHECKED_PRESSED + } else if hovered { + tokens::RADIO_BORDER_CHECKED_HOVER + } else { + tokens::RADIO_BORDER_CHECKED + } + } else { + if disabled { + tokens::RADIO_BORDER_DISABLED + } else if pressed && !activate_on_press { + tokens::RADIO_BORDER_PRESSED + } else if hovered { + tokens::RADIO_BORDER_HOVER + } else { + tokens::RADIO_BORDER + } }; - let mark_token = match disabled { - true => tokens::RADIO_MARK_DISABLED, - false => tokens::RADIO_MARK, + let mark_token = if disabled { + tokens::RADIO_MARK_DISABLED + } else if pressed && !activate_on_press { + tokens::RADIO_MARK_PRESSED + } else if hovered { + tokens::RADIO_MARK_HOVER + } else { + tokens::RADIO_MARK }; let font_color_token = match disabled { @@ -324,7 +375,7 @@ fn set_radio_styles( if mark_color.0 != mark_token { commands .entity(mark_ent) - .insert(ThemeBorderColor(mark_token)); + .insert(ThemeBackgroundColor(mark_token)); } // Change mark visibility diff --git a/crates/bevy_feathers/src/dark_theme.rs b/crates/bevy_feathers/src/dark_theme.rs index 8be4fde80c78d..7092ed0f72688 100644 --- a/crates/bevy_feathers/src/dark_theme.rs +++ b/crates/bevy_feathers/src/dark_theme.rs @@ -105,13 +105,29 @@ pub fn create_dark_theme() -> ThemeProps { ), // Radio (tokens::RADIO_BORDER, palette::GRAY_3), - (tokens::RADIO_BORDER_HOVER, palette::GRAY_3.lighter(0.1)), + (tokens::RADIO_BORDER_HOVER, palette::GRAY_3.lighter(0.05)), + (tokens::RADIO_BORDER_PRESSED, palette::GRAY_3.lighter(0.1)), ( tokens::RADIO_BORDER_DISABLED, palette::GRAY_3.with_alpha(0.5), ), + (tokens::RADIO_BORDER_CHECKED, palette::ACCENT), + ( + tokens::RADIO_BORDER_CHECKED_HOVER, + palette::ACCENT.lighter(0.05), + ), + ( + tokens::RADIO_BORDER_CHECKED_PRESSED, + palette::ACCENT.lighter(0.1), + ), + ( + tokens::RADIO_BORDER_CHECKED_DISABLED, + palette::GRAY_3.with_alpha(0.5), + ), (tokens::RADIO_MARK, palette::ACCENT), - (tokens::RADIO_MARK_DISABLED, palette::ACCENT.with_alpha(0.5)), + (tokens::RADIO_MARK_HOVER, palette::ACCENT.lighter(0.05)), + (tokens::RADIO_MARK_PRESSED, palette::ACCENT.lighter(0.1)), + (tokens::RADIO_MARK_DISABLED, palette::GRAY_3.with_alpha(0.5)), (tokens::RADIO_TEXT, palette::LIGHT_GRAY_1), ( tokens::RADIO_TEXT_DISABLED, diff --git a/crates/bevy_feathers/src/tokens.rs b/crates/bevy_feathers/src/tokens.rs index 16e7306b10872..21869d427191c 100644 --- a/crates/bevy_feathers/src/tokens.rs +++ b/crates/bevy_feathers/src/tokens.rs @@ -147,15 +147,34 @@ pub const CHECKBOX_TEXT_DISABLED: ThemeToken = // Radio button -/// Radio border around the checkmark +/// Border around the radio button pub const RADIO_BORDER: ThemeToken = ThemeToken::new_static("feathers.radio.border"); -/// Radio border around the checkmark (hovered) +/// Border around the radio button (hovered) pub const RADIO_BORDER_HOVER: ThemeToken = ThemeToken::new_static("feathers.radio.border.hover"); -/// Radio border around the checkmark (disabled) +/// Border around the radio button (pressed) +pub const RADIO_BORDER_PRESSED: ThemeToken = + ThemeToken::new_static("feathers.radio.border.pressed"); +/// Border around the radio button (disabled) pub const RADIO_BORDER_DISABLED: ThemeToken = ThemeToken::new_static("feathers.radio.border.disabled"); +/// Border around the radio button (checked) +pub const RADIO_BORDER_CHECKED: ThemeToken = + ThemeToken::new_static("feathers.radio.border.checked"); +/// Border around the radio button (checked+hovered) +pub const RADIO_BORDER_CHECKED_HOVER: ThemeToken = + ThemeToken::new_static("feathers.radio.border.checked.hover"); +/// Border around the radio button (checked+pressed) +pub const RADIO_BORDER_CHECKED_PRESSED: ThemeToken = + ThemeToken::new_static("feathers.radio.border.checked.pressed"); +/// Border around the radio button (checked+disabled) +pub const RADIO_BORDER_CHECKED_DISABLED: ThemeToken = + ThemeToken::new_static("feathers.radio.border.checked.disabled"); /// Radio check mark pub const RADIO_MARK: ThemeToken = ThemeToken::new_static("feathers.radio.mark"); +/// Radio check mark (hovered) +pub const RADIO_MARK_HOVER: ThemeToken = ThemeToken::new_static("feathers.radio.mark.hover"); +/// Radio check mark (pressed) +pub const RADIO_MARK_PRESSED: ThemeToken = ThemeToken::new_static("feathers.radio.mark.pressed"); /// Radio check mark (disabled) pub const RADIO_MARK_DISABLED: ThemeToken = ThemeToken::new_static("feathers.radio.mark.disabled"); /// Radio label text diff --git a/crates/bevy_ui_widgets/src/checkbox.rs b/crates/bevy_ui_widgets/src/checkbox.rs index d02fb932a9547..e8293556b05b0 100644 --- a/crates/bevy_ui_widgets/src/checkbox.rs +++ b/crates/bevy_ui_widgets/src/checkbox.rs @@ -15,8 +15,7 @@ use bevy_input_focus::{FocusedInput, InputFocus, InputFocusVisible}; use bevy_picking::events::{Cancel, Click, DragEnd, Pointer, Press, Release}; use bevy_ui::{Checkable, Checked, InteractionDisabled, Pressed}; -use crate::ActivateOnPress; -use crate::ValueChange; +use crate::{ActivateOnPress, ValueChange}; use bevy_ecs::entity::Entity; /// Headless widget implementation for checkboxes. The [`Checked`] component represents the current diff --git a/crates/bevy_ui_widgets/src/radio.rs b/crates/bevy_ui_widgets/src/radio.rs index b48c02345de92..f09f70641b31b 100644 --- a/crates/bevy_ui_widgets/src/radio.rs +++ b/crates/bevy_ui_widgets/src/radio.rs @@ -6,18 +6,18 @@ use bevy_ecs::{ entity::Entity, hierarchy::{ChildOf, Children}, observer::On, - query::{Has, With}, + query::{Has, With, Without}, reflect::ReflectComponent, system::{Commands, Query}, }; use bevy_input::keyboard::{KeyCode, KeyboardInput}; use bevy_input::ButtonState; use bevy_input_focus::FocusedInput; -use bevy_picking::events::{Click, Pointer}; +use bevy_picking::events::{Cancel, Click, DragEnd, Pointer, Press, Release}; use bevy_reflect::Reflect; -use bevy_ui::{Checkable, Checked, InteractionDisabled}; +use bevy_ui::{Checkable, Checked, InteractionDisabled, Pressed}; -use crate::ValueChange; +use crate::{ActivateOnPress, ValueChange}; /// Headless widget implementation for a "radio button group". This component is used to group /// multiple [`RadioButton`] components together, allowing them to behave as a single unit. It @@ -188,18 +188,21 @@ fn radio_button_on_key_input( } fn radio_button_on_click( - mut ev: On>, + mut click: On>, q_group: Query<(), With>, - q_radio: Query<(Has, Has), With>, + q_radio: Query< + (Has, Has), + (With, Without), + >, q_parents: Query<&ChildOf>, mut commands: Commands, ) { - let Ok((disabled, checked)) = q_radio.get(ev.entity) else { + let Ok((disabled, checked)) = q_radio.get(click.entity) else { // Not a radio button return; }; - ev.propagate(false); + click.propagate(false); // Radio button is disabled or already checked if disabled || checked { @@ -207,13 +210,86 @@ fn radio_button_on_click( } trigger_radio_button_and_radio_group_value_change( - ev.entity, + click.entity, &q_group, &q_parents, &mut commands, ); } +fn radio_button_on_pointer_down( + mut press: On>, + q_group: Query<(), With>, + mut q_radio: Query< + ( + Entity, + Has, + Has, + Has, + Has, + ), + With, + >, + q_parents: Query<&ChildOf>, + mut commands: Commands, +) { + if let Ok((radio, disabled, checked, pressed, activate_on_press)) = + q_radio.get_mut(press.entity) + { + press.propagate(false); + if !disabled && !pressed { + commands.entity(radio).insert(Pressed); + if activate_on_press && !checked { + trigger_radio_button_and_radio_group_value_change( + press.entity, + &q_group, + &q_parents, + &mut commands, + ); + } + } + } +} + +fn radio_button_on_pointer_up( + mut release: On>, + mut q_radio: Query<(Entity, Has, Has), With>, + mut commands: Commands, +) { + if let Ok((radio, disabled, pressed)) = q_radio.get_mut(release.entity) { + release.propagate(false); + if !disabled && pressed { + commands.entity(radio).remove::(); + } + } +} + +fn radio_button_on_pointer_drag_end( + mut drag_end: On>, + mut q_radio: Query<(Entity, Has, Has), With>, + mut commands: Commands, +) { + if let Ok((radio, disabled, pressed)) = q_radio.get_mut(drag_end.entity) { + drag_end.propagate(false); + if !disabled && pressed { + commands.entity(radio).remove::(); + } + } +} + +fn checkbox_on_pointer_cancel( + mut cancel: On>, + mut q_radio: Query<(Entity, Has, Has), With>, + mut commands: Commands, +) { + if let Ok((radio, disabled, pressed)) = q_radio.get_mut(cancel.entity) { + cancel.propagate(false); + if !disabled && pressed { + commands.entity(radio).remove::(); + } + } +} + fn trigger_radio_button_and_radio_group_value_change( radio_button: Entity, q_group: &Query<(), With>, @@ -247,6 +323,10 @@ impl Plugin for RadioGroupPlugin { fn build(&self, app: &mut App) { app.add_observer(radio_group_on_key_input) .add_observer(radio_button_on_click) - .add_observer(radio_button_on_key_input); + .add_observer(radio_button_on_key_input) + .add_observer(radio_button_on_pointer_down) + .add_observer(radio_button_on_pointer_up) + .add_observer(radio_button_on_pointer_drag_end) + .add_observer(checkbox_on_pointer_cancel); } } diff --git a/examples/ui/widgets/feathers_gallery.rs b/examples/ui/widgets/feathers_gallery.rs index b3a1ee0dc5a7b..45266fdfd003f 100644 --- a/examples/ui/widgets/feathers_gallery.rs +++ b/examples/ui/widgets/feathers_gallery.rs @@ -342,7 +342,7 @@ fn demo_column_1() -> impl Scene { ( checkbox(CheckboxProps { caption: Box::new(bsn_list!( - (Text("Disabled+Checked") ThemedText), + (Text("Checked+Disabled") ThemedText), )), }) InteractionDisabled @@ -384,9 +384,9 @@ fn demo_column_1() -> impl Scene { })), (radio(RadioProps { caption: Box::new(bsn_list!( - (Text("Three") ThemedText), + (Text("Fast Click") ThemedText), )), - })), + }) ActivateOnPress), (radio(RadioProps { caption: Box::new(bsn_list!( (Text("Disabled") ThemedText),