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