Skip to main content

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}