Path: blob/main/crates/bevy_feathers/src/controls/radio.rs
6596 views
use bevy_app::{Plugin, PreUpdate};1use bevy_camera::visibility::Visibility;2use bevy_core_widgets::CoreRadio;3use bevy_ecs::{4bundle::Bundle,5children,6component::Component,7entity::Entity,8hierarchy::{ChildOf, Children},9lifecycle::RemovedComponents,10query::{Added, Changed, Has, Or, With},11reflect::ReflectComponent,12schedule::IntoScheduleConfigs,13spawn::{Spawn, SpawnRelated, SpawnableList},14system::{Commands, Query},15};16use bevy_input_focus::tab_navigation::TabIndex;17use bevy_picking::{hover::Hovered, PickingSystems};18use bevy_reflect::{prelude::ReflectDefault, Reflect};19use bevy_ui::{20AlignItems, BorderRadius, Checked, Display, FlexDirection, InteractionDisabled, JustifyContent,21Node, UiRect, Val,22};2324use crate::{25constants::{fonts, size},26cursor::EntityCursor,27font_styles::InheritableFont,28handle_or_path::HandleOrPath,29theme::{ThemeBackgroundColor, ThemeBorderColor, ThemeFontColor},30tokens,31};3233/// Marker for the radio outline34#[derive(Component, Default, Clone, Reflect)]35#[reflect(Component, Clone, Default)]36struct RadioOutline;3738/// Marker for the radio check mark39#[derive(Component, Default, Clone, Reflect)]40#[reflect(Component, Clone, Default)]41struct RadioMark;4243/// Template function to spawn a radio.44///45/// # Arguments46/// * `props` - construction properties for the radio.47/// * `overrides` - a bundle of components that are merged in with the normal radio components.48/// * `label` - the label of the radio.49pub fn radio<C: SpawnableList<ChildOf> + Send + Sync + 'static, B: Bundle>(50overrides: B,51label: C,52) -> impl Bundle {53(54Node {55display: Display::Flex,56flex_direction: FlexDirection::Row,57justify_content: JustifyContent::Start,58align_items: AlignItems::Center,59column_gap: Val::Px(4.0),60..Default::default()61},62CoreRadio,63Hovered::default(),64EntityCursor::System(bevy_window::SystemCursorIcon::Pointer),65TabIndex(0),66ThemeFontColor(tokens::RADIO_TEXT),67InheritableFont {68font: HandleOrPath::Path(fonts::REGULAR.to_owned()),69font_size: 14.0,70},71overrides,72Children::spawn((73Spawn((74Node {75display: Display::Flex,76align_items: AlignItems::Center,77justify_content: JustifyContent::Center,78width: size::RADIO_SIZE,79height: size::RADIO_SIZE,80border: UiRect::all(Val::Px(2.0)),81..Default::default()82},83RadioOutline,84BorderRadius::MAX,85ThemeBorderColor(tokens::RADIO_BORDER),86children![(87// Cheesy checkmark: rotated node with L-shaped border.88Node {89width: Val::Px(8.),90height: Val::Px(8.),91..Default::default()92},93BorderRadius::MAX,94RadioMark,95ThemeBackgroundColor(tokens::RADIO_MARK),96)],97)),98label,99)),100)101}102103fn update_radio_styles(104q_radioes: Query<105(106Entity,107Has<InteractionDisabled>,108Has<Checked>,109&Hovered,110&ThemeFontColor,111),112(113With<CoreRadio>,114Or<(Changed<Hovered>, Added<Checked>, Added<InteractionDisabled>)>,115),116>,117q_children: Query<&Children>,118mut q_outline: Query<&ThemeBorderColor, With<RadioOutline>>,119mut q_mark: Query<&ThemeBackgroundColor, With<RadioMark>>,120mut commands: Commands,121) {122for (radio_ent, disabled, checked, hovered, font_color) in q_radioes.iter() {123let Some(outline_ent) = q_children124.iter_descendants(radio_ent)125.find(|en| q_outline.contains(*en))126else {127continue;128};129let Some(mark_ent) = q_children130.iter_descendants(radio_ent)131.find(|en| q_mark.contains(*en))132else {133continue;134};135let outline_border = q_outline.get_mut(outline_ent).unwrap();136let mark_color = q_mark.get_mut(mark_ent).unwrap();137set_radio_styles(138radio_ent,139outline_ent,140mark_ent,141disabled,142checked,143hovered.0,144outline_border,145mark_color,146font_color,147&mut commands,148);149}150}151152fn update_radio_styles_remove(153q_radioes: Query<154(155Entity,156Has<InteractionDisabled>,157Has<Checked>,158&Hovered,159&ThemeFontColor,160),161With<CoreRadio>,162>,163q_children: Query<&Children>,164mut q_outline: Query<&ThemeBorderColor, With<RadioOutline>>,165mut q_mark: Query<&ThemeBackgroundColor, With<RadioMark>>,166mut removed_disabled: RemovedComponents<InteractionDisabled>,167mut removed_checked: RemovedComponents<Checked>,168mut commands: Commands,169) {170removed_disabled171.read()172.chain(removed_checked.read())173.for_each(|ent| {174if let Ok((radio_ent, disabled, checked, hovered, font_color)) = q_radioes.get(ent) {175let Some(outline_ent) = q_children176.iter_descendants(radio_ent)177.find(|en| q_outline.contains(*en))178else {179return;180};181let Some(mark_ent) = q_children182.iter_descendants(radio_ent)183.find(|en| q_mark.contains(*en))184else {185return;186};187let outline_border = q_outline.get_mut(outline_ent).unwrap();188let mark_color = q_mark.get_mut(mark_ent).unwrap();189set_radio_styles(190radio_ent,191outline_ent,192mark_ent,193disabled,194checked,195hovered.0,196outline_border,197mark_color,198font_color,199&mut commands,200);201}202});203}204205fn set_radio_styles(206radio_ent: Entity,207outline_ent: Entity,208mark_ent: Entity,209disabled: bool,210checked: bool,211hovered: bool,212outline_border: &ThemeBorderColor,213mark_color: &ThemeBackgroundColor,214font_color: &ThemeFontColor,215commands: &mut Commands,216) {217let outline_border_token = match (disabled, hovered) {218(true, _) => tokens::RADIO_BORDER_DISABLED,219(false, true) => tokens::RADIO_BORDER_HOVER,220_ => tokens::RADIO_BORDER,221};222223let mark_token = match disabled {224true => tokens::RADIO_MARK_DISABLED,225false => tokens::RADIO_MARK,226};227228let font_color_token = match disabled {229true => tokens::RADIO_TEXT_DISABLED,230false => tokens::RADIO_TEXT,231};232233let cursor_shape = match disabled {234true => bevy_window::SystemCursorIcon::NotAllowed,235false => bevy_window::SystemCursorIcon::Pointer,236};237238// Change outline border239if outline_border.0 != outline_border_token {240commands241.entity(outline_ent)242.insert(ThemeBorderColor(outline_border_token));243}244245// Change mark color246if mark_color.0 != mark_token {247commands248.entity(mark_ent)249.insert(ThemeBorderColor(mark_token));250}251252// Change mark visibility253commands.entity(mark_ent).insert(match checked {254true => Visibility::Visible,255false => Visibility::Hidden,256});257258// Change font color259if font_color.0 != font_color_token {260commands261.entity(radio_ent)262.insert(ThemeFontColor(font_color_token));263}264265// Change cursor shape266commands267.entity(radio_ent)268.insert(EntityCursor::System(cursor_shape));269}270271/// Plugin which registers the systems for updating the radio styles.272pub struct RadioPlugin;273274impl Plugin for RadioPlugin {275fn build(&self, app: &mut bevy_app::App) {276app.add_systems(277PreUpdate,278(update_radio_styles, update_radio_styles_remove).in_set(PickingSystems::Last),279);280}281}282283284