1use bevy::{
10 color::palettes::basic::*,
11 input_focus::{
12 tab_navigation::{TabGroup, TabIndex, TabNavigationPlugin},
13 InputDispatchPlugin,
14 },
15 picking::hover::Hovered,
16 prelude::*,
17 ui::{Checked, InteractionDisabled, Pressed},
18 ui_widgets::{
19 checkbox_self_update, observe, Activate, Button, Checkbox, CoreSliderDragState,
20 RadioButton, RadioGroup, Slider, SliderRange, SliderThumb, SliderValue, TrackClick,
21 UiWidgetsPlugins, ValueChange,
22 },
23};
24
25fn main() {
26 App::new()
27 .add_plugins((
28 DefaultPlugins,
29 UiWidgetsPlugins,
30 InputDispatchPlugin,
31 TabNavigationPlugin,
32 ))
33 .insert_resource(DemoWidgetStates {
34 slider_value: 50.0,
35 slider_click: TrackClick::Snap,
36 })
37 .add_systems(Startup, setup)
38 .add_systems(
39 Update,
40 (
41 update_widget_values,
42 update_button_style,
43 update_button_style2,
44 update_slider_style.after(update_widget_values),
45 update_slider_style2.after(update_widget_values),
46 update_checkbox_or_radio_style.after(update_widget_values),
47 update_checkbox_or_radio_style2.after(update_widget_values),
48 toggle_disabled,
49 ),
50 )
51 .run();
52}
53
54const NORMAL_BUTTON: Color = Color::srgb(0.15, 0.15, 0.15);
55const HOVERED_BUTTON: Color = Color::srgb(0.25, 0.25, 0.25);
56const PRESSED_BUTTON: Color = Color::srgb(0.35, 0.75, 0.35);
57const SLIDER_TRACK: Color = Color::srgb(0.05, 0.05, 0.05);
58const SLIDER_THUMB: Color = Color::srgb(0.35, 0.75, 0.35);
59const ELEMENT_OUTLINE: Color = Color::srgb(0.45, 0.45, 0.45);
60const ELEMENT_FILL: Color = Color::srgb(0.35, 0.75, 0.35);
61const ELEMENT_FILL_DISABLED: Color = Color::srgb(0.5019608, 0.5019608, 0.5019608);
62
63#[derive(Component)]
65struct DemoButton;
66
67#[derive(Component, Default)]
69struct DemoSlider;
70
71#[derive(Component, Default)]
73struct DemoSliderThumb;
74
75#[derive(Component, Default)]
77struct DemoCheckbox;
78
79#[derive(Component, Default)]
82struct DemoRadio(TrackClick);
83
84#[derive(Resource)]
91struct DemoWidgetStates {
92 slider_value: f32,
93 slider_click: TrackClick,
94}
95
96fn update_widget_values(
98 res: Res<DemoWidgetStates>,
99 mut sliders: Query<(Entity, &mut Slider), With<DemoSlider>>,
100 radios: Query<(Entity, &DemoRadio, Has<Checked>)>,
101 mut commands: Commands,
102) {
103 if res.is_changed() {
104 for (slider_ent, mut slider) in sliders.iter_mut() {
105 commands
106 .entity(slider_ent)
107 .insert(SliderValue(res.slider_value));
108 slider.track_click = res.slider_click;
109 }
110
111 for (radio_id, radio_value, checked) in radios.iter() {
112 let will_be_checked = radio_value.0 == res.slider_click;
113 if will_be_checked != checked {
114 if will_be_checked {
115 commands.entity(radio_id).insert(Checked);
116 } else {
117 commands.entity(radio_id).remove::<Checked>();
118 }
119 }
120 }
121 }
122}
123
124fn setup(mut commands: Commands, assets: Res<AssetServer>) {
125 commands.spawn(Camera2d);
127 commands.spawn(demo_root(&assets));
128}
129
130fn demo_root(asset_server: &AssetServer) -> impl Bundle {
131 (
132 Node {
133 width: percent(100),
134 height: percent(100),
135 align_items: AlignItems::Center,
136 justify_content: JustifyContent::Center,
137 display: Display::Flex,
138 flex_direction: FlexDirection::Column,
139 row_gap: px(10),
140 ..default()
141 },
142 TabGroup::default(),
143 children![
144 (
145 button(asset_server),
146 observe(|_activate: On<Activate>| {
147 info!("Button clicked!");
148 }),
149 ),
150 (
151 slider(0.0, 100.0, 50.0),
152 observe(
153 |value_change: On<ValueChange<f32>>,
154 mut widget_states: ResMut<DemoWidgetStates>| {
155 widget_states.slider_value = value_change.value;
156 },
157 )
158 ),
159 (
160 checkbox(asset_server, "Checkbox"),
161 observe(checkbox_self_update)
162 ),
163 (
164 radio_group(asset_server),
165 observe(
166 |value_change: On<ValueChange<Entity>>,
167 mut widget_states: ResMut<DemoWidgetStates>,
168 q_radios: Query<&DemoRadio>| {
169 if let Ok(radio) = q_radios.get(value_change.value) {
170 widget_states.slider_click = radio.0;
171 }
172 },
173 )
174 ),
175 Text::new("Press 'D' to toggle widget disabled states"),
176 ],
177 )
178}
179
180fn button(asset_server: &AssetServer) -> impl Bundle {
181 (
182 Node {
183 width: px(150),
184 height: px(65),
185 border: UiRect::all(px(5)),
186 justify_content: JustifyContent::Center,
187 align_items: AlignItems::Center,
188 ..default()
189 },
190 DemoButton,
191 Button,
192 Hovered::default(),
193 TabIndex(0),
194 BorderColor::all(Color::BLACK),
195 BorderRadius::MAX,
196 BackgroundColor(NORMAL_BUTTON),
197 children![(
198 Text::new("Button"),
199 TextFont {
200 font: asset_server.load("fonts/FiraSans-Bold.ttf"),
201 font_size: 33.0,
202 ..default()
203 },
204 TextColor(Color::srgb(0.9, 0.9, 0.9)),
205 TextShadow::default(),
206 )],
207 )
208}
209
210fn update_button_style(
211 mut buttons: Query<
212 (
213 Has<Pressed>,
214 &Hovered,
215 Has<InteractionDisabled>,
216 &mut BackgroundColor,
217 &mut BorderColor,
218 &Children,
219 ),
220 (
221 Or<(
222 Changed<Pressed>,
223 Changed<Hovered>,
224 Added<InteractionDisabled>,
225 )>,
226 With<DemoButton>,
227 ),
228 >,
229 mut text_query: Query<&mut Text>,
230) {
231 for (pressed, hovered, disabled, mut color, mut border_color, children) in &mut buttons {
232 let mut text = text_query.get_mut(children[0]).unwrap();
233 set_button_style(
234 disabled,
235 hovered.get(),
236 pressed,
237 &mut color,
238 &mut border_color,
239 &mut text,
240 );
241 }
242}
243
244fn update_button_style2(
246 mut buttons: Query<
247 (
248 Has<Pressed>,
249 &Hovered,
250 Has<InteractionDisabled>,
251 &mut BackgroundColor,
252 &mut BorderColor,
253 &Children,
254 ),
255 With<DemoButton>,
256 >,
257 mut removed_depressed: RemovedComponents<Pressed>,
258 mut removed_disabled: RemovedComponents<InteractionDisabled>,
259 mut text_query: Query<&mut Text>,
260) {
261 removed_depressed
262 .read()
263 .chain(removed_disabled.read())
264 .for_each(|entity| {
265 if let Ok((pressed, hovered, disabled, mut color, mut border_color, children)) =
266 buttons.get_mut(entity)
267 {
268 let mut text = text_query.get_mut(children[0]).unwrap();
269 set_button_style(
270 disabled,
271 hovered.get(),
272 pressed,
273 &mut color,
274 &mut border_color,
275 &mut text,
276 );
277 }
278 });
279}
280
281fn set_button_style(
282 disabled: bool,
283 hovered: bool,
284 pressed: bool,
285 color: &mut BackgroundColor,
286 border_color: &mut BorderColor,
287 text: &mut Text,
288) {
289 match (disabled, hovered, pressed) {
290 (true, _, _) => {
292 **text = "Disabled".to_string();
293 *color = NORMAL_BUTTON.into();
294 border_color.set_all(GRAY);
295 }
296
297 (false, true, true) => {
299 **text = "Press".to_string();
300 *color = PRESSED_BUTTON.into();
301 border_color.set_all(RED);
302 }
303
304 (false, true, false) => {
306 **text = "Hover".to_string();
307 *color = HOVERED_BUTTON.into();
308 border_color.set_all(WHITE);
309 }
310
311 (false, false, _) => {
313 **text = "Button".to_string();
314 *color = NORMAL_BUTTON.into();
315 border_color.set_all(BLACK);
316 }
317 }
318}
319
320fn slider(min: f32, max: f32, value: f32) -> impl Bundle {
322 (
323 Node {
324 display: Display::Flex,
325 flex_direction: FlexDirection::Column,
326 justify_content: JustifyContent::Center,
327 align_items: AlignItems::Stretch,
328 justify_items: JustifyItems::Center,
329 column_gap: px(4),
330 height: px(12),
331 width: percent(30),
332 ..default()
333 },
334 Name::new("Slider"),
335 Hovered::default(),
336 DemoSlider,
337 Slider {
338 track_click: TrackClick::Snap,
339 },
340 SliderValue(value),
341 SliderRange::new(min, max),
342 TabIndex(0),
343 Children::spawn((
344 Spawn((
346 Node {
347 height: px(6),
348 ..default()
349 },
350 BackgroundColor(SLIDER_TRACK), BorderRadius::all(px(3)),
352 )),
353 Spawn((
357 Node {
358 display: Display::Flex,
359 position_type: PositionType::Absolute,
360 left: px(0),
361 right: px(12),
363 top: px(0),
364 bottom: px(0),
365 ..default()
366 },
367 children![(
368 DemoSliderThumb,
370 SliderThumb,
371 Node {
372 display: Display::Flex,
373 width: px(12),
374 height: px(12),
375 position_type: PositionType::Absolute,
376 left: percent(0), ..default()
378 },
379 BorderRadius::MAX,
380 BackgroundColor(SLIDER_THUMB),
381 )],
382 )),
383 )),
384 )
385}
386
387fn update_slider_style(
389 sliders: Query<
390 (
391 Entity,
392 &SliderValue,
393 &SliderRange,
394 &Hovered,
395 &CoreSliderDragState,
396 Has<InteractionDisabled>,
397 ),
398 (
399 Or<(
400 Changed<SliderValue>,
401 Changed<SliderRange>,
402 Changed<Hovered>,
403 Changed<CoreSliderDragState>,
404 Added<InteractionDisabled>,
405 )>,
406 With<DemoSlider>,
407 ),
408 >,
409 children: Query<&Children>,
410 mut thumbs: Query<(&mut Node, &mut BackgroundColor, Has<DemoSliderThumb>), Without<DemoSlider>>,
411) {
412 for (slider_ent, value, range, hovered, drag_state, disabled) in sliders.iter() {
413 for child in children.iter_descendants(slider_ent) {
414 if let Ok((mut thumb_node, mut thumb_bg, is_thumb)) = thumbs.get_mut(child)
415 && is_thumb
416 {
417 thumb_node.left = percent(range.thumb_position(value.0) * 100.0);
418 thumb_bg.0 = thumb_color(disabled, hovered.0 | drag_state.dragging);
419 }
420 }
421 }
422}
423
424fn update_slider_style2(
425 sliders: Query<
426 (
427 Entity,
428 &Hovered,
429 &CoreSliderDragState,
430 Has<InteractionDisabled>,
431 ),
432 With<DemoSlider>,
433 >,
434 children: Query<&Children>,
435 mut thumbs: Query<(&mut BackgroundColor, Has<DemoSliderThumb>), Without<DemoSlider>>,
436 mut removed_disabled: RemovedComponents<InteractionDisabled>,
437) {
438 removed_disabled.read().for_each(|entity| {
439 if let Ok((slider_ent, hovered, drag_state, disabled)) = sliders.get(entity) {
440 for child in children.iter_descendants(slider_ent) {
441 if let Ok((mut thumb_bg, is_thumb)) = thumbs.get_mut(child)
442 && is_thumb
443 {
444 thumb_bg.0 = thumb_color(disabled, hovered.0 | drag_state.dragging);
445 }
446 }
447 }
448 });
449}
450
451fn thumb_color(disabled: bool, hovered: bool) -> Color {
452 match (disabled, hovered) {
453 (true, _) => ELEMENT_FILL_DISABLED,
454
455 (false, true) => SLIDER_THUMB.lighter(0.3),
456
457 _ => SLIDER_THUMB,
458 }
459}
460
461fn checkbox(asset_server: &AssetServer, caption: &str) -> impl Bundle {
463 (
464 Node {
465 display: Display::Flex,
466 flex_direction: FlexDirection::Row,
467 justify_content: JustifyContent::FlexStart,
468 align_items: AlignItems::Center,
469 align_content: AlignContent::Center,
470 column_gap: px(4),
471 ..default()
472 },
473 Name::new("Checkbox"),
474 Hovered::default(),
475 DemoCheckbox,
476 Checkbox,
477 TabIndex(0),
478 Children::spawn((
479 Spawn((
480 Node {
482 display: Display::Flex,
483 width: px(16),
484 height: px(16),
485 border: UiRect::all(px(2)),
486 ..default()
487 },
488 BorderColor::all(ELEMENT_OUTLINE), BorderRadius::all(px(3)),
490 children![
491 (
493 Node {
494 display: Display::Flex,
495 width: px(8),
496 height: px(8),
497 position_type: PositionType::Absolute,
498 left: px(2),
499 top: px(2),
500 ..default()
501 },
502 BackgroundColor(ELEMENT_FILL),
503 ),
504 ],
505 )),
506 Spawn((
507 Text::new(caption),
508 TextFont {
509 font: asset_server.load("fonts/FiraSans-Bold.ttf"),
510 font_size: 20.0,
511 ..default()
512 },
513 )),
514 )),
515 )
516}
517
518fn update_checkbox_or_radio_style(
520 mut q_checkbox: Query<
521 (Has<Checked>, &Hovered, Has<InteractionDisabled>, &Children),
522 (
523 Or<(With<DemoCheckbox>, With<DemoRadio>)>,
524 Or<(
525 Added<DemoCheckbox>,
526 Changed<Hovered>,
527 Added<Checked>,
528 Added<InteractionDisabled>,
529 )>,
530 ),
531 >,
532 mut q_border_color: Query<
533 (&mut BorderColor, &mut Children),
534 (Without<DemoCheckbox>, Without<DemoRadio>),
535 >,
536 mut q_bg_color: Query<&mut BackgroundColor, (Without<DemoCheckbox>, Without<Children>)>,
537) {
538 for (checked, Hovered(is_hovering), is_disabled, children) in q_checkbox.iter_mut() {
539 let Some(border_id) = children.first() else {
540 continue;
541 };
542
543 let Ok((mut border_color, border_children)) = q_border_color.get_mut(*border_id) else {
544 continue;
545 };
546
547 let Some(mark_id) = border_children.first() else {
548 warn!("Checkbox does not have a mark entity.");
549 continue;
550 };
551
552 let Ok(mut mark_bg) = q_bg_color.get_mut(*mark_id) else {
553 warn!("Checkbox mark entity lacking a background color.");
554 continue;
555 };
556
557 set_checkbox_or_radio_style(
558 is_disabled,
559 *is_hovering,
560 checked,
561 &mut border_color,
562 &mut mark_bg,
563 );
564 }
565}
566
567fn update_checkbox_or_radio_style2(
568 mut q_checkbox: Query<
569 (Has<Checked>, &Hovered, Has<InteractionDisabled>, &Children),
570 Or<(With<DemoCheckbox>, With<DemoRadio>)>,
571 >,
572 mut q_border_color: Query<
573 (&mut BorderColor, &mut Children),
574 (Without<DemoCheckbox>, Without<DemoRadio>),
575 >,
576 mut q_bg_color: Query<
577 &mut BackgroundColor,
578 (Without<DemoCheckbox>, Without<DemoRadio>, Without<Children>),
579 >,
580 mut removed_checked: RemovedComponents<Checked>,
581 mut removed_disabled: RemovedComponents<InteractionDisabled>,
582) {
583 removed_checked
584 .read()
585 .chain(removed_disabled.read())
586 .for_each(|entity| {
587 if let Ok((checked, Hovered(is_hovering), is_disabled, children)) =
588 q_checkbox.get_mut(entity)
589 {
590 let Some(border_id) = children.first() else {
591 return;
592 };
593
594 let Ok((mut border_color, border_children)) = q_border_color.get_mut(*border_id)
595 else {
596 return;
597 };
598
599 let Some(mark_id) = border_children.first() else {
600 warn!("Checkbox does not have a mark entity.");
601 return;
602 };
603
604 let Ok(mut mark_bg) = q_bg_color.get_mut(*mark_id) else {
605 warn!("Checkbox mark entity lacking a background color.");
606 return;
607 };
608
609 set_checkbox_or_radio_style(
610 is_disabled,
611 *is_hovering,
612 checked,
613 &mut border_color,
614 &mut mark_bg,
615 );
616 }
617 });
618}
619
620fn set_checkbox_or_radio_style(
621 disabled: bool,
622 hovering: bool,
623 checked: bool,
624 border_color: &mut BorderColor,
625 mark_bg: &mut BackgroundColor,
626) {
627 let color: Color = if disabled {
628 ELEMENT_OUTLINE.with_alpha(0.2)
630 } else if hovering {
631 ELEMENT_OUTLINE.lighter(0.2)
633 } else {
634 ELEMENT_OUTLINE
636 };
637
638 border_color.set_all(color);
640
641 let mark_color: Color = match (disabled, checked) {
642 (true, true) => ELEMENT_FILL_DISABLED,
643 (false, true) => ELEMENT_FILL,
644 (_, false) => Srgba::NONE.into(),
645 };
646
647 if mark_bg.0 != mark_color {
648 mark_bg.0 = mark_color;
650 }
651}
652
653fn radio_group(asset_server: &AssetServer) -> impl Bundle {
655 (
656 Node {
657 display: Display::Flex,
658 flex_direction: FlexDirection::Column,
659 align_items: AlignItems::Start,
660 column_gap: px(4),
661 ..default()
662 },
663 Name::new("RadioGroup"),
664 RadioGroup,
665 TabIndex::default(),
666 children![
667 (radio(asset_server, TrackClick::Drag, "Slider Drag"),),
668 (radio(asset_server, TrackClick::Step, "Slider Step"),),
669 (radio(asset_server, TrackClick::Snap, "Slider Snap"),)
670 ],
671 )
672}
673
674fn radio(asset_server: &AssetServer, value: TrackClick, caption: &str) -> impl Bundle {
676 (
677 Node {
678 display: Display::Flex,
679 flex_direction: FlexDirection::Row,
680 justify_content: JustifyContent::FlexStart,
681 align_items: AlignItems::Center,
682 align_content: AlignContent::Center,
683 column_gap: px(4),
684 ..default()
685 },
686 Name::new("RadioButton"),
687 Hovered::default(),
688 DemoRadio(value),
689 RadioButton,
690 Children::spawn((
691 Spawn((
692 Node {
694 display: Display::Flex,
695 width: px(16),
696 height: px(16),
697 border: UiRect::all(px(2)),
698 ..default()
699 },
700 BorderColor::all(ELEMENT_OUTLINE), BorderRadius::MAX,
702 children![
703 (
705 Node {
706 display: Display::Flex,
707 width: px(8),
708 height: px(8),
709 position_type: PositionType::Absolute,
710 left: px(2),
711 top: px(2),
712 ..default()
713 },
714 BorderRadius::MAX,
715 BackgroundColor(ELEMENT_FILL),
716 ),
717 ],
718 )),
719 Spawn((
720 Text::new(caption),
721 TextFont {
722 font: asset_server.load("fonts/FiraSans-Bold.ttf"),
723 font_size: 20.0,
724 ..default()
725 },
726 )),
727 )),
728 )
729}
730
731fn toggle_disabled(
732 input: Res<ButtonInput<KeyCode>>,
733 mut interaction_query: Query<
734 (Entity, Has<InteractionDisabled>),
735 Or<(
736 With<Button>,
737 With<Slider>,
738 With<Checkbox>,
739 With<RadioButton>,
740 )>,
741 >,
742 mut commands: Commands,
743) {
744 if input.just_pressed(KeyCode::KeyD) {
745 for (entity, disabled) in &mut interaction_query {
746 if disabled {
747 info!("Widget enabled");
748 commands.entity(entity).remove::<InteractionDisabled>();
749 } else {
750 info!("Widget disabled");
751 commands.entity(entity).insert(InteractionDisabled);
752 }
753 }
754 }
755}