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").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
409fn toggle_debug_overlay(
410 input: Res<ButtonInput<KeyCode>>,
411 mut debug_options: ResMut<GlobalUiDebugOptions>,
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
439pub 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}