//! This example illustrates the various features of Bevy UI using bevy_bundletree::BundleTree. use bevy::{ a11y::{ accesskit::{NodeBuilder, Role}, AccessibilityNode, }, color::palettes::css::GREEN, input::mouse::{MouseScrollUnit, MouseWheel}, prelude::*, winit::WinitSettings }; use bevy_bundletree::*; fn main() { App::new() .add_plugins(DefaultPlugins) // Only run the app when there is user input. This will significantly reduce CPU/GPU use. .insert_resource(WinitSettings::desktop_app()) .add_systems(Startup, setup) .add_systems(Update, mouse_scroll) .run(); } /// Define an enum to represent all possible node types in the UI tree. #[allow(clippy::large_enum_variant)] #[derive(IntoBundleTree, BundleEnum)] enum UiNode { Node(NodeBundle), Text(TextBundle), TextNode((NodeBundle, Text)), LabelText((TextBundle, Label)), AccessibilityLabelText((TextBundle, Label, AccessibilityNode)), AccessibilityScrollList((NodeBundle, ScrollingList, AccessibilityNode)), ImageNode((NodeBundle, UiImage)), } fn setup(mut commands: Commands, asset_server: Res) { // Camera commands.spawn(Camera2dBundle::default()); let tree: BundleTree = // Root node NodeBundle { style: Style { width: Val::Percent(100.0), height: Val::Percent(100.0), justify_content: JustifyContent::SpaceBetween, ..default() }, ..default() }.with_children([ // Left vertical fill (border) NodeBundle { style: Style { width: Val::Px(200.), border: UiRect::all(Val::Px(2.)), ..default() }, background_color: Color::srgb(0.65, 0.65, 0.65).into(), ..default() }.with_children([ // Left vertical fill (content) NodeBundle { style: Style { width: Val::Percent(100.), ..default() }, background_color: Color::srgb(0.15, 0.15, 0.15).into(), ..default() }.with_children([ // Text BundleTree::new((TextBundle::from_section( "Text Example", TextStyle { font: asset_server.load("fonts/FiraSans-Bold.ttf"), font_size: 30.0, ..default() }, ) .with_style(Style { margin: UiRect::all(Val::Px(5.)), ..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, ))]), ]), // Right vertical fill NodeBundle { style: Style { flex_direction: FlexDirection::Column, justify_content: JustifyContent::Center, align_items: AlignItems::Center, width: Val::Px(200.), ..default() }, background_color: Color::srgb(0.15, 0.15, 0.15).into(), ..default() }.with_children([ // Title ( TextBundle::from_section( "Scrolling list", TextStyle { font: asset_server.load("fonts/FiraSans-Bold.ttf"), font_size: 25., ..default() }, ), Label ).into_tree(), // List with hidden overflow NodeBundle { style: Style { flex_direction: FlexDirection::Column, align_self: AlignSelf::Stretch, height: Val::Percent(50.), overflow: Overflow::clip_y(), ..default() }, background_color: Color::srgb(0.10, 0.10, 0.10).into(), ..default() }.with_children([ // Moving panel ( NodeBundle { style: Style { flex_direction: FlexDirection::Column, align_items: AlignItems::Center, ..default() }, ..default() }, ScrollingList::default(), AccessibilityNode(NodeBuilder::new(Role::List)), ).with_children({ // List items let mut children = Vec::new(); for i in 0..30 { children.push(BundleTree::new(( TextBundle::from_section( format!("Item {i}"), TextStyle { font: asset_server .load("fonts/FiraSans-Bold.ttf"), font_size: 20., ..default() }, ), Label, AccessibilityNode(NodeBuilder::new(Role::ListItem)), ))); } children }) ]) ]), // Node NodeBundle { style: Style { width: Val::Px(200.0), height: Val::Px(200.0), position_type: PositionType::Absolute, left: Val::Px(210.), bottom: Val::Px(10.), border: UiRect::all(Val::Px(20.)), ..default() }, border_color: GREEN.into(), background_color: Color::srgb(0.4, 0.4, 1.).into(), ..default() }.with_children([ BundleTree::new(NodeBundle { style: Style { width: Val::Percent(100.0), height: Val::Percent(100.0), ..default() }, background_color: Color::srgb(0.8, 0.8, 1.).into(), ..default() }) ]), // Render order test: reddest in the back, whitest in the front (flex center) NodeBundle { style: Style { width: Val::Percent(100.0), height: Val::Percent(100.0), position_type: PositionType::Absolute, align_items: AlignItems::Center, justify_content: JustifyContent::Center, ..default() }, ..default() }.with_children([ NodeBundle { style: Style { width: Val::Px(100.0), height: Val::Px(100.0), ..default() }, background_color: Color::srgb(1.0, 0.0, 0.).into(), ..default() }.with_children([ NodeBundle { style: Style { // 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() }, background_color: Color::srgb(1.0, 0.3, 0.3).into(), ..default() }.into_tree(), NodeBundle { style: Style { width: Val::Percent(100.0), height: Val::Percent(100.0), position_type: PositionType::Absolute, left: Val::Px(40.), bottom: Val::Px(40.), ..default() }, background_color: Color::srgb(1.0, 0.5, 0.5).into(), ..default() }.into_tree(), NodeBundle { style: Style { width: Val::Percent(100.0), height: Val::Percent(100.0), position_type: PositionType::Absolute, left: Val::Px(60.), bottom: Val::Px(60.), ..default() }, background_color: Color::srgb(1.0, 0.7, 0.7).into(), ..default() }.into_tree(), // Alpha test NodeBundle { style: Style { width: Val::Percent(100.0), height: Val::Percent(100.0), position_type: PositionType::Absolute, left: Val::Px(80.), bottom: Val::Px(80.), ..default() }, background_color: Color::srgba(1.0, 0.9, 0.9, 0.4).into(), ..default() }.into_tree() ]) ]), // Bevy logo (flex center) NodeBundle { style: Style { width: Val::Percent(100.0), position_type: PositionType::Absolute, justify_content: JustifyContent::Center, align_items: AlignItems::FlexStart, ..default() }, ..default() }.with_children([ // Bevy logo (image) // A `NodeBundle` is used to display the logo the image as an `ImageBundle` can't automatically // size itself with a child node present. ( NodeBundle { style: Style { width: Val::Px(500.0), height: Val::Px(125.0), margin: UiRect::top(Val::VMin(5.)), ..default() }, // a `NodeBundle` is transparent by default, so to see the image we have to its color to `WHITE` background_color: Color::WHITE.into(), ..default() }, UiImage::new(asset_server.load("branding/bevy_logo_dark_big.png")) ).with_children([ // 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. ( NodeBundle { style: Style { display: Display::None, ..Default::default() }, ..Default::default() }, Text::from_section("Bevy logo", TextStyle::default()), ).into_tree() ])]) ]); commands.spawn_tree(tree); } #[derive(Component, Default)] struct ScrollingList { position: f32, } fn mouse_scroll( mut mouse_wheel_events: EventReader, mut query_list: Query<(&mut ScrollingList, &mut Style, &Parent, &Node)>, query_node: Query<&Node>, ) { for mouse_wheel_event in mouse_wheel_events.read() { for (mut scrolling_list, mut style, parent, list_node) in &mut query_list { let items_height = list_node.size().y; let container_height = query_node.get(parent.get()).unwrap().size().y; let max_scroll = (items_height - container_height).max(0.); let dy = match mouse_wheel_event.unit { MouseScrollUnit::Line => mouse_wheel_event.y * 20., MouseScrollUnit::Pixel => mouse_wheel_event.y, }; scrolling_list.position += dy; scrolling_list.position = scrolling_list.position.clamp(-max_scroll, 0.); style.top = Val::Px(scrolling_list.position); } } }