testbed for UI (#18091)

# Objective

- have a testbed for UI

## Solution

- move previous `ui` example to `full_ui`
- add a testbed ui with several scenes
- `ui_layout_rounding` is one of those scenes, so remove it as a
standalone example

the previous `ui` / new `full_ui` is I think still useful as it has some
things like scroll, debug ui that are not shown anywhere else
This commit is contained in:
François Mockers 2025-03-04 12:02:55 +01:00 committed by GitHub
parent ff1143ec87
commit 019a6fde25
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 944 additions and 469 deletions

View File

@ -1,6 +1,4 @@
(
events: [
(100, Screenshot),
(200, AppExit),
]
)

View File

@ -4168,11 +4168,11 @@ doc-scrape-examples = true
hidden = true
[[example]]
name = "testbed_ui_layout_rounding"
path = "examples/testbed/ui_layout_rounding.rs"
name = "testbed_full_ui"
path = "examples/testbed/full_ui.rs"
doc-scrape-examples = true
[package.metadata.example.testbed_ui_layout_rounding]
[package.metadata.example.testbed_full_ui]
hidden = true
[[example]]

436
examples/testbed/full_ui.rs Normal file
View File

@ -0,0 +1,436 @@
//! This example illustrates the various features of Bevy UI.
use std::f32::consts::PI;
use accesskit::{Node as Accessible, Role};
use bevy::{
a11y::AccessibilityNode,
color::palettes::{basic::LIME, css::DARK_GRAY},
input::mouse::{MouseScrollUnit, MouseWheel},
picking::hover::HoverMap,
prelude::*,
ui::widget::NodeImageMode,
};
fn main() {
let mut app = App::new();
app.add_plugins(DefaultPlugins)
.add_systems(Startup, setup)
.add_systems(Update, update_scroll_position);
#[cfg(feature = "bevy_ui_debug")]
app.add_systems(Update, toggle_debug_overlay);
app.run();
}
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
// Camera
commands.spawn((Camera2d, IsDefaultUiCamera, BoxShadowSamples(6)));
// root node
commands
.spawn(Node {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
justify_content: JustifyContent::SpaceBetween,
..default()
})
.insert(Pickable::IGNORE)
.with_children(|parent| {
// left vertical fill (border)
parent
.spawn((
Node {
width: Val::Px(200.),
border: UiRect::all(Val::Px(2.)),
..default()
},
BackgroundColor(Color::srgb(0.65, 0.65, 0.65)),
))
.with_children(|parent| {
// left vertical fill (content)
parent
.spawn((
Node {
width: Val::Percent(100.),
flex_direction: FlexDirection::Column,
padding: UiRect::all(Val::Px(5.)),
row_gap: Val::Px(5.),
..default()
},
BackgroundColor(Color::srgb(0.15, 0.15, 0.15)),
Visibility::Visible,
))
.with_children(|parent| {
// text
parent.spawn((
Text::new("Text Example"),
TextFont {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 25.0,
..default()
},
// Because this is a distinct label widget and
// not button/list item text, this is necessary
// for accessibility to treat the text accordingly.
Label,
));
#[cfg(feature = "bevy_ui_debug")]
{
// Debug overlay text
parent.spawn((
Text::new("Press Space to toggle debug outlines."),
TextFont {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
..default()
},
Label,
));
parent.spawn((
Text::new("V: toggle UI root's visibility"),
TextFont {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 12.,
..default()
},
Label,
));
parent.spawn((
Text::new("S: toggle outlines for hidden nodes"),
TextFont {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 12.,
..default()
},
Label,
));
parent.spawn((
Text::new("C: toggle outlines for clipped nodes"),
TextFont {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 12.,
..default()
},
Label,
));
}
#[cfg(not(feature = "bevy_ui_debug"))]
parent.spawn((
Text::new("Try enabling feature \"bevy_ui_debug\"."),
TextFont {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
..default()
},
Label,
));
});
});
// right vertical fill
parent
.spawn(Node {
flex_direction: FlexDirection::Column,
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
width: Val::Px(200.),
..default()
})
.with_children(|parent| {
// Title
parent.spawn((
Text::new("Scrolling list"),
TextFont {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 21.,
..default()
},
Label,
));
// Scrolling list
parent
.spawn((
Node {
flex_direction: FlexDirection::Column,
align_self: AlignSelf::Stretch,
height: Val::Percent(50.),
overflow: Overflow::scroll_y(),
..default()
},
BackgroundColor(Color::srgb(0.10, 0.10, 0.10)),
))
.with_children(|parent| {
// List items
for i in 0..25 {
parent
.spawn((
Text(format!("Item {i}")),
TextFont {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
..default()
},
Label,
AccessibilityNode(Accessible::new(Role::ListItem)),
))
.insert(Pickable {
should_block_lower: false,
..default()
});
}
});
});
parent
.spawn(Node {
left: Val::Px(210.),
bottom: Val::Px(10.),
position_type: PositionType::Absolute,
..default()
})
.with_children(|parent| {
parent
.spawn((
Node {
width: Val::Px(200.0),
height: Val::Px(200.0),
border: UiRect::all(Val::Px(20.)),
flex_direction: FlexDirection::Column,
justify_content: JustifyContent::Center,
..default()
},
BorderColor(LIME.into()),
BackgroundColor(Color::srgb(0.8, 0.8, 1.)),
))
.with_children(|parent| {
parent.spawn((
ImageNode::new(asset_server.load("branding/bevy_logo_light.png")),
// Uses the transform to rotate the logo image by 45 degrees
Transform::from_rotation(Quat::from_rotation_z(0.25 * PI)),
BorderRadius::all(Val::Px(10.)),
Outline {
width: Val::Px(2.),
offset: Val::Px(4.),
color: DARK_GRAY.into(),
},
));
});
});
let shadow_style = ShadowStyle {
color: Color::BLACK.with_alpha(0.5),
blur_radius: Val::Px(2.),
x_offset: Val::Px(10.),
y_offset: Val::Px(10.),
..default()
};
// render order test: reddest in the back, whitest in the front (flex center)
parent
.spawn(Node {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
position_type: PositionType::Absolute,
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..default()
})
.insert(Pickable::IGNORE)
.with_children(|parent| {
parent
.spawn((
Node {
width: Val::Px(100.0),
height: Val::Px(100.0),
..default()
},
BackgroundColor(Color::srgb(1.0, 0.0, 0.)),
BoxShadow::from(shadow_style),
))
.with_children(|parent| {
parent.spawn((
Node {
// Take the size of the parent node.
width: Val::Percent(100.0),
height: Val::Percent(100.0),
position_type: PositionType::Absolute,
left: Val::Px(20.),
bottom: Val::Px(20.),
..default()
},
BackgroundColor(Color::srgb(1.0, 0.3, 0.3)),
BoxShadow::from(shadow_style),
));
parent.spawn((
Node {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
position_type: PositionType::Absolute,
left: Val::Px(40.),
bottom: Val::Px(40.),
..default()
},
BackgroundColor(Color::srgb(1.0, 0.5, 0.5)),
BoxShadow::from(shadow_style),
));
parent.spawn((
Node {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
position_type: PositionType::Absolute,
left: Val::Px(60.),
bottom: Val::Px(60.),
..default()
},
BackgroundColor(Color::srgb(0.0, 0.7, 0.7)),
BoxShadow::from(shadow_style),
));
// alpha test
parent.spawn((
Node {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
position_type: PositionType::Absolute,
left: Val::Px(80.),
bottom: Val::Px(80.),
..default()
},
BackgroundColor(Color::srgba(1.0, 0.9, 0.9, 0.4)),
BoxShadow::from(ShadowStyle {
color: Color::BLACK.with_alpha(0.3),
..shadow_style
}),
));
});
});
// bevy logo (flex center)
parent
.spawn(Node {
width: Val::Percent(100.0),
position_type: PositionType::Absolute,
justify_content: JustifyContent::Center,
align_items: AlignItems::FlexStart,
..default()
})
.with_children(|parent| {
// bevy logo (image)
parent
.spawn((
ImageNode::new(asset_server.load("branding/bevy_logo_dark_big.png"))
.with_mode(NodeImageMode::Stretch),
Node {
width: Val::Px(500.0),
height: Val::Px(125.0),
margin: UiRect::top(Val::VMin(5.)),
..default()
},
))
.with_children(|parent| {
// alt text
// This UI node takes up no space in the layout and the `Text` component is used by the accessibility module
// and is not rendered.
parent.spawn((
Node {
display: Display::None,
..default()
},
Text::new("Bevy logo"),
));
});
});
// four bevy icons demonstrating image flipping
parent
.spawn(Node {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
position_type: PositionType::Absolute,
justify_content: JustifyContent::Center,
align_items: AlignItems::FlexEnd,
column_gap: Val::Px(10.),
padding: UiRect::all(Val::Px(10.)),
..default()
})
.insert(Pickable::IGNORE)
.with_children(|parent| {
for (flip_x, flip_y) in
[(false, false), (false, true), (true, true), (true, false)]
{
parent.spawn((
ImageNode {
image: asset_server.load("branding/icon.png"),
flip_x,
flip_y,
..default()
},
Node {
// The height will be chosen automatically to preserve the image's aspect ratio
width: Val::Px(75.),
..default()
},
));
}
});
});
}
#[cfg(feature = "bevy_ui_debug")]
// The system that will enable/disable the debug outlines around the nodes
fn toggle_debug_overlay(
input: Res<ButtonInput<KeyCode>>,
mut debug_options: ResMut<UiDebugOptions>,
mut root_node_query: Query<&mut Visibility, (With<Node>, Without<ChildOf>)>,
) {
info_once!("The debug outlines are enabled, press Space to turn them on/off");
if input.just_pressed(KeyCode::Space) {
// The toggle method will enable the debug overlay if disabled and disable if enabled
debug_options.toggle();
}
if input.just_pressed(KeyCode::KeyS) {
// Toggle debug outlines for nodes with `ViewVisibility` set to false.
debug_options.show_hidden = !debug_options.show_hidden;
}
if input.just_pressed(KeyCode::KeyC) {
// Toggle outlines for clipped UI nodes.
debug_options.show_clipped = !debug_options.show_clipped;
}
if input.just_pressed(KeyCode::KeyV) {
for mut visibility in root_node_query.iter_mut() {
// Toggle the UI root node's visibility
visibility.toggle_inherited_hidden();
}
}
}
/// Updates the scroll position of scrollable nodes in response to mouse input
pub fn update_scroll_position(
mut mouse_wheel_events: EventReader<MouseWheel>,
hover_map: Res<HoverMap>,
mut scrolled_node_query: Query<&mut ScrollPosition>,
keyboard_input: Res<ButtonInput<KeyCode>>,
) {
for mouse_wheel_event in mouse_wheel_events.read() {
let (mut dx, mut dy) = match mouse_wheel_event.unit {
MouseScrollUnit::Line => (mouse_wheel_event.x * 20., mouse_wheel_event.y * 20.),
MouseScrollUnit::Pixel => (mouse_wheel_event.x, mouse_wheel_event.y),
};
if keyboard_input.pressed(KeyCode::ShiftLeft) || keyboard_input.pressed(KeyCode::ShiftRight)
{
std::mem::swap(&mut dx, &mut dy);
}
for (_pointer, pointer_map) in hover_map.iter() {
for (entity, _hit) in pointer_map.iter() {
if let Ok(mut scroll_position) = scrolled_node_query.get_mut(*entity) {
scroll_position.offset_x -= dx;
scroll_position.offset_y -= dy;
}
}
}
}
}

