From 90cc10554b2f547dd22f3a1fb1d70e8c32e6cdb4 Mon Sep 17 00:00:00 2001 From: swetar-mecha Date: Mon, 12 May 2025 15:26:05 +0530 Subject: [PATCH 01/10] feat: add select widget --- .../src/core_select.rs | 148 ++++++++++ .../src/interaction_states.rs | 56 ++++ .../bevy_additional_core_widgets/src/lib.rs | 7 +- crates/bevy_styled_widgets/src/lib.rs | 8 +- crates/bevy_styled_widgets/src/ui/mod.rs | 3 +- .../src/ui/select/builder.rs | 252 ++++++++++++++++++ .../src/ui/select/components.rs | 52 ++++ .../bevy_styled_widgets/src/ui/select/mod.rs | 9 + .../src/ui/select/plugin.rs | 15 ++ .../src/ui/select/systems.rs | 136 ++++++++++ examples/select.rs | 212 +++++++++++++++ 11 files changed, 892 insertions(+), 6 deletions(-) create mode 100644 crates/bevy_additional_core_widgets/src/core_select.rs create mode 100644 crates/bevy_additional_core_widgets/src/interaction_states.rs create mode 100644 crates/bevy_styled_widgets/src/ui/select/builder.rs create mode 100644 crates/bevy_styled_widgets/src/ui/select/components.rs create mode 100644 crates/bevy_styled_widgets/src/ui/select/mod.rs create mode 100644 crates/bevy_styled_widgets/src/ui/select/plugin.rs create mode 100644 crates/bevy_styled_widgets/src/ui/select/systems.rs create mode 100644 examples/select.rs diff --git a/crates/bevy_additional_core_widgets/src/core_select.rs b/crates/bevy_additional_core_widgets/src/core_select.rs new file mode 100644 index 0000000..ac24678 --- /dev/null +++ b/crates/bevy_additional_core_widgets/src/core_select.rs @@ -0,0 +1,148 @@ +use accesskit::Role; +use bevy::{ + a11y::AccessibilityNode, + ecs::system::SystemId, + input_focus::{InputFocus, InputFocusVisible}, + prelude::*, +}; + +use bevy_core_widgets::{ButtonClicked, Checked, InteractionDisabled, ValueChange}; + +use crate::{ListBoxOptionState, interaction_states::SelectHasPopup}; + +#[derive(Component, Debug)] +#[require(AccessibilityNode(accesskit::Node::new(Role::ListBox)), SelectHasPopup)] +pub struct CoreSelectTrigger { + pub on_click: Option>>, +} + +pub fn select_on_pointer_click( + mut trigger: Trigger>, + q_state: Query<( + &CoreSelectTrigger, + &SelectHasPopup, + Has, + )>, + mut focus: ResMut, + mut focus_visible: ResMut, + mut commands: Commands, +) { + if let Ok((select_trigger, SelectHasPopup(clicked), disabled)) = q_state.get(trigger.target()) { + let select_id = trigger.target(); + focus.0 = Some(select_id); + focus_visible.0 = false; + trigger.propagate(false); + + if !disabled { + let is_open = clicked; + let new_clicked = !is_open; + + if let Some(on_click) = select_trigger.on_click { + commands.run_system_with(on_click, new_clicked); + } else { + commands.trigger_targets(ValueChange(SelectHasPopup(new_clicked)), select_id); + } + } + } +} + +#[derive(Component, Debug)] +#[require(AccessibilityNode(accesskit::Node::new(Role::ListBox)))] +pub struct CoreSelectContent { + pub on_change: Option>>, +} + +fn select_root_on_button_click( + mut trigger: Trigger, + q_group: Query<(&CoreSelectContent, &Children)>, + q_select_item: Query<(&Checked, &ChildOf, Has), With>, + mut focus: ResMut, + mut focus_visible: ResMut, + mut commands: Commands, +) { + let select_id = trigger.target(); + + // Find the select item button that was clicked. + let Ok((_, child_of, _)) = q_select_item.get(select_id) else { + return; + }; + + // Find the parent CoreSelectContent of the clicked select item button. + let group_id = child_of.parent(); + let Ok((CoreSelectContent { on_change }, group_children)) = q_group.get(group_id) else { + warn!("select item button clicked without a valid CoreSelectContent parent"); + return; + }; + + // Set focus to group and hide focus ring + focus.0 = Some(group_id); + focus_visible.0 = false; + + // Get all the select root children. + let select_children = group_children + .iter() + .filter_map(|child_id| match q_select_item.get(child_id) { + Ok((checked, _, false)) => Some((child_id, checked.0)), + Ok((_, _, true)) => None, + Err(_) => None, + }) + .collect::>(); + + if select_children.is_empty() { + return; // No enabled select item buttons in the group + } + + trigger.propagate(false); + let current_select_item = select_children + .iter() + .find(|(_, checked)| *checked) + .map(|(id, _)| *id); + + if current_select_item == Some(select_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, select_id); + } else { + commands.trigger_targets(ValueChange(select_id), group_id); + } +} + +#[derive(Component, Debug)] +#[require(AccessibilityNode(accesskit::Node::new(Role::ListBoxOption)), Checked)] +pub struct CoreSelectItem; + +fn select_item_on_pointer_click( + mut trigger: Trigger>, + q_state: Query<(&Checked, Has), With>, + mut focus: ResMut, + mut focus_visible: ResMut, + mut commands: Commands, +) { + if let Ok((checked, disabled)) = q_state.get(trigger.target()) { + let checkbox_id = trigger.target(); + focus.0 = Some(checkbox_id); + focus_visible.0 = false; + trigger.propagate(false); + if checked.0 || disabled { + // If the radio is already checked, or disabled, we do nothing. + return; + } + commands.trigger_targets(ButtonClicked, trigger.target()); + } +} + +// ----- + +pub struct CoreSelectPlugin; + +impl Plugin for CoreSelectPlugin { + fn build(&self, app: &mut App) { + app.add_observer(select_on_pointer_click) + .add_observer(select_root_on_button_click) + .add_observer(select_item_on_pointer_click); + } +} diff --git a/crates/bevy_additional_core_widgets/src/interaction_states.rs b/crates/bevy_additional_core_widgets/src/interaction_states.rs new file mode 100644 index 0000000..b3792e0 --- /dev/null +++ b/crates/bevy_additional_core_widgets/src/interaction_states.rs @@ -0,0 +1,56 @@ +use bevy::{ + a11y::AccessibilityNode, + ecs::{component::HookContext, world::DeferredWorld}, + prelude::Component, +}; +/// Component that indicates whether the select widget has a popup. +#[derive(Component, Default, Debug)] +#[component(immutable, on_add = on_add_has_popup, on_replace = on_add_has_popup)] +pub struct SelectHasPopup(pub bool); + +// Hook to set the a11y "HasPopup" state when the select widget is added or updated. +pub fn on_add_has_popup(mut world: DeferredWorld, context: HookContext) { + let mut entt = world.entity_mut(context.entity); + let has_popup = entt.get::().unwrap().0; + + if let Some(mut accessibility) = entt.get_mut::() { + if has_popup == true { + accessibility.set_has_popup(accesskit::HasPopup::Listbox); // Set to Listbox + } else { + accessibility.clear_has_popup(); // Clear the HasPopup property + } + } else { + eprintln!("Error in on_add_has_popup()"); + } +} + +/// Component that indicates the state of a ListBoxOption. +#[derive(Component, Default, Debug)] +#[component(on_add = on_add_listbox_option)] +pub struct ListBoxOptionState { + pub label: String, + pub is_selected: bool, +} + +// Hook to set the a11y properties for a ListBoxOption when added or updated. +fn on_add_listbox_option(mut world: DeferredWorld, context: HookContext) { + let mut entt = world.entity_mut(context.entity); + + let (label, is_selected) = { + let state = entt.get::().unwrap(); + (state.label.clone(), state.is_selected) + }; + + if let Some(mut accessibility) = entt.get_mut::() { + accessibility.set_label(&*label); + + // Set the selected state + if is_selected { + accessibility.set_selected(true); + } else { + accessibility.clear_selected(); + } + } else { + eprintln!("Error in on_add_has_popup()"); + } +} diff --git a/crates/bevy_additional_core_widgets/src/lib.rs b/crates/bevy_additional_core_widgets/src/lib.rs index cbda89d..0036b5b 100644 --- a/crates/bevy_additional_core_widgets/src/lib.rs +++ b/crates/bevy_additional_core_widgets/src/lib.rs @@ -1,13 +1,16 @@ use bevy::app::{App, Plugin}; +mod core_select; mod core_switch; - +mod interaction_states; +pub use core_select::{CoreSelectContent, CoreSelectItem, CoreSelectPlugin, CoreSelectTrigger}; pub use core_switch::{CoreSwitch, CoreSwitchPlugin}; +pub use interaction_states::{ListBoxOptionState, SelectHasPopup}; pub struct AdditionalCoreWidgetsPlugin; impl Plugin for AdditionalCoreWidgetsPlugin { fn build(&self, app: &mut App) { - app.add_plugins((CoreSwitchPlugin,)); + app.add_plugins((CoreSwitchPlugin, CoreSelectPlugin)); } } diff --git a/crates/bevy_styled_widgets/src/lib.rs b/crates/bevy_styled_widgets/src/lib.rs index 6b3fdc4..fe6a7b2 100644 --- a/crates/bevy_styled_widgets/src/lib.rs +++ b/crates/bevy_styled_widgets/src/lib.rs @@ -12,9 +12,9 @@ use bevy_core_widgets::CoreWidgetsPlugin; use themes::fonts::FontAssets; use ui::{ - button::StyledButtonPlugin, checkbox::StyledCheckboxPlugin, input::StyledInputPlugin, - progress::StyledProgessPlugin, slider::StyledSliderPlugin, radio_group::StyledRadioGroupPlugin, switch::StyledSwitchPlugin, + progress::StyledProgessPlugin, radio_group::StyledRadioGroupPlugin, + select::StyledSelectTriggerPlugin, slider::StyledSliderPlugin, switch::StyledSwitchPlugin, text::StyledTextPlugin, toggle::StyledTogglePlugin, }; @@ -35,6 +35,7 @@ impl Plugin for StyledWidgetsPligin { StyledCheckboxPlugin, StyledSliderPlugin, StyledRadioGroupPlugin, + StyledSelectTriggerPlugin, )); app.init_collection::(); } @@ -49,8 +50,9 @@ pub mod prelude { pub use crate::ui::checkbox::*; pub use crate::ui::input::*; pub use crate::ui::progress::*; - pub use crate::ui::slider::*; pub use crate::ui::radio_group::*; + pub use crate::ui::select::*; + pub use crate::ui::slider::*; pub use crate::ui::switch::*; pub use crate::ui::text::*; pub use crate::ui::toggle::*; diff --git a/crates/bevy_styled_widgets/src/ui/mod.rs b/crates/bevy_styled_widgets/src/ui/mod.rs index 2bf609f..eb25a46 100644 --- a/crates/bevy_styled_widgets/src/ui/mod.rs +++ b/crates/bevy_styled_widgets/src/ui/mod.rs @@ -7,4 +7,5 @@ pub mod switch; pub mod text; pub mod toggle; -pub mod radio_group; \ No newline at end of file +pub mod radio_group; +pub mod select; diff --git a/crates/bevy_styled_widgets/src/ui/select/builder.rs b/crates/bevy_styled_widgets/src/ui/select/builder.rs new file mode 100644 index 0000000..dc5ac0d --- /dev/null +++ b/crates/bevy_styled_widgets/src/ui/select/builder.rs @@ -0,0 +1,252 @@ +use bevy::{ + a11y::AccessibilityNode, color::palettes::css::GRAY, ecs::system::SystemId, + input_focus::tab_navigation::TabIndex, prelude::*, window::SystemCursorIcon, + winit::cursor::CursorIcon, +}; +use bevy_additional_core_widgets::{ + CoreSelectContent, CoreSelectItem, CoreSelectTrigger, ListBoxOptionState, SelectHasPopup, +}; +use bevy_core_widgets::{Checked, hover::Hovering}; + +use super::{ + StyledSelect, + components::{AccessibleName, StyledSelectItem}, +}; +use accesskit::{Node as Accessible, Role}; + +#[derive(Component, Default)] +pub struct SelectContent; + +#[derive(Component, Debug, Clone)] +pub struct SelectedValue(pub String); + +#[derive(Default, Clone)] +pub struct SelectItemBuilder { + pub selected: bool, + pub on_change: Option>>, + pub disabled: bool, + pub key: Option, + pub value: String, +} + +impl SelectItemBuilder { + pub fn selected(mut self, selected: bool) -> Self { + self.selected = selected; + self + } + + pub fn on_change(mut self, system_id: SystemId>) -> Self { + self.on_change = Some(system_id); + self + } + + pub fn disabled(mut self) -> Self { + self.disabled = true; + self + } + + pub fn key>(mut self, key: S) -> Self { + self.key = Some(key.into()); + self + } + + pub fn value>(mut self, value: S) -> Self { + self.value = value.into(); + self + } + + pub fn build(self) -> impl Bundle { + let is_selected = self.selected; + let is_disabled = self.disabled; + let width = 144.0; + let height = 52.0; + + let key = self.key.clone().unwrap_or_else(|| "".to_string()); + // select content- dropdown + let child_nodes = Children::spawn(( + Spawn(( + Node { + display: Display::Block, + flex_direction: FlexDirection::Row, + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + align_content: AlignContent::Center, + min_width: Val::Px(width), + min_height: Val::Px(height), + ..default() + }, + Name::new(self.key.clone().unwrap_or("".to_string())), + Text::new(self.value.clone()), + TextFont { + font_size: 14.0, + ..default() + }, + )), + // + )); + + ( + Node { + display: Display::None, // Initially hidden + flex_direction: FlexDirection::Column, + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + width: Val::Percent(100.0), + height: Val::Auto, + padding: UiRect::axes(Val::Px(0.0), Val::Px(4.0)), + ..default() + }, + GlobalZIndex(99), + SelectContent, + CoreSelectItem, + Name::new("Select Item"), + AccessibilityNode(Accessible::new(Role::ListBoxOption)), + Hovering::default(), + CursorIcon::System(SystemCursorIcon::Pointer), + ListBoxOptionState { + label: self.value.clone(), + is_selected: false, + }, + StyledSelectItem { + selected: self.selected, + on_change: self.on_change, + disabled: self.disabled, + key: self.key, + value: self.value.clone(), + }, + SelectedValue(self.value.clone()), + Checked(is_selected), + AccessibleName(key.clone()), + TabIndex(0), + child_nodes, + ) + } +} + +#[derive(Component, Default)] +pub struct SelectRoot; + +#[derive(Component, Default)] +pub struct SelectTrigger; + +#[derive(Default, Clone)] +pub struct SelectBuilder { + pub on_click: Option>>, + pub on_change: Option>>, + pub children: Vec, + pub selected_value: Option, + pub disabled: bool, +} + +impl SelectBuilder { + pub fn on_change(mut self, system_id: SystemId>) -> Self { + self.on_change = Some(system_id); + self + } + + pub fn on_click(mut self, system_id: SystemId>) -> Self { + self.on_click = Some(system_id); + self + } + + pub fn children(mut self, options: impl IntoIterator) -> Self { + self.children.extend(options); + self + } + + pub fn selected_value>(mut self, selected_value: S) -> Self { + self.selected_value = Some(selected_value.into()); + self + } + + pub fn disabled(mut self) -> Self { + self.disabled = true; + self + } + + // pub fn build(self) -> (impl Bundle, Vec) { + pub fn build(self) -> (impl Bundle, impl Bundle, impl Bundle, Vec) { + let button_width = 144.0; + let button_height = 52.0; + + let parent_bundle = ( + Node { + display: Display::Flex, + flex_direction: FlexDirection::Column, + align_items: AlignItems::FlexStart, + justify_content: JustifyContent::FlexStart, + ..default() + }, + SelectRoot, + AccessibilityNode(Accessible::new(Role::ListBox)), + ); + + let select_trigger_bundle = Children::spawn((Spawn(( + Node { + display: Display::Flex, + flex_direction: FlexDirection::Row, + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + align_content: AlignContent::Center, + min_width: Val::Px(button_width), + min_height: Val::Px(button_height), + padding: UiRect::axes(Val::Px(28.), Val::Px(10.)), + ..default() + }, + StyledSelect { + options: self.children.clone(), + on_click: self.on_click, + selected_value: self.selected_value.clone(), + disabled: self.disabled, + on_change: self.on_change, + }, + BackgroundColor(GRAY.into()), + Name::new(self.selected_value.clone().unwrap_or("Select".to_string())), // Name::new("Select"), + Hovering::default(), + CursorIcon::System(SystemCursorIcon::Pointer), + SelectHasPopup(false), + SelectTrigger, + CoreSelectTrigger { + on_click: self.on_click, + }, + AccessibilityNode(Accessible::new(Role::Button)), + TabIndex(0), + BorderRadius::default(), + BorderColor::default(), + Text::new(self.selected_value.clone().unwrap_or("Select".to_string())), + TextFont { + font_size: 14., + ..Default::default() + }, + )),)); + + let select_content_bundle = ( + Node { + display: Display::Flex, + flex_direction: FlexDirection::Column, // Dropdown items in a column + justify_content: JustifyContent::FlexStart, // Align children to the start + align_items: AlignItems::FlexStart, // Align items to the start + align_content: AlignContent::FlexStart, // Align content to the start + width: Val::Px(button_width), // Match the width of the trigger button + // max_height: Val::Px(200.0), + ..default() + }, + CoreSelectContent { + on_change: self.on_change, + }, + ); + + let child_bundles = self + .children + .into_iter() + .map(|builder| builder.build()) + .collect::>(); + + ( + parent_bundle, + select_trigger_bundle, + select_content_bundle, + child_bundles, + ) + } +} diff --git a/crates/bevy_styled_widgets/src/ui/select/components.rs b/crates/bevy_styled_widgets/src/ui/select/components.rs new file mode 100644 index 0000000..53eed63 --- /dev/null +++ b/crates/bevy_styled_widgets/src/ui/select/components.rs @@ -0,0 +1,52 @@ +use super::builder::SelectItemBuilder; +use bevy::a11y::AccessibilityNode; +use bevy::ecs::{component::HookContext, system::SystemId, world::DeferredWorld}; +use bevy::prelude::*; + +#[derive(Component, Reflect)] +#[reflect(from_reflect = false)] +pub struct StyledSelectItem { + pub selected: bool, + #[reflect(ignore)] + pub on_change: Option>>, + pub disabled: bool, + pub key: Option, + pub value: String, +} + +impl StyledSelectItem { + pub fn builder() -> super::builder::SelectItemBuilder { + super::builder::SelectItemBuilder::default() + } +} + +#[derive(Component, Reflect)] +#[reflect(from_reflect = false)] +pub struct StyledSelect { + #[reflect(ignore)] + pub options: Vec, + #[reflect(ignore)] + pub on_click: Option>>, + #[reflect(ignore)] + pub on_change: Option>>, + pub selected_value: Option, + pub disabled: bool, +} + +impl StyledSelect { + pub fn builder() -> super::builder::SelectBuilder { + super::builder::SelectBuilder::default() + } +} + +#[derive(Component, Default)] +#[component(immutable, on_add = on_set_label, on_replace = on_set_label)] +pub struct AccessibleName(pub String); + +fn on_set_label(mut world: DeferredWorld, context: HookContext) { + let mut entt = world.entity_mut(context.entity); + let name = entt.get::().unwrap().0.clone(); + if let Some(mut accessibility) = entt.get_mut::() { + accessibility.set_label(name.as_str()); + } +} diff --git a/crates/bevy_styled_widgets/src/ui/select/mod.rs b/crates/bevy_styled_widgets/src/ui/select/mod.rs new file mode 100644 index 0000000..c07d16e --- /dev/null +++ b/crates/bevy_styled_widgets/src/ui/select/mod.rs @@ -0,0 +1,9 @@ +mod builder; +mod components; +mod plugin; +mod systems; + +pub use builder::*; +pub use components::*; +pub use plugin::StyledSelectTriggerPlugin; +pub use systems::*; diff --git a/crates/bevy_styled_widgets/src/ui/select/plugin.rs b/crates/bevy_styled_widgets/src/ui/select/plugin.rs new file mode 100644 index 0000000..346bc0b --- /dev/null +++ b/crates/bevy_styled_widgets/src/ui/select/plugin.rs @@ -0,0 +1,15 @@ +use bevy::prelude::*; + +use super::{ + on_select_item_selection, on_select_triggered, open_select_popup, update_select_visuals, +}; + +pub struct StyledSelectTriggerPlugin; +impl Plugin for StyledSelectTriggerPlugin { + fn build(&self, app: &mut App) { + app.add_observer(on_select_triggered) + .add_systems(Update, open_select_popup) + .add_observer(on_select_item_selection) + .add_systems(Update, update_select_visuals); + } +} diff --git a/crates/bevy_styled_widgets/src/ui/select/systems.rs b/crates/bevy_styled_widgets/src/ui/select/systems.rs new file mode 100644 index 0000000..ca8df04 --- /dev/null +++ b/crates/bevy_styled_widgets/src/ui/select/systems.rs @@ -0,0 +1,136 @@ +use bevy::{color::palettes::css::LIGHT_GRAY, prelude::*}; +use bevy_additional_core_widgets::{ + CoreSelectContent, CoreSelectItem, CoreSelectTrigger, ListBoxOptionState, SelectHasPopup, +}; +use bevy_core_widgets::{Checked, InteractionDisabled, ValueChange, hover::Hovering}; + +use super::{SelectContent, StyledSelect, StyledSelectItem, builder::SelectedValue}; + +#[allow(clippy::type_complexity)] +pub fn on_select_triggered( + mut trigger: Trigger>, + query: Query<&StyledSelect>, + mut commands: Commands, +) { + trigger.propagate(false); + + let clicked = &trigger.event().0; + let entity = trigger.target(); + + commands.entity(entity).insert(SelectHasPopup(clicked.0)); + + if let Ok(styled_select) = query.get(entity) { + if styled_select.disabled { + commands.entity(entity).insert(InteractionDisabled); + } + if let Some(system_id) = styled_select.on_click { + // Defer the callback system using commands + commands.run_system_with(system_id, clicked.0); + } + } +} + +pub fn open_select_popup( + mut content_query: Query<&mut Node, With>, + has_popup_query: Query<&SelectHasPopup, Changed>, +) { + for SelectHasPopup(is_open) in has_popup_query.iter() { + if *is_open { + for mut content in content_query.iter_mut() { + content.display = Display::Flex; + } + } else { + for mut content in content_query.iter_mut() { + content.display = Display::None; + } + } + } +} + +pub fn on_select_item_selection( + mut trigger: Trigger>, + q_select_content: Query<&Children, With>, + q_select_item: Query<(&ChildOf, &SelectedValue), With>, + mut q_trigger_text: Query<(&mut Text, &mut Name), With>, + mut commands: Commands, +) { + trigger.propagate(false); + if q_select_content.contains(trigger.target()) { + let selected_entity = trigger.event().0; + + let (child_of, selected_value) = q_select_item.get(selected_entity.clone()).unwrap(); + let group_children = q_select_content.get(child_of.parent()).unwrap(); + + for select_item_child in group_children.iter() { + if let Ok((_, value)) = q_select_item.get(select_item_child) { + commands + .entity(select_item_child) + .insert(Checked(value.0 == selected_value.0.clone())); + } + } + + // Update the SelectTrigger text to match selected_value + for (mut text, mut name) in q_trigger_text.iter_mut() { + text.0 = selected_value.0.clone(); + name.set(selected_value.0.clone()); + } + + commands + .entity(trigger.target()) + .insert(SelectHasPopup(false)); + } +} + +#[allow(clippy::type_complexity)] +pub fn update_select_visuals( + mut query_set: ParamSet<( + Query< + ( + &Hovering, + &SelectHasPopup, + &mut BackgroundColor, + Has, + ), + (With, With), + >, + Query< + ( + &Hovering, + &mut BackgroundColor, + &ListBoxOptionState, + Has, + &StyledSelectItem, + &Checked, + ), + With, + >, + )>, +) { + // Query 0: Trigger + for (hovering, has_popup, mut bg_color, is_disabled) in query_set.p0().iter_mut() { + if is_disabled { + *bg_color = BackgroundColor(LIGHT_GRAY.into()); + } else if has_popup.0 { + *bg_color = BackgroundColor(bevy::color::palettes::css::LIGHT_GRAY.into()); + } else if has_popup.0 || hovering.0 { + *bg_color = BackgroundColor(bevy::color::palettes::css::LIGHT_GRAY.into()); + } else { + *bg_color = BackgroundColor(bevy::color::palettes::css::GRAY.into()); + } + } + + // Query 1: Items + for (hovering, mut bg_color, option_state, is_disabled, item, Checked(checked)) in + query_set.p1().iter_mut() + { + if item.disabled || is_disabled { + *bg_color = BackgroundColor(LIGHT_GRAY.into()); + } else if hovering.0 { + *bg_color = BackgroundColor(bevy::color::palettes::css::LIGHT_GRAY.into()); + } else if item.selected || option_state.is_selected || *checked { + *bg_color = BackgroundColor(bevy::color::palettes::css::DARK_GREY.into()); + } else { + *bg_color = BackgroundColor(bevy::color::palettes::css::GRAY.into()); + } + } +} diff --git a/examples/select.rs b/examples/select.rs new file mode 100644 index 0000000..5a40e38 --- /dev/null +++ b/examples/select.rs @@ -0,0 +1,212 @@ +use bevy::{input_focus::tab_navigation::TabGroup, prelude::*, winit::WinitSettings}; +use bevy_core_widgets::Checked; +use bevy_styled_widgets::prelude::*; + +fn main() { + App::new() + .add_plugins((DefaultPlugins, StyledWidgetsPligin)) + .insert_resource(ThemeManager::default()) + .insert_resource(WinitSettings::desktop_app()) + .add_systems(Startup, setup_view_root) + .add_systems(Update, update_root_background) + .run(); +} + +#[derive(Component)] +struct ThemeToggleButton; + +#[derive(Component)] +struct RootWindow; + +fn update_root_background( + theme_manager: Res, + mut query: Query<&mut BackgroundColor, With>, +) { + for mut bg_color in query.iter_mut() { + let theme_styles = theme_manager.styles.clone(); + let color = theme_styles.panel.background_color; + bg_color.0 = color; + } +} + +fn toggle_mode(mut theme_manager: ResMut) { + let current_mode = theme_manager.current_mode; + let new_mode = match current_mode { + ThemeMode::Light => ThemeMode::Dark, + ThemeMode::Dark => ThemeMode::Light, + }; + theme_manager.set_theme_mode(new_mode); +} + +fn set_theme(id: ThemeId) -> impl FnMut(ResMut) + Clone { + move |mut theme_manager: ResMut| { + theme_manager.set_theme(id.clone()); + } +} + +fn setup_view_root(mut commands: Commands) { + commands.spawn(Camera2d); + + let on_toogle_theme_mode = commands.register_system(toggle_mode); + + // Example theme change handlers (register your real handlers) + let on_default_theme = commands.register_system(set_theme(ThemeId("default".into()))); + let on_red_theme = commands.register_system(set_theme(ThemeId("red".into()))); + let on_rose_theme = commands.register_system(set_theme(ThemeId("rose".into()))); + let on_orange_theme = commands.register_system(set_theme(ThemeId("orange".into()))); + let on_green_theme = commands.register_system(set_theme(ThemeId("green".into()))); + let on_blue_theme = commands.register_system(set_theme(ThemeId("blue".into()))); + let on_yellow_theme = commands.register_system(set_theme(ThemeId("yellow".into()))); + let on_violet_theme = commands.register_system(set_theme(ThemeId("violet".into()))); + + let options = vec![ + StyledSelectItem::builder() + .key("Juice".to_string()) + .value("Juice".to_string()), + StyledSelectItem::builder() + .key("Tea".to_string()) + .value("Tea".to_string()), + StyledSelectItem::builder() + .key("Coffee".to_string()) + .value("Coffee".to_string()), + ]; + + let (group_bundle, select_root, select_content, children) = + StyledSelect::builder().children(options).build(); + + commands + .spawn(( + Node { + display: Display::Flex, + flex_direction: FlexDirection::Column, + position_type: PositionType::Absolute, + left: Val::Px(0.), + top: Val::Px(0.), + right: Val::Px(0.), + bottom: Val::Px(0.), + padding: UiRect::all(Val::Px(3.)), + row_gap: Val::Px(18.), + ..Default::default() + }, + RootWindow, + TabGroup::default(), + )) + .with_children(|parent| { + parent.spawn(( + Node { + display: Display::Flex, + flex_direction: FlexDirection::Row, + justify_content: JustifyContent::End, + align_items: AlignItems::Center, + column_gap: Val::Px(6.0), + padding: UiRect::axes(Val::Px(12.0), Val::Px(0.0)), + ..default() + }, + Children::spawn(( + Spawn( + StyledButton::builder() + .text("Default") + .variant(ButtonVariant::Ghost) + .on_click(on_default_theme) + .build(), + ), + Spawn( + StyledButton::builder() + .text("Red") + .variant(ButtonVariant::Ghost) + .on_click(on_red_theme) + .build(), + ), + Spawn( + StyledButton::builder() + .text("Rose") + .variant(ButtonVariant::Ghost) + .on_click(on_rose_theme) + .build(), + ), + Spawn( + StyledButton::builder() + .text("Orange") + .variant(ButtonVariant::Ghost) + .on_click(on_orange_theme) + .build(), + ), + Spawn( + StyledButton::builder() + .text("Green") + .variant(ButtonVariant::Ghost) + .on_click(on_green_theme) + .build(), + ), + Spawn( + StyledButton::builder() + .text("Blue") + .variant(ButtonVariant::Ghost) + .on_click(on_blue_theme) + .build(), + ), + Spawn( + StyledButton::builder() + .text("Yellow") + .variant(ButtonVariant::Ghost) + .on_click(on_yellow_theme) + .build(), + ), + Spawn( + StyledButton::builder() + .text("Violet") + .variant(ButtonVariant::Ghost) + .on_click(on_violet_theme) + .build(), + ), + )), + )); + + parent.spawn(( + Node { + display: Display::Flex, + flex_direction: FlexDirection::Row, + justify_content: JustifyContent::End, + align_items: AlignItems::Center, + column_gap: Val::Px(6.0), + padding: UiRect::axes(Val::Px(12.0), Val::Px(0.0)), + ..default() + }, + Children::spawn(Spawn(( + StyledButton::builder() + .icon("theme_mode_toggle") + .on_click(on_toogle_theme_mode) + .variant(ButtonVariant::Secondary) + .build(), + ThemeToggleButton, + ))), + )); + + parent.spawn( + StyledText::builder() + .content("Select") + .font_size(24.0) + .build(), + ); + + parent + .spawn((Node { + display: Display::Flex, + flex_direction: FlexDirection::Row, + align_items: AlignItems::Start, + align_content: AlignContent::Start, + padding: UiRect::axes(Val::Px(12.0), Val::Px(0.0)), + width: Val::Px(45.), + height: Val::Px(60.), + ..default() + },)) + .insert(group_bundle) + .insert(select_root) + .insert(select_content) + .with_children(|group_parent| { + for child in children { + group_parent.spawn(child); + } + }); + }); +} From a73e197f09c5cb941fa6b9adb04f298eb3302fa9 Mon Sep 17 00:00:00 2001 From: swetar-mecha Date: Mon, 12 May 2025 16:10:11 +0530 Subject: [PATCH 02/10] fix: select alignments --- .../src/ui/select/builder.rs | 52 ++++++++-------- .../src/ui/select/systems.rs | 61 +++++++++++++------ 2 files changed, 67 insertions(+), 46 deletions(-) diff --git a/crates/bevy_styled_widgets/src/ui/select/builder.rs b/crates/bevy_styled_widgets/src/ui/select/builder.rs index dc5ac0d..1a8a547 100644 --- a/crates/bevy_styled_widgets/src/ui/select/builder.rs +++ b/crates/bevy_styled_widgets/src/ui/select/builder.rs @@ -58,7 +58,6 @@ impl SelectItemBuilder { pub fn build(self) -> impl Bundle { let is_selected = self.selected; let is_disabled = self.disabled; - let width = 144.0; let height = 52.0; let key = self.key.clone().unwrap_or_else(|| "".to_string()); @@ -66,21 +65,23 @@ impl SelectItemBuilder { let child_nodes = Children::spawn(( Spawn(( Node { - display: Display::Block, + display: Display::Flex, flex_direction: FlexDirection::Row, - justify_content: JustifyContent::Center, + justify_content: JustifyContent::FlexStart, align_items: AlignItems::Center, - align_content: AlignContent::Center, - min_width: Val::Px(width), - min_height: Val::Px(height), + padding: UiRect::axes(Val::Px(8.0), Val::Px(4.0)), + width: Val::Percent(100.0), + height: Val::Px(height), ..default() }, Name::new(self.key.clone().unwrap_or("".to_string())), - Text::new(self.value.clone()), - TextFont { - font_size: 14.0, - ..default() - }, + Children::spawn(Spawn(( + Text::new(self.value.clone()), + TextFont { + font_size: 14., + ..Default::default() + }, + ))), )), // )); @@ -89,8 +90,8 @@ impl SelectItemBuilder { Node { display: Display::None, // Initially hidden flex_direction: FlexDirection::Column, - justify_content: JustifyContent::Center, - align_items: AlignItems::Center, + justify_content: JustifyContent::FlexStart, + align_items: AlignItems::Stretch, width: Val::Percent(100.0), height: Val::Auto, padding: UiRect::axes(Val::Px(0.0), Val::Px(4.0)), @@ -185,12 +186,11 @@ impl SelectBuilder { Node { display: Display::Flex, flex_direction: FlexDirection::Row, - justify_content: JustifyContent::Center, + justify_content: JustifyContent::FlexStart, align_items: AlignItems::Center, - align_content: AlignContent::Center, min_width: Val::Px(button_width), min_height: Val::Px(button_height), - padding: UiRect::axes(Val::Px(28.), Val::Px(10.)), + padding: UiRect::axes(Val::Px(4.), Val::Px(4.)), ..default() }, StyledSelect { @@ -213,22 +213,20 @@ impl SelectBuilder { TabIndex(0), BorderRadius::default(), BorderColor::default(), - Text::new(self.selected_value.clone().unwrap_or("Select".to_string())), - TextFont { - font_size: 14., - ..Default::default() - }, + Children::spawn(Spawn(( + Text::new(self.selected_value.clone().unwrap_or("Select".to_string())), + TextFont { + font_size: 14., + ..Default::default() + }, + ))), )),)); let select_content_bundle = ( Node { display: Display::Flex, - flex_direction: FlexDirection::Column, // Dropdown items in a column - justify_content: JustifyContent::FlexStart, // Align children to the start - align_items: AlignItems::FlexStart, // Align items to the start - align_content: AlignContent::FlexStart, // Align content to the start - width: Val::Px(button_width), // Match the width of the trigger button - // max_height: Val::Px(200.0), + flex_direction: FlexDirection::Column, + width: Val::Px(button_width), ..default() }, CoreSelectContent { diff --git a/crates/bevy_styled_widgets/src/ui/select/systems.rs b/crates/bevy_styled_widgets/src/ui/select/systems.rs index ca8df04..bfddfc0 100644 --- a/crates/bevy_styled_widgets/src/ui/select/systems.rs +++ b/crates/bevy_styled_widgets/src/ui/select/systems.rs @@ -51,34 +51,57 @@ pub fn on_select_item_selection( mut trigger: Trigger>, q_select_content: Query<&Children, With>, q_select_item: Query<(&ChildOf, &SelectedValue), With>, - mut q_trigger_text: Query<(&mut Text, &mut Name), With>, + q_select_trigger: Query<(&StyledSelect, &Children), With>, + mut q_text: Query<&mut Text>, + mut q_name: Query<&mut Name>, mut commands: Commands, ) { trigger.propagate(false); - if q_select_content.contains(trigger.target()) { - let selected_entity = trigger.event().0; - let (child_of, selected_value) = q_select_item.get(selected_entity.clone()).unwrap(); - let group_children = q_select_content.get(child_of.parent()).unwrap(); + let target = trigger.target(); - for select_item_child in group_children.iter() { - if let Ok((_, value)) = q_select_item.get(select_item_child) { - commands - .entity(select_item_child) - .insert(Checked(value.0 == selected_value.0.clone())); - } - } + // Ensure the trigger target is CoreSelectContent + if !q_select_content.contains(target) { + return; + } + + let selected_entity = trigger.event().0; + + // Get the selected item's value and the parent content it belongs to + let (child_of, selected_value) = match q_select_item.get(selected_entity) { + Ok(res) => res, + Err(_) => return, + }; + + let group_children = match q_select_content.get(child_of.parent()) { + Ok(children) => children, + Err(_) => return, + }; - // Update the SelectTrigger text to match selected_value - for (mut text, mut name) in q_trigger_text.iter_mut() { - text.0 = selected_value.0.clone(); - name.set(selected_value.0.clone()); + // Deselect all others in the same CoreSelectContent group + for child in group_children.iter() { + if let Ok((_, value)) = q_select_item.get(child) { + commands + .entity(child) + .insert(Checked(value.0 == selected_value.0)); } + } + + // Update the trigger text + for (_styled_select, trigger_children) in q_select_trigger.iter() { + for child in trigger_children.iter() { + if let Ok(mut text) = q_text.get_mut(child) { + text.0 = selected_value.0.clone(); + } - commands - .entity(trigger.target()) - .insert(SelectHasPopup(false)); + if let Ok(mut name) = q_name.get_mut(child) { + name.set(selected_value.0.clone()); + } + } } + + // Close dropdown + commands.entity(target).insert(SelectHasPopup(false)); } #[allow(clippy::type_complexity)] From 639cbed9a803a2757bb2fc18c759bc852ddc4713 Mon Sep 17 00:00:00 2001 From: swetar-mecha Date: Mon, 12 May 2025 19:29:12 +0530 Subject: [PATCH 03/10] change hover color --- crates/bevy_styled_widgets/src/ui/select/systems.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_styled_widgets/src/ui/select/systems.rs b/crates/bevy_styled_widgets/src/ui/select/systems.rs index bfddfc0..26e2c4e 100644 --- a/crates/bevy_styled_widgets/src/ui/select/systems.rs +++ b/crates/bevy_styled_widgets/src/ui/select/systems.rs @@ -149,7 +149,7 @@ pub fn update_select_visuals( if item.disabled || is_disabled { *bg_color = BackgroundColor(LIGHT_GRAY.into()); } else if hovering.0 { - *bg_color = BackgroundColor(bevy::color::palettes::css::LIGHT_GRAY.into()); + *bg_color = BackgroundColor(bevy::color::palettes::css::SLATE_GREY.into()); } else if item.selected || option_state.is_selected || *checked { *bg_color = BackgroundColor(bevy::color::palettes::css::DARK_GREY.into()); } else { From 835ab18c723f093dcb72e4fbf39a8aa7eaee4809 Mon Sep 17 00:00:00 2001 From: swetar-mecha Date: Mon, 12 May 2025 19:29:47 +0530 Subject: [PATCH 04/10] core select update for key --- .../src/core_select.rs | 103 +++++++++++++++++- 1 file changed, 98 insertions(+), 5 deletions(-) diff --git a/crates/bevy_additional_core_widgets/src/core_select.rs b/crates/bevy_additional_core_widgets/src/core_select.rs index ac24678..c5fa057 100644 --- a/crates/bevy_additional_core_widgets/src/core_select.rs +++ b/crates/bevy_additional_core_widgets/src/core_select.rs @@ -2,7 +2,8 @@ use accesskit::Role; use bevy::{ a11y::AccessibilityNode, ecs::system::SystemId, - input_focus::{InputFocus, InputFocusVisible}, + input::{ButtonState, keyboard::KeyboardInput}, + input_focus::{FocusedInput, InputFocus, InputFocusVisible}, prelude::*, }; @@ -52,7 +53,7 @@ pub struct CoreSelectContent { pub on_change: Option>>, } -fn select_root_on_button_click( +fn select_content_on_button_click( mut trigger: Trigger, q_group: Query<(&CoreSelectContent, &Children)>, q_select_item: Query<(&Checked, &ChildOf, Has), With>, @@ -99,11 +100,11 @@ fn select_root_on_button_click( .map(|(id, _)| *id); if current_select_item == Some(select_id) { - // If they clicked the currently checked radio button, do nothing + // If they clicked the currently checked item, do nothing return; } - // Trigger the on_change event for the newly checked radio button + // Trigger the on_change event for the newly checked item if let Some(on_change) = on_change { commands.run_system_with(*on_change, select_id); } else { @@ -111,6 +112,97 @@ fn select_root_on_button_click( } } +fn select_content_on_key_input( + mut trigger: Trigger>, + q_group: Query<(&CoreSelectContent, &Children)>, + q_select_item: Query<(&Checked, &ChildOf, Has), With>, + mut commands: Commands, +) { + if let Ok((CoreSelectContent { on_change }, group_children)) = q_group.get(trigger.target()) { + let event = &trigger.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; + trigger.propagate(false); + + let select_children = group_children + .iter() + .filter_map(|child_id| match q_select_item.get(child_id) { + Ok((checked, _, false)) => Some((child_id, checked.0)), + Ok((_, _, true)) => None, + Err(_) => None, + }) + .collect::>(); + + if select_children.is_empty() { + return; // No select items in the group + } + let current_index = select_children + .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 select item in the group + if current_index == 0 { + // If we're at the first one, wrap around to the last + select_children.len() - 1 + } else { + // Move to the previous one + current_index - 1 + } + } + KeyCode::ArrowDown | KeyCode::ArrowRight => { + // Navigate to the next select item in the group + if current_index >= select_children.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 select item in the group + 0 + } + KeyCode::End => { + // Navigate to the last select item in the group + select_children.len() - 1 + } + _ => { + return; + } + }; + + if current_index == next_index { + // If the next index is the same as the current, do nothing + return; + } + + let (next_id, _) = select_children[next_index]; + + // Trigger the on_change event for the newly checked select item + if let Some(on_change) = on_change { + commands.run_system_with(*on_change, next_id); + } else { + commands.trigger_targets(ValueChange(next_id), trigger.target()); + } + } + } +} + #[derive(Component, Debug)] #[require(AccessibilityNode(accesskit::Node::new(Role::ListBoxOption)), Checked)] pub struct CoreSelectItem; @@ -142,7 +234,8 @@ pub struct CoreSelectPlugin; impl Plugin for CoreSelectPlugin { fn build(&self, app: &mut App) { app.add_observer(select_on_pointer_click) - .add_observer(select_root_on_button_click) + .add_observer(select_content_on_button_click) + .add_observer(select_content_on_key_input) .add_observer(select_item_on_pointer_click); } } From f09dc147cb013a6d37069beb3ab4b198c1ea226b Mon Sep 17 00:00:00 2001 From: swetar-mecha Date: Mon, 12 May 2025 19:31:48 +0530 Subject: [PATCH 05/10] chore: add select button sizes(add) --- crates/bevy_styled_widgets/src/themes/mod.rs | 3 +- .../bevy_styled_widgets/src/themes/select.rs | 99 +++++++++++++++++++ .../bevy_styled_widgets/src/themes/styles.rs | 7 +- .../src/ui/select/components.rs | 12 +++ 4 files changed, 119 insertions(+), 2 deletions(-) create mode 100644 crates/bevy_styled_widgets/src/themes/select.rs diff --git a/crates/bevy_styled_widgets/src/themes/mod.rs b/crates/bevy_styled_widgets/src/themes/mod.rs index d4b9da7..ecb5fd4 100644 --- a/crates/bevy_styled_widgets/src/themes/mod.rs +++ b/crates/bevy_styled_widgets/src/themes/mod.rs @@ -10,7 +10,8 @@ pub use manager::*; pub mod checkbox; pub mod progress; -pub mod slider; pub mod radio; +pub mod select; +pub mod slider; pub mod switch; pub mod toggle; diff --git a/crates/bevy_styled_widgets/src/themes/select.rs b/crates/bevy_styled_widgets/src/themes/select.rs new file mode 100644 index 0000000..0ab4c4e --- /dev/null +++ b/crates/bevy_styled_widgets/src/themes/select.rs @@ -0,0 +1,99 @@ +use bevy::prelude::*; + +use super::ThemeColors; + +#[derive(Debug, Clone)] +pub struct SelectButtonStyle { + // Colors + pub normal_background: Color, + pub hovered_background: Color, + pub pressed_background: Color, + pub text_color: Color, + pub border_color: Color, + + // Effects + pub shadow_color: Color, + pub shadow_offset: Vec2, + pub shadow_blur: f32, + + // Transitions + pub transition_duration: f32, // in seconds +} + +// Button size properties +#[derive(Debug, Clone)] +pub struct SelectButtonSizeProperties { + pub font_size: f32, + pub icon_size: f32, + pub min_width: f32, + pub min_height: f32, + pub border_width: f32, + pub border_radius: f32, + pub padding_horizontal: f32, + pub padding_vertical: f32, +} + +// Collection of size variants +#[derive(Debug, Clone)] +pub struct SelectButtonSizeStyles { + pub xsmall: SelectButtonSizeProperties, + pub small: SelectButtonSizeProperties, + pub medium: SelectButtonSizeProperties, + pub large: SelectButtonSizeProperties, + pub xlarge: SelectButtonSizeProperties, +} + +pub fn select_button_sizes() -> SelectButtonSizeStyles { + SelectButtonSizeStyles { + xsmall: SelectButtonSizeProperties { + padding_horizontal: 6.0, + padding_vertical: 2.0, + font_size: 12.0, + icon_size: 12.0, + min_width: 56.0, + min_height: 28.0, + border_width: 1.0, + border_radius: 4.0, + }, + small: SelectButtonSizeProperties { + padding_horizontal: 12.0, + padding_vertical: 4.0, + font_size: 14.0, + icon_size: 14.0, + min_width: 80.0, + min_height: 32.0, + border_width: 1.0, + border_radius: 5.0, + }, + medium: SelectButtonSizeProperties { + padding_horizontal: 16.0, + padding_vertical: 8.0, + font_size: 15.0, + icon_size: 16.0, + min_width: 100.0, + min_height: 36.0, + border_width: 1.5, + border_radius: 6.0, + }, + large: SelectButtonSizeProperties { + padding_horizontal: 24.0, + padding_vertical: 8.0, + font_size: 16.0, + icon_size: 18.0, + min_width: 120.0, + min_height: 40.0, + border_width: 2.0, + border_radius: 6.0, + }, + xlarge: SelectButtonSizeProperties { + padding_horizontal: 28.0, + padding_vertical: 10.0, + font_size: 18.0, + icon_size: 20.0, + min_width: 144.0, + min_height: 52.0, + border_width: 2.0, + border_radius: 8.0, + }, + } +} diff --git a/crates/bevy_styled_widgets/src/themes/styles.rs b/crates/bevy_styled_widgets/src/themes/styles.rs index daab61f..6e84e88 100644 --- a/crates/bevy_styled_widgets/src/themes/styles.rs +++ b/crates/bevy_styled_widgets/src/themes/styles.rs @@ -2,6 +2,8 @@ use std::collections::HashMap; use bevy::prelude::*; +use crate::prelude::SelectButtonSize; + use super::{ ThemeModeConfigs, button::{ButtonSizeStyles, ButtonVariantStyles, button_sizes}, @@ -9,8 +11,9 @@ use super::{ input::InputStyle, panel::PanelStyle, progress::ProgressStyle, - slider::SliderStyle, radio::{RadioButtonSizeStyles, RadioButtonVariantStyles, radio_button_sizes}, + select::{SelectButtonSizeStyles, select_button_sizes}, + slider::SliderStyle, switch::{SwitchSizeStyles, SwitchVariantStyles, switch_sizes}, text::TextStyle, toggle::{ToggleSizeStyles, ToggleVariantStyles, toggle_sizes}, @@ -34,6 +37,7 @@ pub struct ThemeStyles { pub slider: SliderStyle, pub radio_buttons: RadioButtonVariantStyles, pub radio_button_sizes: RadioButtonSizeStyles, + pub select_sizes: SelectButtonSizeStyles, } impl ThemeStyles { @@ -55,6 +59,7 @@ impl ThemeStyles { slider: SliderStyle::from_colors(configs.colors.clone()), radio_buttons: RadioButtonVariantStyles::from_colors(configs.colors.clone()), radio_button_sizes: radio_button_sizes(), + select_sizes: select_button_sizes(), } } } diff --git a/crates/bevy_styled_widgets/src/ui/select/components.rs b/crates/bevy_styled_widgets/src/ui/select/components.rs index 53eed63..fab18a9 100644 --- a/crates/bevy_styled_widgets/src/ui/select/components.rs +++ b/crates/bevy_styled_widgets/src/ui/select/components.rs @@ -29,6 +29,8 @@ pub struct StyledSelect { pub on_click: Option>>, #[reflect(ignore)] pub on_change: Option>>, + #[reflect(ignore)] + pub size: Option, pub selected_value: Option, pub disabled: bool, } @@ -39,6 +41,16 @@ impl StyledSelect { } } +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Component)] +pub enum SelectButtonSize { + XSmall, + Small, + #[default] + Medium, + Large, + XLarge, +} + #[derive(Component, Default)] #[component(immutable, on_add = on_set_label, on_replace = on_set_label)] pub struct AccessibleName(pub String); From 207673173e50d1bebc25324026989855c74009ac Mon Sep 17 00:00:00 2001 From: swetar-mecha Date: Tue, 13 May 2025 20:15:05 +0530 Subject: [PATCH 06/10] update: select styles and sizes support --- .../bevy_styled_widgets/src/themes/select.rs | 50 ++++++-- .../bevy_styled_widgets/src/themes/styles.rs | 6 +- .../src/ui/select/builder.rs | 87 ++++++++++--- .../src/ui/select/systems.rs | 116 +++++++++++++++--- examples/select.rs | 48 ++++++-- 5 files changed, 248 insertions(+), 59 deletions(-) diff --git a/crates/bevy_styled_widgets/src/themes/select.rs b/crates/bevy_styled_widgets/src/themes/select.rs index 0ab4c4e..9281c5d 100644 --- a/crates/bevy_styled_widgets/src/themes/select.rs +++ b/crates/bevy_styled_widgets/src/themes/select.rs @@ -3,21 +3,47 @@ use bevy::prelude::*; use super::ThemeColors; #[derive(Debug, Clone)] -pub struct SelectButtonStyle { +pub struct SelectButtonStyles { // Colors - pub normal_background: Color, - pub hovered_background: Color, - pub pressed_background: Color, - pub text_color: Color, - pub border_color: Color, + // trigger + pub button_background: Color, + pub button_text_color: Color, + pub button_border_color: Color, - // Effects - pub shadow_color: Color, - pub shadow_offset: Vec2, - pub shadow_blur: f32, + // items + pub popover_background: Color, + pub popover_border_color: Color, - // Transitions - pub transition_duration: f32, // in seconds + pub hovered_item_background: Color, + pub active_item_background: Color, + + pub active_border_color: Color, + + pub disabled_background: Color, + pub disabled_text_color: Color, + pub disabled_border_color: Color, +} + +impl SelectButtonStyles { + pub fn from_colors(colors: ThemeColors) -> Self { + Self { + button_background: colors.primary, + button_text_color: colors.primary_foreground, + button_border_color: colors.border, + + popover_background: colors.primary, + popover_border_color: colors.border, + + hovered_item_background: colors.primary.with_alpha(0.9), + active_item_background: colors.primary.with_alpha(0.7), + + active_border_color: colors.border, + + disabled_background: colors.secondary.with_alpha(0.5), + disabled_text_color: colors.secondary.with_alpha(0.5), + disabled_border_color: Color::NONE, + } + } } // Button size properties diff --git a/crates/bevy_styled_widgets/src/themes/styles.rs b/crates/bevy_styled_widgets/src/themes/styles.rs index 6e84e88..be79899 100644 --- a/crates/bevy_styled_widgets/src/themes/styles.rs +++ b/crates/bevy_styled_widgets/src/themes/styles.rs @@ -2,8 +2,6 @@ use std::collections::HashMap; use bevy::prelude::*; -use crate::prelude::SelectButtonSize; - use super::{ ThemeModeConfigs, button::{ButtonSizeStyles, ButtonVariantStyles, button_sizes}, @@ -12,7 +10,7 @@ use super::{ panel::PanelStyle, progress::ProgressStyle, radio::{RadioButtonSizeStyles, RadioButtonVariantStyles, radio_button_sizes}, - select::{SelectButtonSizeStyles, select_button_sizes}, + select::{SelectButtonSizeStyles, SelectButtonStyles, select_button_sizes}, slider::SliderStyle, switch::{SwitchSizeStyles, SwitchVariantStyles, switch_sizes}, text::TextStyle, @@ -38,6 +36,7 @@ pub struct ThemeStyles { pub radio_buttons: RadioButtonVariantStyles, pub radio_button_sizes: RadioButtonSizeStyles, pub select_sizes: SelectButtonSizeStyles, + pub select_button_styles: SelectButtonStyles, } impl ThemeStyles { @@ -60,6 +59,7 @@ impl ThemeStyles { radio_buttons: RadioButtonVariantStyles::from_colors(configs.colors.clone()), radio_button_sizes: radio_button_sizes(), select_sizes: select_button_sizes(), + select_button_styles: SelectButtonStyles::from_colors(configs.colors.clone()), } } } diff --git a/crates/bevy_styled_widgets/src/ui/select/builder.rs b/crates/bevy_styled_widgets/src/ui/select/builder.rs index 1a8a547..06e0cda 100644 --- a/crates/bevy_styled_widgets/src/ui/select/builder.rs +++ b/crates/bevy_styled_widgets/src/ui/select/builder.rs @@ -8,8 +8,10 @@ use bevy_additional_core_widgets::{ }; use bevy_core_widgets::{Checked, hover::Hovering}; +use crate::themes::ThemeManager; + use super::{ - StyledSelect, + SelectButtonSize, StyledSelect, components::{AccessibleName, StyledSelectItem}, }; use accesskit::{Node as Accessible, Role}; @@ -27,6 +29,7 @@ pub struct SelectItemBuilder { pub disabled: bool, pub key: Option, pub value: String, + pub size: Option, } impl SelectItemBuilder { @@ -55,12 +58,30 @@ impl SelectItemBuilder { self } + pub fn size(mut self, size: SelectButtonSize) -> Self { + self.size = Some(size); + self + } + pub fn build(self) -> impl Bundle { let is_selected = self.selected; - let is_disabled = self.disabled; - let height = 52.0; + // let is_disabled = self.disabled; + + let theme_manager = ThemeManager::default(); + let select_button_size_styles = theme_manager.styles.select_sizes.clone(); + // Update size styles + let select_button_size_style = match self.size.unwrap_or_default() { + SelectButtonSize::XSmall => select_button_size_styles.xsmall, + SelectButtonSize::Small => select_button_size_styles.small, + SelectButtonSize::Medium => select_button_size_styles.medium, + SelectButtonSize::Large => select_button_size_styles.large, + SelectButtonSize::XLarge => select_button_size_styles.xlarge, + }; + let height = select_button_size_style.min_height; + let font_size = select_button_size_style.font_size; let key = self.key.clone().unwrap_or_else(|| "".to_string()); + // select content- dropdown let child_nodes = Children::spawn(( Spawn(( @@ -78,7 +99,7 @@ impl SelectItemBuilder { Children::spawn(Spawn(( Text::new(self.value.clone()), TextFont { - font_size: 14., + font_size: font_size, ..Default::default() }, ))), @@ -132,11 +153,12 @@ pub struct SelectTrigger; #[derive(Default, Clone)] pub struct SelectBuilder { - pub on_click: Option>>, - pub on_change: Option>>, - pub children: Vec, - pub selected_value: Option, - pub disabled: bool, + on_click: Option>>, + on_change: Option>>, + children: Vec, + selected_value: Option, + disabled: bool, + size: Option, } impl SelectBuilder { @@ -165,10 +187,27 @@ impl SelectBuilder { self } - // pub fn build(self) -> (impl Bundle, Vec) { + pub fn size(mut self, size: SelectButtonSize) -> Self { + self.size = Some(size); + self + } + pub fn build(self) -> (impl Bundle, impl Bundle, impl Bundle, Vec) { - let button_width = 144.0; - let button_height = 52.0; + let theme_manager = ThemeManager::default(); + let select_button_size_styles = theme_manager.styles.select_sizes.clone(); + // Update size styles + let select_button_size_style = match self.size.unwrap_or_default() { + SelectButtonSize::XSmall => select_button_size_styles.xsmall, + SelectButtonSize::Small => select_button_size_styles.small, + SelectButtonSize::Medium => select_button_size_styles.medium, + SelectButtonSize::Large => select_button_size_styles.large, + SelectButtonSize::XLarge => select_button_size_styles.xlarge, + }; + + let button_width = select_button_size_style.min_width; + let button_height = select_button_size_style.min_height; + + let font_size = select_button_size_style.font_size; let parent_bundle = ( Node { @@ -199,6 +238,7 @@ impl SelectBuilder { selected_value: self.selected_value.clone(), disabled: self.disabled, on_change: self.on_change, + size: self.size, }, BackgroundColor(GRAY.into()), Name::new(self.selected_value.clone().unwrap_or("Select".to_string())), // Name::new("Select"), @@ -211,12 +251,18 @@ impl SelectBuilder { }, AccessibilityNode(Accessible::new(Role::Button)), TabIndex(0), - BorderRadius::default(), + BorderRadius { + top_left: Val::Px(select_button_size_style.border_radius), + top_right: Val::Px(select_button_size_style.border_radius), + bottom_left: Val::Px(select_button_size_style.border_radius), + bottom_right: Val::Px(select_button_size_style.border_radius), + }, + SelectedValue(self.selected_value.clone().unwrap_or("Select".to_string())), BorderColor::default(), Children::spawn(Spawn(( Text::new(self.selected_value.clone().unwrap_or("Select".to_string())), TextFont { - font_size: 14., + font_size: font_size, ..Default::default() }, ))), @@ -224,11 +270,20 @@ impl SelectBuilder { let select_content_bundle = ( Node { - display: Display::Flex, + display: Display::None, flex_direction: FlexDirection::Column, + border: UiRect::all(Val::Px(2.0)), width: Val::Px(button_width), + height: Val::Auto, ..default() }, + BorderRadius { + top_left: Val::Px(select_button_size_style.border_radius), + top_right: Val::Px(select_button_size_style.border_radius), + bottom_left: Val::Px(select_button_size_style.border_radius), + bottom_right: Val::Px(select_button_size_style.border_radius), + }, + SelectHasPopup(false), CoreSelectContent { on_change: self.on_change, }, @@ -237,7 +292,7 @@ impl SelectBuilder { let child_bundles = self .children .into_iter() - .map(|builder| builder.build()) + .map(|builder| builder.size(self.size.unwrap_or_default()).build()) .collect::>(); ( diff --git a/crates/bevy_styled_widgets/src/ui/select/systems.rs b/crates/bevy_styled_widgets/src/ui/select/systems.rs index 26e2c4e..c7b162b 100644 --- a/crates/bevy_styled_widgets/src/ui/select/systems.rs +++ b/crates/bevy_styled_widgets/src/ui/select/systems.rs @@ -1,10 +1,14 @@ -use bevy::{color::palettes::css::LIGHT_GRAY, prelude::*}; +use bevy::prelude::*; use bevy_additional_core_widgets::{ CoreSelectContent, CoreSelectItem, CoreSelectTrigger, ListBoxOptionState, SelectHasPopup, }; use bevy_core_widgets::{Checked, InteractionDisabled, ValueChange, hover::Hovering}; -use super::{SelectContent, StyledSelect, StyledSelectItem, builder::SelectedValue}; +use crate::themes::ThemeManager; + +use super::{ + SelectButtonSize, SelectContent, StyledSelect, StyledSelectItem, builder::SelectedValue, +}; #[allow(clippy::type_complexity)] pub fn on_select_triggered( @@ -31,16 +35,25 @@ pub fn on_select_triggered( } pub fn open_select_popup( - mut content_query: Query<&mut Node, With>, + mut query_set: ParamSet<( + Query<&mut Node, With>, + Query<&mut Node, With>, + )>, has_popup_query: Query<&SelectHasPopup, Changed>, ) { for SelectHasPopup(is_open) in has_popup_query.iter() { if *is_open { - for mut content in content_query.iter_mut() { + for mut content in query_set.p0().iter_mut() { + content.display = Display::Flex; + } + for mut content in query_set.p1().iter_mut() { content.display = Display::Flex; } } else { - for mut content in content_query.iter_mut() { + for mut content in query_set.p0().iter_mut() { + content.display = Display::None; + } + for mut content in query_set.p1().iter_mut() { content.display = Display::None; } } @@ -106,15 +119,20 @@ pub fn on_select_item_selection( #[allow(clippy::type_complexity)] pub fn update_select_visuals( + theme_manager: Res, mut query_set: ParamSet<( Query< ( + &mut Node, &Hovering, &SelectHasPopup, + &mut StyledSelect, &mut BackgroundColor, + &mut BorderColor, + &mut BorderRadius, Has, ), - (With, With), + (With, With), >, Query< ( @@ -128,17 +146,57 @@ pub fn update_select_visuals( With, >, )>, + mut core_select_content_query: Query< + ( + &mut Node, + &mut BorderColor, + &mut BorderRadius, + &StyledSelect, + ), + (With, Without), + >, ) { + let select_button_styles = theme_manager.styles.select_button_styles.clone(); + // Query 0: Trigger - for (hovering, has_popup, mut bg_color, is_disabled) in query_set.p0().iter_mut() { + for ( + mut select_trigger_node, + Hovering(is_hovering), + SelectHasPopup(has_popup), + select_button, + mut bg_color, + mut border_color, + mut border_radius, + is_disabled, + ) in query_set.p0().iter_mut() + { + let select_button_size_styles = theme_manager.styles.select_sizes.clone(); + + let select_button_size_style = match select_button.size.unwrap_or_default() { + SelectButtonSize::XSmall => select_button_size_styles.xsmall, + SelectButtonSize::Small => select_button_size_styles.small, + SelectButtonSize::Medium => select_button_size_styles.medium, + SelectButtonSize::Large => select_button_size_styles.large, + SelectButtonSize::XLarge => select_button_size_styles.xlarge, + }; + + select_trigger_node.border = UiRect::all(Val::Px(select_button_size_style.border_width)); + + // border radius + border_radius.top_left = Val::Px(select_button_size_style.border_radius); + border_radius.top_right = Val::Px(select_button_size_style.border_radius); + border_radius.bottom_left = Val::Px(select_button_size_style.border_radius); + border_radius.bottom_right = Val::Px(select_button_size_style.border_radius); + if is_disabled { - *bg_color = BackgroundColor(LIGHT_GRAY.into()); - } else if has_popup.0 { - *bg_color = BackgroundColor(bevy::color::palettes::css::LIGHT_GRAY.into()); - } else if has_popup.0 || hovering.0 { - *bg_color = BackgroundColor(bevy::color::palettes::css::LIGHT_GRAY.into()); + *bg_color = BackgroundColor(select_button_styles.disabled_background); + *border_color = BorderColor(select_button_styles.disabled_border_color); + } else if *has_popup || *is_hovering { + *bg_color = BackgroundColor(select_button_styles.button_background); + *border_color = BorderColor(select_button_styles.active_border_color); } else { - *bg_color = BackgroundColor(bevy::color::palettes::css::GRAY.into()); + *bg_color = BackgroundColor(select_button_styles.button_background); + *border_color = BorderColor(select_button_styles.active_border_color); } } @@ -147,13 +205,37 @@ pub fn update_select_visuals( query_set.p1().iter_mut() { if item.disabled || is_disabled { - *bg_color = BackgroundColor(LIGHT_GRAY.into()); + *bg_color = BackgroundColor(select_button_styles.disabled_background); } else if hovering.0 { - *bg_color = BackgroundColor(bevy::color::palettes::css::SLATE_GREY.into()); + *bg_color = BackgroundColor(select_button_styles.hovered_item_background); } else if item.selected || option_state.is_selected || *checked { - *bg_color = BackgroundColor(bevy::color::palettes::css::DARK_GREY.into()); + *bg_color = BackgroundColor(select_button_styles.active_item_background); } else { - *bg_color = BackgroundColor(bevy::color::palettes::css::GRAY.into()); + *bg_color = BackgroundColor(select_button_styles.popover_background); } } + + // Update the CoreSelectContent background and border color + for (mut core_select_content_node, mut border_color, mut border_radius, select) in + core_select_content_query.iter_mut() + { + let select_button_size_styles = theme_manager.styles.select_sizes.clone(); + + let select_button_size_style = match select.size.unwrap_or_default() { + SelectButtonSize::XSmall => select_button_size_styles.xsmall, + SelectButtonSize::Small => select_button_size_styles.small, + SelectButtonSize::Medium => select_button_size_styles.medium, + SelectButtonSize::Large => select_button_size_styles.large, + SelectButtonSize::XLarge => select_button_size_styles.xlarge, + }; + core_select_content_node.border = + UiRect::all(Val::Px(select_button_size_style.border_width)); + *border_color = BorderColor(select_button_styles.popover_border_color); + + // border radius + border_radius.top_left = Val::Px(select_button_size_style.border_radius); + border_radius.top_right = Val::Px(select_button_size_style.border_radius); + border_radius.bottom_left = Val::Px(select_button_size_style.border_radius); + border_radius.bottom_right = Val::Px(select_button_size_style.border_radius); + } } diff --git a/examples/select.rs b/examples/select.rs index 5a40e38..d191437 100644 --- a/examples/select.rs +++ b/examples/select.rs @@ -44,10 +44,28 @@ fn set_theme(id: ThemeId) -> impl FnMut(ResMut) + Clone { } } +fn run_on_select_item_selected( + In(selected_entity): In, + q_select_content: Query<&Children>, + select_query: Query<(&ChildOf, &SelectedValue)>, + mut commands: Commands, +) { + if let Ok((child_of, selected_value)) = select_query.get(selected_entity) { + let group_children = q_select_content.get(child_of.parent()).unwrap(); + for select_item_child in group_children.iter() { + if let Ok((_, value)) = select_query.get(select_item_child) { + commands + .entity(select_item_child) + .insert(Checked(value.0 == selected_value.0)); + } + } + } +} + fn setup_view_root(mut commands: Commands) { commands.spawn(Camera2d); - let on_toogle_theme_mode = commands.register_system(toggle_mode); + let on_toggle_theme_mode = commands.register_system(toggle_mode); // Example theme change handlers (register your real handlers) let on_default_theme = commands.register_system(set_theme(ThemeId("default".into()))); @@ -71,8 +89,13 @@ fn setup_view_root(mut commands: Commands) { .value("Coffee".to_string()), ]; - let (group_bundle, select_root, select_content, children) = - StyledSelect::builder().children(options).build(); + // default medium size + let (parent_bundle, select_trigger_bundle, select_content_bundle, child_bundles) = + StyledSelect::builder() + .children(options.clone()) + .size(SelectButtonSize::Large) + // .on_change(commands.register_system(run_on_select_item_selected)) + .build(); commands .spawn(( @@ -175,7 +198,7 @@ fn setup_view_root(mut commands: Commands) { Children::spawn(Spawn(( StyledButton::builder() .icon("theme_mode_toggle") - .on_click(on_toogle_theme_mode) + .on_click(on_toggle_theme_mode) .variant(ButtonVariant::Secondary) .build(), ThemeToggleButton, @@ -200,13 +223,16 @@ fn setup_view_root(mut commands: Commands) { height: Val::Px(60.), ..default() },)) - .insert(group_bundle) - .insert(select_root) - .insert(select_content) - .with_children(|group_parent| { - for child in children { - group_parent.spawn(child); - } + .insert(parent_bundle) + .insert(select_trigger_bundle) + .with_children(|parent| { + parent + .spawn(select_content_bundle) + .with_children(|content| { + for child in child_bundles { + content.spawn(child); + } + }); }); }); } From 8a8cb9ef6bec0158407eb30bc99f0e6eb5e769b6 Mon Sep 17 00:00:00 2001 From: swetar-mecha Date: Mon, 26 May 2025 10:48:13 +0530 Subject: [PATCH 07/10] fix: select widget multiple instance behaviour --- .../src/core_select.rs | 55 +-- .../src/interaction_states.rs | 73 ++-- .../bevy_additional_core_widgets/src/lib.rs | 2 +- .../src/ui/select/builder.rs | 317 ++++++++++-------- .../src/ui/select/components.rs | 2 +- .../src/ui/select/plugin.rs | 4 +- .../src/ui/select/systems.rs | 218 +++++++----- examples/select.rs | 69 +++- 8 files changed, 434 insertions(+), 306 deletions(-) diff --git a/crates/bevy_additional_core_widgets/src/core_select.rs b/crates/bevy_additional_core_widgets/src/core_select.rs index c5fa057..498c5c3 100644 --- a/crates/bevy_additional_core_widgets/src/core_select.rs +++ b/crates/bevy_additional_core_widgets/src/core_select.rs @@ -7,33 +7,34 @@ use bevy::{ prelude::*, }; -use bevy_core_widgets::{ButtonClicked, Checked, InteractionDisabled, ValueChange}; +use bevy_core_widgets::{ButtonClicked, InteractionDisabled, ValueChange}; -use crate::{ListBoxOptionState, interaction_states::SelectHasPopup}; +use crate::{IsSelected, interaction_states::DropdownOpen}; #[derive(Component, Debug)] -#[require(AccessibilityNode(accesskit::Node::new(Role::ListBox)), SelectHasPopup)] +#[require(AccessibilityNode(accesskit::Node::new(Role::ComboBox)), DropdownOpen)] pub struct CoreSelectTrigger { pub on_click: Option>>, } pub fn select_on_pointer_click( mut trigger: Trigger>, - q_state: Query<( - &CoreSelectTrigger, - &SelectHasPopup, - Has, - )>, + q_state: Query<(&CoreSelectTrigger, &DropdownOpen, Has)>, mut focus: ResMut, mut focus_visible: ResMut, mut commands: Commands, ) { - if let Ok((select_trigger, SelectHasPopup(clicked), disabled)) = q_state.get(trigger.target()) { + if let Ok((select_trigger, DropdownOpen(clicked), disabled)) = q_state.get(trigger.target()) { let select_id = trigger.target(); focus.0 = Some(select_id); focus_visible.0 = false; trigger.propagate(false); + if disabled { + // If the select is disabled, do nothing + return; + } + if !disabled { let is_open = clicked; let new_clicked = !is_open; @@ -41,7 +42,7 @@ pub fn select_on_pointer_click( if let Some(on_click) = select_trigger.on_click { commands.run_system_with(on_click, new_clicked); } else { - commands.trigger_targets(ValueChange(SelectHasPopup(new_clicked)), select_id); + commands.trigger_targets(ValueChange(DropdownOpen(new_clicked)), select_id); } } } @@ -56,7 +57,7 @@ pub struct CoreSelectContent { fn select_content_on_button_click( mut trigger: Trigger, q_group: Query<(&CoreSelectContent, &Children)>, - q_select_item: Query<(&Checked, &ChildOf, Has), With>, + q_select_item: Query<(&IsSelected, &ChildOf, Has), With>, mut focus: ResMut, mut focus_visible: ResMut, mut commands: Commands, @@ -83,7 +84,7 @@ fn select_content_on_button_click( let select_children = group_children .iter() .filter_map(|child_id| match q_select_item.get(child_id) { - Ok((checked, _, false)) => Some((child_id, checked.0)), + Ok((is_selected, _, false)) => Some((child_id, is_selected.0)), Ok((_, _, true)) => None, Err(_) => None, }) @@ -96,15 +97,15 @@ fn select_content_on_button_click( trigger.propagate(false); let current_select_item = select_children .iter() - .find(|(_, checked)| *checked) + .find(|(_, is_selected)| *is_selected) .map(|(id, _)| *id); if current_select_item == Some(select_id) { - // If they clicked the currently checked item, do nothing + // If they clicked the currently selected item, do nothing return; } - // Trigger the on_change event for the newly checked item + // Trigger the on_change event for the newly selected item if let Some(on_change) = on_change { commands.run_system_with(*on_change, select_id); } else { @@ -115,7 +116,7 @@ fn select_content_on_button_click( fn select_content_on_key_input( mut trigger: Trigger>, q_group: Query<(&CoreSelectContent, &Children)>, - q_select_item: Query<(&Checked, &ChildOf, Has), With>, + q_select_item: Query<(&IsSelected, &ChildOf, Has), With>, mut commands: Commands, ) { if let Ok((CoreSelectContent { on_change }, group_children)) = q_group.get(trigger.target()) { @@ -138,7 +139,7 @@ fn select_content_on_key_input( let select_children = group_children .iter() .filter_map(|child_id| match q_select_item.get(child_id) { - Ok((checked, _, false)) => Some((child_id, checked.0)), + Ok((is_selected, _, false)) => Some((child_id, is_selected.0)), Ok((_, _, true)) => None, Err(_) => None, }) @@ -149,8 +150,8 @@ fn select_content_on_key_input( } let current_index = select_children .iter() - .position(|(_, checked)| *checked) - .unwrap_or(usize::MAX); // Default to invalid index if none are checked + .position(|(_, is_selected)| *is_selected) + .unwrap_or(usize::MAX); // Default to invalid index if none are selected let next_index = match key_code { KeyCode::ArrowUp | KeyCode::ArrowLeft => { @@ -193,7 +194,7 @@ fn select_content_on_key_input( let (next_id, _) = select_children[next_index]; - // Trigger the on_change event for the newly checked select item + // Trigger the on_change event for the newly selected item if let Some(on_change) = on_change { commands.run_system_with(*on_change, next_id); } else { @@ -204,31 +205,31 @@ fn select_content_on_key_input( } #[derive(Component, Debug)] -#[require(AccessibilityNode(accesskit::Node::new(Role::ListBoxOption)), Checked)] +#[require( + AccessibilityNode(accesskit::Node::new(Role::ListBoxOption)), + IsSelected +)] pub struct CoreSelectItem; fn select_item_on_pointer_click( mut trigger: Trigger>, - q_state: Query<(&Checked, Has), With>, + q_state: Query<(&IsSelected, Has), With>, mut focus: ResMut, mut focus_visible: ResMut, mut commands: Commands, ) { - if let Ok((checked, disabled)) = q_state.get(trigger.target()) { + if let Ok((is_selected, disabled)) = q_state.get(trigger.target()) { let checkbox_id = trigger.target(); focus.0 = Some(checkbox_id); focus_visible.0 = false; trigger.propagate(false); - if checked.0 || disabled { - // If the radio is already checked, or disabled, we do nothing. + if is_selected.0 || disabled { return; } commands.trigger_targets(ButtonClicked, trigger.target()); } } -// ----- - pub struct CoreSelectPlugin; impl Plugin for CoreSelectPlugin { diff --git a/crates/bevy_additional_core_widgets/src/interaction_states.rs b/crates/bevy_additional_core_widgets/src/interaction_states.rs index b3792e0..7d29a1b 100644 --- a/crates/bevy_additional_core_widgets/src/interaction_states.rs +++ b/crates/bevy_additional_core_widgets/src/interaction_states.rs @@ -3,54 +3,61 @@ use bevy::{ ecs::{component::HookContext, world::DeferredWorld}, prelude::Component, }; -/// Component that indicates whether the select widget has a popup. + +/// Component that indicates whether the select widget has a drodown. #[derive(Component, Default, Debug)] -#[component(immutable, on_add = on_add_has_popup, on_replace = on_add_has_popup)] -pub struct SelectHasPopup(pub bool); +#[component(immutable, on_add = update_expanded_a11y, on_replace = update_expanded_a11y)] +pub struct DropdownOpen(pub bool); -// Hook to set the a11y "HasPopup" state when the select widget is added or updated. -pub fn on_add_has_popup(mut world: DeferredWorld, context: HookContext) { +pub fn update_expanded_a11y(mut world: DeferredWorld, context: HookContext) { let mut entt = world.entity_mut(context.entity); - let has_popup = entt.get::().unwrap().0; - - if let Some(mut accessibility) = entt.get_mut::() { - if has_popup == true { - accessibility.set_has_popup(accesskit::HasPopup::Listbox); // Set to Listbox - } else { - accessibility.clear_has_popup(); // Clear the HasPopup property - } + let is_open = entt.get::().unwrap().0; + + if let Some(mut accessibility_node) = entt.get_mut::() { + accessibility_node.set_expanded(is_open); + accessibility_node.set_has_popup(accesskit::HasPopup::Listbox); // Set to Listbox } else { - eprintln!("Error in on_add_has_popup()"); + eprintln!("Error in update_expanded_a11y()"); } } -/// Component that indicates the state of a ListBoxOption. +/// Component that indicates whether the item is selected #[derive(Component, Default, Debug)] -#[component(on_add = on_add_listbox_option)] -pub struct ListBoxOptionState { +#[component(on_add = update_is_selected_a11y, on_replace = update_is_selected_a11y)] +pub struct IsSelected(pub bool); + +pub fn update_is_selected_a11y(mut world: DeferredWorld, context: HookContext) { + let mut entt = world.entity_mut(context.entity); + let is_selected = entt.get::().unwrap().0; + + if let Some(mut accessibility_node) = entt.get_mut::() { + accessibility_node.set_selected(is_selected); + } else { + eprintln!("Error in update_is_selected_a11y()"); + } +} + +/// Component that indicates the selected item + +#[derive(Component, Debug)] +#[component(on_add = update_selected_item_a11y, on_replace = update_selected_item_a11y)] +pub struct SelectedItem { pub label: String, - pub is_selected: bool, + pub value: String, } -// Hook to set the a11y properties for a ListBoxOption when added or updated. -fn on_add_listbox_option(mut world: DeferredWorld, context: HookContext) { +pub fn update_selected_item_a11y(mut world: DeferredWorld, context: HookContext) { let mut entt = world.entity_mut(context.entity); - let (label, is_selected) = { - let state = entt.get::().unwrap(); - (state.label.clone(), state.is_selected) + let (label, value) = { + let selected_item = entt.get::().unwrap(); + (selected_item.label.clone(), selected_item.value.clone()) }; - if let Some(mut accessibility) = entt.get_mut::() { - accessibility.set_label(&*label); - - // Set the selected state - if is_selected { - accessibility.set_selected(true); - } else { - accessibility.clear_selected(); - } + if let Some(mut accessibility_node) = entt.get_mut::() { + accessibility_node.set_label(label); + accessibility_node.set_value(value); } else { - eprintln!("Error in on_add_has_popup()"); + eprintln!("Error in update_selected_item_a11y()"); } } diff --git a/crates/bevy_additional_core_widgets/src/lib.rs b/crates/bevy_additional_core_widgets/src/lib.rs index 0036b5b..77a4fb9 100644 --- a/crates/bevy_additional_core_widgets/src/lib.rs +++ b/crates/bevy_additional_core_widgets/src/lib.rs @@ -5,7 +5,7 @@ mod interaction_states; pub use core_select::{CoreSelectContent, CoreSelectItem, CoreSelectPlugin, CoreSelectTrigger}; pub use core_switch::{CoreSwitch, CoreSwitchPlugin}; -pub use interaction_states::{ListBoxOptionState, SelectHasPopup}; +pub use interaction_states::{DropdownOpen, IsSelected, SelectedItem}; pub struct AdditionalCoreWidgetsPlugin; impl Plugin for AdditionalCoreWidgetsPlugin { diff --git a/crates/bevy_styled_widgets/src/ui/select/builder.rs b/crates/bevy_styled_widgets/src/ui/select/builder.rs index 06e0cda..e5ffea2 100644 --- a/crates/bevy_styled_widgets/src/ui/select/builder.rs +++ b/crates/bevy_styled_widgets/src/ui/select/builder.rs @@ -1,12 +1,11 @@ use bevy::{ - a11y::AccessibilityNode, color::palettes::css::GRAY, ecs::system::SystemId, - input_focus::tab_navigation::TabIndex, prelude::*, window::SystemCursorIcon, - winit::cursor::CursorIcon, + a11y::AccessibilityNode, ecs::system::SystemId, input_focus::tab_navigation::TabIndex, + prelude::*, window::SystemCursorIcon, winit::cursor::CursorIcon, }; use bevy_additional_core_widgets::{ - CoreSelectContent, CoreSelectItem, CoreSelectTrigger, ListBoxOptionState, SelectHasPopup, + CoreSelectContent, CoreSelectItem, CoreSelectTrigger, DropdownOpen, IsSelected, SelectedItem, }; -use bevy_core_widgets::{Checked, hover::Hovering}; +use bevy_core_widgets::hover::Hovering; use crate::themes::ThemeManager; @@ -17,140 +16,28 @@ use super::{ use accesskit::{Node as Accessible, Role}; #[derive(Component, Default)] -pub struct SelectContent; +pub struct SelectWidget; // marker -#[derive(Component, Debug, Clone)] -pub struct SelectedValue(pub String); +#[derive(Component, Default)] +pub struct SelectTrigger; // marker -#[derive(Default, Clone)] -pub struct SelectItemBuilder { - pub selected: bool, - pub on_change: Option>>, - pub disabled: bool, - pub key: Option, +#[derive(Component)] +pub struct DropdownContainer; // marker + +// marker +#[derive(Component)] +pub struct DropdownOption { pub value: String, - pub size: Option, + pub disabled: bool, } -impl SelectItemBuilder { - pub fn selected(mut self, selected: bool) -> Self { - self.selected = selected; - self - } - - pub fn on_change(mut self, system_id: SystemId>) -> Self { - self.on_change = Some(system_id); - self - } - - pub fn disabled(mut self) -> Self { - self.disabled = true; - self - } - - pub fn key>(mut self, key: S) -> Self { - self.key = Some(key.into()); - self - } - - pub fn value>(mut self, value: S) -> Self { - self.value = value.into(); - self - } - - pub fn size(mut self, size: SelectButtonSize) -> Self { - self.size = Some(size); - self - } - - pub fn build(self) -> impl Bundle { - let is_selected = self.selected; - // let is_disabled = self.disabled; - - let theme_manager = ThemeManager::default(); - let select_button_size_styles = theme_manager.styles.select_sizes.clone(); - // Update size styles - let select_button_size_style = match self.size.unwrap_or_default() { - SelectButtonSize::XSmall => select_button_size_styles.xsmall, - SelectButtonSize::Small => select_button_size_styles.small, - SelectButtonSize::Medium => select_button_size_styles.medium, - SelectButtonSize::Large => select_button_size_styles.large, - SelectButtonSize::XLarge => select_button_size_styles.xlarge, - }; - let height = select_button_size_style.min_height; - let font_size = select_button_size_style.font_size; - - let key = self.key.clone().unwrap_or_else(|| "".to_string()); - - // select content- dropdown - let child_nodes = Children::spawn(( - Spawn(( - Node { - display: Display::Flex, - flex_direction: FlexDirection::Row, - justify_content: JustifyContent::FlexStart, - align_items: AlignItems::Center, - padding: UiRect::axes(Val::Px(8.0), Val::Px(4.0)), - width: Val::Percent(100.0), - height: Val::Px(height), - ..default() - }, - Name::new(self.key.clone().unwrap_or("".to_string())), - Children::spawn(Spawn(( - Text::new(self.value.clone()), - TextFont { - font_size: font_size, - ..Default::default() - }, - ))), - )), - // - )); - - ( - Node { - display: Display::None, // Initially hidden - flex_direction: FlexDirection::Column, - justify_content: JustifyContent::FlexStart, - align_items: AlignItems::Stretch, - width: Val::Percent(100.0), - height: Val::Auto, - padding: UiRect::axes(Val::Px(0.0), Val::Px(4.0)), - ..default() - }, - GlobalZIndex(99), - SelectContent, - CoreSelectItem, - Name::new("Select Item"), - AccessibilityNode(Accessible::new(Role::ListBoxOption)), - Hovering::default(), - CursorIcon::System(SystemCursorIcon::Pointer), - ListBoxOptionState { - label: self.value.clone(), - is_selected: false, - }, - StyledSelectItem { - selected: self.selected, - on_change: self.on_change, - disabled: self.disabled, - key: self.key, - value: self.value.clone(), - }, - SelectedValue(self.value.clone()), - Checked(is_selected), - AccessibleName(key.clone()), - TabIndex(0), - child_nodes, - ) - } +// marker +#[derive(Component)] +pub struct SelectState { + pub selected: Option, + pub is_open: bool, } -#[derive(Component, Default)] -pub struct SelectRoot; - -#[derive(Component, Default)] -pub struct SelectTrigger; - #[derive(Default, Clone)] pub struct SelectBuilder { on_click: Option>>, @@ -195,6 +82,8 @@ impl SelectBuilder { pub fn build(self) -> (impl Bundle, impl Bundle, impl Bundle, Vec) { let theme_manager = ThemeManager::default(); let select_button_size_styles = theme_manager.styles.select_sizes.clone(); + let select_button_styles = theme_manager.styles.select_button_styles.clone(); + // Update size styles let select_button_size_style = match self.size.unwrap_or_default() { SelectButtonSize::XSmall => select_button_size_styles.xsmall, @@ -209,7 +98,9 @@ impl SelectBuilder { let font_size = select_button_size_style.font_size; - let parent_bundle = ( + // Root: SelectWidget + + let root = ( Node { display: Display::Flex, flex_direction: FlexDirection::Column, @@ -217,11 +108,17 @@ impl SelectBuilder { justify_content: JustifyContent::FlexStart, ..default() }, - SelectRoot, - AccessibilityNode(Accessible::new(Role::ListBox)), + SelectWidget, + SelectState { + selected: self.selected_value.clone(), + is_open: false, + }, + AccessibilityNode(Accessible::new(Role::ComboBox)), + DropdownOpen(false), + AccessibleName(self.selected_value.clone().unwrap_or("Select".to_string())), ); - let select_trigger_bundle = Children::spawn((Spawn(( + let trigger = Children::spawn((Spawn(( Node { display: Display::Flex, flex_direction: FlexDirection::Row, @@ -240,15 +137,10 @@ impl SelectBuilder { on_change: self.on_change, size: self.size, }, - BackgroundColor(GRAY.into()), + BackgroundColor(select_button_styles.button_background.into()), Name::new(self.selected_value.clone().unwrap_or("Select".to_string())), // Name::new("Select"), Hovering::default(), CursorIcon::System(SystemCursorIcon::Pointer), - SelectHasPopup(false), - SelectTrigger, - CoreSelectTrigger { - on_click: self.on_click, - }, AccessibilityNode(Accessible::new(Role::Button)), TabIndex(0), BorderRadius { @@ -257,7 +149,11 @@ impl SelectBuilder { bottom_left: Val::Px(select_button_size_style.border_radius), bottom_right: Val::Px(select_button_size_style.border_radius), }, + SelectTrigger, SelectedValue(self.selected_value.clone().unwrap_or("Select".to_string())), + CoreSelectTrigger { + on_click: self.on_click, + }, BorderColor::default(), Children::spawn(Spawn(( Text::new(self.selected_value.clone().unwrap_or("Select".to_string())), @@ -268,7 +164,8 @@ impl SelectBuilder { ))), )),)); - let select_content_bundle = ( + // Dropdown container + let dropdown = ( Node { display: Display::None, flex_direction: FlexDirection::Column, @@ -277,16 +174,18 @@ impl SelectBuilder { height: Val::Auto, ..default() }, + BackgroundColor(select_button_styles.popover_background.into()), BorderRadius { top_left: Val::Px(select_button_size_style.border_radius), top_right: Val::Px(select_button_size_style.border_radius), bottom_left: Val::Px(select_button_size_style.border_radius), bottom_right: Val::Px(select_button_size_style.border_radius), }, - SelectHasPopup(false), + DropdownContainer, // marker CoreSelectContent { on_change: self.on_change, }, + AccessibilityNode(Accessible::new(Role::ListBox)), ); let child_bundles = self @@ -295,11 +194,133 @@ impl SelectBuilder { .map(|builder| builder.size(self.size.unwrap_or_default()).build()) .collect::>(); + (root, trigger, dropdown, child_bundles) + } +} + +#[derive(Component, Default)] +pub struct SelectContent; + +#[derive(Component, Debug, Clone)] +pub struct SelectedValue(pub String); + +#[derive(Default, Clone)] +pub struct SelectItemBuilder { + pub selected: bool, + pub on_change: Option>>, + pub disabled: bool, + pub label: Option, + pub value: String, + pub size: Option, +} + +impl SelectItemBuilder { + pub fn selected(mut self, selected: bool) -> Self { + self.selected = selected; + self + } + + pub fn on_change(mut self, system_id: SystemId>) -> Self { + self.on_change = Some(system_id); + self + } + + pub fn disabled(mut self) -> Self { + self.disabled = true; + self + } + + pub fn label>(mut self, label: S) -> Self { + self.label = Some(label.into()); + self + } + + pub fn value>(mut self, value: S) -> Self { + self.value = value.into(); + self + } + + pub fn size(mut self, size: SelectButtonSize) -> Self { + self.size = Some(size); + self + } + + pub fn build(self) -> impl Bundle { + let theme_manager = ThemeManager::default(); + let select_button_size_styles = theme_manager.styles.select_sizes.clone(); + // Update size styles + let select_button_size_style = match self.size.unwrap_or_default() { + SelectButtonSize::XSmall => select_button_size_styles.xsmall, + SelectButtonSize::Small => select_button_size_styles.small, + SelectButtonSize::Medium => select_button_size_styles.medium, + SelectButtonSize::Large => select_button_size_styles.large, + SelectButtonSize::XLarge => select_button_size_styles.xlarge, + }; + let height = select_button_size_style.min_height; + let font_size = select_button_size_style.font_size; + + // select content- dropdown + let child_nodes = Children::spawn(( + Spawn(( + Node { + display: Display::Flex, + flex_direction: FlexDirection::Row, + justify_content: JustifyContent::FlexStart, + align_items: AlignItems::Center, + padding: UiRect::axes(Val::Px(8.0), Val::Px(4.0)), + width: Val::Percent(100.0), + height: Val::Px(height), + ..default() + }, + Name::new(self.label.clone().unwrap_or("".to_string())), + Children::spawn(Spawn(( + Text::new(self.value.clone()), + TextFont { + font_size: font_size, + ..Default::default() + }, + ))), + )), + // + )); + ( - parent_bundle, - select_trigger_bundle, - select_content_bundle, - child_bundles, + Node { + display: Display::Flex, // Initially hidden + flex_direction: FlexDirection::Column, + justify_content: JustifyContent::FlexStart, + align_items: AlignItems::Stretch, + width: Val::Percent(100.0), + height: Val::Auto, + padding: UiRect::axes(Val::Px(0.0), Val::Px(4.0)), + ..default() + }, + GlobalZIndex(99), // to ensure it appears above other UI elements + SelectContent, + CoreSelectItem, + Name::new("Select Item"), + Hovering::default(), + CursorIcon::System(SystemCursorIcon::Pointer), + StyledSelectItem { + selected: self.selected, + on_change: self.on_change, + disabled: self.disabled, + label: self.label.clone(), + value: self.value.clone(), + }, + SelectedValue(self.value.clone()), + DropdownOption { + value: self.value.clone(), + disabled: self.disabled, + }, + SelectedItem { + label: self.label.clone().unwrap_or(self.value.clone()), + value: self.value.clone(), + }, + IsSelected(self.selected), + AccessibilityNode(Accessible::new(Role::ListBoxOption)), + TabIndex(0), + child_nodes, ) } } diff --git a/crates/bevy_styled_widgets/src/ui/select/components.rs b/crates/bevy_styled_widgets/src/ui/select/components.rs index fab18a9..a980450 100644 --- a/crates/bevy_styled_widgets/src/ui/select/components.rs +++ b/crates/bevy_styled_widgets/src/ui/select/components.rs @@ -10,7 +10,7 @@ pub struct StyledSelectItem { #[reflect(ignore)] pub on_change: Option>>, pub disabled: bool, - pub key: Option, + pub label: Option, pub value: String, } diff --git a/crates/bevy_styled_widgets/src/ui/select/plugin.rs b/crates/bevy_styled_widgets/src/ui/select/plugin.rs index 346bc0b..fb12c0e 100644 --- a/crates/bevy_styled_widgets/src/ui/select/plugin.rs +++ b/crates/bevy_styled_widgets/src/ui/select/plugin.rs @@ -1,14 +1,14 @@ use bevy::prelude::*; use super::{ - on_select_item_selection, on_select_triggered, open_select_popup, update_select_visuals, + on_select_item_selection, on_select_triggered, open_select_content, update_select_visuals, }; pub struct StyledSelectTriggerPlugin; impl Plugin for StyledSelectTriggerPlugin { fn build(&self, app: &mut App) { app.add_observer(on_select_triggered) - .add_systems(Update, open_select_popup) + .add_systems(Update, open_select_content) .add_observer(on_select_item_selection) .add_systems(Update, update_select_visuals); } diff --git a/crates/bevy_styled_widgets/src/ui/select/systems.rs b/crates/bevy_styled_widgets/src/ui/select/systems.rs index c7b162b..b3a5636 100644 --- a/crates/bevy_styled_widgets/src/ui/select/systems.rs +++ b/crates/bevy_styled_widgets/src/ui/select/systems.rs @@ -1,60 +1,66 @@ +use std::process::Child; + use bevy::prelude::*; use bevy_additional_core_widgets::{ - CoreSelectContent, CoreSelectItem, CoreSelectTrigger, ListBoxOptionState, SelectHasPopup, + CoreSelectContent, CoreSelectItem, CoreSelectTrigger, DropdownOpen, IsSelected, }; -use bevy_core_widgets::{Checked, InteractionDisabled, ValueChange, hover::Hovering}; +use bevy_core_widgets::{InteractionDisabled, ValueChange, hover::Hovering}; use crate::themes::ThemeManager; use super::{ - SelectButtonSize, SelectContent, StyledSelect, StyledSelectItem, builder::SelectedValue, + DropdownContainer, SelectButtonSize, SelectContent, SelectState, SelectTrigger, SelectWidget, + StyledSelect, StyledSelectItem, builder::SelectedValue, }; -#[allow(clippy::type_complexity)] pub fn on_select_triggered( - mut trigger: Trigger>, - query: Query<&StyledSelect>, + mut trigger: Trigger>, mut commands: Commands, + all_triggers: Query<(Entity, &StyledSelect), With>, ) { trigger.propagate(false); let clicked = &trigger.event().0; - let entity = trigger.target(); - commands.entity(entity).insert(SelectHasPopup(clicked.0)); + let clicked_entity = trigger.target(); - if let Ok(styled_select) = query.get(entity) { + // Close all other dropdowns and update the clicked one + for (entity, styled_select) in &all_triggers { if styled_select.disabled { commands.entity(entity).insert(InteractionDisabled); + continue; } - if let Some(system_id) = styled_select.on_click { - // Defer the callback system using commands - commands.run_system_with(system_id, clicked.0); + + if entity == clicked_entity { + commands.entity(entity).insert(DropdownOpen(clicked.0)); + if let Some(system_id) = styled_select.on_click { + commands.run_system_with(system_id, clicked.0); + } + } else { + commands.entity(entity).insert(DropdownOpen(false)); } } } -pub fn open_select_popup( - mut query_set: ParamSet<( - Query<&mut Node, With>, - Query<&mut Node, With>, - )>, - has_popup_query: Query<&SelectHasPopup, Changed>, +pub fn open_select_content( + query_open_widgets: Query<(Entity, &DropdownOpen), Changed>, + root_query: Query<(&Children, Entity), With>, + mut dropdown_query: Query<(Entity, &mut Node), With>, ) { - for SelectHasPopup(is_open) in has_popup_query.iter() { - if *is_open { - for mut content in query_set.p0().iter_mut() { - content.display = Display::Flex; - } - for mut content in query_set.p1().iter_mut() { - content.display = Display::Flex; - } - } else { - for mut content in query_set.p0().iter_mut() { - content.display = Display::None; - } - for mut content in query_set.p1().iter_mut() { - content.display = Display::None; + for (widget_entity, DropdownOpen(is_open)) in &query_open_widgets { + // Find the root SelectWidget this DropdownOpen belongs to + for (children, root_entity) in &root_query { + if children.contains(&widget_entity) { + // Update its dropdown container + for &child in children { + if let Ok((dropdown_entity, mut node)) = dropdown_query.get_mut(child) { + node.display = if *is_open { + Display::Flex + } else { + Display::None + }; + } + } } } } @@ -62,59 +68,71 @@ pub fn open_select_popup( pub fn on_select_item_selection( mut trigger: Trigger>, + q_select_widget: Query<(&Children, Entity), With>, q_select_content: Query<&Children, With>, q_select_item: Query<(&ChildOf, &SelectedValue), With>, - q_select_trigger: Query<(&StyledSelect, &Children), With>, + q_select_trigger: Query<&Children, With>, mut q_text: Query<&mut Text>, mut q_name: Query<&mut Name>, mut commands: Commands, ) { trigger.propagate(false); - let target = trigger.target(); + let target = trigger.target(); // the CoreSelectContent entity - // Ensure the trigger target is CoreSelectContent - if !q_select_content.contains(target) { - return; - } + // Only proceed if the trigger target is a valid CoreSelectContent + let group_children = match q_select_content.get(target) { + Ok(children) => children, + Err(_) => return, + }; let selected_entity = trigger.event().0; - // Get the selected item's value and the parent content it belongs to + // Get the selected item's value and its parent (CoreSelectContent) let (child_of, selected_value) = match q_select_item.get(selected_entity) { Ok(res) => res, Err(_) => return, }; - let group_children = match q_select_content.get(child_of.parent()) { - Ok(children) => children, - Err(_) => return, + // 1. Find the root SelectWidget this content belongs to + let mut widget_entity = None; + for (children, root) in &q_select_widget { + if children.contains(&target) { + widget_entity = Some((root, children)); + break; + } + } + + let (widget_entity, widget_children) = match widget_entity { + Some(val) => val, + None => return, }; - // Deselect all others in the same CoreSelectContent group + // 2. Deselect all other CoreSelectItems in the same content group for child in group_children.iter() { if let Ok((_, value)) = q_select_item.get(child) { commands .entity(child) - .insert(Checked(value.0 == selected_value.0)); + .insert(IsSelected(value.0 == selected_value.0)); } } - // Update the trigger text - for (_styled_select, trigger_children) in q_select_trigger.iter() { - for child in trigger_children.iter() { - if let Ok(mut text) = q_text.get_mut(child) { - text.0 = selected_value.0.clone(); - } - - if let Ok(mut name) = q_name.get_mut(child) { - name.set(selected_value.0.clone()); + // 3. Update the text and name of the trigger that belongs to this specific widget + for child in widget_children.iter() { + if let Ok(trigger_children) = q_select_trigger.get(child) { + for grandchild in trigger_children.iter() { + if let Ok(mut text) = q_text.get_mut(grandchild) { + text.0 = selected_value.0.clone(); + } + if let Ok(mut name) = q_name.get_mut(grandchild) { + name.set(selected_value.0.clone()); + } } } } - // Close dropdown - commands.entity(target).insert(SelectHasPopup(false)); + // 4. Close the dropdown (just for this widget) + commands.entity(target).insert(DropdownOpen(false)); } #[allow(clippy::type_complexity)] @@ -123,9 +141,10 @@ pub fn update_select_visuals( mut query_set: ParamSet<( Query< ( + Entity, &mut Node, &Hovering, - &SelectHasPopup, + &DropdownOpen, &mut StyledSelect, &mut BackgroundColor, &mut BorderColor, @@ -136,33 +155,47 @@ pub fn update_select_visuals( >, Query< ( + Entity, &Hovering, &mut BackgroundColor, - &ListBoxOptionState, Has, &StyledSelectItem, - &Checked, + &IsSelected, + &ChildOf, ), With, >, + Query< + ( + Entity, + &mut Node, + &mut BackgroundColor, + &mut BorderColor, + &mut BorderRadius, + &StyledSelect, + ), + (With, Without), + >, )>, - mut core_select_content_query: Query< - ( - &mut Node, - &mut BorderColor, - &mut BorderRadius, - &StyledSelect, - ), - (With, Without), - >, ) { let select_button_styles = theme_manager.styles.select_button_styles.clone(); - // Query 0: Trigger + // Store active roots from the triggers. + let mut active_roots = Vec::new(); + for (trigger_entity, _, Hovering(is_hovering), DropdownOpen(is_open), _, _, _, _, _) in + query_set.p0().iter() + { + if *is_open || *is_hovering { + active_roots.push(trigger_entity); + } + } + + // === Part 2: Update trigger visuals for active roots only === for ( - mut select_trigger_node, + trigger_entity, + mut node, Hovering(is_hovering), - SelectHasPopup(has_popup), + DropdownOpen(is_open), select_button, mut bg_color, mut border_color, @@ -170,8 +203,10 @@ pub fn update_select_visuals( is_disabled, ) in query_set.p0().iter_mut() { + if !active_roots.contains(&trigger_entity) { + continue; + } let select_button_size_styles = theme_manager.styles.select_sizes.clone(); - let select_button_size_style = match select_button.size.unwrap_or_default() { SelectButtonSize::XSmall => select_button_size_styles.xsmall, SelectButtonSize::Small => select_button_size_styles.small, @@ -180,9 +215,7 @@ pub fn update_select_visuals( SelectButtonSize::XLarge => select_button_size_styles.xlarge, }; - select_trigger_node.border = UiRect::all(Val::Px(select_button_size_style.border_width)); - - // border radius + node.border = UiRect::all(Val::Px(select_button_size_style.border_width)); border_radius.top_left = Val::Px(select_button_size_style.border_radius); border_radius.top_right = Val::Px(select_button_size_style.border_radius); border_radius.bottom_left = Val::Px(select_button_size_style.border_radius); @@ -191,7 +224,7 @@ pub fn update_select_visuals( if is_disabled { *bg_color = BackgroundColor(select_button_styles.disabled_background); *border_color = BorderColor(select_button_styles.disabled_border_color); - } else if *has_popup || *is_hovering { + } else if *is_open || *is_hovering { *bg_color = BackgroundColor(select_button_styles.button_background); *border_color = BorderColor(select_button_styles.active_border_color); } else { @@ -200,27 +233,38 @@ pub fn update_select_visuals( } } - // Query 1: Items - for (hovering, mut bg_color, option_state, is_disabled, item, Checked(checked)) in - query_set.p1().iter_mut() + // === Part 3: Update CoreSelectItem visuals, including hovered state === + for ( + _item_entity, + hovering, + mut bg_color, + is_disabled, + item, + IsSelected(is_checked), + child_of, + ) in query_set.p1().iter_mut() { + // Optionally, you may remove any active root check so that every item updates. + // if !active_roots.contains(&child_of.parent()) { + // continue; + // } + if item.disabled || is_disabled { *bg_color = BackgroundColor(select_button_styles.disabled_background); } else if hovering.0 { *bg_color = BackgroundColor(select_button_styles.hovered_item_background); - } else if item.selected || option_state.is_selected || *checked { + } else if item.selected || *is_checked { *bg_color = BackgroundColor(select_button_styles.active_item_background); } else { *bg_color = BackgroundColor(select_button_styles.popover_background); } } - // Update the CoreSelectContent background and border color - for (mut core_select_content_node, mut border_color, mut border_radius, select) in - core_select_content_query.iter_mut() + // === Part 4: Update CoreSelectContent visuals === + for (entity, mut node, mut bg_color, mut border_color, mut border_radius, select) in + query_set.p2().iter_mut() { let select_button_size_styles = theme_manager.styles.select_sizes.clone(); - let select_button_size_style = match select.size.unwrap_or_default() { SelectButtonSize::XSmall => select_button_size_styles.xsmall, SelectButtonSize::Small => select_button_size_styles.small, @@ -228,11 +272,13 @@ pub fn update_select_visuals( SelectButtonSize::Large => select_button_size_styles.large, SelectButtonSize::XLarge => select_button_size_styles.xlarge, }; - core_select_content_node.border = - UiRect::all(Val::Px(select_button_size_style.border_width)); + + // Update the background based on a new field (for example, content_background) + *bg_color = BackgroundColor(select_button_styles.popover_background); + + node.border = UiRect::all(Val::Px(select_button_size_style.border_width)); *border_color = BorderColor(select_button_styles.popover_border_color); - // border radius border_radius.top_left = Val::Px(select_button_size_style.border_radius); border_radius.top_right = Val::Px(select_button_size_style.border_radius); border_radius.bottom_left = Val::Px(select_button_size_style.border_radius); diff --git a/examples/select.rs b/examples/select.rs index d191437..5806db2 100644 --- a/examples/select.rs +++ b/examples/select.rs @@ -1,10 +1,11 @@ use bevy::{input_focus::tab_navigation::TabGroup, prelude::*, winit::WinitSettings}; +use bevy_additional_core_widgets::IsSelected; use bevy_core_widgets::Checked; use bevy_styled_widgets::prelude::*; fn main() { App::new() - .add_plugins((DefaultPlugins, StyledWidgetsPligin)) + .add_plugins((DefaultPlugins, StyledWidgetsPlugin)) .insert_resource(ThemeManager::default()) .insert_resource(WinitSettings::desktop_app()) .add_systems(Startup, setup_view_root) @@ -44,7 +45,7 @@ fn set_theme(id: ThemeId) -> impl FnMut(ResMut) + Clone { } } -fn run_on_select_item_selected( +fn run_on_select_changed( In(selected_entity): In, q_select_content: Query<&Children>, select_query: Query<(&ChildOf, &SelectedValue)>, @@ -77,24 +78,43 @@ fn setup_view_root(mut commands: Commands) { let on_yellow_theme = commands.register_system(set_theme(ThemeId("yellow".into()))); let on_violet_theme = commands.register_system(set_theme(ThemeId("violet".into()))); + let select_on_change_system_id = commands.register_system(run_on_select_changed); + + // TODO: try with/without key/value + let options_l = vec![ + StyledSelectItem::builder() + .label("Option 1".to_string()) + .value("Option 1".to_string()), + StyledSelectItem::builder() + .label("Option 2".to_string()) + .value("Option 2".to_string()), + StyledSelectItem::builder() + .label("Option 3".to_string()) + .value("Option 3".to_string()), + ]; + let options = vec![ StyledSelectItem::builder() - .key("Juice".to_string()) + .label("Juice".to_string()) .value("Juice".to_string()), StyledSelectItem::builder() - .key("Tea".to_string()) + .label("Tea".to_string()) .value("Tea".to_string()), StyledSelectItem::builder() - .key("Coffee".to_string()) + .label("Coffee".to_string()) .value("Coffee".to_string()), ]; - // default medium size let (parent_bundle, select_trigger_bundle, select_content_bundle, child_bundles) = StyledSelect::builder() .children(options.clone()) - .size(SelectButtonSize::Large) - // .on_change(commands.register_system(run_on_select_item_selected)) + // .on_change(select_on_change_system_id) + .build(); + + let (parent_bundle_l, select_trigger_bundle_l, select_content_bundle_l, child_bundles_l) = + StyledSelect::builder() + .children(options_l.clone()) + .size(SelectButtonSize::XLarge) .build(); commands @@ -234,5 +254,38 @@ fn setup_view_root(mut commands: Commands) { } }); }); + + parent.spawn( + StyledText::builder() + .content("Sizes") + .font_size(24.0) + .build(), + ); + + parent + .spawn((Node { + display: Display::Flex, + flex_direction: FlexDirection::Row, + align_items: AlignItems::Start, + align_content: AlignContent::Start, + padding: UiRect::axes(Val::Px(12.0), Val::Px(0.0)), + width: Val::Px(45.), + height: Val::Px(60.), + ..default() + },)) + .insert(parent_bundle_l) + .insert(select_trigger_bundle_l) + .with_children(|parent| { + parent + .spawn(select_content_bundle_l) + .with_children(|content| { + for child in child_bundles_l { + content.spawn(child); + } + }); + }); }); + + // understand spawning diff + // start from query } From 3786cadc1535c9dd911ae3a4ce73976ce7b48858 Mon Sep 17 00:00:00 2001 From: swetar-mecha Date: Mon, 26 May 2025 16:29:03 +0530 Subject: [PATCH 08/10] fix: select visuals system queries --- .../src/core_select.rs | 114 +++++++++--------- .../bevy_styled_widgets/src/themes/select.rs | 17 ++- .../bevy_styled_widgets/src/themes/styles.rs | 6 +- .../src/ui/select/builder.rs | 41 ++----- .../src/ui/select/systems.rs | 63 +++++----- examples/select.rs | 14 +-- 6 files changed, 111 insertions(+), 144 deletions(-) diff --git a/crates/bevy_additional_core_widgets/src/core_select.rs b/crates/bevy_additional_core_widgets/src/core_select.rs index 498c5c3..91f282a 100644 --- a/crates/bevy_additional_core_widgets/src/core_select.rs +++ b/crates/bevy_additional_core_widgets/src/core_select.rs @@ -25,17 +25,12 @@ pub fn select_on_pointer_click( mut commands: Commands, ) { if let Ok((select_trigger, DropdownOpen(clicked), disabled)) = q_state.get(trigger.target()) { - let select_id = trigger.target(); - focus.0 = Some(select_id); - focus_visible.0 = false; - trigger.propagate(false); - - if disabled { - // If the select is disabled, do nothing - return; - } - if !disabled { + let select_id = trigger.target(); + focus.0 = Some(select_id); + focus_visible.0 = false; + trigger.propagate(false); + let is_open = clicked; let new_clicked = !is_open; @@ -65,51 +60,56 @@ fn select_content_on_button_click( let select_id = trigger.target(); // Find the select item button that was clicked. - let Ok((_, child_of, _)) = q_select_item.get(select_id) else { + let Ok((_, child_of, disabled)) = q_select_item.get(select_id) else { return; }; - // Find the parent CoreSelectContent of the clicked select item button. - let group_id = child_of.parent(); - let Ok((CoreSelectContent { on_change }, group_children)) = q_group.get(group_id) else { - warn!("select item button clicked without a valid CoreSelectContent parent"); - return; - }; + if !disabled { + // If the select item button is not disabled, propagate the click event. + trigger.propagate(false); - // Set focus to group and hide focus ring - focus.0 = Some(group_id); - focus_visible.0 = false; - - // Get all the select root children. - let select_children = group_children - .iter() - .filter_map(|child_id| match q_select_item.get(child_id) { - Ok((is_selected, _, false)) => Some((child_id, is_selected.0)), - Ok((_, _, true)) => None, - Err(_) => None, - }) - .collect::>(); - - if select_children.is_empty() { - return; // No enabled select item buttons in the group - } + // Find the parent CoreSelectContent of the clicked select item button. + let group_id = child_of.parent(); + let Ok((CoreSelectContent { on_change }, group_children)) = q_group.get(group_id) else { + warn!("select item button clicked without a valid CoreSelectContent parent"); + return; + }; - trigger.propagate(false); - let current_select_item = select_children - .iter() - .find(|(_, is_selected)| *is_selected) - .map(|(id, _)| *id); + // Set focus to group and hide focus ring + focus.0 = Some(group_id); + focus_visible.0 = false; - if current_select_item == Some(select_id) { - // If they clicked the currently selected item, do nothing - return; - } + // Get all the select root children. + let select_children = group_children + .iter() + .filter_map(|child_id| match q_select_item.get(child_id) { + Ok((is_selected, _, false)) => Some((child_id, is_selected.0)), + Ok((_, _, true)) => None, + Err(_) => None, + }) + .collect::>(); + + if select_children.is_empty() { + return; // No enabled select item buttons in the group + } + + trigger.propagate(false); + let current_select_item = select_children + .iter() + .find(|(_, is_selected)| *is_selected) + .map(|(id, _)| *id); + + if current_select_item == Some(select_id) { + // If they clicked the currently selected item, do nothing + return; + } - // Trigger the on_change event for the newly selected item - if let Some(on_change) = on_change { - commands.run_system_with(*on_change, select_id); - } else { - commands.trigger_targets(ValueChange(select_id), group_id); + // Trigger the on_change event for the newly selected item + if let Some(on_change) = on_change { + commands.run_system_with(*on_change, select_id); + } else { + commands.trigger_targets(ValueChange(select_id), group_id); + } } } @@ -219,14 +219,18 @@ fn select_item_on_pointer_click( mut commands: Commands, ) { if let Ok((is_selected, disabled)) = q_state.get(trigger.target()) { - let checkbox_id = trigger.target(); - focus.0 = Some(checkbox_id); - focus_visible.0 = false; - trigger.propagate(false); - if is_selected.0 || disabled { - return; + if !disabled { + // If the select item button is not disabled, propagate the click event. + trigger.propagate(false); + let checkbox_id = trigger.target(); + focus.0 = Some(checkbox_id); + focus_visible.0 = false; + trigger.propagate(false); + if is_selected.0 || disabled { + return; + } + commands.trigger_targets(ButtonClicked, trigger.target()); } - commands.trigger_targets(ButtonClicked, trigger.target()); } } diff --git a/crates/bevy_styled_widgets/src/themes/select.rs b/crates/bevy_styled_widgets/src/themes/select.rs index 9281c5d..2eac500 100644 --- a/crates/bevy_styled_widgets/src/themes/select.rs +++ b/crates/bevy_styled_widgets/src/themes/select.rs @@ -3,12 +3,11 @@ use bevy::prelude::*; use super::ThemeColors; #[derive(Debug, Clone)] -pub struct SelectButtonStyles { - // Colors +pub struct SelectStyles { // trigger - pub button_background: Color, - pub button_text_color: Color, - pub button_border_color: Color, + pub background: Color, + pub text_color: Color, + pub border_color: Color, // items pub popover_background: Color, @@ -24,12 +23,12 @@ pub struct SelectButtonStyles { pub disabled_border_color: Color, } -impl SelectButtonStyles { +impl SelectStyles { pub fn from_colors(colors: ThemeColors) -> Self { Self { - button_background: colors.primary, - button_text_color: colors.primary_foreground, - button_border_color: colors.border, + background: colors.primary, + text_color: colors.primary_foreground, + border_color: colors.border, popover_background: colors.primary, popover_border_color: colors.border, diff --git a/crates/bevy_styled_widgets/src/themes/styles.rs b/crates/bevy_styled_widgets/src/themes/styles.rs index be79899..23b676b 100644 --- a/crates/bevy_styled_widgets/src/themes/styles.rs +++ b/crates/bevy_styled_widgets/src/themes/styles.rs @@ -10,7 +10,7 @@ use super::{ panel::PanelStyle, progress::ProgressStyle, radio::{RadioButtonSizeStyles, RadioButtonVariantStyles, radio_button_sizes}, - select::{SelectButtonSizeStyles, SelectButtonStyles, select_button_sizes}, + select::{SelectButtonSizeStyles, SelectStyles, select_button_sizes}, slider::SliderStyle, switch::{SwitchSizeStyles, SwitchVariantStyles, switch_sizes}, text::TextStyle, @@ -36,7 +36,7 @@ pub struct ThemeStyles { pub radio_buttons: RadioButtonVariantStyles, pub radio_button_sizes: RadioButtonSizeStyles, pub select_sizes: SelectButtonSizeStyles, - pub select_button_styles: SelectButtonStyles, + pub select_styles: SelectStyles, } impl ThemeStyles { @@ -59,7 +59,7 @@ impl ThemeStyles { radio_buttons: RadioButtonVariantStyles::from_colors(configs.colors.clone()), radio_button_sizes: radio_button_sizes(), select_sizes: select_button_sizes(), - select_button_styles: SelectButtonStyles::from_colors(configs.colors.clone()), + select_styles: SelectStyles::from_colors(configs.colors.clone()), } } } diff --git a/crates/bevy_styled_widgets/src/ui/select/builder.rs b/crates/bevy_styled_widgets/src/ui/select/builder.rs index e5ffea2..e4f2a67 100644 --- a/crates/bevy_styled_widgets/src/ui/select/builder.rs +++ b/crates/bevy_styled_widgets/src/ui/select/builder.rs @@ -18,25 +18,11 @@ use accesskit::{Node as Accessible, Role}; #[derive(Component, Default)] pub struct SelectWidget; // marker -#[derive(Component, Default)] -pub struct SelectTrigger; // marker - -#[derive(Component)] -pub struct DropdownContainer; // marker - -// marker #[derive(Component)] -pub struct DropdownOption { - pub value: String, - pub disabled: bool, -} +pub struct StyledSelectText; // marker -// marker #[derive(Component)] -pub struct SelectState { - pub selected: Option, - pub is_open: bool, -} +pub struct StyledSelectOptionText; // marker #[derive(Default, Clone)] pub struct SelectBuilder { @@ -82,7 +68,7 @@ impl SelectBuilder { pub fn build(self) -> (impl Bundle, impl Bundle, impl Bundle, Vec) { let theme_manager = ThemeManager::default(); let select_button_size_styles = theme_manager.styles.select_sizes.clone(); - let select_button_styles = theme_manager.styles.select_button_styles.clone(); + let select_button_styles = theme_manager.styles.select_styles.clone(); // Update size styles let select_button_size_style = match self.size.unwrap_or_default() { @@ -99,7 +85,6 @@ impl SelectBuilder { let font_size = select_button_size_style.font_size; // Root: SelectWidget - let root = ( Node { display: Display::Flex, @@ -109,10 +94,6 @@ impl SelectBuilder { ..default() }, SelectWidget, - SelectState { - selected: self.selected_value.clone(), - is_open: false, - }, AccessibilityNode(Accessible::new(Role::ComboBox)), DropdownOpen(false), AccessibleName(self.selected_value.clone().unwrap_or("Select".to_string())), @@ -137,7 +118,7 @@ impl SelectBuilder { on_change: self.on_change, size: self.size, }, - BackgroundColor(select_button_styles.button_background.into()), + BackgroundColor(select_button_styles.background.into()), Name::new(self.selected_value.clone().unwrap_or("Select".to_string())), // Name::new("Select"), Hovering::default(), CursorIcon::System(SystemCursorIcon::Pointer), @@ -149,7 +130,6 @@ impl SelectBuilder { bottom_left: Val::Px(select_button_size_style.border_radius), bottom_right: Val::Px(select_button_size_style.border_radius), }, - SelectTrigger, SelectedValue(self.selected_value.clone().unwrap_or("Select".to_string())), CoreSelectTrigger { on_click: self.on_click, @@ -161,6 +141,7 @@ impl SelectBuilder { font_size: font_size, ..Default::default() }, + StyledSelectText, ))), )),)); @@ -181,7 +162,6 @@ impl SelectBuilder { bottom_left: Val::Px(select_button_size_style.border_radius), bottom_right: Val::Px(select_button_size_style.border_radius), }, - DropdownContainer, // marker CoreSelectContent { on_change: self.on_change, }, @@ -198,9 +178,6 @@ impl SelectBuilder { } } -#[derive(Component, Default)] -pub struct SelectContent; - #[derive(Component, Debug, Clone)] pub struct SelectedValue(pub String); @@ -272,13 +249,14 @@ impl SelectItemBuilder { height: Val::Px(height), ..default() }, - Name::new(self.label.clone().unwrap_or("".to_string())), + Name::new(self.label.clone().unwrap_or(self.value.to_string())), Children::spawn(Spawn(( Text::new(self.value.clone()), TextFont { font_size: font_size, ..Default::default() }, + StyledSelectOptionText, ))), )), // @@ -296,7 +274,6 @@ impl SelectItemBuilder { ..default() }, GlobalZIndex(99), // to ensure it appears above other UI elements - SelectContent, CoreSelectItem, Name::new("Select Item"), Hovering::default(), @@ -309,10 +286,6 @@ impl SelectItemBuilder { value: self.value.clone(), }, SelectedValue(self.value.clone()), - DropdownOption { - value: self.value.clone(), - disabled: self.disabled, - }, SelectedItem { label: self.label.clone().unwrap_or(self.value.clone()), value: self.value.clone(), diff --git a/crates/bevy_styled_widgets/src/ui/select/systems.rs b/crates/bevy_styled_widgets/src/ui/select/systems.rs index b3a5636..24b28f8 100644 --- a/crates/bevy_styled_widgets/src/ui/select/systems.rs +++ b/crates/bevy_styled_widgets/src/ui/select/systems.rs @@ -1,5 +1,3 @@ -use std::process::Child; - use bevy::prelude::*; use bevy_additional_core_widgets::{ CoreSelectContent, CoreSelectItem, CoreSelectTrigger, DropdownOpen, IsSelected, @@ -9,14 +7,13 @@ use bevy_core_widgets::{InteractionDisabled, ValueChange, hover::Hovering}; use crate::themes::ThemeManager; use super::{ - DropdownContainer, SelectButtonSize, SelectContent, SelectState, SelectTrigger, SelectWidget, - StyledSelect, StyledSelectItem, builder::SelectedValue, + SelectButtonSize, SelectWidget, StyledSelect, StyledSelectItem, builder::SelectedValue, }; pub fn on_select_triggered( mut trigger: Trigger>, mut commands: Commands, - all_triggers: Query<(Entity, &StyledSelect), With>, + all_triggers: Query<(Entity, &StyledSelect), With>, ) { trigger.propagate(false); @@ -24,7 +21,7 @@ pub fn on_select_triggered( let clicked_entity = trigger.target(); - // Close all other dropdowns and update the clicked one + // update the clicked one for (entity, styled_select) in &all_triggers { if styled_select.disabled { commands.entity(entity).insert(InteractionDisabled); @@ -45,7 +42,7 @@ pub fn on_select_triggered( pub fn open_select_content( query_open_widgets: Query<(Entity, &DropdownOpen), Changed>, root_query: Query<(&Children, Entity), With>, - mut dropdown_query: Query<(Entity, &mut Node), With>, + mut dropdown_query: Query<(Entity, &mut Node), With>, ) { for (widget_entity, DropdownOpen(is_open)) in &query_open_widgets { // Find the root SelectWidget this DropdownOpen belongs to @@ -70,8 +67,9 @@ pub fn on_select_item_selection( mut trigger: Trigger>, q_select_widget: Query<(&Children, Entity), With>, q_select_content: Query<&Children, With>, - q_select_item: Query<(&ChildOf, &SelectedValue), With>, + q_select_item: Query<(&ChildOf, &SelectedValue, &StyledSelectItem), With>, q_select_trigger: Query<&Children, With>, + mut q_text: Query<&mut Text>, mut q_name: Query<&mut Name>, mut commands: Commands, @@ -89,7 +87,7 @@ pub fn on_select_item_selection( let selected_entity = trigger.event().0; // Get the selected item's value and its parent (CoreSelectContent) - let (child_of, selected_value) = match q_select_item.get(selected_entity) { + let (child_of, selected_value, styled_select_item) = match q_select_item.get(selected_entity) { Ok(res) => res, Err(_) => return, }; @@ -108,12 +106,14 @@ pub fn on_select_item_selection( None => return, }; - // 2. Deselect all other CoreSelectItems in the same content group + // 2. Deselect all other CoreSelectItems in the same content group & set selected for child in group_children.iter() { - if let Ok((_, value)) = q_select_item.get(child) { - commands - .entity(child) - .insert(IsSelected(value.0 == selected_value.0)); + if let Ok((_, value, styled_select_item)) = q_select_item.get(child) { + if !styled_select_item.disabled { + commands + .entity(child) + .insert(IsSelected(value.0 == selected_value.0)); + } } } @@ -178,7 +178,7 @@ pub fn update_select_visuals( >, )>, ) { - let select_button_styles = theme_manager.styles.select_button_styles.clone(); + let select_styles = theme_manager.styles.select_styles.clone(); // Store active roots from the triggers. let mut active_roots = Vec::new(); @@ -222,14 +222,14 @@ pub fn update_select_visuals( border_radius.bottom_right = Val::Px(select_button_size_style.border_radius); if is_disabled { - *bg_color = BackgroundColor(select_button_styles.disabled_background); - *border_color = BorderColor(select_button_styles.disabled_border_color); + *bg_color = BackgroundColor(select_styles.disabled_background); + *border_color = BorderColor(select_styles.disabled_border_color); } else if *is_open || *is_hovering { - *bg_color = BackgroundColor(select_button_styles.button_background); - *border_color = BorderColor(select_button_styles.active_border_color); + *bg_color = BackgroundColor(select_styles.background); + *border_color = BorderColor(select_styles.active_border_color); } else { - *bg_color = BackgroundColor(select_button_styles.button_background); - *border_color = BorderColor(select_button_styles.active_border_color); + *bg_color = BackgroundColor(select_styles.background); + *border_color = BorderColor(select_styles.active_border_color); } } @@ -240,23 +240,18 @@ pub fn update_select_visuals( mut bg_color, is_disabled, item, - IsSelected(is_checked), + IsSelected(is_selected), child_of, ) in query_set.p1().iter_mut() { - // Optionally, you may remove any active root check so that every item updates. - // if !active_roots.contains(&child_of.parent()) { - // continue; - // } - if item.disabled || is_disabled { - *bg_color = BackgroundColor(select_button_styles.disabled_background); + *bg_color = BackgroundColor(select_styles.disabled_background); } else if hovering.0 { - *bg_color = BackgroundColor(select_button_styles.hovered_item_background); - } else if item.selected || *is_checked { - *bg_color = BackgroundColor(select_button_styles.active_item_background); + *bg_color = BackgroundColor(select_styles.hovered_item_background); + } else if *is_selected { + *bg_color = BackgroundColor(select_styles.active_item_background); } else { - *bg_color = BackgroundColor(select_button_styles.popover_background); + *bg_color = BackgroundColor(select_styles.popover_background); } } @@ -274,10 +269,10 @@ pub fn update_select_visuals( }; // Update the background based on a new field (for example, content_background) - *bg_color = BackgroundColor(select_button_styles.popover_background); + *bg_color = BackgroundColor(select_styles.popover_background); node.border = UiRect::all(Val::Px(select_button_size_style.border_width)); - *border_color = BorderColor(select_button_styles.popover_border_color); + *border_color = BorderColor(select_styles.popover_border_color); border_radius.top_left = Val::Px(select_button_size_style.border_radius); border_radius.top_right = Val::Px(select_button_size_style.border_radius); diff --git a/examples/select.rs b/examples/select.rs index 5806db2..322674a 100644 --- a/examples/select.rs +++ b/examples/select.rs @@ -1,6 +1,5 @@ use bevy::{input_focus::tab_navigation::TabGroup, prelude::*, winit::WinitSettings}; use bevy_additional_core_widgets::IsSelected; -use bevy_core_widgets::Checked; use bevy_styled_widgets::prelude::*; fn main() { @@ -57,7 +56,7 @@ fn run_on_select_changed( if let Ok((_, value)) = select_query.get(select_item_child) { commands .entity(select_item_child) - .insert(Checked(value.0 == selected_value.0)); + .insert(IsSelected(value.0 == selected_value.0)); } } } @@ -83,13 +82,13 @@ fn setup_view_root(mut commands: Commands) { // TODO: try with/without key/value let options_l = vec![ StyledSelectItem::builder() - .label("Option 1".to_string()) + // .label("Option 1".to_string()) .value("Option 1".to_string()), StyledSelectItem::builder() - .label("Option 2".to_string()) + // .label("Option 2".to_string()) .value("Option 2".to_string()), StyledSelectItem::builder() - .label("Option 3".to_string()) + // .label("Option 3".to_string()) .value("Option 3".to_string()), ]; @@ -114,7 +113,7 @@ fn setup_view_root(mut commands: Commands) { let (parent_bundle_l, select_trigger_bundle_l, select_content_bundle_l, child_bundles_l) = StyledSelect::builder() .children(options_l.clone()) - .size(SelectButtonSize::XLarge) + .size(SelectButtonSize::Large) .build(); commands @@ -285,7 +284,4 @@ fn setup_view_root(mut commands: Commands) { }); }); }); - - // understand spawning diff - // start from query } From ef2e94d937ba95142ffd877f8bddae9bcfdf2717 Mon Sep 17 00:00:00 2001 From: swetar-mecha Date: Mon, 26 May 2025 16:45:52 +0530 Subject: [PATCH 09/10] chore: rename variables for select builder and system --- .../src/ui/select/builder.rs | 60 +++++++++---------- .../src/ui/select/systems.rs | 52 ++++++++-------- examples/select.rs | 13 +--- 3 files changed, 59 insertions(+), 66 deletions(-) diff --git a/crates/bevy_styled_widgets/src/ui/select/builder.rs b/crates/bevy_styled_widgets/src/ui/select/builder.rs index e4f2a67..f9a248b 100644 --- a/crates/bevy_styled_widgets/src/ui/select/builder.rs +++ b/crates/bevy_styled_widgets/src/ui/select/builder.rs @@ -67,22 +67,22 @@ impl SelectBuilder { pub fn build(self) -> (impl Bundle, impl Bundle, impl Bundle, Vec) { let theme_manager = ThemeManager::default(); - let select_button_size_styles = theme_manager.styles.select_sizes.clone(); - let select_button_styles = theme_manager.styles.select_styles.clone(); + let select_size_styles = theme_manager.styles.select_sizes.clone(); + let select_styles = theme_manager.styles.select_styles.clone(); // Update size styles - let select_button_size_style = match self.size.unwrap_or_default() { - SelectButtonSize::XSmall => select_button_size_styles.xsmall, - SelectButtonSize::Small => select_button_size_styles.small, - SelectButtonSize::Medium => select_button_size_styles.medium, - SelectButtonSize::Large => select_button_size_styles.large, - SelectButtonSize::XLarge => select_button_size_styles.xlarge, + let select_size_style = match self.size.unwrap_or_default() { + SelectButtonSize::XSmall => select_size_styles.xsmall, + SelectButtonSize::Small => select_size_styles.small, + SelectButtonSize::Medium => select_size_styles.medium, + SelectButtonSize::Large => select_size_styles.large, + SelectButtonSize::XLarge => select_size_styles.xlarge, }; - let button_width = select_button_size_style.min_width; - let button_height = select_button_size_style.min_height; + let button_width = select_size_style.min_width; + let button_height = select_size_style.min_height; - let font_size = select_button_size_style.font_size; + let font_size = select_size_style.font_size; // Root: SelectWidget let root = ( @@ -118,17 +118,17 @@ impl SelectBuilder { on_change: self.on_change, size: self.size, }, - BackgroundColor(select_button_styles.background.into()), + BackgroundColor(select_styles.background.into()), Name::new(self.selected_value.clone().unwrap_or("Select".to_string())), // Name::new("Select"), Hovering::default(), CursorIcon::System(SystemCursorIcon::Pointer), AccessibilityNode(Accessible::new(Role::Button)), TabIndex(0), BorderRadius { - top_left: Val::Px(select_button_size_style.border_radius), - top_right: Val::Px(select_button_size_style.border_radius), - bottom_left: Val::Px(select_button_size_style.border_radius), - bottom_right: Val::Px(select_button_size_style.border_radius), + top_left: Val::Px(select_size_style.border_radius), + top_right: Val::Px(select_size_style.border_radius), + bottom_left: Val::Px(select_size_style.border_radius), + bottom_right: Val::Px(select_size_style.border_radius), }, SelectedValue(self.selected_value.clone().unwrap_or("Select".to_string())), CoreSelectTrigger { @@ -155,12 +155,12 @@ impl SelectBuilder { height: Val::Auto, ..default() }, - BackgroundColor(select_button_styles.popover_background.into()), + BackgroundColor(select_styles.popover_background.into()), BorderRadius { - top_left: Val::Px(select_button_size_style.border_radius), - top_right: Val::Px(select_button_size_style.border_radius), - bottom_left: Val::Px(select_button_size_style.border_radius), - bottom_right: Val::Px(select_button_size_style.border_radius), + top_left: Val::Px(select_size_style.border_radius), + top_right: Val::Px(select_size_style.border_radius), + bottom_left: Val::Px(select_size_style.border_radius), + bottom_right: Val::Px(select_size_style.border_radius), }, CoreSelectContent { on_change: self.on_change, @@ -224,17 +224,17 @@ impl SelectItemBuilder { pub fn build(self) -> impl Bundle { let theme_manager = ThemeManager::default(); - let select_button_size_styles = theme_manager.styles.select_sizes.clone(); + let select_size_styles = theme_manager.styles.select_sizes.clone(); // Update size styles - let select_button_size_style = match self.size.unwrap_or_default() { - SelectButtonSize::XSmall => select_button_size_styles.xsmall, - SelectButtonSize::Small => select_button_size_styles.small, - SelectButtonSize::Medium => select_button_size_styles.medium, - SelectButtonSize::Large => select_button_size_styles.large, - SelectButtonSize::XLarge => select_button_size_styles.xlarge, + let select_size_style = match self.size.unwrap_or_default() { + SelectButtonSize::XSmall => select_size_styles.xsmall, + SelectButtonSize::Small => select_size_styles.small, + SelectButtonSize::Medium => select_size_styles.medium, + SelectButtonSize::Large => select_size_styles.large, + SelectButtonSize::XLarge => select_size_styles.xlarge, }; - let height = select_button_size_style.min_height; - let font_size = select_button_size_style.font_size; + let height = select_size_style.min_height; + let font_size = select_size_style.font_size; // select content- dropdown let child_nodes = Children::spawn(( diff --git a/crates/bevy_styled_widgets/src/ui/select/systems.rs b/crates/bevy_styled_widgets/src/ui/select/systems.rs index 24b28f8..d8bd84b 100644 --- a/crates/bevy_styled_widgets/src/ui/select/systems.rs +++ b/crates/bevy_styled_widgets/src/ui/select/systems.rs @@ -46,11 +46,11 @@ pub fn open_select_content( ) { for (widget_entity, DropdownOpen(is_open)) in &query_open_widgets { // Find the root SelectWidget this DropdownOpen belongs to - for (children, root_entity) in &root_query { + for (children, _) in &root_query { if children.contains(&widget_entity) { // Update its dropdown container for &child in children { - if let Ok((dropdown_entity, mut node)) = dropdown_query.get_mut(child) { + if let Ok((_, mut node)) = dropdown_query.get_mut(child) { node.display = if *is_open { Display::Flex } else { @@ -206,20 +206,20 @@ pub fn update_select_visuals( if !active_roots.contains(&trigger_entity) { continue; } - let select_button_size_styles = theme_manager.styles.select_sizes.clone(); - let select_button_size_style = match select_button.size.unwrap_or_default() { - SelectButtonSize::XSmall => select_button_size_styles.xsmall, - SelectButtonSize::Small => select_button_size_styles.small, - SelectButtonSize::Medium => select_button_size_styles.medium, - SelectButtonSize::Large => select_button_size_styles.large, - SelectButtonSize::XLarge => select_button_size_styles.xlarge, + let select_size_styles = theme_manager.styles.select_sizes.clone(); + let select_size_style = match select_button.size.unwrap_or_default() { + SelectButtonSize::XSmall => select_size_styles.xsmall, + SelectButtonSize::Small => select_size_styles.small, + SelectButtonSize::Medium => select_size_styles.medium, + SelectButtonSize::Large => select_size_styles.large, + SelectButtonSize::XLarge => select_size_styles.xlarge, }; - node.border = UiRect::all(Val::Px(select_button_size_style.border_width)); - border_radius.top_left = Val::Px(select_button_size_style.border_radius); - border_radius.top_right = Val::Px(select_button_size_style.border_radius); - border_radius.bottom_left = Val::Px(select_button_size_style.border_radius); - border_radius.bottom_right = Val::Px(select_button_size_style.border_radius); + node.border = UiRect::all(Val::Px(select_size_style.border_width)); + border_radius.top_left = Val::Px(select_size_style.border_radius); + border_radius.top_right = Val::Px(select_size_style.border_radius); + border_radius.bottom_left = Val::Px(select_size_style.border_radius); + border_radius.bottom_right = Val::Px(select_size_style.border_radius); if is_disabled { *bg_color = BackgroundColor(select_styles.disabled_background); @@ -259,24 +259,24 @@ pub fn update_select_visuals( for (entity, mut node, mut bg_color, mut border_color, mut border_radius, select) in query_set.p2().iter_mut() { - let select_button_size_styles = theme_manager.styles.select_sizes.clone(); - let select_button_size_style = match select.size.unwrap_or_default() { - SelectButtonSize::XSmall => select_button_size_styles.xsmall, - SelectButtonSize::Small => select_button_size_styles.small, - SelectButtonSize::Medium => select_button_size_styles.medium, - SelectButtonSize::Large => select_button_size_styles.large, - SelectButtonSize::XLarge => select_button_size_styles.xlarge, + let select_size_styles = theme_manager.styles.select_sizes.clone(); + let select_size_style = match select.size.unwrap_or_default() { + SelectButtonSize::XSmall => select_size_styles.xsmall, + SelectButtonSize::Small => select_size_styles.small, + SelectButtonSize::Medium => select_size_styles.medium, + SelectButtonSize::Large => select_size_styles.large, + SelectButtonSize::XLarge => select_size_styles.xlarge, }; // Update the background based on a new field (for example, content_background) *bg_color = BackgroundColor(select_styles.popover_background); - node.border = UiRect::all(Val::Px(select_button_size_style.border_width)); + node.border = UiRect::all(Val::Px(select_size_style.border_width)); *border_color = BorderColor(select_styles.popover_border_color); - border_radius.top_left = Val::Px(select_button_size_style.border_radius); - border_radius.top_right = Val::Px(select_button_size_style.border_radius); - border_radius.bottom_left = Val::Px(select_button_size_style.border_radius); - border_radius.bottom_right = Val::Px(select_button_size_style.border_radius); + border_radius.top_left = Val::Px(select_size_style.border_radius); + border_radius.top_right = Val::Px(select_size_style.border_radius); + border_radius.bottom_left = Val::Px(select_size_style.border_radius); + border_radius.bottom_right = Val::Px(select_size_style.border_radius); } } diff --git a/examples/select.rs b/examples/select.rs index 322674a..40779a2 100644 --- a/examples/select.rs +++ b/examples/select.rs @@ -79,17 +79,10 @@ fn setup_view_root(mut commands: Commands) { let select_on_change_system_id = commands.register_system(run_on_select_changed); - // TODO: try with/without key/value let options_l = vec![ - StyledSelectItem::builder() - // .label("Option 1".to_string()) - .value("Option 1".to_string()), - StyledSelectItem::builder() - // .label("Option 2".to_string()) - .value("Option 2".to_string()), - StyledSelectItem::builder() - // .label("Option 3".to_string()) - .value("Option 3".to_string()), + StyledSelectItem::builder().value("Option 1".to_string()), + StyledSelectItem::builder().value("Option 2".to_string()), + StyledSelectItem::builder().value("Option 3".to_string()), ]; let options = vec![ From 74cdd2fe5481c2ce016aa3c6f2557ca8bf212386 Mon Sep 17 00:00:00 2001 From: swetar-mecha Date: Mon, 26 May 2025 16:54:44 +0530 Subject: [PATCH 10/10] fix: select example with theme toggle icon --- examples/select.rs | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/examples/select.rs b/examples/select.rs index 40779a2..b4eefe4 100644 --- a/examples/select.rs +++ b/examples/select.rs @@ -1,13 +1,38 @@ use bevy::{input_focus::tab_navigation::TabGroup, prelude::*, winit::WinitSettings}; use bevy_additional_core_widgets::IsSelected; +use bevy_asset_loader::prelude::*; +use bevy_asset_loader::{ + loading_state::{LoadingState, LoadingStateAppExt, config::ConfigureLoadingState}, + standard_dynamic_asset::StandardDynamicAssetCollection, +}; use bevy_styled_widgets::prelude::*; +#[derive(AssetCollection, Resource)] +#[allow(dead_code)] +pub struct FontAssets { + #[asset(key = "fonts.icons")] + pub font_icons: Handle, +} + +#[derive(Default, Clone, Eq, PartialEq, Debug, Hash, States)] +pub enum AssetsLoadingState { + #[default] + Loading, + Loaded, +} fn main() { App::new() .add_plugins((DefaultPlugins, StyledWidgetsPlugin)) .insert_resource(ThemeManager::default()) .insert_resource(WinitSettings::desktop_app()) - .add_systems(Startup, setup_view_root) + .init_state::() + .add_loading_state( + LoadingState::new(AssetsLoadingState::Loading) + .continue_to_state(AssetsLoadingState::Loaded) + .with_dynamic_assets_file::("examples/settings.ron") + .load_collection::(), + ) + .add_systems(OnEnter(AssetsLoadingState::Loaded), setup_view_root) .add_systems(Update, update_root_background) .run(); } @@ -62,8 +87,9 @@ fn run_on_select_changed( } } -fn setup_view_root(mut commands: Commands) { +fn setup_view_root(mut commands: Commands, font_assets: Res) { commands.spawn(Camera2d); + let FontAssets { font_icons, .. } = font_assets.into_inner(); let on_toggle_theme_mode = commands.register_system(toggle_mode); @@ -207,14 +233,15 @@ fn setup_view_root(mut commands: Commands) { padding: UiRect::axes(Val::Px(12.0), Val::Px(0.0)), ..default() }, - Children::spawn(Spawn(( + Children::spawn((Spawn(( StyledButton::builder() .icon("theme_mode_toggle") + .font(font_icons.clone()) .on_click(on_toggle_theme_mode) .variant(ButtonVariant::Secondary) .build(), ThemeToggleButton, - ))), + )),)), )); parent.spawn(