Path: blob/main/crates/bevy_feathers/src/controls/checkbox.rs
9374 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_math::Rot2;17use bevy_picking::{hover::Hovered, PickingSystems};18use bevy_reflect::{prelude::ReflectDefault, Reflect};19use bevy_text::FontSize;20use bevy_ui::{21AlignItems, BorderRadius, Checked, Display, FlexDirection, InteractionDisabled, JustifyContent,22Node, PositionType, UiRect, UiTransform, Val,23};24use bevy_ui_widgets::Checkbox;2526use crate::{27constants::{fonts, size},28cursor::EntityCursor,29font_styles::InheritableFont,30handle_or_path::HandleOrPath,31theme::{ThemeBackgroundColor, ThemeBorderColor, ThemeFontColor},32tokens,33};3435/// Marker for the checkbox frame (contains both checkbox and label)36#[derive(Component, Default, Clone, Reflect)]37#[reflect(Component, Clone, Default)]38struct CheckboxFrame;3940/// Marker for the checkbox outline41#[derive(Component, Default, Clone, Reflect)]42#[reflect(Component, Clone, Default)]43struct CheckboxOutline;4445/// Marker for the checkbox check mark46#[derive(Component, Default, Clone, Reflect)]47#[reflect(Component, Clone, Default)]48struct CheckboxMark;4950/// Template function to spawn a checkbox.51///52/// # Arguments53/// * `props` - construction properties for the checkbox.54/// * `overrides` - a bundle of components that are merged in with the normal checkbox components.55/// * `label` - the label of the checkbox.56///57/// # Emitted events58/// * [`bevy_ui_widgets::ValueChange<bool>`] with the new value when the checkbox changes state.59///60/// These events can be disabled by adding an [`bevy_ui::InteractionDisabled`] component to the entity61pub fn checkbox<C: SpawnableList<ChildOf> + Send + Sync + 'static, B: Bundle>(62overrides: B,63label: C,64) -> impl Bundle {65(66Node {67display: Display::Flex,68flex_direction: FlexDirection::Row,69justify_content: JustifyContent::Start,70align_items: AlignItems::Center,71column_gap: Val::Px(4.0),72..Default::default()73},74Checkbox,75CheckboxFrame,76Hovered::default(),77EntityCursor::System(bevy_window::SystemCursorIcon::Pointer),78TabIndex(0),79ThemeFontColor(tokens::CHECKBOX_TEXT),80InheritableFont {81font: HandleOrPath::Path(fonts::REGULAR.to_owned()),82font_size: FontSize::Px(14.0),83},84overrides,85Children::spawn((86Spawn((87Node {88width: size::CHECKBOX_SIZE,89height: size::CHECKBOX_SIZE,90border: UiRect::all(Val::Px(2.0)),91border_radius: BorderRadius::all(Val::Px(4.0)),92..Default::default()93},94CheckboxOutline,95ThemeBackgroundColor(tokens::CHECKBOX_BG),96ThemeBorderColor(tokens::CHECKBOX_BORDER),97children![(98// Cheesy checkmark: rotated node with L-shaped border.99Node {100position_type: PositionType::Absolute,101left: Val::Px(4.0),102top: Val::Px(0.0),103width: Val::Px(6.),104height: Val::Px(11.),105border: UiRect {106bottom: Val::Px(2.0),107right: Val::Px(2.0),108..Default::default()109},110..Default::default()111},112UiTransform::from_rotation(Rot2::FRAC_PI_4),113CheckboxMark,114ThemeBorderColor(tokens::CHECKBOX_MARK),115)],116)),117label,118)),119)120}121122fn update_checkbox_styles(123q_checkboxes: Query<124(125Entity,126Has<InteractionDisabled>,127Has<Checked>,128&Hovered,129&ThemeFontColor,130),131(132With<CheckboxFrame>,133Or<(Changed<Hovered>, Added<Checked>, Added<InteractionDisabled>)>,134),135>,136q_children: Query<&Children>,137mut q_outline: Query<(&ThemeBackgroundColor, &ThemeBorderColor), With<CheckboxOutline>>,138mut q_mark: Query<&ThemeBorderColor, With<CheckboxMark>>,139mut commands: Commands,140) {141for (checkbox_ent, disabled, checked, hovered, font_color) in q_checkboxes.iter() {142let Some(outline_ent) = q_children143.iter_descendants(checkbox_ent)144.find(|en| q_outline.contains(*en))145else {146continue;147};148let Some(mark_ent) = q_children149.iter_descendants(checkbox_ent)150.find(|en| q_mark.contains(*en))151else {152continue;153};154let (outline_bg, outline_border) = q_outline.get_mut(outline_ent).unwrap();155let mark_color = q_mark.get_mut(mark_ent).unwrap();156set_checkbox_styles(157checkbox_ent,158outline_ent,159mark_ent,160disabled,161checked,162hovered.0,163outline_bg,164outline_border,165mark_color,166font_color,167&mut commands,168);169}170}171172fn update_checkbox_styles_remove(173q_checkboxes: Query<174(175Entity,176Has<InteractionDisabled>,177Has<Checked>,178&Hovered,179&ThemeFontColor,180),181With<CheckboxFrame>,182>,183q_children: Query<&Children>,184mut q_outline: Query<(&ThemeBackgroundColor, &ThemeBorderColor), With<CheckboxOutline>>,185mut q_mark: Query<&ThemeBorderColor, With<CheckboxMark>>,186mut removed_disabled: RemovedComponents<InteractionDisabled>,187mut removed_checked: RemovedComponents<Checked>,188mut commands: Commands,189) {190removed_disabled191.read()192.chain(removed_checked.read())193.for_each(|ent| {194if let Ok((checkbox_ent, disabled, checked, hovered, font_color)) =195q_checkboxes.get(ent)196{197let Some(outline_ent) = q_children198.iter_descendants(checkbox_ent)199.find(|en| q_outline.contains(*en))200else {201return;202};203let Some(mark_ent) = q_children204.iter_descendants(checkbox_ent)205.find(|en| q_mark.contains(*en))206else {207return;208};209let (outline_bg, outline_border) = q_outline.get_mut(outline_ent).unwrap();210let mark_color = q_mark.get_mut(mark_ent).unwrap();211set_checkbox_styles(212checkbox_ent,213outline_ent,214mark_ent,215disabled,216checked,217hovered.0,218outline_bg,219outline_border,220mark_color,221font_color,222&mut commands,223);224}225});226}227228fn set_checkbox_styles(229checkbox_ent: Entity,230outline_ent: Entity,231mark_ent: Entity,232disabled: bool,233checked: bool,234hovered: bool,235outline_bg: &ThemeBackgroundColor,236outline_border: &ThemeBorderColor,237mark_color: &ThemeBorderColor,238font_color: &ThemeFontColor,239commands: &mut Commands,240) {241let outline_border_token = match (disabled, hovered) {242(true, _) => tokens::CHECKBOX_BORDER_DISABLED,243(false, true) => tokens::CHECKBOX_BORDER_HOVER,244_ => tokens::CHECKBOX_BORDER,245};246247let outline_bg_token = match (disabled, checked) {248(true, true) => tokens::CHECKBOX_BG_CHECKED_DISABLED,249(true, false) => tokens::CHECKBOX_BG_DISABLED,250(false, true) => tokens::CHECKBOX_BG_CHECKED,251(false, false) => tokens::CHECKBOX_BG,252};253254let mark_token = match disabled {255true => tokens::CHECKBOX_MARK_DISABLED,256false => tokens::CHECKBOX_MARK,257};258259let font_color_token = match disabled {260true => tokens::CHECKBOX_TEXT_DISABLED,261false => tokens::CHECKBOX_TEXT,262};263264let cursor_shape = match disabled {265true => bevy_window::SystemCursorIcon::NotAllowed,266false => bevy_window::SystemCursorIcon::Pointer,267};268269// Change outline background270if outline_bg.0 != outline_bg_token {271commands272.entity(outline_ent)273.insert(ThemeBackgroundColor(outline_bg_token));274}275276// Change outline border277if outline_border.0 != outline_border_token {278commands279.entity(outline_ent)280.insert(ThemeBorderColor(outline_border_token));281}282283// Change mark color284if mark_color.0 != mark_token {285commands286.entity(mark_ent)287.insert(ThemeBorderColor(mark_token));288}289290// Change mark visibility291commands.entity(mark_ent).insert(match checked {292true => Visibility::Inherited,293false => Visibility::Hidden,294});295296// Change font color297if font_color.0 != font_color_token {298commands299.entity(checkbox_ent)300.insert(ThemeFontColor(font_color_token));301}302303// Change cursor shape304commands305.entity(checkbox_ent)306.insert(EntityCursor::System(cursor_shape));307}308309/// Plugin which registers the systems for updating the checkbox styles.310pub struct CheckboxPlugin;311312impl Plugin for CheckboxPlugin {313fn build(&self, app: &mut bevy_app::App) {314app.add_systems(315PreUpdate,316(update_checkbox_styles, update_checkbox_styles_remove).in_set(PickingSystems::Last),317);318}319}320321322