Path: blob/main/crates/bevy_feathers/src/controls/radio.rs
9413 views
use bevy_app::{Plugin, PreUpdate};1use bevy_camera::visibility::Visibility;2use bevy_ecs::{3bundle::Bundle,4children,5component::Component,6entity::Entity,7hierarchy::{ChildOf, Children},8lifecycle::RemovedComponents,9query::{Added, Changed, Has, Or, With},10reflect::ReflectComponent,11schedule::IntoScheduleConfigs,12spawn::{Spawn, SpawnRelated, SpawnableList},13system::{Commands, Query},14};15use bevy_input_focus::tab_navigation::TabIndex;16use bevy_picking::{hover::Hovered, PickingSystems};17use bevy_reflect::{prelude::ReflectDefault, Reflect};18use bevy_text::FontSize;19use bevy_ui::{20AlignItems, BorderRadius, Checked, Display, FlexDirection, InteractionDisabled, JustifyContent,21Node, UiRect, Val,22};23use bevy_ui_widgets::RadioButton;2425use crate::{26constants::{fonts, size},27cursor::EntityCursor,28font_styles::InheritableFont,29handle_or_path::HandleOrPath,30theme::{ThemeBackgroundColor, ThemeBorderColor, ThemeFontColor},31tokens,32};3334/// Marker for the radio outline35#[derive(Component, Default, Clone, Reflect)]36#[reflect(Component, Clone, Default)]37struct RadioOutline;3839/// Marker for the radio check mark40#[derive(Component, Default, Clone, Reflect)]41#[reflect(Component, Clone, Default)]42struct RadioMark;4344/// Template function to spawn a radio.45///46/// # Arguments47/// * `props` - construction properties for the radio.48/// * `overrides` - a bundle of components that are merged in with the normal radio components.49/// * `label` - the label of the radio.50///51/// # Emitted events52/// * [`bevy_ui_widgets::ValueChange<bool>`] with the value true when it becomes checked.53/// * [`bevy_ui_widgets::ValueChange<Entity>`] with the selected entity's id when a new radio button is selected.54///55/// These events can be disabled by adding an [`bevy_ui::InteractionDisabled`] component to the entity56pub fn radio<C: SpawnableList<ChildOf> + Send + Sync + 'static, B: Bundle>(57overrides: B,58label: C,59) -> impl Bundle {60(61Node {62display: Display::Flex,63flex_direction: FlexDirection::Row,64justify_content: JustifyContent::Start,65align_items: AlignItems::Center,66column_gap: Val::Px(4.0),67..Default::default()68},69RadioButton,70Hovered::default(),71EntityCursor::System(bevy_window::SystemCursorIcon::Pointer),72TabIndex(0),73ThemeFontColor(tokens::RADIO_TEXT),74InheritableFont {75font: HandleOrPath::Path(fonts::REGULAR.to_owned()),76font_size: FontSize::Px(14.0),77},78overrides,79Children::spawn((80Spawn((81Node {82display: Display::Flex,83align_items: AlignItems::Center,84justify_content: JustifyContent::Center,85width: size::RADIO_SIZE,86height: size::RADIO_SIZE,87border: UiRect::all(Val::Px(2.0)),88border_radius: BorderRadius::MAX,89..Default::default()90},91RadioOutline,92ThemeBorderColor(tokens::RADIO_BORDER),93children![(94// Cheesy checkmark: rotated node with L-shaped border.95Node {96width: Val::Px(8.),97height: Val::Px(8.),98border_radius: BorderRadius::MAX,99..Default::default()100},101RadioMark,102ThemeBackgroundColor(tokens::RADIO_MARK),103)],104)),105label,106)),107)108}109110fn update_radio_styles(111q_radioes: Query<112(113Entity,114Has<InteractionDisabled>,115Has<Checked>,116&Hovered,117&ThemeFontColor,118),119(120With<RadioButton>,121Or<(Changed<Hovered>, Added<Checked>, Added<InteractionDisabled>)>,122),123>,124q_children: Query<&Children>,125mut q_outline: Query<&ThemeBorderColor, With<RadioOutline>>,126mut q_mark: Query<&ThemeBackgroundColor, With<RadioMark>>,127mut commands: Commands,128) {129for (radio_ent, disabled, checked, hovered, font_color) in q_radioes.iter() {130let Some(outline_ent) = q_children131.iter_descendants(radio_ent)132.find(|en| q_outline.contains(*en))133else {134continue;135};136let Some(mark_ent) = q_children137.iter_descendants(radio_ent)138.find(|en| q_mark.contains(*en))139else {140continue;141};142let outline_border = q_outline.get_mut(outline_ent).unwrap();143let mark_color = q_mark.get_mut(mark_ent).unwrap();144set_radio_styles(145radio_ent,146outline_ent,147mark_ent,148disabled,149checked,150hovered.0,151outline_border,152mark_color,153font_color,154&mut commands,155);156}157}158159fn update_radio_styles_remove(160q_radioes: Query<161(162Entity,163Has<InteractionDisabled>,164Has<Checked>,165&Hovered,166&ThemeFontColor,167),168With<RadioButton>,169>,170q_children: Query<&Children>,171mut q_outline: Query<&ThemeBorderColor, With<RadioOutline>>,172mut q_mark: Query<&ThemeBackgroundColor, With<RadioMark>>,173mut removed_disabled: RemovedComponents<InteractionDisabled>,174mut removed_checked: RemovedComponents<Checked>,175mut commands: Commands,176) {177removed_disabled178.read()179.chain(removed_checked.read())180.for_each(|ent| {181if let Ok((radio_ent, disabled, checked, hovered, font_color)) = q_radioes.get(ent) {182let Some(outline_ent) = q_children183.iter_descendants(radio_ent)184.find(|en| q_outline.contains(*en))185else {186return;187};188let Some(mark_ent) = q_children189.iter_descendants(radio_ent)190.find(|en| q_mark.contains(*en))191else {192return;193};194let outline_border = q_outline.get_mut(outline_ent).unwrap();195let mark_color = q_mark.get_mut(mark_ent).unwrap();196set_radio_styles(197radio_ent,198outline_ent,199mark_ent,200disabled,201checked,202hovered.0,203outline_border,204mark_color,205font_color,206&mut commands,207);208}209});210}211212fn set_radio_styles(213radio_ent: Entity,214outline_ent: Entity,215mark_ent: Entity,216disabled: bool,217checked: bool,218hovered: bool,219outline_border: &ThemeBorderColor,220mark_color: &ThemeBackgroundColor,221font_color: &ThemeFontColor,222commands: &mut Commands,223) {224let outline_border_token = match (disabled, hovered) {225(true, _) => tokens::RADIO_BORDER_DISABLED,226(false, true) => tokens::RADIO_BORDER_HOVER,227_ => tokens::RADIO_BORDER,228};229230let mark_token = match disabled {231true => tokens::RADIO_MARK_DISABLED,232false => tokens::RADIO_MARK,233};234235let font_color_token = match disabled {236true => tokens::RADIO_TEXT_DISABLED,237false => tokens::RADIO_TEXT,238};239240let cursor_shape = match disabled {241true => bevy_window::SystemCursorIcon::NotAllowed,242false => bevy_window::SystemCursorIcon::Pointer,243};244245// Change outline border246if outline_border.0 != outline_border_token {247commands248.entity(outline_ent)249.insert(ThemeBorderColor(outline_border_token));250}251252// Change mark color253if mark_color.0 != mark_token {254commands255.entity(mark_ent)256.insert(ThemeBorderColor(mark_token));257}258259// Change mark visibility260commands.entity(mark_ent).insert(match checked {261true => Visibility::Inherited,262false => Visibility::Hidden,263});264265// Change font color266if font_color.0 != font_color_token {267commands268.entity(radio_ent)269.insert(ThemeFontColor(font_color_token));270}271272// Change cursor shape273commands274.entity(radio_ent)275.insert(EntityCursor::System(cursor_shape));276}277278/// Plugin which registers the systems for updating the radio styles.279pub struct RadioPlugin;280281impl Plugin for RadioPlugin {282fn build(&self, app: &mut bevy_app::App) {283app.add_systems(284PreUpdate,285(update_radio_styles, update_radio_styles_remove).in_set(PickingSystems::Last),286);287}288}289290291