|
| 1 | +use bevy_app::{Plugin, PreUpdate}; |
| 2 | +use bevy_core_widgets::{Callback, CoreCheckbox}; |
| 3 | +use bevy_ecs::{ |
| 4 | + bundle::Bundle, |
| 5 | + children, |
| 6 | + component::Component, |
| 7 | + entity::Entity, |
| 8 | + hierarchy::{ChildOf, Children}, |
| 9 | + lifecycle::RemovedComponents, |
| 10 | + query::{Added, Changed, Has, Or, With}, |
| 11 | + schedule::IntoScheduleConfigs, |
| 12 | + spawn::{Spawn, SpawnRelated, SpawnableList}, |
| 13 | + system::{Commands, In, Query}, |
| 14 | +}; |
| 15 | +use bevy_input_focus::tab_navigation::TabIndex; |
| 16 | +use bevy_math::Rot2; |
| 17 | +use bevy_picking::{hover::Hovered, PickingSystems}; |
| 18 | +use bevy_render::view::Visibility; |
| 19 | +use bevy_ui::{ |
| 20 | + AlignItems, BorderRadius, Checked, Display, FlexDirection, InteractionDisabled, JustifyContent, |
| 21 | + Node, PositionType, UiRect, UiTransform, Val, |
| 22 | +}; |
| 23 | +use bevy_winit::cursor::CursorIcon; |
| 24 | + |
| 25 | +use crate::{ |
| 26 | + constants::{fonts, size}, |
| 27 | + font_styles::InheritableFont, |
| 28 | + handle_or_path::HandleOrPath, |
| 29 | + theme::{ThemeBackgroundColor, ThemeBorderColor, ThemeFontColor}, |
| 30 | + tokens, |
| 31 | +}; |
| 32 | + |
| 33 | +/// Parameters for the checkbox template, passed to [`checkbox`] function. |
| 34 | +#[derive(Default)] |
| 35 | +pub struct CheckboxProps { |
| 36 | + /// Change handler |
| 37 | + pub on_change: Callback<In<bool>>, |
| 38 | +} |
| 39 | + |
| 40 | +/// Marker for the checkbox outline |
| 41 | +#[derive(Component, Default, Clone)] |
| 42 | +struct CheckboxOutline; |
| 43 | + |
| 44 | +/// Marker for the checkbox check mark |
| 45 | +#[derive(Component, Default, Clone)] |
| 46 | +struct CheckboxMark; |
| 47 | + |
| 48 | +/// Template function to spawn a checkbox. |
| 49 | +/// |
| 50 | +/// # Arguments |
| 51 | +/// * `props` - construction properties for the checkbox. |
| 52 | +/// * `overrides` - a bundle of components that are merged in with the normal checkbox components. |
| 53 | +/// * `label` - the label of the checkbox. |
| 54 | +pub fn checkbox<C: SpawnableList<ChildOf> + Send + Sync + 'static, B: Bundle>( |
| 55 | + props: CheckboxProps, |
| 56 | + overrides: B, |
| 57 | + label: C, |
| 58 | +) -> impl Bundle { |
| 59 | + ( |
| 60 | + Node { |
| 61 | + display: Display::Flex, |
| 62 | + flex_direction: FlexDirection::Row, |
| 63 | + justify_content: JustifyContent::Start, |
| 64 | + align_items: AlignItems::Center, |
| 65 | + column_gap: Val::Px(4.0), |
| 66 | + ..Default::default() |
| 67 | + }, |
| 68 | + CoreCheckbox { |
| 69 | + on_change: props.on_change, |
| 70 | + }, |
| 71 | + Hovered::default(), |
| 72 | + CursorIcon::System(bevy_window::SystemCursorIcon::Pointer), |
| 73 | + TabIndex(0), |
| 74 | + ThemeFontColor(tokens::CHECKBOX_TEXT), |
| 75 | + InheritableFont { |
| 76 | + font: HandleOrPath::Path(fonts::REGULAR.to_owned()), |
| 77 | + font_size: 14.0, |
| 78 | + }, |
| 79 | + overrides, |
| 80 | + Children::spawn(( |
| 81 | + Spawn(( |
| 82 | + Node { |
| 83 | + width: size::CHECKBOX_SIZE, |
| 84 | + height: size::CHECKBOX_SIZE, |
| 85 | + border: UiRect::all(Val::Px(2.0)), |
| 86 | + ..Default::default() |
| 87 | + }, |
| 88 | + CheckboxOutline, |
| 89 | + BorderRadius::all(Val::Px(4.0)), |
| 90 | + ThemeBackgroundColor(tokens::CHECKBOX_BG), |
| 91 | + ThemeBorderColor(tokens::CHECKBOX_BORDER), |
| 92 | + children![( |
| 93 | + // Cheesy checkmark: rotated node with L-shaped border. |
| 94 | + Node { |
| 95 | + position_type: PositionType::Absolute, |
| 96 | + left: Val::Px(4.0), |
| 97 | + top: Val::Px(0.0), |
| 98 | + width: Val::Px(6.), |
| 99 | + height: Val::Px(11.), |
| 100 | + border: UiRect { |
| 101 | + bottom: Val::Px(2.0), |
| 102 | + right: Val::Px(2.0), |
| 103 | + ..Default::default() |
| 104 | + }, |
| 105 | + ..Default::default() |
| 106 | + }, |
| 107 | + UiTransform::from_rotation(Rot2::FRAC_PI_4), |
| 108 | + CheckboxMark, |
| 109 | + ThemeBorderColor(tokens::CHECKBOX_MARK), |
| 110 | + )], |
| 111 | + )), |
| 112 | + label, |
| 113 | + )), |
| 114 | + ) |
| 115 | +} |
| 116 | + |
| 117 | +fn update_checkbox_styles( |
| 118 | + q_checkboxes: Query< |
| 119 | + ( |
| 120 | + Entity, |
| 121 | + Has<InteractionDisabled>, |
| 122 | + Has<Checked>, |
| 123 | + &Hovered, |
| 124 | + &ThemeFontColor, |
| 125 | + ), |
| 126 | + ( |
| 127 | + With<CoreCheckbox>, |
| 128 | + Or<(Changed<Hovered>, Added<Checked>, Added<InteractionDisabled>)>, |
| 129 | + ), |
| 130 | + >, |
| 131 | + q_children: Query<&Children>, |
| 132 | + mut q_outline: Query<(&ThemeBackgroundColor, &ThemeBorderColor), With<CheckboxOutline>>, |
| 133 | + mut q_mark: Query<&ThemeBorderColor, With<CheckboxMark>>, |
| 134 | + mut commands: Commands, |
| 135 | +) { |
| 136 | + for (checkbox_ent, disabled, checked, hovered, font_color) in q_checkboxes.iter() { |
| 137 | + let Some(outline_ent) = q_children |
| 138 | + .iter_descendants(checkbox_ent) |
| 139 | + .find(|en| q_outline.contains(*en)) |
| 140 | + else { |
| 141 | + continue; |
| 142 | + }; |
| 143 | + let Some(mark_ent) = q_children |
| 144 | + .iter_descendants(checkbox_ent) |
| 145 | + .find(|en| q_mark.contains(*en)) |
| 146 | + else { |
| 147 | + continue; |
| 148 | + }; |
| 149 | + let (outline_bg, outline_border) = q_outline.get_mut(outline_ent).unwrap(); |
| 150 | + let mark_color = q_mark.get_mut(mark_ent).unwrap(); |
| 151 | + set_checkbox_colors( |
| 152 | + checkbox_ent, |
| 153 | + outline_ent, |
| 154 | + mark_ent, |
| 155 | + disabled, |
| 156 | + checked, |
| 157 | + hovered.0, |
| 158 | + outline_bg, |
| 159 | + outline_border, |
| 160 | + mark_color, |
| 161 | + font_color, |
| 162 | + &mut commands, |
| 163 | + ); |
| 164 | + } |
| 165 | +} |
| 166 | + |
| 167 | +fn update_checkbox_styles_remove( |
| 168 | + q_checkboxes: Query< |
| 169 | + ( |
| 170 | + Entity, |
| 171 | + Has<InteractionDisabled>, |
| 172 | + Has<Checked>, |
| 173 | + &Hovered, |
| 174 | + &ThemeFontColor, |
| 175 | + ), |
| 176 | + With<CoreCheckbox>, |
| 177 | + >, |
| 178 | + q_children: Query<&Children>, |
| 179 | + mut q_outline: Query<(&ThemeBackgroundColor, &ThemeBorderColor), With<CheckboxOutline>>, |
| 180 | + mut q_mark: Query<&ThemeBorderColor, With<CheckboxMark>>, |
| 181 | + mut removed_disabled: RemovedComponents<InteractionDisabled>, |
| 182 | + mut removed_checked: RemovedComponents<Checked>, |
| 183 | + mut commands: Commands, |
| 184 | +) { |
| 185 | + removed_disabled |
| 186 | + .read() |
| 187 | + .chain(removed_checked.read()) |
| 188 | + .for_each(|ent| { |
| 189 | + if let Ok((checkbox_ent, disabled, checked, hovered, font_color)) = |
| 190 | + q_checkboxes.get(ent) |
| 191 | + { |
| 192 | + let Some(outline_ent) = q_children |
| 193 | + .iter_descendants(checkbox_ent) |
| 194 | + .find(|en| q_outline.contains(*en)) |
| 195 | + else { |
| 196 | + return; |
| 197 | + }; |
| 198 | + let Some(mark_ent) = q_children |
| 199 | + .iter_descendants(checkbox_ent) |
| 200 | + .find(|en| q_mark.contains(*en)) |
| 201 | + else { |
| 202 | + return; |
| 203 | + }; |
| 204 | + let (outline_bg, outline_border) = q_outline.get_mut(outline_ent).unwrap(); |
| 205 | + let mark_color = q_mark.get_mut(mark_ent).unwrap(); |
| 206 | + set_checkbox_colors( |
| 207 | + checkbox_ent, |
| 208 | + outline_ent, |
| 209 | + mark_ent, |
| 210 | + disabled, |
| 211 | + checked, |
| 212 | + hovered.0, |
| 213 | + outline_bg, |
| 214 | + outline_border, |
| 215 | + mark_color, |
| 216 | + font_color, |
| 217 | + &mut commands, |
| 218 | + ); |
| 219 | + } |
| 220 | + }); |
| 221 | +} |
| 222 | + |
| 223 | +fn set_checkbox_colors( |
| 224 | + checkbox_ent: Entity, |
| 225 | + outline_ent: Entity, |
| 226 | + mark_ent: Entity, |
| 227 | + disabled: bool, |
| 228 | + checked: bool, |
| 229 | + hovered: bool, |
| 230 | + outline_bg: &ThemeBackgroundColor, |
| 231 | + outline_border: &ThemeBorderColor, |
| 232 | + mark_color: &ThemeBorderColor, |
| 233 | + font_color: &ThemeFontColor, |
| 234 | + commands: &mut Commands, |
| 235 | +) { |
| 236 | + let outline_border_token = match (disabled, hovered) { |
| 237 | + (true, _) => tokens::CHECKBOX_BORDER_DISABLED, |
| 238 | + (false, true) => tokens::CHECKBOX_BORDER_HOVER, |
| 239 | + _ => tokens::CHECKBOX_BORDER, |
| 240 | + }; |
| 241 | + |
| 242 | + let outline_bg_token = match (disabled, checked) { |
| 243 | + (true, true) => tokens::CHECKBOX_BG_CHECKED_DISABLED, |
| 244 | + (true, false) => tokens::CHECKBOX_BG_DISABLED, |
| 245 | + (false, true) => tokens::CHECKBOX_BG_CHECKED, |
| 246 | + (false, false) => tokens::CHECKBOX_BG, |
| 247 | + }; |
| 248 | + |
| 249 | + let mark_token = match disabled { |
| 250 | + true => tokens::CHECKBOX_MARK_DISABLED, |
| 251 | + false => tokens::CHECKBOX_MARK, |
| 252 | + }; |
| 253 | + |
| 254 | + let font_color_token = match disabled { |
| 255 | + true => tokens::CHECKBOX_TEXT_DISABLED, |
| 256 | + false => tokens::CHECKBOX_TEXT, |
| 257 | + }; |
| 258 | + |
| 259 | + // Change outline background |
| 260 | + if outline_bg.0 != outline_bg_token { |
| 261 | + commands |
| 262 | + .entity(outline_ent) |
| 263 | + .insert(ThemeBackgroundColor(outline_bg_token)); |
| 264 | + } |
| 265 | + |
| 266 | + // Change outline border |
| 267 | + if outline_border.0 != outline_border_token { |
| 268 | + commands |
| 269 | + .entity(outline_ent) |
| 270 | + .insert(ThemeBorderColor(outline_border_token)); |
| 271 | + } |
| 272 | + |
| 273 | + // Change mark color |
| 274 | + if mark_color.0 != mark_token { |
| 275 | + commands |
| 276 | + .entity(mark_ent) |
| 277 | + .insert(ThemeBorderColor(mark_token)); |
| 278 | + } |
| 279 | + |
| 280 | + // Change mark visibility |
| 281 | + commands.entity(mark_ent).insert(match checked { |
| 282 | + true => Visibility::Visible, |
| 283 | + false => Visibility::Hidden, |
| 284 | + }); |
| 285 | + |
| 286 | + // Change font color |
| 287 | + if font_color.0 != font_color_token { |
| 288 | + commands |
| 289 | + .entity(checkbox_ent) |
| 290 | + .insert(ThemeFontColor(font_color_token)); |
| 291 | + } |
| 292 | +} |
| 293 | + |
| 294 | +/// Plugin which registers the systems for updating the checkbox styles. |
| 295 | +pub struct CheckboxPlugin; |
| 296 | + |
| 297 | +impl Plugin for CheckboxPlugin { |
| 298 | + fn build(&self, app: &mut bevy_app::App) { |
| 299 | + app.add_systems( |
| 300 | + PreUpdate, |
| 301 | + (update_checkbox_styles, update_checkbox_styles_remove).in_set(PickingSystems::Last), |
| 302 | + ); |
| 303 | + } |
| 304 | +} |
0 commit comments