testbed_ui/
ui.rs

1//! UI testbed
2//!
3//! You can switch scene by pressing the spacebar
4
5mod helpers;
6
7use bevy::prelude::*;
8use helpers::Next;
9
10fn main() {
11    let mut app = App::new();
12    app.add_plugins((DefaultPlugins,))
13        .init_state::<Scene>()
14        .add_systems(OnEnter(Scene::Image), image::setup)
15        .add_systems(OnEnter(Scene::Text), text::setup)
16        .add_systems(OnEnter(Scene::Grid), grid::setup)
17        .add_systems(OnEnter(Scene::Borders), borders::setup)
18        .add_systems(OnEnter(Scene::BoxShadow), box_shadow::setup)
19        .add_systems(OnEnter(Scene::TextWrap), text_wrap::setup)
20        .add_systems(OnEnter(Scene::Overflow), overflow::setup)
21        .add_systems(OnEnter(Scene::Slice), slice::setup)
22        .add_systems(OnEnter(Scene::LayoutRounding), layout_rounding::setup)
23        .add_systems(Update, switch_scene);
24
25    #[cfg(feature = "bevy_ci_testing")]
26    app.add_systems(Update, helpers::switch_scene_in_ci::<Scene>);
27
28    app.run();
29}
30
31#[derive(Debug, Clone, Eq, PartialEq, Hash, States, Default)]
32#[states(scoped_entities)]
33enum Scene {
34    #[default]
35    Image,
36    Text,
37    Grid,
38    Borders,
39    BoxShadow,
40    TextWrap,
41    Overflow,
42    Slice,
43    LayoutRounding,
44}
45
46impl Next for Scene {
47    fn next(&self) -> Self {
48        match self {
49            Scene::Image => Scene::Text,
50            Scene::Text => Scene::Grid,
51            Scene::Grid => Scene::Borders,
52            Scene::Borders => Scene::BoxShadow,
53            Scene::BoxShadow => Scene::TextWrap,
54            Scene::TextWrap => Scene::Overflow,
55            Scene::Overflow => Scene::Slice,
56            Scene::Slice => Scene::LayoutRounding,
57            Scene::LayoutRounding => Scene::Image,
58        }
59    }
60}
61
62fn switch_scene(
63    keyboard: Res<ButtonInput<KeyCode>>,
64    scene: Res<State<Scene>>,
65    mut next_scene: ResMut<NextState<Scene>>,
66) {
67    if keyboard.just_pressed(KeyCode::Space) {
68        info!("Switching scene");
69        next_scene.set(scene.get().next());
70    }
71}
72
73mod image {
74    use bevy::prelude::*;
75
76    pub fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
77        commands.spawn((Camera2d, StateScoped(super::Scene::Image)));
78        commands.spawn((
79            ImageNode::new(asset_server.load("branding/bevy_logo_dark.png")),
80            StateScoped(super::Scene::Image),
81        ));
82    }
83}
84
85mod text {
86    use bevy::prelude::*;
87
88    pub fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
89        commands.spawn((Camera2d, StateScoped(super::Scene::Text)));
90        commands.spawn((
91            Text::new("Hello World."),
92            TextFont {
93                font: asset_server.load("fonts/FiraSans-Bold.ttf"),
94                font_size: 200.,
95                ..default()
96            },
97            StateScoped(super::Scene::Text),
98        ));
99    }
100}
101
102mod grid {
103    use bevy::{color::palettes::css::*, prelude::*};
104
105    pub fn setup(mut commands: Commands) {
106        commands.spawn((Camera2d, StateScoped(super::Scene::Grid)));
107        // Top-level grid (app frame)
108        commands
109            .spawn((
110                Node {
111                    display: Display::Grid,
112                    width: Val::Percent(100.0),
113                    height: Val::Percent(100.0),
114                    grid_template_columns: vec![GridTrack::min_content(), GridTrack::flex(1.0)],
115                    grid_template_rows: vec![
116                        GridTrack::auto(),
117                        GridTrack::flex(1.0),
118                        GridTrack::px(40.),
119                    ],
120                    ..default()
121                },
122                BackgroundColor(Color::WHITE),
123                StateScoped(super::Scene::Grid),
124            ))
125            .with_children(|builder| {
126                // Header
127                builder.spawn((
128                    Node {
129                        display: Display::Grid,
130                        grid_column: GridPlacement::span(2),
131                        padding: UiRect::all(Val::Px(40.0)),
132                        ..default()
133                    },
134                    BackgroundColor(RED.into()),
135                ));
136
137                // Main content grid (auto placed in row 2, column 1)
138                builder
139                    .spawn((
140                        Node {
141                            height: Val::Percent(100.0),
142                            aspect_ratio: Some(1.0),
143                            display: Display::Grid,
144                            grid_template_columns: RepeatedGridTrack::flex(3, 1.0),
145                            grid_template_rows: RepeatedGridTrack::flex(2, 1.0),
146                            row_gap: Val::Px(12.0),
147                            column_gap: Val::Px(12.0),
148                            ..default()
149                        },
150                        BackgroundColor(Color::srgb(0.25, 0.25, 0.25)),
151                    ))
152                    .with_children(|builder| {
153                        builder.spawn((Node::default(), BackgroundColor(ORANGE.into())));
154                        builder.spawn((Node::default(), BackgroundColor(BISQUE.into())));
155                        builder.spawn((Node::default(), BackgroundColor(BLUE.into())));
156                        builder.spawn((Node::default(), BackgroundColor(CRIMSON.into())));
157                        builder.spawn((Node::default(), BackgroundColor(AQUA.into())));
158                    });
159
160                // Right side bar (auto placed in row 2, column 2)
161                builder.spawn((Node::DEFAULT, BackgroundColor(BLACK.into())));
162            });
163    }
164}
165
166mod borders {
167    use bevy::{color::palettes::css::*, prelude::*};
168
169    pub fn setup(mut commands: Commands) {
170        commands.spawn((Camera2d, StateScoped(super::Scene::Borders)));
171        let root = commands
172            .spawn((
173                Node {
174                    flex_wrap: FlexWrap::Wrap,
175                    ..default()
176                },
177                StateScoped(super::Scene::Borders),
178            ))
179            .id();
180
181        // all the different combinations of border edges
182        let borders = [
183            UiRect::default(),
184            UiRect::all(Val::Px(20.)),
185            UiRect::left(Val::Px(20.)),
186            UiRect::vertical(Val::Px(20.)),
187            UiRect {
188                left: Val::Px(40.),
189                top: Val::Px(20.),
190                ..Default::default()
191            },
192            UiRect {
193                right: Val::Px(20.),
194                bottom: Val::Px(30.),
195                ..Default::default()
196            },
197            UiRect {
198                right: Val::Px(20.),
199                top: Val::Px(40.),
200                bottom: Val::Px(20.),
201                ..Default::default()
202            },
203            UiRect {
204                left: Val::Px(20.),
205                top: Val::Px(20.),
206                bottom: Val::Px(20.),
207                ..Default::default()
208            },
209            UiRect {
210                left: Val::Px(20.),
211                right: Val::Px(20.),
212                bottom: Val::Px(40.),
213                ..Default::default()
214            },
215        ];
216
217        let non_zero = |x, y| x != Val::Px(0.) && y != Val::Px(0.);
218        let border_size = |x, y| if non_zero(x, y) { f32::MAX } else { 0. };
219
220        for border in borders {
221            for rounded in [true, false] {
222                let border_node = commands
223                    .spawn((
224                        Node {
225                            width: Val::Px(100.),
226                            height: Val::Px(100.),
227                            border,
228                            margin: UiRect::all(Val::Px(30.)),
229                            align_items: AlignItems::Center,
230                            justify_content: JustifyContent::Center,
231                            ..default()
232                        },
233                        BackgroundColor(MAROON.into()),
234                        BorderColor(RED.into()),
235                        Outline {
236                            width: Val::Px(10.),
237                            offset: Val::Px(10.),
238                            color: Color::WHITE,
239                        },
240                    ))
241                    .id();
242
243                if rounded {
244                    let border_radius = BorderRadius::px(
245                        border_size(border.left, border.top),
246                        border_size(border.right, border.top),
247                        border_size(border.right, border.bottom),
248                        border_size(border.left, border.bottom),
249                    );
250                    commands.entity(border_node).insert(border_radius);
251                }
252
253                commands.entity(root).add_child(border_node);
254            }
255        }
256    }
257}
258
259mod box_shadow {
260    use bevy::{color::palettes::css::*, prelude::*};
261
262    pub fn setup(mut commands: Commands) {
263        commands.spawn((Camera2d, StateScoped(super::Scene::BoxShadow)));
264
265        commands
266            .spawn((
267                Node {
268                    width: Val::Percent(100.0),
269                    height: Val::Percent(100.0),
270                    padding: UiRect::all(Val::Px(30.)),
271                    column_gap: Val::Px(200.),
272                    flex_wrap: FlexWrap::Wrap,
273                    ..default()
274                },
275                BackgroundColor(GREEN.into()),
276                StateScoped(super::Scene::BoxShadow),
277            ))
278            .with_children(|commands| {
279                let example_nodes = [
280                    (
281                        Vec2::splat(100.),
282                        Vec2::ZERO,
283                        10.,
284                        0.,
285                        BorderRadius::bottom_right(Val::Px(10.)),
286                    ),
287                    (Vec2::new(200., 50.), Vec2::ZERO, 10., 0., BorderRadius::MAX),
288                    (
289                        Vec2::new(100., 50.),
290                        Vec2::ZERO,
291                        10.,
292                        10.,
293                        BorderRadius::ZERO,
294                    ),
295                    (
296                        Vec2::splat(100.),
297                        Vec2::splat(20.),
298                        10.,
299                        10.,
300                        BorderRadius::bottom_right(Val::Px(10.)),
301                    ),
302                    (
303                        Vec2::splat(100.),
304                        Vec2::splat(50.),
305                        0.,
306                        10.,
307                        BorderRadius::ZERO,
308                    ),
309                    (
310                        Vec2::new(50., 100.),
311                        Vec2::splat(10.),
312                        0.,
313                        10.,
314                        BorderRadius::MAX,
315                    ),
316                ];
317
318                for (size, offset, spread, blur, border_radius) in example_nodes {
319                    commands.spawn((
320                        Node {
321                            width: Val::Px(size.x),
322                            height: Val::Px(size.y),
323                            border: UiRect::all(Val::Px(2.)),
324                            ..default()
325                        },
326                        BorderColor(WHITE.into()),
327                        border_radius,
328                        BackgroundColor(BLUE.into()),
329                        BoxShadow::new(
330                            Color::BLACK.with_alpha(0.9),
331                            Val::Percent(offset.x),
332                            Val::Percent(offset.y),
333                            Val::Percent(spread),
334                            Val::Px(blur),
335                        ),
336                    ));
337                }
338            });
339    }
340}
341
342mod text_wrap {
343    use bevy::prelude::*;
344
345    pub fn setup(mut commands: Commands) {
346        commands.spawn((Camera2d, StateScoped(super::Scene::TextWrap)));
347
348        let root = commands
349            .spawn((
350                Node {
351                    flex_direction: FlexDirection::Column,
352                    width: Val::Px(200.),
353                    height: Val::Percent(100.),
354                    overflow: Overflow::clip_x(),
355                    ..default()
356                },
357                BackgroundColor(Color::BLACK),
358                StateScoped(super::Scene::TextWrap),
359            ))
360            .id();
361
362        for linebreak in [
363            LineBreak::AnyCharacter,
364            LineBreak::WordBoundary,
365            LineBreak::WordOrCharacter,
366            LineBreak::NoWrap,
367        ] {
368            let messages = [
369                "Lorem ipsum dolor sit amet, consectetur adipiscing elit.".to_string(),
370                "pneumonoultramicroscopicsilicovolcanoconiosis".to_string(),
371            ];
372
373            for (j, message) in messages.into_iter().enumerate() {
374                commands.entity(root).with_child((
375                    Text(message.clone()),
376                    TextLayout::new(JustifyText::Left, linebreak),
377                    BackgroundColor(Color::srgb(0.8 - j as f32 * 0.3, 0., 0.)),
378                ));
379            }
380        }
381    }
382}
383
384mod overflow {
385    use bevy::{color::palettes::css::*, prelude::*};
386
387    pub fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
388        commands.spawn((Camera2d, StateScoped(super::Scene::Overflow)));
389        let image = asset_server.load("branding/icon.png");
390
391        commands
392            .spawn((
393                Node {
394                    width: Val::Percent(100.),
395                    height: Val::Percent(100.),
396                    align_items: AlignItems::Center,
397                    justify_content: JustifyContent::SpaceAround,
398                    ..Default::default()
399                },
400                BackgroundColor(BLUE.into()),
401                StateScoped(super::Scene::Overflow),
402            ))
403            .with_children(|parent| {
404                for overflow in [
405                    Overflow::visible(),
406                    Overflow::clip_x(),
407                    Overflow::clip_y(),
408                    Overflow::clip(),
409                ] {
410                    parent
411                        .spawn((
412                            Node {
413                                width: Val::Px(100.),
414                                height: Val::Px(100.),
415                                padding: UiRect {
416                                    left: Val::Px(25.),
417                                    top: Val::Px(25.),
418                                    ..Default::default()
419                                },
420                                border: UiRect::all(Val::Px(5.)),
421                                overflow,
422                                ..default()
423                            },
424                            BorderColor(RED.into()),
425                            BackgroundColor(Color::WHITE),
426                        ))
427                        .with_children(|parent| {
428                            parent.spawn((
429                                ImageNode::new(image.clone()),
430                                Node {
431                                    min_width: Val::Px(100.),
432                                    min_height: Val::Px(100.),
433                                    ..default()
434                                },
435                                Interaction::default(),
436                                Outline {
437                                    width: Val::Px(2.),
438                                    offset: Val::Px(2.),
439                                    color: Color::NONE,
440                                },
441                            ));
442                        });
443                }
444            });
445    }
446}
447
448mod slice {
449    use bevy::prelude::*;
450
451    pub fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
452        commands.spawn((Camera2d, StateScoped(super::Scene::Slice)));
453        let image = asset_server.load("textures/fantasy_ui_borders/numbered_slices.png");
454
455        let slicer = TextureSlicer {
456            border: BorderRect::all(16.0),
457            center_scale_mode: SliceScaleMode::Tile { stretch_value: 1.0 },
458            sides_scale_mode: SliceScaleMode::Tile { stretch_value: 1.0 },
459            ..default()
460        };
461        commands
462            .spawn((
463                Node {
464                    width: Val::Percent(100.0),
465                    height: Val::Percent(100.0),
466                    align_items: AlignItems::Center,
467                    justify_content: JustifyContent::SpaceAround,
468                    ..default()
469                },
470                StateScoped(super::Scene::Slice),
471            ))
472            .with_children(|parent| {
473                for [w, h] in [[150.0, 150.0], [300.0, 150.0], [150.0, 300.0]] {
474                    parent.spawn((
475                        Button,
476                        ImageNode {
477                            image: image.clone(),
478                            image_mode: NodeImageMode::Sliced(slicer.clone()),
479                            ..default()
480                        },
481                        Node {
482                            width: Val::Px(w),
483                            height: Val::Px(h),
484                            ..default()
485                        },
486                    ));
487                }
488            });
489    }
490}
491
492mod layout_rounding {
493    use bevy::{color::palettes::css::*, prelude::*};
494
495    pub fn setup(mut commands: Commands) {
496        commands.spawn((Camera2d, StateScoped(super::Scene::LayoutRounding)));
497
498        commands
499            .spawn((
500                Node {
501                    display: Display::Grid,
502                    width: Val::Percent(100.),
503                    height: Val::Percent(100.),
504                    grid_template_rows: vec![RepeatedGridTrack::fr(10, 1.)],
505                    ..Default::default()
506                },
507                BackgroundColor(Color::WHITE),
508                StateScoped(super::Scene::LayoutRounding),
509            ))
510            .with_children(|commands| {
511                for i in 2..12 {
512                    commands
513                        .spawn(Node {
514                            display: Display::Grid,
515                            grid_template_columns: vec![RepeatedGridTrack::fr(i, 1.)],
516                            ..Default::default()
517                        })
518                        .with_children(|commands| {
519                            for _ in 0..i {
520                                commands.spawn((
521                                    Node {
522                                        border: UiRect::all(Val::Px(5.)),
523                                        ..Default::default()
524                                    },
525                                    BackgroundColor(MAROON.into()),
526                                    BorderColor(DARK_BLUE.into()),
527                                ));
528                            }
529                        });
530                }
531            });
532    }
533}