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}