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