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.
This commit is contained in:
Nico Burns 2023-09-19 16:14:46 +01:00 committed by GitHub
parent 401b2e77f3
commit b995827013
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 105 additions and 66 deletions

View file

@ -12,17 +12,19 @@ pub fn print_ui_layout_tree(ui_surface: &UiSurface) {
.iter()
.map(|(entity, node)| (*node, *entity))
.collect();
for (&entity, &node) in &ui_surface.window_nodes {
for (&entity, roots) in &ui_surface.window_roots {
let mut out = String::new();
print_node(
ui_surface,
&taffy_to_entity,
entity,
node,
false,
String::new(),
&mut out,
);
for root in roots {
print_node(
ui_surface,
&taffy_to_entity,
entity,
root.implicit_viewport_node,
false,
String::new(),
&mut out,
);
}
bevy_log::info!("Layout tree for window entity: {entity:?}\n{out}");
}
}

View file

@ -15,10 +15,10 @@ use bevy_hierarchy::{Children, Parent};
use bevy_log::warn;
use bevy_math::Vec2;
use bevy_transform::components::Transform;
use bevy_utils::HashMap;
use bevy_utils::{default, HashMap};
use bevy_window::{PrimaryWindow, Window, WindowResolution, WindowScaleFactorChanged};
use std::fmt;
use taffy::{prelude::Size, style_helpers::TaffyMaxContent, Taffy};
use taffy::Taffy;
pub struct LayoutContext {
pub scale_factor: f64,
@ -39,10 +39,18 @@ impl LayoutContext {
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct RootNodePair {
// The implicit "viewport" node created by Bevy
implicit_viewport_node: taffy::node::Node,
// The root (parentless) node specified by the user
user_root_node: taffy::node::Node,
}
#[derive(Resource)]
pub struct UiSurface {
entity_to_taffy: HashMap<Entity, taffy::node::Node>,
window_nodes: HashMap<Entity, taffy::node::Node>,
window_roots: HashMap<Entity, Vec<RootNodePair>>,
taffy: Taffy,
}
@ -57,7 +65,7 @@ impl fmt::Debug for UiSurface {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("UiSurface")
.field("entity_to_taffy", &self.entity_to_taffy)
.field("window_nodes", &self.window_nodes)
.field("window_nodes", &self.window_roots)
.finish()
}
}
@ -68,7 +76,7 @@ impl Default for UiSurface {
taffy.disable_rounding();
Self {
entity_to_taffy: Default::default(),
window_nodes: Default::default(),
window_roots: Default::default(),
taffy,
}
}
@ -132,50 +140,64 @@ without UI components as a child of an entity with UI components, results may be
}
}
/// Retrieve or insert the root layout node and update its size to match the size of the window.
pub fn update_window(&mut self, window: Entity, window_resolution: &WindowResolution) {
let taffy = &mut self.taffy;
let node = self
.window_nodes
.entry(window)
.or_insert_with(|| taffy.new_leaf(taffy::style::Style::default()).unwrap());
taffy
.set_style(
*node,
taffy::style::Style {
size: taffy::geometry::Size {
width: taffy::style::Dimension::Points(
window_resolution.physical_width() as f32
),
height: taffy::style::Dimension::Points(
window_resolution.physical_height() as f32,
),
},
..Default::default()
},
)
.unwrap();
}
/// Set the ui node entities without a [`Parent`] as children to the root node in the taffy layout.
pub fn set_window_children(
&mut self,
parent_window: Entity,
window_id: Entity,
children: impl Iterator<Item = Entity>,
) {
let taffy_node = self.window_nodes.get(&parent_window).unwrap();
let child_nodes = children
.map(|e| *self.entity_to_taffy.get(&e).unwrap())
.collect::<Vec<taffy::node::Node>>();
self.taffy.set_children(*taffy_node, &child_nodes).unwrap();
let viewport_style = taffy::style::Style {
display: taffy::style::Display::Grid,
// Note: Taffy percentages are floats ranging from 0.0 to 1.0.
// So this is setting width:100% and height:100%
size: taffy::geometry::Size {
width: taffy::style::Dimension::Percent(1.0),
height: taffy::style::Dimension::Percent(1.0),
},
align_items: Some(taffy::style::AlignItems::Start),
justify_items: Some(taffy::style::JustifyItems::Start),
..default()
};
let existing_roots = self.window_roots.entry(window_id).or_default();
let mut new_roots = Vec::new();
for entity in children {
let node = *self.entity_to_taffy.get(&entity).unwrap();
let root_node = existing_roots
.iter()
.find(|n| n.user_root_node == node)
.cloned()
.unwrap_or_else(|| RootNodePair {
implicit_viewport_node: self
.taffy
.new_with_children(viewport_style.clone(), &[node])
.unwrap(),
user_root_node: node,
});
new_roots.push(root_node);
}
// Cleanup the implicit root nodes of any user root nodes that have been removed
for old_root in existing_roots {
if !new_roots.contains(old_root) {
self.taffy.remove(old_root.implicit_viewport_node).unwrap();
}
}
self.window_roots.insert(window_id, new_roots);
}
/// Compute the layout for each window entity's corresponding root node in the layout.
pub fn compute_window_layouts(&mut self) {
for window_node in self.window_nodes.values() {
pub fn compute_window_layout(&mut self, window: Entity, window_resolution: &WindowResolution) {
let available_space = taffy::geometry::Size {
width: taffy::style::AvailableSpace::Definite(window_resolution.physical_width() as f32),
height: taffy::style::AvailableSpace::Definite(
window_resolution.physical_height() as f32
),
};
for root_nodes in self.window_roots.entry(window).or_default() {
self.taffy
.compute_layout(*window_node, Size::MAX_CONTENT)
.compute_layout(root_nodes.implicit_viewport_node, available_space)
.unwrap();
}
}
@ -251,11 +273,6 @@ pub fn ui_layout_system(
.read()
.any(|resized_window| resized_window.window == primary_window_entity);
// update window root nodes
for (entity, window) in windows.iter() {
ui_surface.update_window(entity, &window.resolution);
}
let scale_factor = logical_to_physical_factor * ui_scale.0;
let layout_context = LayoutContext::new(scale_factor, physical_size);
@ -302,7 +319,9 @@ pub fn ui_layout_system(
}
// compute layouts
ui_surface.compute_window_layouts();
for (entity, window) in windows.iter() {
ui_surface.compute_window_layout(entity, &window.resolution);
}
let inverse_target_scale_factor = 1. / scale_factor;

View file

@ -70,6 +70,7 @@ fn setup(mut commands: Commands) {
.spawn(NodeBundle {
style: Style {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
flex_direction: FlexDirection::Column,

View file

@ -54,6 +54,7 @@ fn setup_menu(mut commands: Commands) {
style: Style {
// center button
width: Val::Percent(100.),
height: Val::Percent(100.),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
..default()

View file

@ -84,6 +84,7 @@ mod splash {
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
width: Val::Percent(100.0),
height: Val::Percent(100.0),
..default()
},
..default()
@ -151,6 +152,7 @@ mod game {
NodeBundle {
style: Style {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
// center children
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
@ -421,6 +423,7 @@ mod menu {
NodeBundle {
style: Style {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..default()
@ -546,6 +549,7 @@ mod menu {
NodeBundle {
style: Style {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..default()
@ -611,6 +615,7 @@ mod menu {
NodeBundle {
style: Style {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..default()
@ -714,6 +719,7 @@ mod menu {
NodeBundle {
style: Style {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..default()

View file

@ -120,6 +120,7 @@ fn setup_flex(mut commands: Commands, asset_server: Res<AssetServer>, args: Res<
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
width: Val::Percent(100.),
height: Val::Percent(100.),
..default()
},
..default()

View file

@ -54,7 +54,7 @@ fn setup(mut commands: Commands) {
commands
.spawn(NodeBundle {
style: Style {
flex_basis: Val::Percent(100.),
width: Val::Percent(100.),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..default()

View file

@ -14,8 +14,9 @@ fn setup(mut commands: Commands) {
let root = commands
.spawn(NodeBundle {
style: Style {
flex_basis: Val::Percent(100.0),
margin: UiRect::all(Val::Px(25.0)),
align_self: AlignSelf::Stretch,
justify_self: JustifySelf::Stretch,
flex_wrap: FlexWrap::Wrap,
justify_content: JustifyContent::FlexStart,
align_items: AlignItems::FlexStart,

View file

@ -58,6 +58,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
.spawn(NodeBundle {
style: Style {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..default()

View file

@ -82,8 +82,9 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn(Camera2dBundle::default());
commands.spawn(NodeBundle {
style: Style {
width: Val::Percent(100.),
height: Val::Percent(100.),
flex_direction: FlexDirection::Column,
flex_basis: Val::Percent(100.),
align_items: AlignItems::Center,
justify_content: JustifyContent::SpaceEvenly,
..Default::default()
@ -189,9 +190,6 @@ fn spawn_left_panel(builder: &mut ChildBuilder, palette: &[Color; 4]) -> Vec<Ent
.with_children(|parent| {
parent
.spawn(NodeBundle {
style: Style {
..Default::default()
},
background_color: BackgroundColor(Color::BLACK),
..Default::default()
})

View file

@ -25,9 +25,10 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
commands
.spawn(NodeBundle {
style: Style {
width: Val::Percent(100.),
height: Val::Percent(100.),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
width: Val::Percent(100.),
..Default::default()
},
background_color: Color::ANTIQUE_WHITE.into(),

View file

@ -19,6 +19,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
.spawn(NodeBundle {
style: Style {
width: Val::Percent(100.),
height: Val::Percent(100.0),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
flex_direction: FlexDirection::Column,

View file

@ -51,7 +51,8 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
commands
.spawn(NodeBundle {
style: Style {
flex_basis: Val::Percent(100.0),
width: Val::Percent(100.0),
height: Val::Percent(100.0),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
..Default::default()

View file

@ -24,8 +24,9 @@ fn spawn(mut commands: Commands, asset_server: Res<AssetServer>) {
let root = commands
.spawn(NodeBundle {
style: Style {
flex_direction: FlexDirection::Column,
width: Val::Percent(100.),
height: Val::Percent(100.),
flex_direction: FlexDirection::Column,
..Default::default()
},
background_color: Color::BLACK.into(),

View file

@ -20,6 +20,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
.spawn(NodeBundle {
style: Style {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
align_items: AlignItems::Center,
justify_content: JustifyContent::SpaceAround,
..default()

View file

@ -29,6 +29,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
.spawn(NodeBundle {
style: Style {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
justify_content: JustifyContent::SpaceBetween,
..default()
},

View file

@ -40,8 +40,9 @@ fn setup(
commands
.spawn(NodeBundle {
style: Style {
flex_direction: FlexDirection::Column,
width: Val::Percent(100.0),
height: Val::Percent(100.0),
flex_direction: FlexDirection::Column,
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
row_gap: Val::Px(text_style.font_size * 2.),

View file

@ -23,6 +23,7 @@ fn setup(mut commands: Commands) {
.spawn(NodeBundle {
style: Style {
width: Val::Percent(100.),
height: Val::Percent(100.),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..default()

View file

@ -28,6 +28,7 @@ fn setup(mut commands: Commands) {
.spawn(NodeBundle {
style: Style {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
justify_content: JustifyContent::SpaceBetween,
..default()
},