//! This example illustrates the various features of Bevy UI. use bevy::{ a11y::{ accesskit::{NodeBuilder, Role}, AccessibilityNode, }, input::mouse::{MouseScrollUnit, MouseWheel}, prelude::*, winit::WinitSettings, }; 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(); } fn setup(mut commands: Commands, asset_server: Res) { // Camera commands.spawn(Camera2dBundle::default()); // root node commands .spawn(NodeBundle { style: Style { width: Val::Percent(100.0), height: Val::Percent(100.0), justify_content: JustifyContent::SpaceBetween, ..default() }, ..default() }) .with_children(|parent| { // left vertical fill (border) parent .spawn(NodeBundle { style: Style { width: Val::Px(200.), border: UiRect::all(Val::Px(2.)), ..default() }, background_color: LegacyColor::rgb(0.65, 0.65, 0.65).into(), ..default() }) .with_children(|parent| { // left vertical fill (content) parent .spawn(NodeBundle { style: Style { width: Val::Percent(100.), ..default() }, background_color: LegacyColor::rgb(0.15, 0.15, 0.15).into(), ..default() }) .with_children(|parent| { // text parent.spawn(( 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 parent .spawn(NodeBundle { style: Style { flex_direction: FlexDirection::Column, justify_content: JustifyContent::Center, align_items: AlignItems::Center, width: Val::Px(200.), ..default() }, background_color: LegacyColor::rgb(0.15, 0.15, 0.15).into(), ..default() }) .with_children(|parent| { // Title parent.spawn(( TextBundle::from_section( "Scrolling list", TextStyle { font: asset_server.load("fonts/FiraSans-Bold.ttf"), font_size: 25., ..default() }, ), Label, )); // List with hidden overflow parent .spawn(NodeBundle { style: Style { flex_direction: FlexDirection::Column, align_self: AlignSelf::Stretch, height: Val::Percent(50.), overflow: Overflow::clip_y(), ..default() }, background_color: LegacyColor::rgb(0.10, 0.10, 0.10).into(), ..default() }) .with_children(|parent| { // Moving panel parent .spawn(( NodeBundle { style: Style { flex_direction: FlexDirection::Column, align_items: AlignItems::Center, ..default() }, ..default() }, ScrollingList::default(), AccessibilityNode(NodeBuilder::new(Role::List)), )) .with_children(|parent| { // List items for i in 0..30 { parent.spawn(( 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)), )); } }); }); }); parent .spawn(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: LegacyColor::GREEN.into(), background_color: LegacyColor::rgb(0.4, 0.4, 1.).into(), ..default() }) .with_children(|parent| { parent.spawn(NodeBundle { style: Style { width: Val::Percent(100.0), height: Val::Percent(100.0), ..default() }, background_color: LegacyColor::rgb(0.8, 0.8, 1.).into(), ..default() }); }); // render order test: reddest in the back, whitest in the front (flex center) parent .spawn(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(|parent| { parent .spawn(NodeBundle { style: Style { width: Val::Px(100.0), height: Val::Px(100.0), ..default() }, background_color: LegacyColor::rgb(1.0, 0.0, 0.).into(), ..default() }) .with_children(|parent| { parent.spawn(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: LegacyColor::rgb(1.0, 0.3, 0.3).into(), ..default() }); parent.spawn(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: LegacyColor::rgb(1.0, 0.5, 0.5).into(), ..default() }); parent.spawn(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: LegacyColor::rgb(1.0, 0.7, 0.7).into(), ..default() }); // alpha test parent.spawn(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: LegacyColor::rgba(1.0, 0.9, 0.9, 0.4).into(), ..default() }); }); }); // bevy logo (flex center) parent .spawn(NodeBundle { style: Style { width: Val::Percent(100.0), position_type: PositionType::Absolute, justify_content: JustifyContent::Center, align_items: AlignItems::FlexStart, ..default() }, ..default() }) .with_children(|parent| { // 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. parent .spawn(( 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: LegacyColor::WHITE.into(), ..default() }, UiImage::new(asset_server.load("branding/bevy_logo_dark_big.png")), )) .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(( NodeBundle { style: Style { display: Display::None, ..Default::default() }, ..Default::default() }, Text::from_section("Bevy logo", TextStyle::default()), )); }); }); }); } #[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); } } }