bevy/examples/ui/ui.rs
Nico Burns b995827013
Have a separate implicit viewport node per root node + make viewport node Display::Grid (#9637)
# Objective

Make `bevy_ui` "root" nodes more intuitive to use/style by:
- Removing the implicit flexbox styling (such as stretch alignment) that
is applied to them, and replacing it with more intuitive CSS Grid
styling (notably with stretch alignment disabled in both axes).
- Making root nodes layout independently of each other. Instead of there
being a single implicit "viewport" node that all root nodes are children
of, there is now an implicit "viewport" node *per root node*. And layout
of each tree is computed separately.

## Solution

- Remove the global implicit viewport node, and instead create an
implicit viewport node for each user-specified root node.
- Keep track of both the user-specified root nodes and the implicit
viewport nodes in a separate `Vec`.
- Use the window's size as the `available_space` parameter to
`Taffy.compute_layout` rather than setting it on the implicit viewport
node (and set the viewport to `height: 100%; width: 100%` to make this
"just work").

---

## Changelog

- Bevy UI now lays out root nodes independently of each other in
separate layout contexts.
- The implicit viewport node (which contains each user-specified root
node) is now `Display::Grid` with `align_items` and `justify_items` both
set to `Start`.

## Migration Guide

- Bevy UI now lays out root nodes independently of each other in
separate layout contexts. If you were relying on your root nodes being
able to affect each other's layouts, then you may need to wrap them in a
single root node.
- The implicit viewport node (which contains each user-specified root
node) is now `Display::Grid` with `align_items` and `justify_items` both
set to `Start`. You may need to add `height: Val::Percent(100.)` to your
root nodes if you were previously relying on being implicitly set.
2023-09-19 15:14:46 +00:00

337 lines
15 KiB
Rust

//! 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<AssetServer>) {
// 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: Color::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: Color::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,
color: Color::WHITE,
},
)
.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: Color::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.,
color: Color::WHITE,
},
),
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: Color::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.,
color: Color::WHITE,
},
),
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: Color::GREEN.into(),
background_color: Color::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: Color::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: Color::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: Color::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: Color::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: Color::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: Color::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: Color::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<MouseWheel>,
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);
}
}
}