View File

@ -1,436 +1,533 @@
//! This example illustrates the various features of Bevy UI.
//! UI testbed
//!
//! You can switch scene by pressing the spacebar
use std::f32::consts::PI;
mod helpers;
use accesskit::{Node as Accessible, Role};
use bevy::{
a11y::AccessibilityNode,
color::palettes::{basic::LIME, css::DARK_GRAY},
input::mouse::{MouseScrollUnit, MouseWheel},
picking::hover::HoverMap,
prelude::*,
ui::widget::NodeImageMode,
};
use bevy::prelude::*;
use helpers::Next;
fn main() {
let mut app = App::new();
app.add_plugins(DefaultPlugins)
.add_systems(Startup, setup)
.add_systems(Update, update_scroll_position);
app.add_plugins((DefaultPlugins,))
.init_state::<Scene>()
.add_systems(OnEnter(Scene::Image), image::setup)
.add_systems(OnEnter(Scene::Text), text::setup)
.add_systems(OnEnter(Scene::Grid), grid::setup)
.add_systems(OnEnter(Scene::Borders), borders::setup)
.add_systems(OnEnter(Scene::BoxShadow), box_shadow::setup)
.add_systems(OnEnter(Scene::TextWrap), text_wrap::setup)
.add_systems(OnEnter(Scene::Overflow), overflow::setup)
.add_systems(OnEnter(Scene::Slice), slice::setup)
.add_systems(OnEnter(Scene::LayoutRounding), layout_rounding::setup)
.add_systems(Update, switch_scene);
#[cfg(feature = "bevy_ui_debug")]
app.add_systems(Update, toggle_debug_overlay);
#[cfg(feature = "bevy_ci_testing")]
app.add_systems(Update, helpers::switch_scene_in_ci::<Scene>);
app.run();
}
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
// Camera
commands.spawn((Camera2d, IsDefaultUiCamera, BoxShadowSamples(6)));
#[derive(Debug, Clone, Eq, PartialEq, Hash, States, Default)]
#[states(scoped_entities)]
enum Scene {
#[default]
Image,
Text,
Grid,
Borders,
BoxShadow,
TextWrap,
Overflow,
Slice,
LayoutRounding,
}
// root node
commands
.spawn(Node {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
justify_content: JustifyContent::SpaceBetween,
..default()
})
.insert(Pickable::IGNORE)
.with_children(|parent| {
// left vertical fill (border)
parent
.spawn((
impl Next for Scene {
fn next(&self) -> Self {
match self {
Scene::Image => Scene::Text,
Scene::Text => Scene::Grid,
Scene::Grid => Scene::Borders,
Scene::Borders => Scene::BoxShadow,
Scene::BoxShadow => Scene::TextWrap,
Scene::TextWrap => Scene::Overflow,
Scene::Overflow => Scene::Slice,
Scene::Slice => Scene::LayoutRounding,
Scene::LayoutRounding => Scene::Image,
}
}
}
fn switch_scene(
keyboard: Res<ButtonInput<KeyCode>>,
scene: Res<State<Scene>>,
mut next_scene: ResMut<NextState<Scene>>,
) {
if keyboard.just_pressed(KeyCode::Space) {
info!("Switching scene");
next_scene.set(scene.get().next());
}
}
mod image {
use bevy::prelude::*;
pub fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn((Camera2d, StateScoped(super::Scene::Image)));
commands.spawn((
ImageNode::new(asset_server.load("branding/bevy_logo_dark.png")),
StateScoped(super::Scene::Image),
));
}
}
mod text {
use bevy::prelude::*;
pub fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn((Camera2d, StateScoped(super::Scene::Text)));
commands.spawn((
Text::new("Hello World."),
TextFont {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 200.,
..default()
},
StateScoped(super::Scene::Text),
));
}
}
mod grid {
use bevy::{color::palettes::css::*, prelude::*};
pub fn setup(mut commands: Commands) {
commands.spawn((Camera2d, StateScoped(super::Scene::Grid)));
// Top-level grid (app frame)
commands
.spawn((
Node {
display: Display::Grid,
width: Val::Percent(100.0),
height: Val::Percent(100.0),
grid_template_columns: vec![GridTrack::min_content(), GridTrack::flex(1.0)],
grid_template_rows: vec![
GridTrack::auto(),
GridTrack::flex(1.0),
GridTrack::px(40.),
],
..default()
},
BackgroundColor(Color::WHITE),
StateScoped(super::Scene::Grid),
))
.with_children(|builder| {
// Header
builder.spawn((
Node {
width: Val::Px(200.),
border: UiRect::all(Val::Px(2.)),
display: Display::Grid,
grid_column: GridPlacement::span(2),
padding: UiRect::all(Val::Px(40.0)),
..default()
},
BackgroundColor(Color::srgb(0.65, 0.65, 0.65)),
))
.with_children(|parent| {
// left vertical fill (content)
parent
.spawn((
Node {
width: Val::Percent(100.),
flex_direction: FlexDirection::Column,
padding: UiRect::all(Val::Px(5.)),
row_gap: Val::Px(5.),
..default()
},
BackgroundColor(Color::srgb(0.15, 0.15, 0.15)),
Visibility::Visible,
))
.with_children(|parent| {
// text
parent.spawn((
Text::new("Text Example"),
TextFont {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 25.0,
..default()
},
// Because this is a distinct label widget and
// not button/list item text, this is necessary
// for accessibility to treat the text accordingly.
Label,
));
BackgroundColor(RED.into()),
));
#[cfg(feature = "bevy_ui_debug")]
{
// Debug overlay text
parent.spawn((
Text::new("Press Space to toggle debug outlines."),
TextFont {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
..default()
},
Label,
));
parent.spawn((
Text::new("V: toggle UI root's visibility"),
TextFont {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 12.,
..default()
},
Label,
));
parent.spawn((
Text::new("S: toggle outlines for hidden nodes"),
TextFont {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 12.,
..default()
},
Label,
));
parent.spawn((
Text::new("C: toggle outlines for clipped nodes"),
TextFont {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 12.,
..default()
},
Label,
));
}
#[cfg(not(feature = "bevy_ui_debug"))]
parent.spawn((
Text::new("Try enabling feature \"bevy_ui_debug\"."),
TextFont {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
..default()
},
Label,
));
});
});
// right vertical fill
parent
.spawn(Node {
flex_direction: FlexDirection::Column,
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
width: Val::Px(200.),
..default()
})
.with_children(|parent| {
// Title
parent.spawn((
Text::new("Scrolling list"),
TextFont {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 21.,
// Main content grid (auto placed in row 2, column 1)
builder
.spawn((
Node {
height: Val::Percent(100.0),
aspect_ratio: Some(1.0),
display: Display::Grid,
grid_template_columns: RepeatedGridTrack::flex(3, 1.0),
grid_template_rows: RepeatedGridTrack::flex(2, 1.0),
row_gap: Val::Px(12.0),
column_gap: Val::Px(12.0),
..default()
},
Label,
));
// Scrolling list
parent
.spawn((
Node {
flex_direction: FlexDirection::Column,
align_self: AlignSelf::Stretch,
height: Val::Percent(50.),
overflow: Overflow::scroll_y(),
..default()
},
BackgroundColor(Color::srgb(0.10, 0.10, 0.10)),
))
.with_children(|parent| {
// List items
for i in 0..25 {
parent
.spawn((
Text(format!("Item {i}")),
TextFont {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
..default()
},
Label,
AccessibilityNode(Accessible::new(Role::ListItem)),
))
.insert(Pickable {
should_block_lower: false,
..default()
});
}
});
});
BackgroundColor(Color::srgb(0.25, 0.25, 0.25)),
))
.with_children(|builder| {
builder.spawn((Node::default(), BackgroundColor(ORANGE.into())));
builder.spawn((Node::default(), BackgroundColor(BISQUE.into())));
builder.spawn((Node::default(), BackgroundColor(BLUE.into())));
builder.spawn((Node::default(), BackgroundColor(CRIMSON.into())));
builder.spawn((Node::default(), BackgroundColor(AQUA.into())));
});
parent
.spawn(Node {
left: Val::Px(210.),
bottom: Val::Px(10.),
position_type: PositionType::Absolute,
..default()
})
.with_children(|parent| {
parent
.spawn((
Node {
width: Val::Px(200.0),
height: Val::Px(200.0),
border: UiRect::all(Val::Px(20.)),
flex_direction: FlexDirection::Column,
justify_content: JustifyContent::Center,
..default()
},
BorderColor(LIME.into()),
BackgroundColor(Color::srgb(0.8, 0.8, 1.)),
))
.with_children(|parent| {
parent.spawn((
ImageNode::new(asset_server.load("branding/bevy_logo_light.png")),
// Uses the transform to rotate the logo image by 45 degrees
Transform::from_rotation(Quat::from_rotation_z(0.25 * PI)),
BorderRadius::all(Val::Px(10.)),
Outline {
width: Val::Px(2.),
offset: Val::Px(4.),
color: DARK_GRAY.into(),
},
));
});
});
let shadow_style = ShadowStyle {
color: Color::BLACK.with_alpha(0.5),
blur_radius: Val::Px(2.),
x_offset: Val::Px(10.),
y_offset: Val::Px(10.),
..default()
};
// render order test: reddest in the back, whitest in the front (flex center)
parent
.spawn(Node {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
position_type: PositionType::Absolute,
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..default()
})
.insert(Pickable::IGNORE)
.with_children(|parent| {
parent
.spawn((
Node {
width: Val::Px(100.0),
height: Val::Px(100.0),
..default()
},
BackgroundColor(Color::srgb(1.0, 0.0, 0.)),
BoxShadow::from(shadow_style),
))
.with_children(|parent| {
parent.spawn((
Node {
// Take the size of the parent node.
width: Val::Percent(100.0),
height: Val::Percent(100.0),
position_type: PositionType::Absolute,
left: Val::Px(20.),
bottom: Val::Px(20.),
..default()
},
BackgroundColor(Color::srgb(1.0, 0.3, 0.3)),
BoxShadow::from(shadow_style),
));
parent.spawn((
Node {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
position_type: PositionType::Absolute,
left: Val::Px(40.),
bottom: Val::Px(40.),
..default()
},
BackgroundColor(Color::srgb(1.0, 0.5, 0.5)),
BoxShadow::from(shadow_style),
));
parent.spawn((
Node {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
position_type: PositionType::Absolute,
left: Val::Px(60.),
bottom: Val::Px(60.),
..default()
},
BackgroundColor(Color::srgb(0.0, 0.7, 0.7)),
BoxShadow::from(shadow_style),
));
// alpha test
parent.spawn((
Node {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
position_type: PositionType::Absolute,
left: Val::Px(80.),
bottom: Val::Px(80.),
..default()
},
BackgroundColor(Color::srgba(1.0, 0.9, 0.9, 0.4)),
BoxShadow::from(ShadowStyle {
color: Color::BLACK.with_alpha(0.3),
..shadow_style
}),
));
});
});
// bevy logo (flex center)
parent
.spawn(Node {
width: Val::Percent(100.0),
position_type: PositionType::Absolute,
justify_content: JustifyContent::Center,
align_items: AlignItems::FlexStart,
..default()
})
.with_children(|parent| {
// bevy logo (image)
parent
.spawn((
ImageNode::new(asset_server.load("branding/bevy_logo_dark_big.png"))
.with_mode(NodeImageMode::Stretch),
Node {
width: Val::Px(500.0),
height: Val::Px(125.0),
margin: UiRect::top(Val::VMin(5.)),
..default()
},
))
.with_children(|parent| {
// alt text
// This UI node takes up no space in the layout and the `Text` component is used by the accessibility module
// and is not rendered.
parent.spawn((
Node {
display: Display::None,
..default()
},
Text::new("Bevy logo"),
));
});
});
// four bevy icons demonstrating image flipping
parent
.spawn(Node {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
position_type: PositionType::Absolute,
justify_content: JustifyContent::Center,
align_items: AlignItems::FlexEnd,
column_gap: Val::Px(10.),
padding: UiRect::all(Val::Px(10.)),
..default()
})
.insert(Pickable::IGNORE)
.with_children(|parent| {
for (flip_x, flip_y) in
[(false, false), (false, true), (true, true), (true, false)]
{
parent.spawn((
ImageNode {
image: asset_server.load("branding/icon.png"),
flip_x,
flip_y,
..default()
},
Node {
// The height will be chosen automatically to preserve the image's aspect ratio
width: Val::Px(75.),
..default()
},
));
}
});
});
}
#[cfg(feature = "bevy_ui_debug")]
// The system that will enable/disable the debug outlines around the nodes
fn toggle_debug_overlay(
input: Res<ButtonInput<KeyCode>>,
mut debug_options: ResMut<UiDebugOptions>,
mut root_node_query: Query<&mut Visibility, (With<Node>, Without<ChildOf>)>,
) {
info_once!("The debug outlines are enabled, press Space to turn them on/off");
if input.just_pressed(KeyCode::Space) {
// The toggle method will enable the debug overlay if disabled and disable if enabled
debug_options.toggle();
}
if input.just_pressed(KeyCode::KeyS) {
// Toggle debug outlines for nodes with `ViewVisibility` set to false.
debug_options.show_hidden = !debug_options.show_hidden;
}
if input.just_pressed(KeyCode::KeyC) {
// Toggle outlines for clipped UI nodes.
debug_options.show_clipped = !debug_options.show_clipped;
}
if input.just_pressed(KeyCode::KeyV) {
for mut visibility in root_node_query.iter_mut() {
// Toggle the UI root node's visibility
visibility.toggle_inherited_hidden();
}
// Right side bar (auto placed in row 2, column 2)
builder.spawn((Node::DEFAULT, BackgroundColor(BLACK.into())));
});
}
}
/// Updates the scroll position of scrollable nodes in response to mouse input
pub fn update_scroll_position(
mut mouse_wheel_events: EventReader<MouseWheel>,
hover_map: Res<HoverMap>,
mut scrolled_node_query: Query<&mut ScrollPosition>,
keyboard_input: Res<ButtonInput<KeyCode>>,
) {
for mouse_wheel_event in mouse_wheel_events.read() {
let (mut dx, mut dy) = match mouse_wheel_event.unit {
MouseScrollUnit::Line => (mouse_wheel_event.x * 20., mouse_wheel_event.y * 20.),
MouseScrollUnit::Pixel => (mouse_wheel_event.x, mouse_wheel_event.y),
};
mod borders {
use bevy::{color::palettes::css::*, prelude::*};
if keyboard_input.pressed(KeyCode::ShiftLeft) || keyboard_input.pressed(KeyCode::ShiftRight)
{
std::mem::swap(&mut dx, &mut dy);
}
pub fn setup(mut commands: Commands) {
commands.spawn((Camera2d, StateScoped(super::Scene::Borders)));
let root = commands
.spawn((
Node {
flex_wrap: FlexWrap::Wrap,
..default()
},
StateScoped(super::Scene::Borders),
))
.id();
for (_pointer, pointer_map) in hover_map.iter() {
for (entity, _hit) in pointer_map.iter() {
if let Ok(mut scroll_position) = scrolled_node_query.get_mut(*entity) {
scroll_position.offset_x -= dx;
scroll_position.offset_y -= dy;
// all the different combinations of border edges
let borders = [
UiRect::default(),
UiRect::all(Val::Px(20.)),
UiRect::left(Val::Px(20.)),
UiRect::vertical(Val::Px(20.)),
UiRect {
left: Val::Px(40.),
top: Val::Px(20.),
..Default::default()
},
UiRect {
right: Val::Px(20.),
bottom: Val::Px(30.),
..Default::default()
},
UiRect {
right: Val::Px(20.),
top: Val::Px(40.),
bottom: Val::Px(20.),
..Default::default()
},
UiRect {
left: Val::Px(20.),
top: Val::Px(20.),
bottom: Val::Px(20.),
..Default::default()
},
UiRect {
left: Val::Px(20.),
right: Val::Px(20.),
bottom: Val::Px(40.),
..Default::default()
},
];
let non_zero = |x, y| x != Val::Px(0.) && y != Val::Px(0.);
let border_size = |x, y| if non_zero(x, y) { f32::MAX } else { 0. };
for border in borders {
for rounded in [true, false] {
let border_node = commands
.spawn((
Node {
width: Val::Px(100.),
height: Val::Px(100.),
border,
margin: UiRect::all(Val::Px(30.)),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..default()
},
BackgroundColor(MAROON.into()),
BorderColor(RED.into()),
Outline {
width: Val::Px(10.),
offset: Val::Px(10.),
color: Color::WHITE,
},
))
.id();
if rounded {
let border_radius = BorderRadius::px(
border_size(border.left, border.top),
border_size(border.right, border.top),
border_size(border.right, border.bottom),
border_size(border.left, border.bottom),
);
commands.entity(border_node).insert(border_radius);
}
commands.entity(root).add_child(border_node);
}
}
}
}
mod box_shadow {
use bevy::{color::palettes::css::*, prelude::*};
pub fn setup(mut commands: Commands) {
commands.spawn((Camera2d, StateScoped(super::Scene::BoxShadow)));
commands
.spawn((
Node {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
padding: UiRect::all(Val::Px(30.)),
column_gap: Val::Px(200.),
flex_wrap: FlexWrap::Wrap,
..default()
},
BackgroundColor(GREEN.into()),
StateScoped(super::Scene::BoxShadow),
))
.with_children(|commands| {
let example_nodes = [
(
Vec2::splat(100.),
Vec2::ZERO,
10.,
0.,
BorderRadius::bottom_right(Val::Px(10.)),
),
(Vec2::new(200., 50.), Vec2::ZERO, 10., 0., BorderRadius::MAX),
(
Vec2::new(100., 50.),
Vec2::ZERO,
10.,
10.,
BorderRadius::ZERO,
),
(
Vec2::splat(100.),
Vec2::splat(20.),
10.,
10.,
BorderRadius::bottom_right(Val::Px(10.)),
),
(
Vec2::splat(100.),
Vec2::splat(50.),
0.,
10.,
BorderRadius::ZERO,
),
(
Vec2::new(50., 100.),
Vec2::splat(10.),
0.,
10.,
BorderRadius::MAX,
),
];
for (size, offset, spread, blur, border_radius) in example_nodes {
commands.spawn((
Node {
width: Val::Px(size.x),
height: Val::Px(size.y),
border: UiRect::all(Val::Px(2.)),
..default()
},
BorderColor(WHITE.into()),
border_radius,
BackgroundColor(BLUE.into()),
BoxShadow::new(
Color::BLACK.with_alpha(0.9),
Val::Percent(offset.x),
Val::Percent(offset.y),
Val::Percent(spread),
Val::Px(blur),
),
));
}
});
}
}
mod text_wrap {
use bevy::prelude::*;
pub fn setup(mut commands: Commands) {
commands.spawn((Camera2d, StateScoped(super::Scene::TextWrap)));
let root = commands
.spawn((
Node {
flex_direction: FlexDirection::Column,
width: Val::Px(200.),
height: Val::Percent(100.),
overflow: Overflow::clip_x(),
..default()
},
BackgroundColor(Color::BLACK),
StateScoped(super::Scene::TextWrap),
))
.id();
for linebreak in [
LineBreak::AnyCharacter,
LineBreak::WordBoundary,
LineBreak::WordOrCharacter,
LineBreak::NoWrap,
] {
let messages = [
"Lorem ipsum dolor sit amet, consectetur adipiscing elit.".to_string(),
"pneumonoultramicroscopicsilicovolcanoconiosis".to_string(),
];
for (j, message) in messages.into_iter().enumerate() {
commands.entity(root).with_child((
Text(message.clone()),
TextLayout::new(JustifyText::Left, linebreak),
BackgroundColor(Color::srgb(0.8 - j as f32 * 0.3, 0., 0.)),
));
}
}
}
}
mod overflow {
use bevy::{color::palettes::css::*, prelude::*};
pub fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn((Camera2d, StateScoped(super::Scene::Overflow)));
let image = asset_server.load("branding/icon.png");
commands
.spawn((
Node {
width: Val::Percent(100.),
height: Val::Percent(100.),
align_items: AlignItems::Center,
justify_content: JustifyContent::SpaceAround,
..Default::default()
},
BackgroundColor(BLUE.into()),
StateScoped(super::Scene::Overflow),
))
.with_children(|parent| {
for overflow in [
Overflow::visible(),
Overflow::clip_x(),
Overflow::clip_y(),
Overflow::clip(),
] {
parent
.spawn((
Node {
width: Val::Px(100.),
height: Val::Px(100.),
padding: UiRect {
left: Val::Px(25.),
top: Val::Px(25.),
..Default::default()
},
border: UiRect::all(Val::Px(5.)),
overflow,
..default()
},
BorderColor(RED.into()),
BackgroundColor(Color::WHITE),
))
.with_children(|parent| {
parent.spawn((
ImageNode::new(image.clone()),
Node {
min_width: Val::Px(100.),
min_height: Val::Px(100.),
..default()
},
Interaction::default(),
Outline {
width: Val::Px(2.),
offset: Val::Px(2.),
color: Color::NONE,
},
));
});
}
});
}
}
mod slice {
use bevy::prelude::*;
pub fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn((Camera2d, StateScoped(super::Scene::Slice)));
let image = asset_server.load("textures/fantasy_ui_borders/numbered_slices.png");
let slicer = TextureSlicer {
border: BorderRect::all(16.0),
center_scale_mode: SliceScaleMode::Tile { stretch_value: 1.0 },
sides_scale_mode: SliceScaleMode::Tile { stretch_value: 1.0 },
..default()
};
commands
.spawn((
Node {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
align_items: AlignItems::Center,
justify_content: JustifyContent::SpaceAround,
..default()
},
StateScoped(super::Scene::Slice),
))
.with_children(|parent| {
for [w, h] in [[150.0, 150.0], [300.0, 150.0], [150.0, 300.0]] {
parent.spawn((
Button,
ImageNode {
image: image.clone(),
image_mode: NodeImageMode::Sliced(slicer.clone()),
..default()
},
Node {
width: Val::Px(w),
height: Val::Px(h),
..default()
},
));
}
});
}
}
mod layout_rounding {
use bevy::{color::palettes::css::*, prelude::*};
pub fn setup(mut commands: Commands) {
commands.spawn((Camera2d, StateScoped(super::Scene::LayoutRounding)));
commands
.spawn((
Node {
display: Display::Grid,
width: Val::Percent(100.),
height: Val::Percent(100.),
grid_template_rows: vec![RepeatedGridTrack::fr(10, 1.)],
..Default::default()
},
BackgroundColor(Color::WHITE),
StateScoped(super::Scene::LayoutRounding),
))
.with_children(|commands| {
for i in 2..12 {
commands
.spawn(Node {
display: Display::Grid,
grid_template_columns: vec![RepeatedGridTrack::fr(i, 1.)],
..Default::default()
})
.with_children(|commands| {
for _ in 0..i {
commands.spawn((
Node {
border: UiRect::all(Val::Px(5.)),
..Default::default()
},
BackgroundColor(MAROON.into()),
BorderColor(DARK_BLUE.into()),
));
}
});
}
});
}
}

View File

@ -1,56 +0,0 @@
//! Spawns a simple grid layout with nodes laid out covering a white background useful for catching layout rounding errors.
//! Any white lines seen are gaps in the layout are caused by coordinate rounding bugs.
use bevy::{
color::palettes::css::{DARK_BLUE, MAROON},
prelude::*,
ui::UiScale,
winit::WinitSettings,
};
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.insert_resource(WinitSettings::desktop_app())
.insert_resource(UiScale(1.5))
.add_systems(Startup, setup)
.run();
}
fn setup(mut commands: Commands) {
commands.spawn((Camera2d, UiAntiAlias::On));
commands
.spawn((
Node {
display: Display::Grid,
width: Val::Percent(100.),
height: Val::Percent(100.),
grid_template_rows: vec![RepeatedGridTrack::fr(10, 1.)],
..Default::default()
},
BackgroundColor(Color::WHITE),
))
.with_children(|commands| {
for i in 2..12 {
commands
.spawn(Node {
display: Display::Grid,
grid_template_columns: vec![RepeatedGridTrack::fr(i, 1.)],
..Default::default()
})
.with_children(|commands| {
for _ in 0..i {
commands.spawn((
Node {
border: UiRect::all(Val::Px(5.)),
..Default::default()
},
BackgroundColor(MAROON.into()),
BorderColor(DARK_BLUE.into()),
));
}
});
}
});
}