Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/examples/ui/widgets/standard_widgets_observers.rs
9334 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
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
23
fn 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
52
const NORMAL_BUTTON: Color = Color::srgb(0.15, 0.15, 0.15);
53
const HOVERED_BUTTON: Color = Color::srgb(0.25, 0.25, 0.25);
54
const PRESSED_BUTTON: Color = Color::srgb(0.35, 0.75, 0.35);
55
const SLIDER_TRACK: Color = Color::srgb(0.05, 0.05, 0.05);
56
const SLIDER_THUMB: Color = Color::srgb(0.35, 0.75, 0.35);
57
const CHECKBOX_OUTLINE: Color = Color::srgb(0.45, 0.45, 0.45);
58
const CHECKBOX_CHECK: Color = Color::srgb(0.35, 0.75, 0.35);
59
60
/// Marker which identifies buttons with a particular style, in this case the "Demo style".
61
#[derive(Component)]
62
struct DemoButton;
63
64
/// Marker which identifies sliders with a particular style.
65
#[derive(Component, Default)]
66
struct DemoSlider;
67
68
/// Marker which identifies the slider's thumb element.
69
#[derive(Component, Default)]
70
struct DemoSliderThumb;
71
72
/// Marker which identifies checkboxes with a particular style.
73
#[derive(Component, Default)]
74
struct DemoCheckbox;
75
76
/// A struct to hold the state of various widgets shown in the demo.
77
///
78
/// While it is possible to use the widget's own state components as the source of truth,
79
/// in many cases widgets will be used to display dynamic data coming from deeper within the app,
80
/// using some kind of data-binding. This example shows how to maintain an external source of
81
/// truth for widget states.
82
#[derive(Resource)]
83
struct DemoWidgetStates {
84
slider_value: f32,
85
}
86
87
fn setup(mut commands: Commands, assets: Res<AssetServer>) {
88
// ui camera
89
commands.spawn(Camera2d);
90
commands.spawn(demo_root(&assets));
91
}
92
93
fn 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
131
fn 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
border_radius: BorderRadius::MAX,
140
..default()
141
},
142
DemoButton,
143
Button,
144
Hovered::default(),
145
TabIndex(0),
146
BorderColor::all(Color::BLACK),
147
BackgroundColor(NORMAL_BUTTON),
148
children![(
149
Text::new("Button"),
150
TextFont {
151
font: asset_server.load("fonts/FiraSans-Bold.ttf").into(),
152
font_size: FontSize::Px(33.0),
153
..default()
154
},
155
TextColor(Color::srgb(0.9, 0.9, 0.9)),
156
TextShadow::default(),
157
)],
158
)
159
}
160
161
fn 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
// These "removal event checks" exist because the `Remove` event is triggered _before_ the component is actually
187
// removed, meaning it still shows up in the query. We're investigating the best way to improve this scenario.
188
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
// Disabled button
192
(true, _, _) => {
193
**text = "Disabled".to_string();
194
*color = NORMAL_BUTTON.into();
195
border_color.set_all(GRAY);
196
}
197
198
// Pressed and hovered button
199
(false, true, true) => {
200
**text = "Press".to_string();
201
*color = PRESSED_BUTTON.into();
202
border_color.set_all(RED);
203
}
204
205
// Hovered, unpressed button
206
(false, true, false) => {
207
**text = "Hover".to_string();
208
*color = HOVERED_BUTTON.into();
209
border_color.set_all(WHITE);
210
}
211
212
// Unhovered button (either pressed or not).
213
(false, false, _) => {
214
**text = "Button".to_string();
215
*color = NORMAL_BUTTON.into();
216
border_color.set_all(BLACK);
217
}
218
}
219
}
220
}
221
222
/// Create a demo slider
223
fn 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
// Slider background rail
245
Spawn((
246
Node {
247
height: px(6),
248
border_radius: BorderRadius::all(px(3)),
249
..default()
250
},
251
BackgroundColor(SLIDER_TRACK), // Border color for the checkbox
252
)),
253
// Invisible track to allow absolute placement of thumb entity. This is narrower than
254
// the actual slider, which allows us to position the thumb entity using simple
255
// percentages, without having to measure the actual width of the slider thumb.
256
Spawn((
257
Node {
258
display: Display::Flex,
259
position_type: PositionType::Absolute,
260
left: px(0),
261
// Track is short by 12px to accommodate the thumb.
262
right: px(12),
263
top: px(0),
264
bottom: px(0),
265
..default()
266
},
267
children![(
268
// Thumb
269
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), // This will be updated by the slider's value
277
border_radius: BorderRadius::MAX,
278
..default()
279
},
280
BackgroundColor(SLIDER_THUMB),
281
)],
282
)),
283
)),
284
)
285
}
286
287
fn 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
// These "removal event checks" exist because the `Remove` event is triggered _before_ the component is actually
295
// removed, meaning it still shows up in the query. We're investigating the best way to improve this scenario.
296
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
307
fn 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
324
fn 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
334
/// Create a demo checkbox
335
fn 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
// Checkbox outer
354
Node {
355
display: Display::Flex,
356
width: px(16),
357
height: px(16),
358
border: UiRect::all(px(2)),
359
border_radius: BorderRadius::all(px(3)),
360
..default()
361
},
362
BorderColor::all(CHECKBOX_OUTLINE), // Border color for the checkbox
363
children![
364
// Checkbox inner
365
(
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").into(),
383
font_size: FontSize::Px(20.0),
384
..default()
385
},
386
)),
387
)),
388
)
389
}
390
391
fn 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
// These "removal event checks" exist because the `Remove` event is triggered _before_ the component is actually
403
// removed, meaning it still shows up in the query. We're investigating the best way to improve this scenario.
404
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
// If the checkbox is disabled, use a lighter color
427
CHECKBOX_OUTLINE.with_alpha(0.2)
428
} else if hovered {
429
// If hovering, use a lighter color
430
CHECKBOX_OUTLINE.lighter(0.2)
431
} else {
432
// Default color for the checkbox
433
CHECKBOX_OUTLINE
434
};
435
436
// Update the background color of the check mark
437
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
// Update the color of the check mark
447
mark_bg.0 = mark_color;
448
}
449
}
450
}
451
452
/// Update the widget states based on the changing resource.
453
fn 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
467
fn 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
}
487
488