1use bevy::{
8 color::palettes::basic::*,
9 input_focus::{
10 tab_navigation::{TabGroup, TabIndex, TabNavigationPlugin},
11 InputDispatchPlugin,
12 },
13 picking::hover::Hovered,
14 prelude::*,
15 reflect::Is,
16 ui::{Checked, InteractionDisabled, Pressed},
17 ui_widgets::{
18 checkbox_self_update, observe, Activate, Button, Checkbox, Slider, SliderRange,
19 SliderThumb, SliderValue, UiWidgetsPlugins, ValueChange,
20 },
21};
22
23fn main() {
24 App::new()
25 .add_plugins((
26 DefaultPlugins,
27 UiWidgetsPlugins,
28 InputDispatchPlugin,
29 TabNavigationPlugin,
30 ))
31 .insert_resource(DemoWidgetStates { slider_value: 50.0 })
32 .add_systems(Startup, setup)
33 .add_observer(button_on_interaction::<Add, Pressed>)
34 .add_observer(button_on_interaction::<Remove, Pressed>)
35 .add_observer(button_on_interaction::<Add, InteractionDisabled>)
36 .add_observer(button_on_interaction::<Remove, InteractionDisabled>)
37 .add_observer(button_on_interaction::<Insert, Hovered>)
38 .add_observer(slider_on_interaction::<Add, InteractionDisabled>)
39 .add_observer(slider_on_interaction::<Remove, InteractionDisabled>)
40 .add_observer(slider_on_interaction::<Insert, Hovered>)
41 .add_observer(slider_on_change_value::<SliderValue>)
42 .add_observer(slider_on_change_value::<SliderRange>)
43 .add_observer(checkbox_on_interaction::<Add, InteractionDisabled>)
44 .add_observer(checkbox_on_interaction::<Remove, InteractionDisabled>)
45 .add_observer(checkbox_on_interaction::<Insert, Hovered>)
46 .add_observer(checkbox_on_interaction::<Add, Checked>)
47 .add_observer(checkbox_on_interaction::<Remove, Checked>)
48 .add_systems(Update, (update_widget_values, toggle_disabled))
49 .run();
50}
51
52const NORMAL_BUTTON: Color = Color::srgb(0.15, 0.15, 0.15);
53const HOVERED_BUTTON: Color = Color::srgb(0.25, 0.25, 0.25);
54const PRESSED_BUTTON: Color = Color::srgb(0.35, 0.75, 0.35);
55const SLIDER_TRACK: Color = Color::srgb(0.05, 0.05, 0.05);
56const SLIDER_THUMB: Color = Color::srgb(0.35, 0.75, 0.35);
57const CHECKBOX_OUTLINE: Color = Color::srgb(0.45, 0.45, 0.45);
58const CHECKBOX_CHECK: Color = Color::srgb(0.35, 0.75, 0.35);
59
60#[derive(Component)]
62struct DemoButton;
63
64#[derive(Component, Default)]
66struct DemoSlider;
67
68#[derive(Component, Default)]
70struct DemoSliderThumb;
71
72#[derive(Component, Default)]
74struct DemoCheckbox;
75
76#[derive(Resource)]
83struct DemoWidgetStates {
84 slider_value: f32,
85}
86
87fn setup(mut commands: Commands, assets: Res<AssetServer>) {
88 commands.spawn(Camera2d);
90 commands.spawn(demo_root(&assets));
91}
92
93fn demo_root(asset_server: &AssetServer) -> impl Bundle {
94 (
95 Node {
96 width: percent(100),
97 height: percent(100),
98 align_items: AlignItems::Center,
99 justify_content: JustifyContent::Center,
100 display: Display::Flex,
101 flex_direction: FlexDirection::Column,
102 row_gap: px(10),
103 ..default()
104 },
105 TabGroup::default(),
106 children![
107 (
108 button(asset_server),
109 observe(|_activate: On<Activate>| {
110 info!("Button clicked!");
111 }),
112 ),
113 (
114 slider(0.0, 100.0, 50.0),
115 observe(
116 |value_change: On<ValueChange<f32>>,
117 mut widget_states: ResMut<DemoWidgetStates>| {
118 widget_states.slider_value = value_change.value;
119 },
120 )
121 ),
122 (
123 checkbox(asset_server, "Checkbox"),
124 observe(checkbox_self_update),
125 ),
126 Text::new("Press 'D' to toggle widget disabled states"),
127 ],
128 )
129}
130
131fn button(asset_server: &AssetServer) -> impl Bundle {
132 (
133 Node {
134 width: px(150),
135 height: px(65),
136 border: UiRect::all(px(5)),
137 justify_content: JustifyContent::Center,
138 align_items: AlignItems::Center,
139 ..default()
140 },
141 DemoButton,
142 Button,
143 Hovered::default(),
144 TabIndex(0),
145 BorderColor::all(Color::BLACK),
146 BorderRadius::MAX,
147 BackgroundColor(NORMAL_BUTTON),
148 children![(
149 Text::new("Button"),
150 TextFont {
151 font: asset_server.load("fonts/FiraSans-Bold.ttf"),
152 font_size: 33.0,
153 ..default()
154 },
155 TextColor(Color::srgb(0.9, 0.9, 0.9)),
156 TextShadow::default(),
157 )],
158 )
159}
160
161fn button_on_interaction<E: EntityEvent, C: Component>(
162 event: On<E, C>,
163 mut buttons: Query<
164 (
165 &Hovered,
166 Has<InteractionDisabled>,
167 Has<Pressed>,
168 &mut BackgroundColor,
169 &mut BorderColor,
170 &Children,
171 ),
172 With<DemoButton>,
173 >,
174 mut text_query: Query<&mut Text>,
175) {
176 if let Ok((hovered, disabled, pressed, mut color, mut border_color, children)) =
177 buttons.get_mut(event.event_target())
178 {
179 if children.is_empty() {
180 return;
181 }
182 let Ok(mut text) = text_query.get_mut(children[0]) else {
183 return;
184 };
185 let hovered = hovered.get();
186 let pressed = pressed && !(E::is::<Remove>() && C::is::<Pressed>());
189 let disabled = disabled && !(E::is::<Remove>() && C::is::<InteractionDisabled>());
190 match (disabled, hovered, pressed) {
191 (true, _, _) => {
193 **text = "Disabled".to_string();
194 *color = NORMAL_BUTTON.into();
195 border_color.set_all(GRAY);
196 }
197
198 (false, true, true) => {
200 **text = "Press".to_string();
201 *color = PRESSED_BUTTON.into();
202 border_color.set_all(RED);
203 }
204
205 (false, true, false) => {
207 **text = "Hover".to_string();
208 *color = HOVERED_BUTTON.into();
209 border_color.set_all(WHITE);
210 }
211
212 (false, false, _) => {
214 **text = "Button".to_string();
215 *color = NORMAL_BUTTON.into();
216 border_color.set_all(BLACK);
217 }
218 }
219 }
220}
221
222fn slider(min: f32, max: f32, value: f32) -> impl Bundle {
224 (
225 Node {
226 display: Display::Flex,
227 flex_direction: FlexDirection::Column,
228 justify_content: JustifyContent::Center,
229 align_items: AlignItems::Stretch,
230 justify_items: JustifyItems::Center,
231 column_gap: px(4),
232 height: px(12),
233 width: percent(30),
234 ..default()
235 },
236 Name::new("Slider"),
237 Hovered::default(),
238 DemoSlider,
239 Slider::default(),
240 SliderValue(value),
241 SliderRange::new(min, max),
242 TabIndex(0),
243 Children::spawn((
244 Spawn((
246 Node {
247 height: px(6),
248 ..default()
249 },
250 BackgroundColor(SLIDER_TRACK), BorderRadius::all(px(3)),
252 )),
253 Spawn((
257 Node {
258 display: Display::Flex,
259 position_type: PositionType::Absolute,
260 left: px(0),
261 right: px(12),
263 top: px(0),
264 bottom: px(0),
265 ..default()
266 },
267 children![(
268 DemoSliderThumb,
270 SliderThumb,
271 Node {
272 display: Display::Flex,
273 width: px(12),
274 height: px(12),
275 position_type: PositionType::Absolute,
276 left: percent(0), ..default()
278 },
279 BorderRadius::MAX,
280 BackgroundColor(SLIDER_THUMB),
281 )],
282 )),
283 )),
284 )
285}
286
287fn slider_on_interaction<E: EntityEvent, C: Component>(
288 event: On<E, C>,
289 sliders: Query<(Entity, &Hovered, Has<InteractionDisabled>), With<DemoSlider>>,
290 children: Query<&Children>,
291 mut thumbs: Query<(&mut BackgroundColor, Has<DemoSliderThumb>), Without<DemoSlider>>,
292) {
293 if let Ok((slider_ent, hovered, disabled)) = sliders.get(event.event_target()) {
294 let disabled = disabled && !(E::is::<Remove>() && C::is::<InteractionDisabled>());
297 for child in children.iter_descendants(slider_ent) {
298 if let Ok((mut thumb_bg, is_thumb)) = thumbs.get_mut(child)
299 && is_thumb
300 {
301 thumb_bg.0 = thumb_color(disabled, hovered.0);
302 }
303 }
304 }
305}
306
307fn slider_on_change_value<C: Component>(
308 insert: On<Insert, C>,
309 sliders: Query<(Entity, &SliderValue, &SliderRange), With<DemoSlider>>,
310 children: Query<&Children>,
311 mut thumbs: Query<(&mut Node, Has<DemoSliderThumb>), Without<DemoSlider>>,
312) {
313 if let Ok((slider_ent, value, range)) = sliders.get(insert.entity) {
314 for child in children.iter_descendants(slider_ent) {
315 if let Ok((mut thumb_node, is_thumb)) = thumbs.get_mut(child)
316 && is_thumb
317 {
318 thumb_node.left = percent(range.thumb_position(value.0) * 100.0);
319 }
320 }
321 }
322}
323
324fn thumb_color(disabled: bool, hovered: bool) -> Color {
325 match (disabled, hovered) {
326 (true, _) => GRAY.into(),
327
328 (false, true) => SLIDER_THUMB.lighter(0.3),
329
330 _ => SLIDER_THUMB,
331 }
332}
333
334fn checkbox(asset_server: &AssetServer, caption: &str) -> impl Bundle {
336 (
337 Node {
338 display: Display::Flex,
339 flex_direction: FlexDirection::Row,
340 justify_content: JustifyContent::FlexStart,
341 align_items: AlignItems::Center,
342 align_content: AlignContent::Center,
343 column_gap: px(4),
344 ..default()
345 },
346 Name::new("Checkbox"),
347 Hovered::default(),
348 DemoCheckbox,
349 Checkbox,
350 TabIndex(0),
351 Children::spawn((
352 Spawn((
353 Node {
355 display: Display::Flex,
356 width: px(16),
357 height: px(16),
358 border: UiRect::all(px(2)),
359 ..default()
360 },
361 BorderColor::all(CHECKBOX_OUTLINE), BorderRadius::all(px(3)),
363 children![
364 (
366 Node {
367 display: Display::Flex,
368 width: px(8),
369 height: px(8),
370 position_type: PositionType::Absolute,
371 left: px(2),
372 top: px(2),
373 ..default()
374 },
375 BackgroundColor(Srgba::NONE.into()),
376 ),
377 ],
378 )),
379 Spawn((
380 Text::new(caption),
381 TextFont {
382 font: asset_server.load("fonts/FiraSans-Bold.ttf"),
383 font_size: 20.0,
384 ..default()
385 },
386 )),
387 )),
388 )
389}
390
391fn checkbox_on_interaction<E: EntityEvent, C: Component>(
392 event: On<E, C>,
393 checkboxes: Query<
394 (&Hovered, Has<InteractionDisabled>, Has<Checked>, &Children),
395 With<DemoCheckbox>,
396 >,
397 mut borders: Query<(&mut BorderColor, &mut Children), Without<DemoCheckbox>>,
398 mut marks: Query<&mut BackgroundColor, (Without<DemoCheckbox>, Without<Children>)>,
399) {
400 if let Ok((hovered, disabled, checked, children)) = checkboxes.get(event.event_target()) {
401 let hovered = hovered.get();
402 let checked = checked && !(E::is::<Remove>() && C::is::<Checked>());
405 let disabled = disabled && !(E::is::<Remove>() && C::is::<InteractionDisabled>());
406
407 let Some(border_id) = children.first() else {
408 return;
409 };
410
411 let Ok((mut border_color, border_children)) = borders.get_mut(*border_id) else {
412 return;
413 };
414
415 let Some(mark_id) = border_children.first() else {
416 warn!("Checkbox does not have a mark entity.");
417 return;
418 };
419
420 let Ok(mut mark_bg) = marks.get_mut(*mark_id) else {
421 warn!("Checkbox mark entity lacking a background color.");
422 return;
423 };
424
425 let color: Color = if disabled {
426 CHECKBOX_OUTLINE.with_alpha(0.2)
428 } else if hovered {
429 CHECKBOX_OUTLINE.lighter(0.2)
431 } else {
432 CHECKBOX_OUTLINE
434 };
435
436 border_color.set_all(color);
438
439 let mark_color: Color = match (disabled, checked) {
440 (true, true) => CHECKBOX_CHECK.with_alpha(0.5),
441 (false, true) => CHECKBOX_CHECK,
442 (_, false) => Srgba::NONE.into(),
443 };
444
445 if mark_bg.0 != mark_color {
446 mark_bg.0 = mark_color;
448 }
449 }
450}
451
452fn update_widget_values(
454 res: Res<DemoWidgetStates>,
455 mut sliders: Query<Entity, With<DemoSlider>>,
456 mut commands: Commands,
457) {
458 if res.is_changed() {
459 for slider_ent in sliders.iter_mut() {
460 commands
461 .entity(slider_ent)
462 .insert(SliderValue(res.slider_value));
463 }
464 }
465}
466
467fn toggle_disabled(
468 input: Res<ButtonInput<KeyCode>>,
469 mut interaction_query: Query<
470 (Entity, Has<InteractionDisabled>),
471 Or<(With<Button>, With<Slider>, With<Checkbox>)>,
472 >,
473 mut commands: Commands,
474) {
475 if input.just_pressed(KeyCode::KeyD) {
476 for (entity, disabled) in &mut interaction_query {
477 if disabled {
478 info!("Widget enabled");
479 commands.entity(entity).remove::<InteractionDisabled>();
480 } else {
481 info!("Widget disabled");
482 commands.entity(entity).insert(InteractionDisabled);
483 }
484 }
485 }
486}