Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/examples/ui/standard_widgets_observers.rs
6849 views
1
//! This experimental example illustrates how to create widgets using the `bevy_ui_widgets` widget set.
2
//!
3
//! The patterns shown here are likely to change substantially as the `bevy_ui_widgets` crate
4
//! matures, so please exercise caution if you are using this as a reference for your own code,
5
//! and note that there are still "user experience" issues with this API.
6
7
use bevy::{
8
color::palettes::basic::*,
9
ecs::system::SystemId,
10
input_focus::{
11
tab_navigation::{TabGroup, TabIndex, TabNavigationPlugin},
12
InputDispatchPlugin,
13
},
14
picking::hover::Hovered,
15
prelude::*,
16
reflect::Is,
17
ui::{Checked, InteractionDisabled, Pressed},
18
ui_widgets::{
19
Activate, Button, Callback, Checkbox, Slider, SliderRange, SliderThumb, SliderValue,
20
UiWidgetsPlugins, ValueChange,
21
},
22
};
23
24
fn main() {
25
App::new()
26
.add_plugins((
27
DefaultPlugins,
28
UiWidgetsPlugins,
29
InputDispatchPlugin,
30
TabNavigationPlugin,
31
))
32
.insert_resource(DemoWidgetStates { slider_value: 50.0 })
33
.add_systems(Startup, setup)
34
.add_observer(button_on_interaction::<Add, Pressed>)
35
.add_observer(button_on_interaction::<Remove, Pressed>)
36
.add_observer(button_on_interaction::<Add, InteractionDisabled>)
37
.add_observer(button_on_interaction::<Remove, InteractionDisabled>)
38
.add_observer(button_on_interaction::<Insert, Hovered>)
39
.add_observer(slider_on_interaction::<Add, InteractionDisabled>)
40
.add_observer(slider_on_interaction::<Remove, InteractionDisabled>)
41
.add_observer(slider_on_interaction::<Insert, Hovered>)
42
.add_observer(slider_on_change_value::<SliderValue>)
43
.add_observer(slider_on_change_value::<SliderRange>)
44
.add_observer(checkbox_on_interaction::<Add, InteractionDisabled>)
45
.add_observer(checkbox_on_interaction::<Remove, InteractionDisabled>)
46
.add_observer(checkbox_on_interaction::<Insert, Hovered>)
47
.add_observer(checkbox_on_interaction::<Add, Checked>)
48
.add_observer(checkbox_on_interaction::<Remove, Checked>)
49
.add_systems(Update, (update_widget_values, toggle_disabled))
50
.run();
51
}
52
53
const NORMAL_BUTTON: Color = Color::srgb(0.15, 0.15, 0.15);
54
const HOVERED_BUTTON: Color = Color::srgb(0.25, 0.25, 0.25);
55
const PRESSED_BUTTON: Color = Color::srgb(0.35, 0.75, 0.35);
56
const SLIDER_TRACK: Color = Color::srgb(0.05, 0.05, 0.05);
57
const SLIDER_THUMB: Color = Color::srgb(0.35, 0.75, 0.35);
58
const CHECKBOX_OUTLINE: Color = Color::srgb(0.45, 0.45, 0.45);
59
const CHECKBOX_CHECK: Color = Color::srgb(0.35, 0.75, 0.35);
60
61
/// Marker which identifies buttons with a particular style, in this case the "Demo style".
62
#[derive(Component)]
63
struct DemoButton;
64
65
/// Marker which identifies sliders with a particular style.
66
#[derive(Component, Default)]
67
struct DemoSlider;
68
69
/// Marker which identifies the slider's thumb element.
70
#[derive(Component, Default)]
71
struct DemoSliderThumb;
72
73
/// Marker which identifies checkboxes with a particular style.
74
#[derive(Component, Default)]
75
struct DemoCheckbox;
76
77
/// A struct to hold the state of various widgets shown in the demo.
78
///
79
/// While it is possible to use the widget's own state components as the source of truth,
80
/// in many cases widgets will be used to display dynamic data coming from deeper within the app,
81
/// using some kind of data-binding. This example shows how to maintain an external source of
82
/// truth for widget states.
83
#[derive(Resource)]
84
struct DemoWidgetStates {
85
slider_value: f32,
86
}
87
88
fn setup(mut commands: Commands, assets: Res<AssetServer>) {
89
// System to print a value when the button is clicked.
90
let on_click = commands.register_system(|_: In<Activate>| {
91
info!("Button clicked!");
92
});
93
94
// System to update a resource when the slider value changes. Note that we could have
95
// updated the slider value directly, but we want to demonstrate externalizing the state.
96
let on_change_value = commands.register_system(
97
|value: In<ValueChange<f32>>, mut widget_states: ResMut<DemoWidgetStates>| {
98
widget_states.slider_value = value.0.value;
99
},
100
);
101
102
// ui camera
103
commands.spawn(Camera2d);
104
commands.spawn(demo_root(&assets, on_click, on_change_value));
105
}
106
107
fn demo_root(
108
asset_server: &AssetServer,
109
on_click: SystemId<In<Activate>>,
110
on_change_value: SystemId<In<ValueChange<f32>>>,
111
) -> impl Bundle {
112
(
113
Node {
114
width: percent(100),
115
height: percent(100),
116
align_items: AlignItems::Center,
117
justify_content: JustifyContent::Center,
118
display: Display::Flex,
119
flex_direction: FlexDirection::Column,
120
row_gap: px(10),
121
..default()
122
},
123
TabGroup::default(),
124
children![
125
button(asset_server, Callback::System(on_click)),
126
slider(0.0, 100.0, 50.0, Callback::System(on_change_value)),
127
checkbox(asset_server, "Checkbox", Callback::Ignore),
128
Text::new("Press 'D' to toggle widget disabled states"),
129
],
130
)
131
}
132
133
fn button(asset_server: &AssetServer, on_click: Callback<In<Activate>>) -> impl Bundle {
134
(
135
Node {
136
width: px(150),
137
height: px(65),
138
border: UiRect::all(px(5)),
139
justify_content: JustifyContent::Center,
140
align_items: AlignItems::Center,
141
..default()
142
},
143
DemoButton,
144
Button {
145
on_activate: on_click,
146
},
147
Hovered::default(),
148
TabIndex(0),
149
BorderColor::all(Color::BLACK),
150
BorderRadius::MAX,
151
BackgroundColor(NORMAL_BUTTON),
152
children![(
153
Text::new("Button"),
154
TextFont {
155
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
156
font_size: 33.0,
157
..default()
158
},
159
TextColor(Color::srgb(0.9, 0.9, 0.9)),
160
TextShadow::default(),
161
)],
162
)
163
}
164
165
fn button_on_interaction<E: EntityEvent, C: Component>(
166
event: On<E, C>,
167
mut buttons: Query<
168
(
169
&Hovered,
170
Has<InteractionDisabled>,
171
Has<Pressed>,
172
&mut BackgroundColor,
173
&mut BorderColor,
174
&Children,
175
),
176
With<DemoButton>,
177
>,
178
mut text_query: Query<&mut Text>,
179
) {
180
if let Ok((hovered, disabled, pressed, mut color, mut border_color, children)) =
181
buttons.get_mut(event.event_target())
182
{
183
if children.is_empty() {
184
return;
185
}
186
let Ok(mut text) = text_query.get_mut(children[0]) else {
187
return;
188
};
189
let hovered = hovered.get();
190
// These "removal event checks" exist because the `Remove` event is triggered _before_ the component is actually
191
// removed, meaning it still shows up in the query. We're investigating the best way to improve this scenario.
192
let pressed = pressed && !(E::is::<Remove>() && C::is::<Pressed>());
193
let disabled = disabled && !(E::is::<Remove>() && C::is::<InteractionDisabled>());
194
match (disabled, hovered, pressed) {
195
// Disabled button
196
(true, _, _) => {
197
**text = "Disabled".to_string();
198
*color = NORMAL_BUTTON.into();
199
border_color.set_all(GRAY);
200
}
201
202
// Pressed and hovered button
203
(false, true, true) => {
204
**text = "Press".to_string();
205
*color = PRESSED_BUTTON.into();
206
border_color.set_all(RED);
207
}
208
209
// Hovered, unpressed button
210
(false, true, false) => {
211
**text = "Hover".to_string();
212
*color = HOVERED_BUTTON.into();
213
border_color.set_all(WHITE);
214
}
215
216
// Unhovered button (either pressed or not).
217
(false, false, _) => {
218
**text = "Button".to_string();
219
*color = NORMAL_BUTTON.into();
220
border_color.set_all(BLACK);
221
}
222
}
223
}
224
}
225
226
/// Create a demo slider
227
fn slider(
228
min: f32,
229
max: f32,
230
value: f32,
231
on_change: Callback<In<ValueChange<f32>>>,
232
) -> impl Bundle {
233
(
234
Node {
235
display: Display::Flex,
236
flex_direction: FlexDirection::Column,
237
justify_content: JustifyContent::Center,
238
align_items: AlignItems::Stretch,
239
justify_items: JustifyItems::Center,
240
column_gap: px(4),
241
height: px(12),
242
width: percent(30),
243
..default()
244
},
245
Name::new("Slider"),
246
Hovered::default(),
247
DemoSlider,
248
Slider {
249
on_change,
250
..default()
251
},
252
SliderValue(value),
253
SliderRange::new(min, max),
254
TabIndex(0),
255
Children::spawn((
256
// Slider background rail
257
Spawn((
258
Node {
259
height: px(6),
260
..default()
261
},
262
BackgroundColor(SLIDER_TRACK), // Border color for the checkbox
263
BorderRadius::all(px(3)),
264
)),
265
// Invisible track to allow absolute placement of thumb entity. This is narrower than
266
// the actual slider, which allows us to position the thumb entity using simple
267
// percentages, without having to measure the actual width of the slider thumb.
268
Spawn((
269
Node {
270
display: Display::Flex,
271
position_type: PositionType::Absolute,
272
left: px(0),
273
// Track is short by 12px to accommodate the thumb.
274
right: px(12),
275
top: px(0),
276
bottom: px(0),
277
..default()
278
},
279
children![(
280
// Thumb
281
DemoSliderThumb,
282
SliderThumb,
283
Node {
284
display: Display::Flex,
285
width: px(12),
286
height: px(12),
287
position_type: PositionType::Absolute,
288
left: percent(0), // This will be updated by the slider's value
289
..default()
290
},
291
BorderRadius::MAX,
292
BackgroundColor(SLIDER_THUMB),
293
)],
294
)),
295
)),
296
)
297
}
298
299
fn slider_on_interaction<E: EntityEvent, C: Component>(
300
event: On<E, C>,
301
sliders: Query<(Entity, &Hovered, Has<InteractionDisabled>), With<DemoSlider>>,
302
children: Query<&Children>,
303
mut thumbs: Query<(&mut BackgroundColor, Has<DemoSliderThumb>), Without<DemoSlider>>,
304
) {
305
if let Ok((slider_ent, hovered, disabled)) = sliders.get(event.event_target()) {
306
// These "removal event checks" exist because the `Remove` event is triggered _before_ the component is actually
307
// removed, meaning it still shows up in the query. We're investigating the best way to improve this scenario.
308
let disabled = disabled && !(E::is::<Remove>() && C::is::<InteractionDisabled>());
309
for child in children.iter_descendants(slider_ent) {
310
if let Ok((mut thumb_bg, is_thumb)) = thumbs.get_mut(child)
311
&& is_thumb
312
{
313
thumb_bg.0 = thumb_color(disabled, hovered.0);
314
}
315
}
316
}
317
}
318
319
fn slider_on_change_value<C: Component>(
320
insert: On<Insert, C>,
321
sliders: Query<(Entity, &SliderValue, &SliderRange), With<DemoSlider>>,
322
children: Query<&Children>,
323
mut thumbs: Query<(&mut Node, Has<DemoSliderThumb>), Without<DemoSlider>>,
324
) {
325
if let Ok((slider_ent, value, range)) = sliders.get(insert.entity) {
326
for child in children.iter_descendants(slider_ent) {
327
if let Ok((mut thumb_node, is_thumb)) = thumbs.get_mut(child)
328
&& is_thumb
329
{
330
thumb_node.left = percent(range.thumb_position(value.0) * 100.0);
331
}
332
}
333
}
334
}
335
336
fn thumb_color(disabled: bool, hovered: bool) -> Color {
337
match (disabled, hovered) {
338
(true, _) => GRAY.into(),
339
340
(false, true) => SLIDER_THUMB.lighter(0.3),
341
342
_ => SLIDER_THUMB,
343
}
344
}
345
346
/// Create a demo checkbox
347
fn checkbox(
348
asset_server: &AssetServer,
349
caption: &str,
350
on_change: Callback<In<ValueChange<bool>>>,
351
) -> impl Bundle {
352
(
353
Node {
354
display: Display::Flex,
355
flex_direction: FlexDirection::Row,
356
justify_content: JustifyContent::FlexStart,
357
align_items: AlignItems::Center,
358
align_content: AlignContent::Center,
359
column_gap: px(4),
360
..default()
361
},
362
Name::new("Checkbox"),
363
Hovered::default(),
364
DemoCheckbox,
365
Checkbox { on_change },
366
TabIndex(0),
367
Children::spawn((
368
Spawn((
369
// Checkbox outer
370
Node {
371
display: Display::Flex,
372
width: px(16),
373
height: px(16),
374
border: UiRect::all(px(2)),
375
..default()
376
},
377
BorderColor::all(CHECKBOX_OUTLINE), // Border color for the checkbox
378
BorderRadius::all(px(3)),
379
children![
380
// Checkbox inner
381
(
382
Node {
383
display: Display::Flex,
384
width: px(8),
385
height: px(8),
386
position_type: PositionType::Absolute,
387
left: px(2),
388
top: px(2),
389
..default()
390
},
391
BackgroundColor(Srgba::NONE.into()),
392
),
393
],
394
)),
395
Spawn((
396
Text::new(caption),
397
TextFont {
398
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
399
font_size: 20.0,
400
..default()
401
},
402
)),
403
)),
404
)
405
}
406
407
fn checkbox_on_interaction<E: EntityEvent, C: Component>(
408
event: On<E, C>,
409
checkboxes: Query<
410
(&Hovered, Has<InteractionDisabled>, Has<Checked>, &Children),
411
With<DemoCheckbox>,
412
>,
413
mut borders: Query<(&mut BorderColor, &mut Children), Without<DemoCheckbox>>,
414
mut marks: Query<&mut BackgroundColor, (Without<DemoCheckbox>, Without<Children>)>,
415
) {
416
if let Ok((hovered, disabled, checked, children)) = checkboxes.get(event.event_target()) {
417
let hovered = hovered.get();
418
// These "removal event checks" exist because the `Remove` event is triggered _before_ the component is actually
419
// removed, meaning it still shows up in the query. We're investigating the best way to improve this scenario.
420
let checked = checked && !(E::is::<Remove>() && C::is::<Checked>());
421
let disabled = disabled && !(E::is::<Remove>() && C::is::<InteractionDisabled>());
422
423
let Some(border_id) = children.first() else {
424
return;
425
};
426
427
let Ok((mut border_color, border_children)) = borders.get_mut(*border_id) else {
428
return;
429
};
430
431
let Some(mark_id) = border_children.first() else {
432
warn!("Checkbox does not have a mark entity.");
433
return;
434
};
435
436
let Ok(mut mark_bg) = marks.get_mut(*mark_id) else {
437
warn!("Checkbox mark entity lacking a background color.");
438
return;
439
};
440
441
let color: Color = if disabled {
442
// If the checkbox is disabled, use a lighter color
443
CHECKBOX_OUTLINE.with_alpha(0.2)
444
} else if hovered {
445
// If hovering, use a lighter color
446
CHECKBOX_OUTLINE.lighter(0.2)
447
} else {
448
// Default color for the checkbox
449
CHECKBOX_OUTLINE
450
};
451
452
// Update the background color of the check mark
453
border_color.set_all(color);
454
455
let mark_color: Color = match (disabled, checked) {
456
(true, true) => CHECKBOX_CHECK.with_alpha(0.5),
457
(false, true) => CHECKBOX_CHECK,
458
(_, false) => Srgba::NONE.into(),
459
};
460
461
if mark_bg.0 != mark_color {
462
// Update the color of the check mark
463
mark_bg.0 = mark_color;
464
}
465
}
466
}
467
468
/// Update the widget states based on the changing resource.
469
fn update_widget_values(
470
res: Res<DemoWidgetStates>,
471
mut sliders: Query<Entity, With<DemoSlider>>,
472
mut commands: Commands,
473
) {
474
if res.is_changed() {
475
for slider_ent in sliders.iter_mut() {
476
commands
477
.entity(slider_ent)
478
.insert(SliderValue(res.slider_value));
479
}
480
}
481
}
482
483
fn toggle_disabled(
484
input: Res<ButtonInput<KeyCode>>,
485
mut interaction_query: Query<
486
(Entity, Has<InteractionDisabled>),
487
Or<(With<Button>, With<Slider>, With<Checkbox>)>,
488
>,
489
mut commands: Commands,
490
) {
491
if input.just_pressed(KeyCode::KeyD) {
492
for (entity, disabled) in &mut interaction_query {
493
if disabled {
494
info!("Widget enabled");
495
commands.entity(entity).remove::<InteractionDisabled>();
496
} else {
497
info!("Widget disabled");
498
commands.entity(entity).insert(InteractionDisabled);
499
}
500
}
501
}
502
}
503
504