[bevy_ui/layout] Extract UiSurface to its own file (#12801)

This is 1 of 5 iterative PR's that affect bevy_ui/layout

---

# Objective

- Extract `UiSurface` into its own file to make diffs in future PR's
easier to digest

## Solution

- Moved `UiSurface` to its own file
This commit is contained in:
Brett Striker 2024-04-01 18:47:34 -04:00 committed by GitHub
parent d0a5ddacd9
commit cf092d45f9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 253 additions and 231 deletions

View file

@ -1,10 +1,13 @@
use crate::UiSurface;
use bevy_ecs::prelude::Entity;
use bevy_utils::HashMap;
use std::fmt::Write; use std::fmt::Write;
use taffy::prelude::Node; use taffy::prelude::Node;
use taffy::tree::LayoutTree; use taffy::tree::LayoutTree;
use bevy_ecs::prelude::Entity;
use bevy_utils::HashMap;
use crate::layout::ui_surface::UiSurface;
/// Prints a debug representation of the computed layout of the UI layout tree for each window. /// Prints a debug representation of the computed layout of the UI layout tree for each window.
pub fn print_ui_layout_tree(ui_surface: &UiSurface) { pub fn print_ui_layout_tree(ui_surface: &UiSurface) {
let taffy_to_entity: HashMap<Node, Entity> = ui_surface let taffy_to_entity: HashMap<Node, Entity> = ui_surface

View file

@ -1,14 +1,12 @@
mod convert; use thiserror::Error;
pub mod debug;
use crate::{ContentSize, DefaultUiCamera, Node, Outline, Style, TargetCamera, UiScale};
use bevy_ecs::{ use bevy_ecs::{
change_detection::{DetectChanges, DetectChangesMut}, change_detection::{DetectChanges, DetectChangesMut},
entity::{Entity, EntityHashMap}, entity::Entity,
event::EventReader, event::EventReader,
query::{With, Without}, query::{With, Without},
removal_detection::RemovedComponents, removal_detection::RemovedComponents,
system::{Query, Res, ResMut, Resource, SystemParam}, system::{Query, Res, ResMut, SystemParam},
world::Ref, world::Ref,
}; };
use bevy_hierarchy::{Children, Parent}; use bevy_hierarchy::{Children, Parent};
@ -16,11 +14,15 @@ use bevy_math::{UVec2, Vec2};
use bevy_render::camera::{Camera, NormalizedRenderTarget}; use bevy_render::camera::{Camera, NormalizedRenderTarget};
use bevy_transform::components::Transform; use bevy_transform::components::Transform;
use bevy_utils::tracing::warn; use bevy_utils::tracing::warn;
use bevy_utils::{default, HashMap, HashSet}; use bevy_utils::{HashMap, HashSet};
use bevy_window::{PrimaryWindow, Window, WindowScaleFactorChanged}; use bevy_window::{PrimaryWindow, Window, WindowScaleFactorChanged};
use std::fmt; use ui_surface::UiSurface;
use taffy::{tree::LayoutTree, Taffy};
use thiserror::Error; use crate::{ContentSize, DefaultUiCamera, Node, Outline, Style, TargetCamera, UiScale};
mod convert;
pub mod debug;
pub(crate) mod ui_surface;
pub struct LayoutContext { pub struct LayoutContext {
pub scale_factor: f32, pub scale_factor: f32,
@ -41,218 +43,6 @@ 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: EntityHashMap<taffy::node::Node>,
camera_entity_to_taffy: EntityHashMap<EntityHashMap<taffy::node::Node>>,
camera_roots: EntityHashMap<Vec<RootNodePair>>,
taffy: Taffy,
}
fn _assert_send_sync_ui_surface_impl_safe() {
fn _assert_send_sync<T: Send + Sync>() {}
_assert_send_sync::<EntityHashMap<taffy::node::Node>>();
_assert_send_sync::<Taffy>();
_assert_send_sync::<UiSurface>();
}
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("camera_roots", &self.camera_roots)
.finish()
}
}
impl Default for UiSurface {
fn default() -> Self {
let mut taffy = Taffy::new();
taffy.disable_rounding();
Self {
entity_to_taffy: Default::default(),
camera_entity_to_taffy: Default::default(),
camera_roots: Default::default(),
taffy,
}
}
}
impl UiSurface {
/// Retrieves the Taffy node associated with the given UI node entity and updates its style.
/// If no associated Taffy node exists a new Taffy node is inserted into the Taffy layout.
pub fn upsert_node(&mut self, entity: Entity, style: &Style, context: &LayoutContext) {
let mut added = false;
let taffy = &mut self.taffy;
let taffy_node = self.entity_to_taffy.entry(entity).or_insert_with(|| {
added = true;
taffy.new_leaf(convert::from_style(context, style)).unwrap()
});
if !added {
self.taffy
.set_style(*taffy_node, convert::from_style(context, style))
.unwrap();
}
}
/// Update the `MeasureFunc` of the taffy node corresponding to the given [`Entity`] if the node exists.
pub fn try_update_measure(
&mut self,
entity: Entity,
measure_func: taffy::node::MeasureFunc,
) -> Option<()> {
let taffy_node = self.entity_to_taffy.get(&entity)?;
self.taffy.set_measure(*taffy_node, Some(measure_func)).ok()
}
/// Update the children of the taffy node corresponding to the given [`Entity`].
pub fn update_children(&mut self, entity: Entity, children: &Children) {
let mut taffy_children = Vec::with_capacity(children.len());
for child in children {
if let Some(taffy_node) = self.entity_to_taffy.get(child) {
taffy_children.push(*taffy_node);
} else {
warn!(
"Unstyled child in a UI entity hierarchy. You are using an entity \
without UI components as a child of an entity with UI components, results may be unexpected."
);
}
}
let taffy_node = self.entity_to_taffy.get(&entity).unwrap();
self.taffy
.set_children(*taffy_node, &taffy_children)
.unwrap();
}
/// Removes children from the entity's taffy node if it exists. Does nothing otherwise.
pub fn try_remove_children(&mut self, entity: Entity) {
if let Some(taffy_node) = self.entity_to_taffy.get(&entity) {
self.taffy.set_children(*taffy_node, &[]).unwrap();
}
}
/// Removes the measure from the entity's taffy node if it exists. Does nothing otherwise.
pub fn try_remove_measure(&mut self, entity: Entity) {
if let Some(taffy_node) = self.entity_to_taffy.get(&entity) {
self.taffy.set_measure(*taffy_node, None).unwrap();
}
}
/// Set the ui node entities without a [`Parent`] as children to the root node in the taffy layout.
pub fn set_camera_children(
&mut self,
camera_id: Entity,
children: impl Iterator<Item = Entity>,
) {
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 camera_root_node_map = self.camera_entity_to_taffy.entry(camera_id).or_default();
let existing_roots = self.camera_roots.entry(camera_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(|| {
if let Some(previous_parent) = self.taffy.parent(node) {
// remove the root node from the previous implicit node's children
self.taffy.remove_child(previous_parent, node).unwrap();
}
let viewport_node = *camera_root_node_map
.entry(entity)
.or_insert_with(|| self.taffy.new_leaf(viewport_style.clone()).unwrap());
self.taffy.add_child(viewport_node, node).unwrap();
RootNodePair {
implicit_viewport_node: viewport_node,
user_root_node: node,
}
});
new_roots.push(root_node);
}
self.camera_roots.insert(camera_id, new_roots);
}
/// Compute the layout for each window entity's corresponding root node in the layout.
pub fn compute_camera_layout(&mut self, camera: Entity, render_target_resolution: UVec2) {
let Some(camera_root_nodes) = self.camera_roots.get(&camera) else {
return;
};
let available_space = taffy::geometry::Size {
width: taffy::style::AvailableSpace::Definite(render_target_resolution.x as f32),
height: taffy::style::AvailableSpace::Definite(render_target_resolution.y as f32),
};
for root_nodes in camera_root_nodes {
self.taffy
.compute_layout(root_nodes.implicit_viewport_node, available_space)
.unwrap();
}
}
/// Removes each camera entity from the internal map and then removes their associated node from taffy
pub fn remove_camera_entities(&mut self, entities: impl IntoIterator<Item = Entity>) {
for entity in entities {
if let Some(camera_root_node_map) = self.camera_entity_to_taffy.remove(&entity) {
for (_, node) in camera_root_node_map.iter() {
self.taffy.remove(*node).unwrap();
}
}
}
}
/// Removes each entity from the internal map and then removes their associated node from taffy
pub fn remove_entities(&mut self, entities: impl IntoIterator<Item = Entity>) {
for entity in entities {
if let Some(node) = self.entity_to_taffy.remove(&entity) {
self.taffy.remove(node).unwrap();
}
}
}
/// Get the layout geometry for the taffy node corresponding to the ui node [`Entity`].
/// Does not compute the layout geometry, `compute_window_layouts` should be run before using this function.
pub fn get_layout(&self, entity: Entity) -> Result<&taffy::layout::Layout, LayoutError> {
if let Some(taffy_node) = self.entity_to_taffy.get(&entity) {
self.taffy
.layout(*taffy_node)
.map_err(LayoutError::TaffyError)
} else {
warn!(
"Styled child in a non-UI entity hierarchy. You are using an entity \
with UI components as a child of an entity without UI components, results may be unexpected."
);
Err(LayoutError::InvalidHierarchy)
}
}
}
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum LayoutError { pub enum LayoutError {
#[error("Invalid hierarchy")] #[error("Invalid hierarchy")]
@ -533,12 +323,8 @@ fn round_layout_coords(value: Vec2) -> Vec2 {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::layout::round_layout_coords; use taffy::tree::LayoutTree;
use crate::prelude::*;
use crate::ui_layout_system;
use crate::update::update_target_camera_system;
use crate::ContentSize;
use crate::UiSurface;
use bevy_asset::AssetEvent; use bevy_asset::AssetEvent;
use bevy_asset::Assets; use bevy_asset::Assets;
use bevy_core_pipeline::core_2d::Camera2dBundle; use bevy_core_pipeline::core_2d::Camera2dBundle;
@ -566,7 +352,13 @@ mod tests {
use bevy_window::WindowResized; use bevy_window::WindowResized;
use bevy_window::WindowResolution; use bevy_window::WindowResolution;
use bevy_window::WindowScaleFactorChanged; use bevy_window::WindowScaleFactorChanged;
use taffy::tree::LayoutTree;
use crate::layout::round_layout_coords;
use crate::layout::ui_surface::UiSurface;
use crate::prelude::*;
use crate::ui_layout_system;
use crate::update::update_target_camera_system;
use crate::ContentSize;
#[test] #[test]
fn round_layout_coords_must_round_ties_up() { fn round_layout_coords_must_round_ties_up() {

View file

@ -0,0 +1,226 @@
use std::fmt;
use taffy::prelude::LayoutTree;
use taffy::Taffy;
use bevy_ecs::entity::{Entity, EntityHashMap};
use bevy_ecs::prelude::Resource;
use bevy_hierarchy::Children;
use bevy_math::UVec2;
use bevy_utils::default;
use bevy_utils::tracing::warn;
use crate::layout::convert;
use crate::{LayoutContext, LayoutError, Style};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RootNodePair {
// The implicit "viewport" node created by Bevy
pub(super) implicit_viewport_node: taffy::node::Node,
// The root (parentless) node specified by the user
pub(super) user_root_node: taffy::node::Node,
}
#[derive(Resource)]
pub struct UiSurface {
pub(super) entity_to_taffy: EntityHashMap<taffy::node::Node>,
pub(super) camera_entity_to_taffy: EntityHashMap<EntityHashMap<taffy::node::Node>>,
pub(super) camera_roots: EntityHashMap<Vec<RootNodePair>>,
pub(super) taffy: Taffy,
}
fn _assert_send_sync_ui_surface_impl_safe() {
fn _assert_send_sync<T: Send + Sync>() {}
_assert_send_sync::<EntityHashMap<taffy::node::Node>>();
_assert_send_sync::<Taffy>();
_assert_send_sync::<UiSurface>();
}
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("camera_roots", &self.camera_roots)
.finish()
}
}
impl Default for UiSurface {
fn default() -> Self {
let mut taffy = Taffy::new();
taffy.disable_rounding();
Self {
entity_to_taffy: Default::default(),
camera_entity_to_taffy: Default::default(),
camera_roots: Default::default(),
taffy,
}
}
}
impl UiSurface {
/// Retrieves the Taffy node associated with the given UI node entity and updates its style.
/// If no associated Taffy node exists a new Taffy node is inserted into the Taffy layout.
pub fn upsert_node(&mut self, entity: Entity, style: &Style, context: &LayoutContext) {
let mut added = false;
let taffy = &mut self.taffy;
let taffy_node = self.entity_to_taffy.entry(entity).or_insert_with(|| {
added = true;
taffy.new_leaf(convert::from_style(context, style)).unwrap()
});
if !added {
self.taffy
.set_style(*taffy_node, convert::from_style(context, style))
.unwrap();
}
}
/// Update the `MeasureFunc` of the taffy node corresponding to the given [`Entity`] if the node exists.
pub fn try_update_measure(
&mut self,
entity: Entity,
measure_func: taffy::node::MeasureFunc,
) -> Option<()> {
let taffy_node = self.entity_to_taffy.get(&entity)?;
self.taffy.set_measure(*taffy_node, Some(measure_func)).ok()
}
/// Update the children of the taffy node corresponding to the given [`Entity`].
pub fn update_children(&mut self, entity: Entity, children: &Children) {
let mut taffy_children = Vec::with_capacity(children.len());
for child in children {
if let Some(taffy_node) = self.entity_to_taffy.get(child) {
taffy_children.push(*taffy_node);
} else {
warn!(
"Unstyled child in a UI entity hierarchy. You are using an entity \
without UI components as a child of an entity with UI components, results may be unexpected."
);
}
}
let taffy_node = self.entity_to_taffy.get(&entity).unwrap();
self.taffy
.set_children(*taffy_node, &taffy_children)
.unwrap();
}
/// Removes children from the entity's taffy node if it exists. Does nothing otherwise.
pub fn try_remove_children(&mut self, entity: Entity) {
if let Some(taffy_node) = self.entity_to_taffy.get(&entity) {
self.taffy.set_children(*taffy_node, &[]).unwrap();
}
}
/// Removes the measure from the entity's taffy node if it exists. Does nothing otherwise.
pub fn try_remove_measure(&mut self, entity: Entity) {
if let Some(taffy_node) = self.entity_to_taffy.get(&entity) {
self.taffy.set_measure(*taffy_node, None).unwrap();
}
}
/// Set the ui node entities without a [`bevy_hierarchy::Parent`] as children to the root node in the taffy layout.
pub fn set_camera_children(
&mut self,
camera_id: Entity,
children: impl Iterator<Item = Entity>,
) {
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 camera_root_node_map = self.camera_entity_to_taffy.entry(camera_id).or_default();
let existing_roots = self.camera_roots.entry(camera_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(|| {
if let Some(previous_parent) = self.taffy.parent(node) {
// remove the root node from the previous implicit node's children
self.taffy.remove_child(previous_parent, node).unwrap();
}
let viewport_node = *camera_root_node_map
.entry(entity)
.or_insert_with(|| self.taffy.new_leaf(viewport_style.clone()).unwrap());
self.taffy.add_child(viewport_node, node).unwrap();
RootNodePair {
implicit_viewport_node: viewport_node,
user_root_node: node,
}
});
new_roots.push(root_node);
}
self.camera_roots.insert(camera_id, new_roots);
}
/// Compute the layout for each window entity's corresponding root node in the layout.
pub fn compute_camera_layout(&mut self, camera: Entity, render_target_resolution: UVec2) {
let Some(camera_root_nodes) = self.camera_roots.get(&camera) else {
return;
};
let available_space = taffy::geometry::Size {
width: taffy::style::AvailableSpace::Definite(render_target_resolution.x as f32),
height: taffy::style::AvailableSpace::Definite(render_target_resolution.y as f32),
};
for root_nodes in camera_root_nodes {
self.taffy
.compute_layout(root_nodes.implicit_viewport_node, available_space)
.unwrap();
}
}
/// Removes each camera entity from the internal map and then removes their associated node from taffy
pub fn remove_camera_entities(&mut self, entities: impl IntoIterator<Item = Entity>) {
for entity in entities {
if let Some(camera_root_node_map) = self.camera_entity_to_taffy.remove(&entity) {
for (_, node) in camera_root_node_map.iter() {
self.taffy.remove(*node).unwrap();
}
}
}
}
/// Removes each entity from the internal map and then removes their associated node from taffy
pub fn remove_entities(&mut self, entities: impl IntoIterator<Item = Entity>) {
for entity in entities {
if let Some(node) = self.entity_to_taffy.remove(&entity) {
self.taffy.remove(node).unwrap();
}
}
}
/// Get the layout geometry for the taffy node corresponding to the ui node [`Entity`].
/// Does not compute the layout geometry, `compute_window_layouts` should be run before using this function.
pub fn get_layout(&self, entity: Entity) -> Result<&taffy::layout::Layout, LayoutError> {
if let Some(taffy_node) = self.entity_to_taffy.get(&entity) {
self.taffy
.layout(*taffy_node)
.map_err(LayoutError::TaffyError)
} else {
warn!(
"Styled child in a non-UI entity hierarchy. You are using an entity \
with UI components as a child of an entity without UI components, results may be unexpected."
);
Err(LayoutError::InvalidHierarchy)
}
}
}

View file

@ -55,6 +55,7 @@ use bevy_ecs::prelude::*;
use bevy_input::InputSystem; use bevy_input::InputSystem;
use bevy_render::RenderApp; use bevy_render::RenderApp;
use bevy_transform::TransformSystem; use bevy_transform::TransformSystem;
use layout::ui_surface::UiSurface;
use stack::ui_stack_system; use stack::ui_stack_system;
pub use stack::UiStack; pub use stack::UiStack;
use update::{update_clipping_system, update_target_camera_system}; use update::{update_clipping_system, update_target_camera_system};