Path: blob/main/crates/bevy_feathers/src/controls/toggle_switch.rs
6596 views
use accesskit::Role;1use bevy_a11y::AccessibilityNode;2use bevy_app::{Plugin, PreUpdate};3use bevy_core_widgets::{Callback, CoreCheckbox, ValueChange};4use bevy_ecs::{5bundle::Bundle,6children,7component::Component,8entity::Entity,9hierarchy::Children,10lifecycle::RemovedComponents,11query::{Added, Changed, Has, Or, With},12reflect::ReflectComponent,13schedule::IntoScheduleConfigs,14spawn::SpawnRelated,15system::{Commands, In, Query},16world::Mut,17};18use bevy_input_focus::tab_navigation::TabIndex;19use bevy_picking::{hover::Hovered, PickingSystems};20use bevy_reflect::{prelude::ReflectDefault, Reflect};21use bevy_ui::{BorderRadius, Checked, InteractionDisabled, Node, PositionType, UiRect, Val};2223use crate::{24constants::size,25cursor::EntityCursor,26theme::{ThemeBackgroundColor, ThemeBorderColor},27tokens,28};2930/// Parameters for the toggle switch template, passed to [`toggle_switch`] function.31#[derive(Default)]32pub struct ToggleSwitchProps {33/// Change handler34pub on_change: Callback<In<ValueChange<bool>>>,35}3637/// Marker for the toggle switch outline38#[derive(Component, Default, Clone, Reflect)]39#[reflect(Component, Clone, Default)]40struct ToggleSwitchOutline;4142/// Marker for the toggle switch slide43#[derive(Component, Default, Clone, Reflect)]44#[reflect(Component, Clone, Default)]45struct ToggleSwitchSlide;4647/// Template function to spawn a toggle switch.48///49/// # Arguments50/// * `props` - construction properties for the toggle switch.51/// * `overrides` - a bundle of components that are merged in with the normal toggle switch components.52pub fn toggle_switch<B: Bundle>(props: ToggleSwitchProps, overrides: B) -> impl Bundle {53(54Node {55width: size::TOGGLE_WIDTH,56height: size::TOGGLE_HEIGHT,57border: UiRect::all(Val::Px(2.0)),58..Default::default()59},60CoreCheckbox {61on_change: props.on_change,62},63ToggleSwitchOutline,64BorderRadius::all(Val::Px(5.0)),65ThemeBackgroundColor(tokens::SWITCH_BG),66ThemeBorderColor(tokens::SWITCH_BORDER),67AccessibilityNode(accesskit::Node::new(Role::Switch)),68Hovered::default(),69EntityCursor::System(bevy_window::SystemCursorIcon::Pointer),70TabIndex(0),71overrides,72children![(73Node {74position_type: PositionType::Absolute,75left: Val::Percent(0.),76top: Val::Px(0.),77bottom: Val::Px(0.),78width: Val::Percent(50.),79..Default::default()80},81BorderRadius::all(Val::Px(3.0)),82ToggleSwitchSlide,83ThemeBackgroundColor(tokens::SWITCH_SLIDE),84)],85)86}8788fn update_switch_styles(89q_switches: Query<90(91Entity,92Has<InteractionDisabled>,93Has<Checked>,94&Hovered,95&ThemeBackgroundColor,96&ThemeBorderColor,97),98(99With<ToggleSwitchOutline>,100Or<(Changed<Hovered>, Added<Checked>, Added<InteractionDisabled>)>,101),102>,103q_children: Query<&Children>,104mut q_slide: Query<(&mut Node, &ThemeBackgroundColor), With<ToggleSwitchSlide>>,105mut commands: Commands,106) {107for (switch_ent, disabled, checked, hovered, outline_bg, outline_border) in q_switches.iter() {108let Some(slide_ent) = q_children109.iter_descendants(switch_ent)110.find(|en| q_slide.contains(*en))111else {112continue;113};114// Safety: since we just checked the query, should always work.115let (ref mut slide_style, slide_color) = q_slide.get_mut(slide_ent).unwrap();116set_switch_styles(117switch_ent,118slide_ent,119disabled,120checked,121hovered.0,122outline_bg,123outline_border,124slide_style,125slide_color,126&mut commands,127);128}129}130131fn update_switch_styles_remove(132q_switches: Query<133(134Entity,135Has<InteractionDisabled>,136Has<Checked>,137&Hovered,138&ThemeBackgroundColor,139&ThemeBorderColor,140),141With<ToggleSwitchOutline>,142>,143q_children: Query<&Children>,144mut q_slide: Query<(&mut Node, &ThemeBackgroundColor), With<ToggleSwitchSlide>>,145mut removed_disabled: RemovedComponents<InteractionDisabled>,146mut removed_checked: RemovedComponents<Checked>,147mut commands: Commands,148) {149removed_disabled150.read()151.chain(removed_checked.read())152.for_each(|ent| {153if let Ok((switch_ent, disabled, checked, hovered, outline_bg, outline_border)) =154q_switches.get(ent)155{156let Some(slide_ent) = q_children157.iter_descendants(switch_ent)158.find(|en| q_slide.contains(*en))159else {160return;161};162// Safety: since we just checked the query, should always work.163let (ref mut slide_style, slide_color) = q_slide.get_mut(slide_ent).unwrap();164set_switch_styles(165switch_ent,166slide_ent,167disabled,168checked,169hovered.0,170outline_bg,171outline_border,172slide_style,173slide_color,174&mut commands,175);176}177});178}179180fn set_switch_styles(181switch_ent: Entity,182slide_ent: Entity,183disabled: bool,184checked: bool,185hovered: bool,186outline_bg: &ThemeBackgroundColor,187outline_border: &ThemeBorderColor,188slide_style: &mut Mut<Node>,189slide_color: &ThemeBackgroundColor,190commands: &mut Commands,191) {192let outline_border_token = match (disabled, hovered) {193(true, _) => tokens::SWITCH_BORDER_DISABLED,194(false, true) => tokens::SWITCH_BORDER_HOVER,195_ => tokens::SWITCH_BORDER,196};197198let outline_bg_token = match (disabled, checked) {199(true, true) => tokens::SWITCH_BG_CHECKED_DISABLED,200(true, false) => tokens::SWITCH_BG_DISABLED,201(false, true) => tokens::SWITCH_BG_CHECKED,202(false, false) => tokens::SWITCH_BG,203};204205let slide_token = match disabled {206true => tokens::SWITCH_SLIDE_DISABLED,207false => tokens::SWITCH_SLIDE,208};209210let slide_pos = match checked {211true => Val::Percent(50.),212false => Val::Percent(0.),213};214215let cursor_shape = match disabled {216true => bevy_window::SystemCursorIcon::NotAllowed,217false => bevy_window::SystemCursorIcon::Pointer,218};219220// Change outline background221if outline_bg.0 != outline_bg_token {222commands223.entity(switch_ent)224.insert(ThemeBackgroundColor(outline_bg_token));225}226227// Change outline border228if outline_border.0 != outline_border_token {229commands230.entity(switch_ent)231.insert(ThemeBorderColor(outline_border_token));232}233234// Change slide color235if slide_color.0 != slide_token {236commands237.entity(slide_ent)238.insert(ThemeBackgroundColor(slide_token));239}240241// Change slide position242if slide_pos != slide_style.left {243slide_style.left = slide_pos;244}245246// Change cursor shape247commands248.entity(switch_ent)249.insert(EntityCursor::System(cursor_shape));250}251252/// Plugin which registers the systems for updating the toggle switch styles.253pub struct ToggleSwitchPlugin;254255impl Plugin for ToggleSwitchPlugin {256fn build(&self, app: &mut bevy_app::App) {257app.add_systems(258PreUpdate,259(update_switch_styles, update_switch_styles_remove).in_set(PickingSystems::Last),260);261}262}263264265