//! UI testbed //! //! You can switch scene by pressing the spacebar mod helpers; use argh::FromArgs; use bevy::prelude::*; use helpers::Next; #[derive(FromArgs)] /// ui testbed pub struct Args { #[argh(positional)] scene: Option, } fn main() { #[cfg(not(target_arch = "wasm32"))] let args: Args = argh::from_env(); #[cfg(target_arch = "wasm32")] let args: Args = Args::from_args(&[], &[]).unwrap(); let mut app = App::new(); app.add_plugins(DefaultPlugins.set(WindowPlugin { primary_window: Some(Window { // The ViewportCoords scene relies on these specific viewport dimensions, // so let's explicitly define them and set resizable to false resolution: (1280, 720).into(), resizable: false, ..Default::default() }), ..Default::default() })) .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(OnEnter(Scene::LinearGradient), linear_gradient::setup) .add_systems(OnEnter(Scene::RadialGradient), radial_gradient::setup) .add_systems(OnEnter(Scene::Transformations), transformations::setup) .add_systems(OnEnter(Scene::ViewportCoords), viewport_coords::setup) .add_systems(OnEnter(Scene::OuterColor), outer_color::setup) .add_systems(OnEnter(Scene::BoxedContent), boxed_content::setup) .add_systems(OnEnter(Scene::EditableText), editable_text::setup) .add_systems(Update, switch_scene); match args.scene { None => app.init_state::(), Some(scene) => app.insert_state(scene), }; #[cfg(feature = "bevy_ui_debug")] { app.add_systems(OnEnter(Scene::DebugOutlines), debug_outlines::setup); app.add_systems(OnExit(Scene::DebugOutlines), debug_outlines::teardown); } #[cfg(feature = "bevy_ci_testing")] app.add_systems(Update, helpers::switch_scene_in_ci::); app.run(); } #[derive(Debug, Clone, Eq, PartialEq, Hash, States, Default)] #[states(scoped_entities)] enum Scene { #[default] Image, Text, Grid, Borders, BoxShadow, TextWrap, Overflow, Slice, LayoutRounding, LinearGradient, RadialGradient, Transformations, #[cfg(feature = "bevy_ui_debug")] DebugOutlines, ViewportCoords, OuterColor, BoxedContent, EditableText, } impl std::str::FromStr for Scene { type Err = String; fn from_str(s: &str) -> Result { let mut isit = Self::default(); while s.to_lowercase() != format!("{isit:?}").to_lowercase() { isit = isit.next(); if isit == Self::default() { return Err(format!("Invalid Scene name: {s}")); } } Ok(isit) } } 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::LinearGradient, Scene::LinearGradient => Scene::RadialGradient, #[cfg(feature = "bevy_ui_debug")] Scene::RadialGradient => Scene::DebugOutlines, #[cfg(feature = "bevy_ui_debug")] Scene::DebugOutlines => Scene::Transformations, #[cfg(not(feature = "bevy_ui_debug"))] Scene::RadialGradient => Scene::Transformations, Scene::Transformations => Scene::ViewportCoords, Scene::ViewportCoords => Scene::OuterColor, Scene::OuterColor => Scene::BoxedContent, Scene::BoxedContent => Scene::EditableText, Scene::EditableText => 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::color::palettes::css::DARK_GREY; use bevy::prelude::*; pub fn setup(mut commands: Commands, asset_server: Res) { commands.spawn((Camera2d, DespawnOnExit(super::Scene::Image))); commands .spawn(Node { width: percent(100.), height: percent(100.), flex_direction: FlexDirection::Column, justify_content: JustifyContent::SpaceAround, align_items: AlignItems::Stretch, ..default() }) .with_children(|parent| { for [b, p] in [[0, 0], [10, 0], [0, 10], [10, 10]] { for image_path in ["branding/icon.png", "branding/bevy_logo_dark.png"] { parent .spawn(Node { justify_content: JustifyContent::SpaceAround, align_items: AlignItems::Center, ..default() }) .with_children(|parent| { for visual_box in [ VisualBox::BorderBox, VisualBox::PaddingBox, VisualBox::ContentBox, ] { parent.spawn(( ImageNode { image: asset_server.load(image_path), visual_box, ..default() }, Node { border: px(b).all(), padding: px(p).all(), width: px(100.), ..default() }, DespawnOnExit(super::Scene::Image), Outline { color: DARK_GREY.into(), width: px(2.), ..default() }, )); } }); } } }); } } mod text { use bevy::{color::palettes::css::*, prelude::*, text::FontSmoothing}; pub fn setup(mut commands: Commands, asset_server: Res) { commands.spawn((Camera2d, DespawnOnExit(super::Scene::Text))); let mut container = commands.spawn(( Node { flex_direction: FlexDirection::Column, ..default() }, DespawnOnExit(super::Scene::Text), )); container.with_child(( Text::new("Hello World."), TextFont { font: asset_server.load("fonts/FiraSans-Bold.ttf").into(), font_size: FontSize::Px(200.), ..default() }, )); container.with_children(|builder| { let mut grid = builder.spawn(Node { display: Display::Grid, grid_template_columns: vec![GridTrack::flex(1.0), GridTrack::flex(1.0)], padding: UiRect::horizontal(px(5.)), ..default() }); grid.with_children(|grid| { for hinting in [FontHinting::Enabled, FontHinting::Disabled] { let mut content = grid.spawn(Node { flex_direction: FlexDirection::Column, row_gap: px(5.), ..default() }); content.with_child(( Text::new(format!("FontHinting::{:?}", hinting)), TextFont { font: asset_server.load("fonts/FiraSans-Bold.ttf").into(), ..default() }, hinting, )); content.with_child(( Text::new("white "), TextFont { font: asset_server.load("fonts/FiraSans-Bold.ttf").into(), ..default() }, hinting, children![ (TextSpan::new("red "), TextColor(RED.into()),), (TextSpan::new("green "), TextColor(GREEN.into()),), (TextSpan::new("blue "), TextColor(BLUE.into()),), ( TextSpan::new("black"), TextColor(Color::BLACK), TextFont { font: asset_server.load("fonts/FiraSans-Bold.ttf").into(), ..default() }, TextBackgroundColor(Color::WHITE) ), ], )); content.with_child(( Text::new(""), TextFont { font: asset_server.load("fonts/FiraSans-Bold.ttf").into(), ..default() }, hinting, children![ ( TextSpan::new("white "), TextFont { font: asset_server.load("fonts/FiraSans-Bold.ttf").into(), ..default() } ), (TextSpan::new("red "), TextColor(RED.into()),), (TextSpan::new("green "), TextColor(GREEN.into()),), (TextSpan::new("blue "), TextColor(BLUE.into()),), ( TextSpan::new("black"), TextColor(Color::BLACK), TextFont { font: asset_server.load("fonts/FiraSans-Bold.ttf").into(), ..default() }, TextBackgroundColor(Color::WHITE) ), ], )); content.with_child(( Text::new(""), TextFont { font: asset_server.load("fonts/FiraSans-Bold.ttf").into(), ..default() }, hinting, children![ (TextSpan::new(""), TextColor(YELLOW.into()),), TextSpan::new(""), ( TextSpan::new("white "), TextFont { font: asset_server.load("fonts/FiraSans-Bold.ttf").into(), ..default() } ), TextSpan::new(""), (TextSpan::new("red "), TextColor(RED.into()),), TextSpan::new(""), TextSpan::new(""), (TextSpan::new("green "), TextColor(GREEN.into()),), (TextSpan::new(""), TextColor(YELLOW.into()),), (TextSpan::new("blue "), TextColor(BLUE.into()),), TextSpan::new(""), (TextSpan::new(""), TextColor(YELLOW.into()),), ( TextSpan::new("black"), TextColor(Color::BLACK), TextFont { font: asset_server.load("fonts/FiraSans-Bold.ttf").into(), ..default() }, TextBackgroundColor(Color::WHITE) ), TextSpan::new(""), ], )); content.with_child(( hinting, Text::new("FiraSans_"), TextFont { font: asset_server.load("fonts/FiraSans-Bold.ttf").into(), font_size: FontSize::Px(25.), ..default() }, children![ ( TextSpan::new("MonaSans_"), TextFont { font: asset_server .load("fonts/MonaSans-VariableFont.ttf") .into(), font_size: FontSize::Px(25.), ..default() } ), ( TextSpan::new("EBGaramond_"), TextFont { font: asset_server .load("fonts/EBGaramond12-Regular.otf") .into(), font_size: FontSize::Px(25.), ..default() }, ), ( TextSpan::new("FiraMono"), TextFont { font: asset_server.load("fonts/FiraMono-Medium.ttf").into(), font_size: FontSize::Px(25.), ..default() }, ), ], )); content.with_child(( hinting, Text::new("FiraSans "), TextFont { font: asset_server.load("fonts/FiraSans-Bold.ttf").into(), font_size: FontSize::Px(25.), ..default() }, children![ ( TextSpan::new("MonaSans "), TextFont { font: asset_server .load("fonts/MonaSans-VariableFont.ttf") .into(), font_size: FontSize::Px(25.), ..default() } ), ( TextSpan::new("EBGaramond "), TextFont { font: asset_server .load("fonts/EBGaramond12-Regular.otf") .into(), font_size: FontSize::Px(25.), ..default() }, ), ( TextSpan::new("FiraMono"), TextFont { font: asset_server.load("fonts/FiraMono-Medium.ttf").into(), font_size: FontSize::Px(25.), ..default() }, ), ], )); content.with_child(( hinting, Text::new("FiraSans "), TextFont { font: asset_server.load("fonts/FiraSans-Bold.ttf").into(), font_size: FontSize::Px(25.), ..default() }, children![ ( TextSpan::new("MonaSans_"), TextFont { font: asset_server .load("fonts/MonaSans-VariableFont.ttf") .into(), font_size: FontSize::Px(25.), ..default() } ), ( TextSpan::new("EBGaramond "), TextFont { font: asset_server .load("fonts/EBGaramond12-Regular.otf") .into(), font_size: FontSize::Px(25.), ..default() }, ), ( TextSpan::new("FiraMono"), TextFont { font: asset_server.load("fonts/FiraMono-Medium.ttf").into(), font_size: FontSize::Px(25.), ..default() }, ), ], )); content.with_child(( hinting, Text::new("FiraSans"), TextFont { font: asset_server.load("fonts/FiraSans-Bold.ttf").into(), font_size: FontSize::Px(25.), ..default() }, children![ TextSpan::new(" "), ( TextSpan::new("MonaSans"), TextFont { font: asset_server .load("fonts/MonaSans-VariableFont.ttf") .into(), font_size: FontSize::Px(25.), ..default() } ), TextSpan::new(" "), ( TextSpan::new("EBGaramond"), TextFont { font: asset_server .load("fonts/EBGaramond12-Regular.otf") .into(), font_size: FontSize::Px(25.), ..default() }, ), TextSpan::new(" "), ( TextSpan::new("FiraMono"), TextFont { font: asset_server.load("fonts/FiraMono-Medium.ttf").into(), font_size: FontSize::Px(25.), ..default() }, ), ], )); content.with_child(( hinting, Text::new("Fira Sans_"), TextFont { font: asset_server.load("fonts/FiraSans-Bold.ttf").into(), font_size: FontSize::Px(25.), ..default() }, children![ ( TextSpan::new("Mona Sans_"), TextFont { font: asset_server .load("fonts/MonaSans-VariableFont.ttf") .into(), font_size: FontSize::Px(25.), ..default() } ), ( TextSpan::new("EB Garamond_"), TextFont { font: asset_server .load("fonts/EBGaramond12-Regular.otf") .into(), font_size: FontSize::Px(25.), ..default() }, ), ( TextSpan::new("Fira Mono"), TextFont { font: asset_server.load("fonts/FiraMono-Medium.ttf").into(), font_size: FontSize::Px(25.), ..default() }, ), ], )); content.with_child(( hinting, Text::new("FontWeight(100)_"), TextFont { font: "Mona Sans".into(), font_size: FontSize::Px(25.), weight: FontWeight(100), ..default() }, children![ ( TextSpan::new("FontWeight(500)_"), TextFont { font: "Mona Sans".into(), font_size: FontSize::Px(25.), weight: FontWeight(500), ..default() } ), ( TextSpan::new("FontWeight(900)"), TextFont { font: "Mona Sans".into(), font_size: FontSize::Px(25.), weight: FontWeight(900), ..default() }, ), ], )); content.with_child(( hinting, Text::new("FiraSans_"), TextFont { font: asset_server.load("fonts/FiraSans-Bold.ttf").into(), font_size: FontSize::Px(25.), weight: FontWeight(900), ..default() }, children![ ( TextSpan::new("MonaSans_"), TextFont { font: asset_server .load("fonts/MonaSans-VariableFont.ttf") .into(), font_size: FontSize::Px(25.), weight: FontWeight(700), ..default() } ), ( TextSpan::new("EBGaramond_"), TextFont { font: asset_server .load("fonts/EBGaramond12-Regular.otf") .into(), font_size: FontSize::Px(25.), weight: FontWeight(500), ..default() }, ), ( TextSpan::new("FiraMono"), TextFont { font: asset_server.load("fonts/FiraMono-Medium.ttf").into(), font_size: FontSize::Px(25.), weight: FontWeight(300), ..default() }, ), ], )); content.with_child(( hinting, Text::new("FiraSans\t"), TextFont { font: asset_server.load("fonts/FiraSans-Bold.ttf").into(), font_size: FontSize::Px(25.), ..default() }, children![ ( TextSpan::new("MonaSans\t"), TextFont { font: asset_server .load("fonts/MonaSans-VariableFont.ttf") .into(), font_size: FontSize::Px(25.), ..default() } ), ( TextSpan::new("EBGaramond\t"), TextFont { font: asset_server .load("fonts/EBGaramond12-Regular.otf") .into(), font_size: FontSize::Px(25.), ..default() }, ), ( TextSpan::new("FiraMono"), TextFont { font: asset_server.load("fonts/FiraMono-Medium.ttf").into(), font_size: FontSize::Px(25.), ..default() }, ), ], )); for font_smoothing in [FontSmoothing::AntiAliased, FontSmoothing::None] { content.with_child(( Text::new(format!("FontSmoothing::{:?}", font_smoothing)), TextFont { font: asset_server.load("fonts/MonaSans-VariableFont.ttf").into(), font_size: FontSize::Px(25.), font_smoothing, ..default() }, )); } } }); }); } } mod grid { use bevy::{color::palettes::css::*, prelude::*}; pub fn setup(mut commands: Commands) { commands.spawn((Camera2d, DespawnOnExit(super::Scene::Grid))); // Top-level grid (app frame) commands.spawn(( Node { display: Display::Grid, width: percent(100), height: percent(100), 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), DespawnOnExit(super::Scene::Grid), children![ // Header ( Node { display: Display::Grid, grid_column: GridPlacement::span(2), padding: UiRect::all(px(40)), ..default() }, BackgroundColor(RED.into()), ), // Main content grid (auto placed in row 2, column 1) ( Node { height: percent(100), 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: px(12), column_gap: px(12), ..default() }, BackgroundColor(Color::srgb(0.25, 0.25, 0.25)), children![ (Node::default(), BackgroundColor(ORANGE.into())), (Node::default(), BackgroundColor(BISQUE.into())), (Node::default(), BackgroundColor(BLUE.into())), (Node::default(), BackgroundColor(CRIMSON.into())), (Node::default(), BackgroundColor(AQUA.into())), ] ), // Right side bar (auto placed in row 2, column 2) (Node::DEFAULT, BackgroundColor(BLACK.into())), ], )); } } mod borders { use bevy::{color::palettes::css::*, prelude::*}; pub fn setup(mut commands: Commands) { commands.spawn((Camera2d, DespawnOnExit(super::Scene::Borders))); let root = commands .spawn(( Node { flex_wrap: FlexWrap::Wrap, ..default() }, DespawnOnExit(super::Scene::Borders), )) .id(); // all the different combinations of border edges let borders = [ UiRect::default(), UiRect::all(px(20)), UiRect::left(px(20)), UiRect::vertical(px(20)), UiRect { left: px(40), top: px(20), ..Default::default() }, UiRect { right: px(20), bottom: px(30), ..Default::default() }, UiRect { right: px(20), top: px(40), bottom: px(20), ..Default::default() }, UiRect { left: px(20), top: px(20), bottom: px(20), ..Default::default() }, UiRect { left: px(20), right: px(20), bottom: px(40), ..Default::default() }, ]; let non_zero = |x, y| x != px(0) && y != 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: px(100), height: px(100), border, margin: UiRect::all(px(30)), align_items: AlignItems::Center, justify_content: JustifyContent::Center, border_radius: if rounded { 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), ) } else { BorderRadius::ZERO }, ..default() }, BackgroundColor(MAROON.into()), BorderColor::all(RED), Outline { width: px(10), offset: px(10), color: Color::WHITE, }, )) .id(); 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, DespawnOnExit(super::Scene::BoxShadow))); commands .spawn(( Node { width: percent(100), height: percent(100), padding: UiRect::all(px(30)), column_gap: px(200), flex_wrap: FlexWrap::Wrap, ..default() }, BackgroundColor(GREEN.into()), DespawnOnExit(super::Scene::BoxShadow), )) .with_children(|commands| { let example_nodes = [ ( Vec2::splat(100.), Vec2::ZERO, 10., 0., BorderRadius::bottom_right(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(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: px(size.x), height: px(size.y), border: UiRect::all(px(2)), border_radius, ..default() }, BorderColor::all(WHITE), BackgroundColor(BLUE.into()), BoxShadow::new( Color::BLACK.with_alpha(0.9), percent(offset.x), percent(offset.y), percent(spread), px(blur), ), )); } }); } } mod text_wrap { use bevy::prelude::*; pub fn setup(mut commands: Commands) { commands.spawn((Camera2d, DespawnOnExit(super::Scene::TextWrap))); let root = commands .spawn(( Node { flex_direction: FlexDirection::Column, width: px(200), height: percent(100), overflow: Overflow::clip_x(), ..default() }, BackgroundColor(Color::BLACK), DespawnOnExit(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(Justify::Left, linebreak), BackgroundColor(Color::srgb(0.8 - j as f32 * 0.3, 0., 0.)), )); } } commands.spawn(( Node { position_type: PositionType::Absolute, padding: UiRect::px(10.0, 10.0, 8.0, 6.0), top: px(300.0), left: px(300.0), width: vmin(30.0), ..default() }, BackgroundColor::from(bevy::color::palettes::css::GREEN), Text::new("initial text here"), TextColor(Color::WHITE), TextLayout { justify: Justify::Left, linebreak: LineBreak::WordBoundary, }, DespawnOnExit(super::Scene::TextWrap), )); } } mod overflow { use bevy::{color::palettes::css::*, prelude::*}; pub fn setup(mut commands: Commands, asset_server: Res) { commands.spawn((Camera2d, DespawnOnExit(super::Scene::Overflow))); let image = asset_server.load("branding/icon.png"); commands .spawn(( Node { width: percent(100), height: percent(100), align_items: AlignItems::Center, justify_content: JustifyContent::SpaceAround, ..Default::default() }, BackgroundColor(BLUE.into()), DespawnOnExit(super::Scene::Overflow), )) .with_children(|parent| { for overflow in [ Overflow::visible(), Overflow::clip_x(), Overflow::clip_y(), Overflow::clip(), ] { parent .spawn(( Node { width: px(100), height: px(100), padding: UiRect { left: px(25), top: px(25), ..Default::default() }, border: UiRect::all(px(5)), overflow, ..default() }, BorderColor::all(RED), BackgroundColor(Color::WHITE), )) .with_children(|parent| { parent.spawn(( ImageNode::new(image.clone()), Node { min_width: px(100), min_height: px(100), ..default() }, Interaction::default(), Outline { width: px(2), offset: px(2), color: Color::NONE, }, )); }); } }); } } mod slice { use bevy::prelude::*; pub fn setup(mut commands: Commands, asset_server: Res) { commands.spawn((Camera2d, DespawnOnExit(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: percent(100), height: percent(100), flex_direction: FlexDirection::Column, justify_content: JustifyContent::SpaceAround, align_content: AlignContent::Center, ..default() }, DespawnOnExit(super::Scene::Slice), )) .with_children(|parent| { for visual_box in [ VisualBox::BorderBox, VisualBox::PaddingBox, VisualBox::ContentBox, ] { parent .spawn(Node { justify_content: JustifyContent::SpaceAround, ..default() }) .with_children(|parent| { for [w, h] in [[200.0, 200.0], [300.0, 200.0], [150., 200.0]] { parent.spawn(( Button, ImageNode { image: image.clone(), image_mode: NodeImageMode::Sliced(slicer.clone()), visual_box, ..default() }, Node { width: px(w), height: px(h), border: px(20.).all(), padding: px(20.).all(), ..default() }, Outline { width: px(2.), ..default() }, )); } parent.spawn(( ImageNode { image: asset_server .load("textures/fantasy_ui_borders/panel-border-010.png"), image_mode: NodeImageMode::Sliced(TextureSlicer { border: BorderRect::all(22.0), center_scale_mode: SliceScaleMode::Stretch, sides_scale_mode: SliceScaleMode::Stretch, max_corner_scale: 1.0, }), visual_box, ..Default::default() }, Node { width: px(200), height: px(200), border: px(20.).all(), padding: px(20.).all(), ..default() }, Outline { color: bevy::color::palettes::css::DARK_CYAN.into(), width: px(2.), ..default() }, BackgroundColor(bevy::color::palettes::css::NAVY.into()), )); }); } }); } } mod layout_rounding { use bevy::{color::palettes::css::*, prelude::*}; pub fn setup(mut commands: Commands) { commands.spawn((Camera2d, DespawnOnExit(super::Scene::LayoutRounding))); commands .spawn(( Node { display: Display::Grid, width: percent(100), height: percent(100), grid_template_rows: vec![RepeatedGridTrack::fr(10, 1.)], ..Default::default() }, BackgroundColor(Color::WHITE), DespawnOnExit(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(px(5)), ..Default::default() }, BackgroundColor(MAROON.into()), BorderColor::all(DARK_BLUE), )); } }); } }); } } mod linear_gradient { use bevy::camera::Camera2d; use bevy::color::palettes::css::BLUE; use bevy::color::palettes::css::LIME; use bevy::color::palettes::css::RED; use bevy::color::palettes::css::YELLOW; use bevy::color::Color; use bevy::ecs::prelude::*; use bevy::state::state_scoped::DespawnOnExit; use bevy::text::TextFont; use bevy::ui::AlignItems; use bevy::ui::BackgroundGradient; use bevy::ui::ColorStop; use bevy::ui::GridPlacement; use bevy::ui::InterpolationColorSpace; use bevy::ui::JustifyContent; use bevy::ui::LinearGradient; use bevy::ui::Node; use bevy::ui::PositionType; use bevy::utils::default; pub fn setup(mut commands: Commands) { commands.spawn((Camera2d, DespawnOnExit(super::Scene::LinearGradient))); commands .spawn(( Node { flex_direction: bevy::ui::FlexDirection::Column, width: bevy::ui::percent(100), height: bevy::ui::percent(100), justify_content: JustifyContent::Center, align_items: AlignItems::Center, row_gap: bevy::ui::px(5), ..default() }, DespawnOnExit(super::Scene::LinearGradient), )) .with_children(|commands| { let mut i = 0; commands .spawn(Node { display: bevy::ui::Display::Grid, row_gap: bevy::ui::px(4), column_gap: bevy::ui::px(4), ..Default::default() }) .with_children(|commands| { for stops in [ vec![ColorStop::auto(RED), ColorStop::auto(YELLOW)], vec![ ColorStop::auto(Color::BLACK), ColorStop::auto(RED), ColorStop::auto(Color::WHITE), ], vec![ Color::hsl(180.71191, 0.0, 0.3137255).into(), Color::hsl(180.71191, 0.5, 0.3137255).into(), Color::hsl(180.71191, 1.0, 0.3137255).into(), ], vec![ Color::hsl(180.71191, 0.825, 0.0).into(), Color::hsl(180.71191, 0.825, 0.5).into(), Color::hsl(180.71191, 0.825, 1.0).into(), ], vec![ Color::hsl(0.0 + 0.0001, 1.0, 0.5).into(), Color::hsl(180.0, 1.0, 0.5).into(), Color::hsl(360.0 - 0.0001, 1.0, 0.5).into(), ], vec![ Color::WHITE.into(), RED.into(), LIME.into(), BLUE.into(), Color::BLACK.into(), ], ] { for color_space in [ InterpolationColorSpace::LinearRgba, InterpolationColorSpace::Srgba, InterpolationColorSpace::Oklaba, InterpolationColorSpace::Oklcha, InterpolationColorSpace::OklchaLong, InterpolationColorSpace::Hsla, InterpolationColorSpace::HslaLong, InterpolationColorSpace::Hsva, InterpolationColorSpace::HsvaLong, ] { let row = i % 18 + 1; let column = i / 18 + 1; i += 1; commands.spawn(( Node { grid_row: GridPlacement::start(row as i16 + 1), grid_column: GridPlacement::start(column as i16 + 1), justify_content: JustifyContent::SpaceEvenly, ..Default::default() }, children![( Node { height: bevy::ui::px(30), width: bevy::ui::px(300), justify_content: JustifyContent::Center, ..Default::default() }, BackgroundGradient::from(LinearGradient { color_space, angle: LinearGradient::TO_RIGHT, stops: stops.clone(), }), children![ Node { position_type: PositionType::Absolute, ..default() }, TextFont::from_font_size(10.), bevy::ui::widget::Text(format!("{color_space:?}")), ] )], )); } } }); }); } } mod radial_gradient { use bevy::color::palettes::css::RED; use bevy::color::palettes::tailwind::GRAY_700; use bevy::prelude::*; use bevy::ui::ColorStop; const CELL_SIZE: f32 = 80.; const GAP: f32 = 10.; pub fn setup(mut commands: Commands) { let color_stops = vec![ ColorStop::new(Color::BLACK, px(5)), ColorStop::new(Color::WHITE, px(5)), ColorStop::new(Color::WHITE, percent(100)), ColorStop::auto(RED), ]; commands.spawn((Camera2d, DespawnOnExit(super::Scene::RadialGradient))); commands .spawn(( Node { width: percent(100), height: percent(100), display: Display::Grid, align_items: AlignItems::Start, grid_template_columns: vec![RepeatedGridTrack::px( GridTrackRepetition::AutoFill, CELL_SIZE, )], grid_auto_flow: GridAutoFlow::Row, row_gap: px(GAP), column_gap: px(GAP), padding: UiRect::all(px(GAP)), ..default() }, DespawnOnExit(super::Scene::RadialGradient), )) .with_children(|commands| { for (shape, shape_label) in [ (RadialGradientShape::ClosestSide, "ClosestSide"), (RadialGradientShape::FarthestSide, "FarthestSide"), (RadialGradientShape::Circle(percent(55)), "Circle(55%)"), (RadialGradientShape::FarthestCorner, "FarthestCorner"), ] { for (position, position_label) in [ (UiPosition::TOP_LEFT, "TOP_LEFT"), (UiPosition::LEFT, "LEFT"), (UiPosition::BOTTOM_LEFT, "BOTTOM_LEFT"), (UiPosition::TOP, "TOP"), (UiPosition::CENTER, "CENTER"), (UiPosition::BOTTOM, "BOTTOM"), (UiPosition::TOP_RIGHT, "TOP_RIGHT"), (UiPosition::RIGHT, "RIGHT"), (UiPosition::BOTTOM_RIGHT, "BOTTOM_RIGHT"), ] { for (w, h) in [(CELL_SIZE, CELL_SIZE), (CELL_SIZE, CELL_SIZE / 2.)] { commands .spawn(( BackgroundColor(GRAY_700.into()), Node { display: Display::Grid, width: px(CELL_SIZE), ..Default::default() }, )) .with_children(|commands| { commands.spawn(( Node { margin: UiRect::all(px(2)), ..default() }, Text(format!("{shape_label}\n{position_label}")), TextFont::from_font_size(9.), )); commands.spawn(( Node { width: px(w), height: px(h), ..default() }, BackgroundGradient::from(RadialGradient { stops: color_stops.clone(), position, shape, ..default() }), )); }); } } } }); } } mod transformations { use bevy::{color::palettes::css::*, prelude::*}; pub fn setup(mut commands: Commands) { commands.spawn((Camera2d, DespawnOnExit(super::Scene::Transformations))); commands .spawn(( Node { width: percent(100), height: percent(100), display: Display::Block, ..default() }, DespawnOnExit(super::Scene::Transformations), )) .with_children(|parent| { for (transformation, label, background) in [ ( UiTransform::from_rotation(Rot2::degrees(45.)), "Rotate 45 degrees", RED, ), ( UiTransform::from_scale(Vec2::new(2., 0.5)), "Scale 2.x 0.5y", GREEN, ), ( UiTransform::from_translation(Val2::px(-50., 50.)), "Translate -50px x +50px y", BLUE, ), ( UiTransform { translation: Val2::px(50., 0.), scale: Vec2::new(-1., 1.), rotation: Rot2::degrees(30.), }, "T 50px x\nS -1.x (refl)\nR 30deg", DARK_CYAN, ), ] { parent .spawn((Node { width: percent(100), margin: UiRect { top: px(50), bottom: px(50), ..default() }, align_items: AlignItems::Center, justify_content: JustifyContent::SpaceAround, ..default() },)) .with_children(|row| { row.spawn(( Text::new("Before Tf"), Node { width: px(100), height: px(100), border_radius: BorderRadius::bottom_right(px(25.)), ..default() }, BackgroundColor(background.into()), TextFont::default(), )); row.spawn(( Text::new(label), Node { width: px(100), height: px(100), border_radius: BorderRadius::bottom_right(px(25.)), ..default() }, BackgroundColor(background.into()), transformation, TextFont::default(), )); }); } }); } } #[cfg(feature = "bevy_ui_debug")] mod debug_outlines { use bevy::{ color::palettes::css::{BLUE, GRAY, RED}, prelude::*, ui_render::UiDebugOptions, }; pub fn setup(mut commands: Commands, mut debug_options: ResMut) { debug_options.enabled = true; debug_options.line_width = 5.; debug_options.line_color_override = Some(LinearRgba::GREEN); debug_options.show_hidden = true; debug_options.show_clipped = true; let debug_options: UiDebugOptions = (*debug_options.as_ref()).into(); commands.spawn((Camera2d, DespawnOnExit(super::Scene::DebugOutlines))); commands .spawn(( Node { width: percent(100), height: percent(50), align_items: AlignItems::Center, justify_content: JustifyContent::SpaceAround, ..default() }, DespawnOnExit(super::Scene::DebugOutlines), )) .with_children(|parent| { parent.spawn(( Node { width: px(100), height: px(100), ..default() }, BackgroundColor(GRAY.into()), UiTransform::from_rotation(Rot2::degrees(45.)), )); parent.spawn((Text::new("Regular Text"), TextFont::default())); parent.spawn(( Node { width: px(100), height: px(100), ..default() }, Text::new("Invisible"), BackgroundColor(GRAY.into()), TextFont::default(), Visibility::Hidden, )); parent .spawn(( Node { width: px(100), height: px(100), padding: UiRect { left: px(25), top: px(25), ..Default::default() }, overflow: Overflow::clip(), ..default() }, BackgroundColor(RED.into()), )) .with_children(|child| { child.spawn(( Node { min_width: px(100), min_height: px(100), ..default() }, BackgroundColor(BLUE.into()), )); }); }); commands .spawn(( Node { width: percent(100), height: percent(50), top: percent(50), align_items: AlignItems::Center, justify_content: JustifyContent::SpaceAround, ..default() }, DespawnOnExit(super::Scene::DebugOutlines), )) .with_children(|parent| { parent.spawn(( Node { width: px(200), height: px(200), border: UiRect { top: px(10), bottom: px(20), left: px(30), right: px(40), }, border_radius: BorderRadius::bottom_right(px(10)), padding: UiRect { top: px(40), bottom: px(30), left: px(20), right: px(10), }, ..default() }, children![( Text::new("border padding content outlines"), TextFont::default(), UiDebugOptions { enabled: false, ..default() } )], UiDebugOptions { outline_border_box: true, outline_padding_box: true, outline_content_box: true, ignore_border_radius: false, ..debug_options }, )); // Vertical scrollbar (non-functional) parent.spawn(( Node { flex_direction: FlexDirection::Column, width: px(90), height: px(230), overflow: Overflow::scroll_y(), scrollbar_width: 20., ..default() }, ScrollPosition(Vec2::new(180., 180.)), UiDebugOptions { line_width: 3., outline_scrollbars: true, show_hidden: false, show_clipped: false, ..debug_options }, Children::spawn(SpawnIter((0..20).map(move |i| { ( Node::default(), children![( Text(format!("Item {i}")), UiDebugOptions { enabled: false, ..default() } )], UiDebugOptions { enabled: false, ..default() }, ) }))), )); // Horizontal scrollbar (non-functional) parent.spawn(( Node { flex_direction: FlexDirection::Row, width: px(156), height: px(70), overflow: Overflow::scroll_x(), scrollbar_width: 10., ..default() }, UiDebugOptions { line_width: 3., outline_scrollbars: true, show_hidden: false, show_clipped: false, ..debug_options }, Children::spawn(SpawnIter((0..20).map(move |i| { ( Node::default(), children![( Text(format!("Item {i}")), UiDebugOptions { enabled: false, ..default() } )], UiDebugOptions { enabled: false, ..default() }, ) }))), )); // bi-directional scrollbar (non-functional) parent.spawn(( Node { flex_direction: FlexDirection::Column, width: px(230), height: px(125), overflow: Overflow::scroll(), scrollbar_width: 20., ..default() }, ScrollPosition(Vec2::new(300., 0.)), UiDebugOptions { line_width: 3., outline_scrollbars: true, show_hidden: false, show_clipped: false, ..debug_options }, Children::spawn(SpawnIter((0..6).map(move |i| { ( Node { flex_direction: FlexDirection::Row, ..default() }, Children::spawn(SpawnIter((0..6).map({ move |j| { ( Text(format!("Item {}", (i * 5) + j)), UiDebugOptions { enabled: false, ..default() }, ) } }))), UiDebugOptions { enabled: false, ..default() }, ) }))), )); }); } pub fn teardown(mut debug_options: ResMut) { *debug_options = GlobalUiDebugOptions::default(); } } mod viewport_coords { use bevy::{color::palettes::css::*, prelude::*}; const PALETTE: [Srgba; 9] = [RED, WHITE, BEIGE, AQUA, CRIMSON, NAVY, AZURE, LIME, BLACK]; pub fn setup(mut commands: Commands) { commands.spawn((Camera2d, DespawnOnExit(super::Scene::ViewportCoords))); commands .spawn(( Node { width: vw(100), height: vh(100), border: UiRect::axes(vw(5), vh(5)), flex_wrap: FlexWrap::Wrap, ..default() }, BorderColor::all(PALETTE[0]), DespawnOnExit(super::Scene::ViewportCoords), )) .with_children(|builder| { builder.spawn(( Node { width: vw(30), height: vh(30), border: UiRect::all(vmin(5)), ..default() }, BackgroundColor(PALETTE[1].into()), BorderColor::all(PALETTE[8]), )); builder.spawn(( Node { width: vw(60), height: vh(30), ..default() }, BackgroundColor(PALETTE[2].into()), )); builder.spawn(( Node { width: vw(45), height: vh(30), border: UiRect::left(vmax(45. / 2.)), ..default() }, BackgroundColor(PALETTE[3].into()), BorderColor::all(PALETTE[7]), )); builder.spawn(( Node { width: vw(45), height: vh(30), border: UiRect::right(vmax(45. / 2.)), ..default() }, BackgroundColor(PALETTE[4].into()), BorderColor::all(PALETTE[7]), )); builder.spawn(( Node { width: vw(60), height: vh(30), ..default() }, BackgroundColor(PALETTE[5].into()), )); builder.spawn(( Node { width: vw(30), height: vh(30), border: UiRect::all(vmin(5)), ..default() }, BackgroundColor(PALETTE[6].into()), BorderColor::all(PALETTE[8]), )); }); } } mod outer_color { use bevy::prelude::*; pub fn setup(mut commands: Commands) { let radius = percent(33.); let width = px(10.); commands.spawn((Camera2d, DespawnOnExit(super::Scene::OuterColor))); commands .spawn(( Node { display: Display::Grid, grid_template_columns: RepeatedGridTrack::px(3, 200.), grid_template_rows: RepeatedGridTrack::px(3, 200.), margin: UiRect::AUTO, ..default() }, DespawnOnExit(super::Scene::OuterColor), )) .with_children(|builder| { for (border, border_radius, invert) in [ (UiRect::ZERO, BorderRadius::bottom_right(radius), true), (UiRect::top(width), BorderRadius::top(radius), false), (UiRect::ZERO, BorderRadius::bottom_left(radius), true), (UiRect::left(width), BorderRadius::left(radius), false), (UiRect::all(width), BorderRadius::all(radius), true), (UiRect::right(width), BorderRadius::right(radius), false), (UiRect::ZERO, BorderRadius::top_right(radius), true), (UiRect::bottom(width), BorderRadius::bottom(radius), false), (UiRect::ZERO, BorderRadius::top_left(radius), true), ] { builder .spawn(( Node { width: px(200.), height: px(200.), border_radius, border, ..default() }, BorderColor::all(bevy::color::palettes::css::RED), )) .insert_if(BackgroundColor(Color::WHITE), || !invert) .insert_if(OuterColor(Color::WHITE), || invert); } }); } } mod boxed_content { use bevy::color::palettes::css::RED; use bevy::prelude::*; pub fn setup(mut commands: Commands) { commands.spawn((Camera2d, DespawnOnExit(super::Scene::BoxedContent))); commands .spawn(( Node { margin: auto().all(), column_gap: px(30), ..default() }, DespawnOnExit(super::Scene::BoxedContent), )) .with_children(|builder| { for (heading, text_justify) in [ ("Left", Justify::Left), ("Center", Justify::Center), ("Right", Justify::Right), ] { builder .spawn(Node { flex_direction: FlexDirection::Column, align_items: AlignItems::Center, justify_content: JustifyContent::Start, row_gap: px(20), ..default() }) .with_children(|builder| { builder.spawn(( Node::default(), Text::new(format!("{heading} justify")), TextFont::from_font_size(FontSize::Px(14.)), TextLayout::justify(Justify::Center), )); builder.spawn(( Node::default(), Text::new("This text has\nno border or padding."), TextFont::from_font_size(FontSize::Px(10.)), TextLayout::justify(text_justify), Outline { width: px(2), color: Color::WHITE, ..Default::default() }, )); builder.spawn(( Node { border: px(10).all(), ..default() }, Text::new("This text has\na border but no padding."), TextFont::from_font_size(FontSize::Px(10.)), TextLayout::justify(text_justify), BorderColor::all(RED), Outline { width: px(2), color: Color::WHITE, ..Default::default() }, )); builder.spawn(( Node { padding: px(20).all(), ..default() }, Text::new("This text has\npadding but no border."), TextFont::from_font_size(FontSize::Px(10.)), TextLayout::justify(text_justify), Outline { width: px(2), color: Color::WHITE, ..Default::default() }, )); builder.spawn(( Node { border: px(10).all(), padding: px(20).all(), ..default() }, Text::new("This text has\nborder and padding."), TextFont::from_font_size(FontSize::Px(10.)), TextLayout::justify(text_justify), BorderColor::all(RED), Outline { width: px(2), color: Color::WHITE, ..Default::default() }, )); builder.spawn(( Node { border: px(10).left(), ..default() }, Text::new("This text has\na left border and no padding."), TextFont::from_font_size(FontSize::Px(10.)), TextLayout::justify(text_justify), BorderColor::all(RED), Outline { width: px(2), color: Color::WHITE, ..Default::default() }, )); builder.spawn(( Node { border: px(10).right(), ..default() }, Text::new("This text has\na right border and no padding."), TextFont::from_font_size(FontSize::Px(10.)), TextLayout::justify(text_justify), BorderColor::all(RED), Outline { width: px(2), color: Color::WHITE, ..Default::default() }, )); builder.spawn(( Node { padding: px(20).top().with_right(px(20)), ..default() }, Text::new("This text has\npadding on its top and right."), TextFont::from_font_size(FontSize::Px(10.)), TextLayout::justify(text_justify), BorderColor::all(RED), Outline { width: px(2), color: Color::WHITE, ..Default::default() }, )); builder.spawn(( Node { padding: px(20).bottom().with_left(px(20)), ..default() }, Text::new("This text has\npadding on its bottom and left."), TextFont::from_font_size(FontSize::Px(10.)), TextLayout::justify(text_justify), BorderColor::all(RED), Outline { width: px(2), color: Color::WHITE, ..Default::default() }, )); builder.spawn(( Node { padding: px(20).top().with_left(px(20)), border: px(10).bottom().with_right(px(10)), ..default() }, Text::new( "This text has\npadding on its top and left\nand a border on its bottom and right.", ), TextFont::from_font_size(FontSize::Px(10.)), TextLayout::justify(text_justify), BorderColor::all(RED), Outline { width: px(2), color: Color::WHITE, ..Default::default() }, )); }); } }); } } mod editable_text { use bevy::color::palettes::css::YELLOW; use bevy::prelude::*; use bevy::text::EditableText; use bevy::text::TextEdit; use bevy::ui::widget::TextScroll; pub fn setup(mut commands: Commands) { commands.spawn((Camera2d, DespawnOnExit(super::Scene::EditableText))); commands.spawn(( Node { flex_direction: FlexDirection::Column, align_items: AlignItems::Center, justify_content: JustifyContent::Center, width: vw(100), height: vh(100), row_gap: px(25.), ..default() }, DespawnOnExit(super::Scene::EditableText), children![ ( EditableText { pending_edits: vec![TextEdit::Insert("Single line EditableText".into())], ..default() }, Node { width: px(200.), border: px(2).all(), ..default() }, BorderColor::all(YELLOW), ), ( EditableText { pending_edits: vec![ TextEdit::Insert( "1. Multiline EditableText\n2.\n3.\n4.\n5.\n6.\n7.\n8.\n9.\n10." .into() ), TextEdit::TextStart(false), ], visible_lines: Some(8.), ..default() }, TextScroll::default(), Node { width: px(350.), border: px(2).all(), ..default() }, BorderColor::all(YELLOW), ), ( EditableText { pending_edits: vec![ TextEdit::Insert( "1. Multiline EditableText\n2.\n3.\n4.\n5.\n6.\n7.\n8.\n9.\n10." .into() ), TextEdit::TextEnd(true), ], visible_lines: Some(8.), ..default() }, TextScroll::default(), Node { width: px(350.), border: px(2).all(), ..default() }, BorderColor::all(YELLOW), ), ], )); } }