testbed_full_ui/full_ui.rs
1//! This example illustrates the various features of Bevy UI.
2
3use std::f32::consts::PI;
4
5use accesskit::{Node as Accessible, Role};
6use 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
19fn 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
31fn 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
408fn 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
438pub fn update_scroll_position(
439 mut mouse_wheel_reader: MessageReader<MouseWheel>,
440 hover_map: Res<HoverMap>,
441 mut scrolled_node_query: Query<(&mut ScrollPosition, &ComputedNode), Without<Scrollbar>>,
442 keyboard_input: Res<ButtonInput<KeyCode>>,
443) {
444 for mouse_wheel in mouse_wheel_reader.read() {
445 let (mut dx, mut dy) = match mouse_wheel.unit {
446 MouseScrollUnit::Line => (mouse_wheel.x * 20., mouse_wheel.y * 20.),
447 MouseScrollUnit::Pixel => (mouse_wheel.x, mouse_wheel.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}