From 019a6fde2539690b3d942167ec3b7645c75d2c06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Mockers?= Date: Tue, 4 Mar 2025 12:02:55 +0100 Subject: [PATCH] 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 --- .github/example-run/testbed_ui.ron | 2 - Cargo.toml | 6 +- examples/testbed/full_ui.rs | 436 +++++++++++++ examples/testbed/ui.rs | 865 ++++++++++++++----------- examples/testbed/ui_layout_rounding.rs | 56 -- 5 files changed, 920 insertions(+), 445 deletions(-) create mode 100644 examples/testbed/full_ui.rs delete mode 100644 examples/testbed/ui_layout_rounding.rs diff --git a/.github/example-run/testbed_ui.ron b/.github/example-run/testbed_ui.ron index 579f791d66400..3e2b22dd983eb 100644 --- a/.github/example-run/testbed_ui.ron +++ b/.github/example-run/testbed_ui.ron @@ -1,6 +1,4 @@ ( events: [ - (100, Screenshot), - (200, AppExit), ] ) diff --git a/Cargo.toml b/Cargo.toml index 0cb5f97e9a9b9..2c5785346e303 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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]] diff --git a/examples/testbed/full_ui.rs b/examples/testbed/full_ui.rs new file mode 100644 index 0000000000000..4c28cf04cd60f --- /dev/null +++ b/examples/testbed/full_ui.rs @@ -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) { + // 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>, + mut debug_options: ResMut, + mut root_node_query: Query<&mut Visibility, (With, Without)>, +) { + 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, + hover_map: Res, + mut scrolled_node_query: Query<&mut ScrollPosition>, + keyboard_input: Res>, +) { + 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; + } + } + } + } +} diff --git a/examples/testbed/ui.rs b/examples/testbed/ui.rs index 4c28cf04cd60f..0693a4d098c18 100644 --- a/examples/testbed/ui.rs +++ b/examples/testbed/ui.rs @@ -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::() + .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::); app.run(); } -fn setup(mut commands: Commands, asset_server: Res) { - // 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>, + scene: Res>, + mut next_scene: ResMut>, +) { + 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) { + 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) { + 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, - )); + // 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() + }, + 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(( - Text::new("V: toggle UI root's visibility"), - TextFont { - font: asset_server.load("fonts/FiraSans-Bold.ttf"), - font_size: 12., - ..default() - }, - Label, - )); + // Right side bar (auto placed in row 2, column 2) + builder.spawn((Node::DEFAULT, BackgroundColor(BLACK.into()))); + }); + } +} - 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.), +mod borders { + use bevy::{color::palettes::css::*, prelude::*}; + + pub fn setup(mut commands: Commands) { + commands.spawn((Camera2d, StateScoped(super::Scene::Borders))); + let root = commands + .spawn(( + Node { + flex_wrap: FlexWrap::Wrap, ..default() - }) - .with_children(|parent| { - // Title - parent.spawn(( - Text::new("Scrolling list"), - TextFont { - font: asset_server.load("fonts/FiraSans-Bold.ttf"), - font_size: 21., + }, + StateScoped(super::Scene::Borders), + )) + .id(); + + // 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() }, - 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(MAROON.into()), + BorderColor(RED.into()), + Outline { + width: Val::Px(10.), + offset: Val::Px(10.), + color: Color::WHITE, + }, + )) + .id(); - 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(), - }, - )); - }); - }); + 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); + } - 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() - }; + commands.entity(root).add_child(border_node); + } + } + } +} + +mod box_shadow { + use bevy::{color::palettes::css::*, prelude::*}; - // render order test: reddest in the back, whitest in the front (flex center) - parent - .spawn(Node { + 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), - position_type: PositionType::Absolute, - align_items: AlignItems::Center, - justify_content: JustifyContent::Center, + padding: UiRect::all(Val::Px(30.)), + column_gap: Val::Px(200.), + flex_wrap: FlexWrap::Wrap, ..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, + }, + 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() - }) - .with_children(|parent| { - // bevy logo (image) + }, + 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) { + 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(( - 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.)), + 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| { - // 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(( + ImageNode::new(image.clone()), Node { - display: Display::None, + min_width: Val::Px(100.), + min_height: Val::Px(100.), ..default() }, - Text::new("Bevy logo"), + Interaction::default(), + Outline { + width: Val::Px(2.), + offset: Val::Px(2.), + color: Color::NONE, + }, )); }); - }); - - // 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>, - mut debug_options: ResMut, - mut root_node_query: Query<&mut Visibility, (With, Without)>, -) { - 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; - } +mod slice { + use bevy::prelude::*; - if input.just_pressed(KeyCode::KeyC) { - // Toggle outlines for clipped UI nodes. - debug_options.show_clipped = !debug_options.show_clipped; - } + pub fn setup(mut commands: Commands, asset_server: Res) { + commands.spawn((Camera2d, StateScoped(super::Scene::Slice))); + let image = asset_server.load("textures/fantasy_ui_borders/numbered_slices.png"); - 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(); - } + 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() + }, + )); + } + }); } } -/// Updates the scroll position of scrollable nodes in response to mouse input -pub fn update_scroll_position( - mut mouse_wheel_events: EventReader, - hover_map: Res, - mut scrolled_node_query: Query<&mut ScrollPosition>, - keyboard_input: Res>, -) { - 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 layout_rounding { + 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::LayoutRounding))); - 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; + 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()), + )); + } + }); } - } - } + }); } } diff --git a/examples/testbed/ui_layout_rounding.rs b/examples/testbed/ui_layout_rounding.rs deleted file mode 100644 index be653039d0471..0000000000000 --- a/examples/testbed/ui_layout_rounding.rs +++ /dev/null @@ -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()), - )); - } - }); - } - }); -}