Path: blob/main/crates/bevy_feathers/src/controls/checkbox.rs
6596 views
use bevy_app::{Plugin, PreUpdate};1use bevy_camera::visibility::Visibility;2use bevy_core_widgets::{Callback, CoreCheckbox, ValueChange};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, In, Query},15};16use bevy_input_focus::tab_navigation::TabIndex;17use bevy_math::Rot2;18use bevy_picking::{hover::Hovered, PickingSystems};19use bevy_reflect::{prelude::ReflectDefault, Reflect};20use bevy_ui::{21AlignItems, BorderRadius, Checked, Display, FlexDirection, InteractionDisabled, JustifyContent,22Node, PositionType, UiRect, UiTransform, Val,23};2425use crate::{26constants::{fonts, size},27cursor::EntityCursor,28font_styles::InheritableFont,29handle_or_path::HandleOrPath,30theme::{ThemeBackgroundColor, ThemeBorderColor, ThemeFontColor},31tokens,32};3334/// Parameters for the checkbox template, passed to [`checkbox`] function.35#[derive(Default)]36pub struct CheckboxProps {37/// Change handler38pub on_change: Callback<In<ValueChange<bool>>>,39}4041/// Marker for the checkbox frame (contains both checkbox and label)42#[derive(Component, Default, Clone, Reflect)]43#[reflect(Component, Clone, Default)]44struct CheckboxFrame;4546/// Marker for the checkbox outline47#[derive(Component, Default, Clone, Reflect)]48#[reflect(Component, Clone, Default)]49struct CheckboxOutline;5051/// Marker for the checkbox check mark52#[derive(Component, Default, Clone, Reflect)]53#[reflect(Component, Clone, Default)]54struct CheckboxMark;5556/// Template function to spawn a checkbox.57///58/// # Arguments59/// * `props` - construction properties for the checkbox.60/// * `overrides` - a bundle of components that are merged in with the normal checkbox components.61/// * `label` - the label of the checkbox.62pub fn checkbox<C: SpawnableList<ChildOf> + Send + Sync + 'static, B: Bundle>(63props: CheckboxProps,64overrides: B,65label: C,66) -> impl Bundle {67(68Node {69display: Display::Flex,70flex_direction: FlexDirection::Row,71justify_content: JustifyContent::Start,72align_items: AlignItems::Center,73column_gap: Val::Px(4.0),74..Default::default()75},76CoreCheckbox {77on_change: props.on_change,78},79CheckboxFrame,80Hovered::default(),81EntityCursor::System(bevy_window::SystemCursorIcon::Pointer),82TabIndex(0),83ThemeFontColor(tokens::CHECKBOX_TEXT),84InheritableFont {85font: HandleOrPath::Path(fonts::REGULAR.to_owned()),86font_size: 14.0,87},88overrides,89Children::spawn((90Spawn((91Node {92width: size::CHECKBOX_SIZE,93height: size::CHECKBOX_SIZE,94border: UiRect::all(Val::Px(2.0)),95..Default::default()96},97CheckboxOutline,98BorderRadius::all(Val::Px(4.0)),99ThemeBackgroundColor(tokens::CHECKBOX_BG),100ThemeBorderColor(tokens::CHECKBOX_BORDER),101children![(102// Cheesy checkmark: rotated node with L-shaped border.103Node {104position_type: PositionType::Absolute,105left: Val::Px(4.0),106top: Val::Px(0.0),107width: Val::Px(6.),108height: Val::Px(11.),109border: UiRect {110bottom: Val::Px(2.0),111right: Val::Px(2.0),112..Default::default()113},114..Default::default()115},116UiTransform::from_rotation(Rot2::FRAC_PI_4),117CheckboxMark,118ThemeBorderColor(tokens::CHECKBOX_MARK),119)],120)),121label,122)),123)124}125126fn update_checkbox_styles(127q_checkboxes: Query<128(129Entity,130Has<InteractionDisabled>,131Has<Checked>,132&Hovered,133&ThemeFontColor,134),135(136With<CheckboxFrame>,137Or<(Changed<Hovered>, Added<Checked>, Added<InteractionDisabled>)>,138),139>,140q_children: Query<&Children>,141mut q_outline: Query<(&ThemeBackgroundColor, &ThemeBorderColor), With<CheckboxOutline>>,142mut q_mark: Query<&ThemeBorderColor, With<CheckboxMark>>,143mut commands: Commands,144) {145for (checkbox_ent, disabled, checked, hovered, font_color) in q_checkboxes.iter() {146let Some(outline_ent) = q_children147.iter_descendants(checkbox_ent)148.find(|en| q_outline.contains(*en))149else {150continue;151};152let Some(mark_ent) = q_children153.iter_descendants(checkbox_ent)154.find(|en| q_mark.contains(*en))155else {156continue;157};158let (outline_bg, outline_border) = q_outline.get_mut(outline_ent).unwrap();159let mark_color = q_mark.get_mut(mark_ent).unwrap();160set_checkbox_styles(161checkbox_ent,162outline_ent,163mark_ent,164disabled,165checked,166hovered.0,167outline_bg,168outline_border,169mark_color,170font_color,171&mut commands,172);173}174}175176fn update_checkbox_styles_remove(177q_checkboxes: Query<178(179Entity,180Has<InteractionDisabled>,181Has<Checked>,182&Hovered,183&ThemeFontColor,184),185With<CheckboxFrame>,186>,187q_children: Query<&Children>,188mut q_outline: Query<(&ThemeBackgroundColor, &ThemeBorderColor), With<CheckboxOutline>>,189mut q_mark: Query<&ThemeBorderColor, With<CheckboxMark>>,190mut removed_disabled: RemovedComponents<InteractionDisabled>,191mut removed_checked: RemovedComponents<Checked>,192mut commands: Commands,193) {194removed_disabled195.read()196.chain(removed_checked.read())197.for_each(|ent| {198if let Ok((checkbox_ent, disabled, checked, hovered, font_color)) =199q_checkboxes.get(ent)200{201let Some(outline_ent) = q_children202.iter_descendants(checkbox_ent)203.find(|en| q_outline.contains(*en))204else {205return;206};207let Some(mark_ent) = q_children208.iter_descendants(checkbox_ent)209.find(|en| q_mark.contains(*en))210else {211return;212};213let (outline_bg, outline_border) = q_outline.get_mut(outline_ent).unwrap();214let mark_color = q_mark.get_mut(mark_ent).unwrap();215set_checkbox_styles(216checkbox_ent,217outline_ent,218mark_ent,219disabled,220checked,221hovered.0,222outline_bg,223outline_border,224mark_color,225font_color,226&mut commands,227);228}229});230}231232fn set_checkbox_styles(233checkbox_ent: Entity,234outline_ent: Entity,235mark_ent: Entity,236disabled: bool,237checked: bool,238hovered: bool,239outline_bg: &ThemeBackgroundColor,240outline_border: &ThemeBorderColor,241mark_color: &ThemeBorderColor,242font_color: &ThemeFontColor,243commands: &mut Commands,244) {245let outline_border_token = match (disabled, hovered) {246(true, _) => tokens::CHECKBOX_BORDER_DISABLED,247(false, true) => tokens::CHECKBOX_BORDER_HOVER,248_ => tokens::CHECKBOX_BORDER,249};250251let outline_bg_token = match (disabled, checked) {252(true, true) => tokens::CHECKBOX_BG_CHECKED_DISABLED,253(true, false) => tokens::CHECKBOX_BG_DISABLED,254(false, true) => tokens::CHECKBOX_BG_CHECKED,255(false, false) => tokens::CHECKBOX_BG,256};257258let mark_token = match disabled {259true => tokens::CHECKBOX_MARK_DISABLED,260false => tokens::CHECKBOX_MARK,261};262263let font_color_token = match disabled {264true => tokens::CHECKBOX_TEXT_DISABLED,265false => tokens::CHECKBOX_TEXT,266};267268let cursor_shape = match disabled {269true => bevy_window::SystemCursorIcon::NotAllowed,270false => bevy_window::SystemCursorIcon::Pointer,271};272273// Change outline background274if outline_bg.0 != outline_bg_token {275commands276.entity(outline_ent)277.insert(ThemeBackgroundColor(outline_bg_token));278}279280// Change outline border281if outline_border.0 != outline_border_token {282commands283.entity(outline_ent)284.insert(ThemeBorderColor(outline_border_token));285}286287// Change mark color288if mark_color.0 != mark_token {289commands290.entity(mark_ent)291.insert(ThemeBorderColor(mark_token));292}293294// Change mark visibility295commands.entity(mark_ent).insert(match checked {296true => Visibility::Visible,297false => Visibility::Hidden,298});299300// Change font color301if font_color.0 != font_color_token {302commands303.entity(checkbox_ent)304.insert(ThemeFontColor(font_color_token));305}306307// Change cursor shape308commands309.entity(checkbox_ent)310.insert(EntityCursor::System(cursor_shape));311}312313/// Plugin which registers the systems for updating the checkbox styles.314pub struct CheckboxPlugin;315316impl Plugin for CheckboxPlugin {317fn build(&self, app: &mut bevy_app::App) {318app.add_systems(319PreUpdate,320(update_checkbox_styles, update_checkbox_styles_remove).in_set(PickingSystems::Last),321);322}323}324325326