Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/examples/testbed/full_ui.rs
9328 views
1
//! This example illustrates the various features of Bevy UI.
2
3
use std::f32::consts::PI;
4
5
use accesskit::{Node as Accessible, Role};
6
use bevy::{
7
a11y::AccessibilityNode,
8
color::palettes::{
9
basic::LIME,
10
css::{DARK_GRAY, NAVY},
11
},
12
input::mouse::{MouseScrollUnit, MouseWheel},
13
picking::hover::HoverMap,
14
prelude::*,
15
ui::widget::NodeImageMode,
16
ui_widgets::Scrollbar,
17
};
18
19
fn main() {
20
let mut app = App::new();
21
app.add_plugins(DefaultPlugins)
22
.add_systems(Startup, setup)
23
.add_systems(Update, update_scroll_position);
24
25
#[cfg(feature = "bevy_ui_debug")]
26
app.add_systems(Update, toggle_debug_overlay);
27
28
app.run();
29
}
30
31
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
32
// Camera
33
commands.spawn((Camera2d, IsDefaultUiCamera, BoxShadowSamples(6)));
34
35
// root node
36
commands
37
.spawn(Node {
38
width: percent(100),
39
height: percent(100),
40
justify_content: JustifyContent::SpaceBetween,
41
..default()
42
})
43
.insert(Pickable::IGNORE)
44
.with_children(|parent| {
45
// left vertical fill (border)
46
parent
47
.spawn((
48
Node {
49
width: px(200),
50
border: UiRect::all(px(2)),
51
..default()
52
},
53
BackgroundColor(Color::srgb(0.65, 0.65, 0.65)),
54
))
55
.with_children(|parent| {
56
// left vertical fill (content)
57
parent
58
.spawn((
59
Node {
60
width: percent(100),
61
flex_direction: FlexDirection::Column,
62
padding: UiRect::all(px(5)),
63
row_gap: px(5),
64
..default()
65
},
66
BackgroundColor(Color::srgb(0.15, 0.15, 0.15)),
67
Visibility::Visible,
68
))
69
.with_children(|parent| {
70
// text
71
parent.spawn((
72
Text::new("Text Example"),
73
TextFont {
74
font: asset_server.load("fonts/FiraSans-Bold.ttf").into(),
75
font_size: FontSize::Px(25.0),
76
..default()
77
},
78
// Because this is a distinct label widget and
79
// not button/list item text, this is necessary
80
// for accessibility to treat the text accordingly.
81
Label,
82
));
83
84
#[cfg(feature = "bevy_ui_debug")]
85
{
86
// Debug overlay text
87
parent.spawn((
88
Text::new("Press Space to toggle debug outlines."),
89
TextFont {
90
font: asset_server.load("fonts/FiraSans-Bold.ttf").into(),
91
..default()
92
},
93
Label,
94
));
95
96
parent.spawn((
97
Text::new("V: toggle UI root's visibility"),
98
TextFont {
99
font: asset_server.load("fonts/FiraSans-Bold.ttf").into(),
100
font_size: FontSize::Px(12.),
101
..default()
102
},
103
Label,
104
));
105
106
parent.spawn((
107
Text::new("S: toggle outlines for hidden nodes"),
108
TextFont {
109
font: asset_server.load("fonts/FiraSans-Bold.ttf").into(),
110
font_size: FontSize::Px(12.),
111
..default()
112
},
113
Label,
114
));
115
parent.spawn((
116
Text::new("C: toggle outlines for clipped nodes"),
117
TextFont {
118
font: asset_server.load("fonts/FiraSans-Bold.ttf").into(),
119
font_size: FontSize::Px(12.),
120
..default()
121
},
122
Label,
123
));
124
}
125
#[cfg(not(feature = "bevy_ui_debug"))]
126
parent.spawn((
127
Text::new("Try enabling feature \"bevy_ui_debug\"."),
128
TextFont {
129
font: asset_server.load("fonts/FiraSans-Bold.ttf").into(),
130
..default()
131
},
132
Label,
133
));
134
});
135
});
136
// right vertical fill
137
parent
138
.spawn(Node {
139
flex_direction: FlexDirection::Column,
140
justify_content: JustifyContent::Center,
141
align_items: AlignItems::Center,
142
width: px(200),
143
..default()
144
})
145
.with_children(|parent| {
146
// Title
147
parent.spawn((
148
Text::new("Scrolling list"),
149
TextFont {
150
font: asset_server.load("fonts/FiraSans-Bold.ttf").into(),
151
font_size: FontSize::Px(21.),
152
..default()
153
},
154
Label,
155
));
156
// Scrolling list
157
parent
158
.spawn((
159
Node {
160
flex_direction: FlexDirection::Column,
161
align_self: AlignSelf::Stretch,
162
height: percent(50),
163
overflow: Overflow::scroll_y(),
164
..default()
165
},
166
BackgroundColor(Color::srgb(0.10, 0.10, 0.10)),
167
))
168
.with_children(|parent| {
169
parent
170
.spawn((
171
Node {
172
flex_direction: FlexDirection::Column,
173
..Default::default()
174
},
175
BackgroundGradient::from(LinearGradient::to_bottom(vec![
176
ColorStop::auto(NAVY),
177
ColorStop::auto(Color::BLACK),
178
])),
179
Pickable {
180
should_block_lower: false,
181
..Default::default()
182
},
183
))
184
.with_children(|parent| {
185
// List items
186
for i in 0..25 {
187
parent
188
.spawn((
189
Text(format!("Item {i}")),
190
TextFont {
191
font: asset_server
192
.load("fonts/FiraSans-Bold.ttf")
193
.into(),
194
..default()
195
},
196
Label,
197
AccessibilityNode(Accessible::new(Role::ListItem)),
198
))
199
.insert(Pickable {
200
should_block_lower: false,
201
..default()
202
});
203
}
204
});
205
});
206
});
207
208
parent
209
.spawn(Node {
210
left: px(210),
211
bottom: px(10),
212
position_type: PositionType::Absolute,
213
..default()
214
})
215
.with_children(|parent| {
216
parent
217
.spawn((
218
Node {
219
width: px(200),
220
height: px(200),
221
border: UiRect::all(px(20)),
222
flex_direction: FlexDirection::Column,
223
justify_content: JustifyContent::Center,
224
..default()
225
},
226
BorderColor::all(LIME),
227
BackgroundColor(Color::srgb(0.8, 0.8, 1.)),
228
))
229
.with_children(|parent| {
230
parent.spawn((
231
ImageNode::new(asset_server.load("branding/bevy_logo_light.png")),
232
// Uses the transform to rotate the logo image by 45 degrees
233
Node {
234
border_radius: BorderRadius::all(px(10)),
235
..Default::default()
236
},
237
UiTransform {
238
rotation: Rot2::radians(0.25 * PI),
239
..Default::default()
240
},
241
Outline {
242
width: px(2),
243
offset: px(4),
244
color: DARK_GRAY.into(),
245
},
246
));
247
});
248
});
249
250
let shadow_style = ShadowStyle {
251
color: Color::BLACK.with_alpha(0.5),
252
blur_radius: px(2),
253
x_offset: px(10),
254
y_offset: px(10),
255
..default()
256
};
257
258
// render order test: reddest in the back, whitest in the front (flex center)
259
parent
260
.spawn(Node {
261
width: percent(100),
262
height: percent(100),
263
position_type: PositionType::Absolute,
264
align_items: AlignItems::Center,
265
justify_content: JustifyContent::Center,
266
..default()
267
})
268
.insert(Pickable::IGNORE)
269
.with_children(|parent| {
270
parent
271
.spawn((
272
Node {
273
width: px(100),
274
height: px(100),
275
..default()
276
},
277
BackgroundColor(Color::srgb(1.0, 0.0, 0.)),
278
BoxShadow::from(shadow_style),
279
))
280
.with_children(|parent| {
281
parent.spawn((
282
Node {
283
// Take the size of the parent node.
284
width: percent(100),
285
height: percent(100),
286
position_type: PositionType::Absolute,
287
left: px(20),
288
bottom: px(20),
289
..default()
290
},
291
BackgroundColor(Color::srgb(1.0, 0.3, 0.3)),
292
BoxShadow::from(shadow_style),
293
));
294
parent.spawn((
295
Node {
296
width: percent(100),
297
height: percent(100),
298
position_type: PositionType::Absolute,
299
left: px(40),
300
bottom: px(40),
301
..default()
302
},
303
BackgroundColor(Color::srgb(1.0, 0.5, 0.5)),
304
BoxShadow::from(shadow_style),
305
));
306
parent.spawn((
307
Node {
308
width: percent(100),
309
height: percent(100),
310
position_type: PositionType::Absolute,
311
left: px(60),
312
bottom: px(60),
313
..default()
314
},
315
BackgroundColor(Color::srgb(0.0, 0.7, 0.7)),
316
BoxShadow::from(shadow_style),
317
));
318
// alpha test
319
parent.spawn((
320
Node {
321
width: percent(100),
322
height: percent(100),
323
position_type: PositionType::Absolute,
324
left: px(80),
325
bottom: px(80),
326
..default()
327
},
328
BackgroundColor(Color::srgba(1.0, 0.9, 0.9, 0.4)),
329
BoxShadow::from(ShadowStyle {
330
color: Color::BLACK.with_alpha(0.3),
331
..shadow_style
332
}),
333
));
334
});
335
});
336
// bevy logo (flex center)
337
parent
338
.spawn(Node {
339
width: percent(100),
340
position_type: PositionType::Absolute,
341
justify_content: JustifyContent::Center,
342
align_items: AlignItems::FlexStart,
343
..default()
344
})
345
.with_children(|parent| {
346
// bevy logo (image)
347
parent
348
.spawn((
349
ImageNode::new(asset_server.load("branding/bevy_logo_dark_big.png"))
350
.with_mode(NodeImageMode::Stretch),
351
Node {
352
width: px(500),
353
height: px(125),
354
margin: UiRect::top(vmin(5)),
355
..default()
356
},
357
))
358
.with_children(|parent| {
359
// alt text
360
// This UI node takes up no space in the layout and the `Text` component is used by the accessibility module
361
// and is not rendered.
362
parent.spawn((
363
Node {
364
display: Display::None,
365
..default()
366
},
367
Text::new("Bevy logo"),
368
));
369
});
370
});
371
372
// four bevy icons demonstrating image flipping
373
parent
374
.spawn(Node {
375
width: percent(100),
376
height: percent(100),
377
position_type: PositionType::Absolute,
378
justify_content: JustifyContent::Center,
379
align_items: AlignItems::FlexEnd,
380
column_gap: px(10),
381
padding: UiRect::all(px(10)),
382
..default()
383
})
384
.insert(Pickable::IGNORE)
385
.with_children(|parent| {
386
for (flip_x, flip_y) in
387
[(false, false), (false, true), (true, true), (true, false)]
388
{
389
parent.spawn((
390
ImageNode {
391
image: asset_server.load("branding/icon.png"),
392
flip_x,
393
flip_y,
394
..default()
395
},
396
Node {
397
// The height will be chosen automatically to preserve the image's aspect ratio
398
width: px(75),
399
..default()
400
},
401
));
402
}
403
});
404
});
405
}
406
407
#[cfg(feature = "bevy_ui_debug")]
408
// The system that will enable/disable the debug outlines around the nodes
409
fn toggle_debug_overlay(
410
input: Res<ButtonInput<KeyCode>>,
411
mut debug_options: ResMut<UiDebugOptions>,
412
mut root_node_query: Query<&mut Visibility, (With<Node>, Without<ChildOf>)>,
413
) {
414
info_once!("The debug outlines are enabled, press Space to turn them on/off");
415
if input.just_pressed(KeyCode::Space) {
416
// The toggle method will enable the debug overlay if disabled and disable if enabled
417
debug_options.toggle();
418
}
419
420
if input.just_pressed(KeyCode::KeyS) {
421
// Toggle debug outlines for nodes with `ViewVisibility` set to false.
422
debug_options.show_hidden = !debug_options.show_hidden;
423
}
424
425
if input.just_pressed(KeyCode::KeyC) {
426
// Toggle outlines for clipped UI nodes.
427
debug_options.show_clipped = !debug_options.show_clipped;
428
}
429
430
if input.just_pressed(KeyCode::KeyV) {
431
for mut visibility in root_node_query.iter_mut() {
432
// Toggle the UI root node's visibility
433
visibility.toggle_inherited_hidden();
434
}
435
}
436
}
437
438
/// Updates the scroll position of scrollable nodes in response to mouse input
439
pub fn update_scroll_position(
440
mut mouse_wheel_reader: MessageReader<MouseWheel>,
441
hover_map: Res<HoverMap>,
442
mut scrolled_node_query: Query<(&mut ScrollPosition, &ComputedNode), Without<Scrollbar>>,
443
keyboard_input: Res<ButtonInput<KeyCode>>,
444
) {
445
for mouse_wheel in mouse_wheel_reader.read() {
446
let (mut dx, mut dy) = match mouse_wheel.unit {
447
MouseScrollUnit::Line => (mouse_wheel.x * 20., mouse_wheel.y * 20.),
448
MouseScrollUnit::Pixel => (mouse_wheel.x, mouse_wheel.y),
449
};
450
451
if keyboard_input.pressed(KeyCode::ShiftLeft) || keyboard_input.pressed(KeyCode::ShiftRight)
452
{
453
std::mem::swap(&mut dx, &mut dy);
454
}
455
456
for (_pointer, pointer_map) in hover_map.iter() {
457
for (entity, _hit) in pointer_map.iter() {
458
if let Ok((mut scroll_position, scroll_content)) =
459
scrolled_node_query.get_mut(*entity)
460
{
461
let visible_size = scroll_content.size();
462
let content_size = scroll_content.content_size();
463
464
let range = (content_size.y - visible_size.y).max(0.)
465
* scroll_content.inverse_scale_factor;
466
467
scroll_position.x -= dx;
468
scroll_position.y = (scroll_position.y - dy).clamp(0., range);
469
}
470
}
471
}
472
}
473
}
474
475