Path: blob/main/crates/bevy_feathers/src/controls/toggle_switch.rs
9358 views
use accesskit::Role;1use bevy_a11y::AccessibilityNode;2use bevy_app::{Plugin, PreUpdate};3use bevy_ecs::{4bundle::Bundle,5children,6component::Component,7entity::Entity,8hierarchy::Children,9lifecycle::RemovedComponents,10query::{Added, Changed, Has, Or, With},11reflect::ReflectComponent,12schedule::IntoScheduleConfigs,13system::{Commands, Query},14world::Mut,15};16use bevy_input_focus::tab_navigation::TabIndex;17use bevy_picking::{hover::Hovered, PickingSystems};18use bevy_reflect::{prelude::ReflectDefault, Reflect};19use bevy_ui::{BorderRadius, Checked, InteractionDisabled, Node, PositionType, UiRect, Val};20use bevy_ui_widgets::Checkbox;2122use crate::{23constants::size,24cursor::EntityCursor,25theme::{ThemeBackgroundColor, ThemeBorderColor},26tokens,27};2829/// Marker for the toggle switch outline30#[derive(Component, Default, Clone, Reflect)]31#[reflect(Component, Clone, Default)]32struct ToggleSwitchOutline;3334/// Marker for the toggle switch slide35#[derive(Component, Default, Clone, Reflect)]36#[reflect(Component, Clone, Default)]37struct ToggleSwitchSlide;3839/// Template function to spawn a toggle switch.40///41/// # Arguments42/// * `props` - construction properties for the toggle switch.43/// * `overrides` - a bundle of components that are merged in with the normal toggle switch components.44///45/// # Emitted events46/// * [`bevy_ui_widgets::ValueChange<bool>`] with the new value when the toggle switch changes state.47///48/// These events can be disabled by adding an [`bevy_ui::InteractionDisabled`] component to the bundle49pub fn toggle_switch<B: Bundle>(overrides: B) -> impl Bundle {50(51Node {52width: size::TOGGLE_WIDTH,53height: size::TOGGLE_HEIGHT,54border: UiRect::all(Val::Px(2.0)),55border_radius: BorderRadius::all(Val::Px(5.0)),56..Default::default()57},58Checkbox,59ToggleSwitchOutline,60ThemeBackgroundColor(tokens::SWITCH_BG),61ThemeBorderColor(tokens::SWITCH_BORDER),62AccessibilityNode(accesskit::Node::new(Role::Switch)),63Hovered::default(),64EntityCursor::System(bevy_window::SystemCursorIcon::Pointer),65TabIndex(0),66overrides,67children![(68Node {69position_type: PositionType::Absolute,70left: Val::Percent(0.),71top: Val::Px(0.),72bottom: Val::Px(0.),73width: Val::Percent(50.),74border_radius: BorderRadius::all(Val::Px(3.0)),75..Default::default()76},77ToggleSwitchSlide,78ThemeBackgroundColor(tokens::SWITCH_SLIDE),79)],80)81}8283fn update_switch_styles(84q_switches: Query<85(86Entity,87Has<InteractionDisabled>,88Has<Checked>,89&Hovered,90&ThemeBackgroundColor,91&ThemeBorderColor,92),93(94With<ToggleSwitchOutline>,95Or<(Changed<Hovered>, Added<Checked>, Added<InteractionDisabled>)>,96),97>,98q_children: Query<&Children>,99mut q_slide: Query<(&mut Node, &ThemeBackgroundColor), With<ToggleSwitchSlide>>,100mut commands: Commands,101) {102for (switch_ent, disabled, checked, hovered, outline_bg, outline_border) in q_switches.iter() {103let Some(slide_ent) = q_children104.iter_descendants(switch_ent)105.find(|en| q_slide.contains(*en))106else {107continue;108};109// Safety: since we just checked the query, should always work.110let (ref mut slide_style, slide_color) = q_slide.get_mut(slide_ent).unwrap();111set_switch_styles(112switch_ent,113slide_ent,114disabled,115checked,116hovered.0,117outline_bg,118outline_border,119slide_style,120slide_color,121&mut commands,122);123}124}125126fn update_switch_styles_remove(127q_switches: Query<128(129Entity,130Has<InteractionDisabled>,131Has<Checked>,132&Hovered,133&ThemeBackgroundColor,134&ThemeBorderColor,135),136With<ToggleSwitchOutline>,137>,138q_children: Query<&Children>,139mut q_slide: Query<(&mut Node, &ThemeBackgroundColor), With<ToggleSwitchSlide>>,140mut removed_disabled: RemovedComponents<InteractionDisabled>,141mut removed_checked: RemovedComponents<Checked>,142mut commands: Commands,143) {144removed_disabled145.read()146.chain(removed_checked.read())147.for_each(|ent| {148if let Ok((switch_ent, disabled, checked, hovered, outline_bg, outline_border)) =149q_switches.get(ent)150{151let Some(slide_ent) = q_children152.iter_descendants(switch_ent)153.find(|en| q_slide.contains(*en))154else {155return;156};157// Safety: since we just checked the query, should always work.158let (ref mut slide_style, slide_color) = q_slide.get_mut(slide_ent).unwrap();159set_switch_styles(160switch_ent,161slide_ent,162disabled,163checked,164hovered.0,165outline_bg,166outline_border,167slide_style,168slide_color,169&mut commands,170);171}172});173}174175fn set_switch_styles(176switch_ent: Entity,177slide_ent: Entity,178disabled: bool,179checked: bool,180hovered: bool,181outline_bg: &ThemeBackgroundColor,182outline_border: &ThemeBorderColor,183slide_style: &mut Mut<Node>,184slide_color: &ThemeBackgroundColor,185commands: &mut Commands,186) {187let outline_border_token = match (disabled, hovered) {188(true, _) => tokens::SWITCH_BORDER_DISABLED,189(false, true) => tokens::SWITCH_BORDER_HOVER,190_ => tokens::SWITCH_BORDER,191};192193let outline_bg_token = match (disabled, checked) {194(true, true) => tokens::SWITCH_BG_CHECKED_DISABLED,195(true, false) => tokens::SWITCH_BG_DISABLED,196(false, true) => tokens::SWITCH_BG_CHECKED,197(false, false) => tokens::SWITCH_BG,198};199200let slide_token = match disabled {201true => tokens::SWITCH_SLIDE_DISABLED,202false => tokens::SWITCH_SLIDE,203};204205let slide_pos = match checked {206true => Val::Percent(50.),207false => Val::Percent(0.),208};209210let cursor_shape = match disabled {211true => bevy_window::SystemCursorIcon::NotAllowed,212false => bevy_window::SystemCursorIcon::Pointer,213};214215// Change outline background216if outline_bg.0 != outline_bg_token {217commands218.entity(switch_ent)219.insert(ThemeBackgroundColor(outline_bg_token));220}221222// Change outline border223if outline_border.0 != outline_border_token {224commands225.entity(switch_ent)226.insert(ThemeBorderColor(outline_border_token));227}228229// Change slide color230if slide_color.0 != slide_token {231commands232.entity(slide_ent)233.insert(ThemeBackgroundColor(slide_token));234}235236// Change slide position237if slide_pos != slide_style.left {238slide_style.left = slide_pos;239}240241// Change cursor shape242commands243.entity(switch_ent)244.insert(EntityCursor::System(cursor_shape));245}246247/// Plugin which registers the systems for updating the toggle switch styles.248pub struct ToggleSwitchPlugin;249250impl Plugin for ToggleSwitchPlugin {251fn build(&self, app: &mut bevy_app::App) {252app.add_systems(253PreUpdate,254(update_switch_styles, update_switch_styles_remove).in_set(PickingSystems::Last),255);256}257}258259260