Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/examples/testbed/full_ui.rs
6592 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
core_widgets::CoreScrollbar,
13
input::mouse::{MouseScrollUnit, MouseWheel},
14
picking::hover::HoverMap,
15
prelude::*,
16
ui::widget::NodeImageMode,
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"),
75
font_size: 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"),
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"),
100
font_size: 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"),
110
font_size: 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"),
119
font_size: 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"),
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"),
151
font_size: 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
..default()
194
},
195
Label,
196
AccessibilityNode(Accessible::new(Role::ListItem)),
197
))
198
.insert(Pickable {
199
should_block_lower: false,
200
..default()
201
});
202
}
203
});
204
});
205
});
206
207
parent
208
.spawn(Node {
209
left: px(210),
210
bottom: px(10),
211
position_type: PositionType::Absolute,
212
..default()
213
})
214
.with_children(|parent| {
215
parent
216
.spawn((
217
Node {
218
width: px(200),
219
height: px(200),
220
border: UiRect::all(px(20)),
221
flex_direction: FlexDirection::Column,
222
justify_content: JustifyContent::Center,
223
..default()
224
},
225
BorderColor::all(LIME),
226
BackgroundColor(Color::srgb(0.8, 0.8, 1.)),
227
))
228
.with_children(|parent| {
229
parent.spawn((
230
ImageNode::new(asset_server.load("branding/bevy_logo_light.png")),
231
// Uses the transform to rotate the logo image by 45 degrees
232
Node {
233
..Default::default()
234
},
235
UiTransform {
236
rotation: Rot2::radians(0.25 * PI),
237
..Default::default()
238
},
239
BorderRadius::all(px(10)),
240
Outline {
241
width: px(2),
242
offset: px(4),
243
color: DARK_GRAY.into(),
244
},
245
));
246
});
247
});
248
249
let shadow_style = ShadowStyle {
250
color: Color::BLACK.with_alpha(0.5),
251
blur_radius: px(2),
252
x_offset: px(10),
253
y_offset: px(10),
254
..default()
255
};
256
257
// render order test: reddest in the back, whitest in the front (flex center)
258
parent
259
.spawn(Node {
260
width: percent(100),
261
height: percent(100),
262
position_type: PositionType::Absolute,
263
align_items: AlignItems::Center,
264
justify_content: JustifyContent::Center,
265
..default()
266
})
267
.insert(Pickable::IGNORE)
268
.with_children(|parent| {
269
parent
270
.spawn((
271
Node {
272
width: px(100),
273
height: px(100),
274
..default()
275
},
276
BackgroundColor(Color::srgb(1.0, 0.0, 0.)),
277
BoxShadow::from(shadow_style),
278
))
279
.with_children(|parent| {
280
parent.spawn((
281
Node {
282
// Take the size of the parent node.
283
width: percent(100),
284
height: percent(100),
285
position_type: PositionType::Absolute,
286
left: px(20),
287
bottom: px(20),
288
..default()
289
},
290
BackgroundColor(Color::srgb(1.0, 0.3, 0.3)),
291
BoxShadow::from(shadow_style),
292
));
293
parent.spawn((
294
Node {
295
width: percent(100),
296
height: percent(100),
297
position_type: PositionType::Absolute,
298
left: px(40),
299
bottom: px(40),
300
..default()
301
},
302
BackgroundColor(Color::srgb(1.0, 0.5, 0.5)),
303
BoxShadow::from(shadow_style),
304
));
305
parent.spawn((
306
Node {
307
width: percent(100),
308
height: percent(100),
309
position_type: PositionType::Absolute,
310
left: px(60),
311
bottom: px(60),
312
..default()
313
},
314
BackgroundColor(Color::srgb(0.0, 0.7, 0.7)),
315
BoxShadow::from(shadow_style),
316
));
317
// alpha test
318
parent.spawn((
319
Node {
320
width: percent(100),
321
height: percent(100),
322
position_type: PositionType::Absolute,
323
left: px(80),
324
bottom: px(80),
325
..default()
326
},
327
BackgroundColor(Color::srgba(1.0, 0.9, 0.9, 0.4)),
328
BoxShadow::from(ShadowStyle {
329
color: Color::BLACK.with_alpha(0.3),
330
..shadow_style
331
}),
332
));
333
});
334
});
335
// bevy logo (flex center)
336
parent
337
.spawn(Node {
338
width: percent(100),
339
position_type: PositionType::Absolute,
340
justify_content: JustifyContent::Center,
341
align_items: AlignItems::FlexStart,
342
..default()
343
})
344
.with_children(|parent| {
345
// bevy logo (image)
346
parent
347
.spawn((
348
ImageNode::new(asset_server.load("branding/bevy_logo_dark_big.png"))
349
.with_mode(NodeImageMode::Stretch),
350
Node {
351
width: px(500),
352
height: px(125),
353
margin: UiRect::top(vmin(5)),
354
..default()
355
},
356
))
357
.with_children(|parent| {
358
// alt text
359
// This UI node takes up no space in the layout and the `Text` component is used by the accessibility module
360
// and is not rendered.
361
parent.spawn((
362
Node {
363
display: Display::None,
364
..default()
365
},
366
Text::new("Bevy logo"),
367
));
368
});
369
});
370
371
// four bevy icons demonstrating image flipping
372
parent
373
.spawn(Node {
374
width: percent(100),
375
height: percent(100),
376
position_type: PositionType::Absolute,
377
justify_content: JustifyContent::Center,
378
align_items: AlignItems::FlexEnd,
379
column_gap: px(10),
380
padding: UiRect::all(px(10)),
381
..default()
382
})
383
.insert(Pickable::IGNORE)
384
.with_children(|parent| {
385
for (flip_x, flip_y) in
386
[(false, false), (false, true), (true, true), (true, false)]
387
{
388
parent.spawn((
389
ImageNode {
390
image: asset_server.load("branding/icon.png"),
391
flip_x,
392
flip_y,
393
..default()
394
},
395
Node {
396
// The height will be chosen automatically to preserve the image's aspect ratio
397
width: px(75),
398
..default()
399
},
400
));
401
}
402
});
403
});
404
}
405
406
#[cfg(feature = "bevy_ui_debug")]
407
// The system that will enable/disable the debug outlines around the nodes
408
fn toggle_debug_overlay(
409
input: Res<ButtonInput<KeyCode>>,
410
mut debug_options: ResMut<UiDebugOptions>,
411
mut root_node_query: Query<&mut Visibility, (With<Node>, Without<ChildOf>)>,
412
) {
413
info_once!("The debug outlines are enabled, press Space to turn them on/off");
414
if input.just_pressed(KeyCode::Space) {
415
// The toggle method will enable the debug overlay if disabled and disable if enabled
416
debug_options.toggle();
417
}
418
419
if input.just_pressed(KeyCode::KeyS) {
420
// Toggle debug outlines for nodes with `ViewVisibility` set to false.
421
debug_options.show_hidden = !debug_options.show_hidden;
422
}
423
424
if input.just_pressed(KeyCode::KeyC) {
425
// Toggle outlines for clipped UI nodes.
426
debug_options.show_clipped = !debug_options.show_clipped;
427
}
428
429
if input.just_pressed(KeyCode::KeyV) {
430
for mut visibility in root_node_query.iter_mut() {
431
// Toggle the UI root node's visibility
432
visibility.toggle_inherited_hidden();
433
}
434
}
435
}
436
437
/// Updates the scroll position of scrollable nodes in response to mouse input
438
pub fn update_scroll_position(
439
mut mouse_wheel_events: EventReader<MouseWheel>,
440
hover_map: Res<HoverMap>,
441
mut scrolled_node_query: Query<(&mut ScrollPosition, &ComputedNode), Without<CoreScrollbar>>,
442
keyboard_input: Res<ButtonInput<KeyCode>>,
443
) {
444
for mouse_wheel_event in mouse_wheel_events.read() {
445
let (mut dx, mut dy) = match mouse_wheel_event.unit {
446
MouseScrollUnit::Line => (mouse_wheel_event.x * 20., mouse_wheel_event.y * 20.),
447
MouseScrollUnit::Pixel => (mouse_wheel_event.x, mouse_wheel_event.y),
448
};
449
450
if keyboard_input.pressed(KeyCode::ShiftLeft) || keyboard_input.pressed(KeyCode::ShiftRight)
451
{
452
std::mem::swap(&mut dx, &mut dy);
453
}
454
455
for (_pointer, pointer_map) in hover_map.iter() {
456
for (entity, _hit) in pointer_map.iter() {
457
if let Ok((mut scroll_position, scroll_content)) =
458
scrolled_node_query.get_mut(*entity)
459
{
460
let visible_size = scroll_content.size();
461
let content_size = scroll_content.content_size();
462
463
let range = (content_size.y - visible_size.y).max(0.)
464
* scroll_content.inverse_scale_factor;
465
466
scroll_position.x -= dx;
467
scroll_position.y = (scroll_position.y - dy).clamp(0., range);
468
}
469
}
470
}
471
}
472
}
473
474