[bevy_ui/layout] Add tests, missing asserts, and missing debug fields for UiSurface (#12803)

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

- [x] Blocked by https://github.com/bevyengine/bevy/pull/12801
- [x] Blocked by https://github.com/bevyengine/bevy/pull/12802

---

# Objective

- Add tests to `UiSurface`
- Add missing asserts in `_assert_send_sync_ui_surface_impl_safe`
- Add missing Debug field print for `camera_entity_to_taffy`

## Solution

- Adds tests to `UiSurface`
- Adds missing asserts in `_assert_send_sync_ui_surface_impl_safe`
- Adds missing impl Debug field print for `camera_entity_to_taffy`

---------

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
This commit is contained in:
Brett Striker 2024-10-15 10:06:17 -04:00 committed by GitHub
parent 4ac528a579
commit de08fb2afa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 486 additions and 7 deletions

View file

@ -55,6 +55,16 @@ impl LayoutContext {
} }
} }
#[cfg(test)]
impl LayoutContext {
pub const TEST_CONTEXT: Self = Self {
scale_factor: 1.0,
physical_size: Vec2::new(1000.0, 1000.0),
min_size: 0.0,
max_size: 1000.0,
};
}
impl Default for LayoutContext { impl Default for LayoutContext {
fn default() -> Self { fn default() -> Self {
Self::DEFAULT Self::DEFAULT
@ -513,7 +523,7 @@ mod tests {
prelude::*, prelude::*,
ui_layout_system, ui_layout_system,
update::update_target_camera_system, update::update_target_camera_system,
ContentSize, ContentSize, LayoutContext,
}; };
#[test] #[test]
@ -1234,4 +1244,57 @@ mod tests {
ui_schedule.run(&mut world); ui_schedule.run(&mut world);
} }
#[test]
fn test_ui_surface_compute_camera_layout() {
use bevy_ecs::prelude::ResMut;
let (mut world, ..) = setup_ui_test_world();
let camera_entity = Entity::from_raw(0);
let root_node_entity = Entity::from_raw(1);
struct TestSystemParam {
camera_entity: Entity,
root_node_entity: Entity,
}
fn test_system(
params: In<TestSystemParam>,
mut ui_surface: ResMut<UiSurface>,
#[cfg(feature = "bevy_text")] mut computed_text_block_query: Query<
&mut bevy_text::ComputedTextBlock,
>,
#[cfg(feature = "bevy_text")] mut font_system: ResMut<bevy_text::CosmicFontSystem>,
) {
ui_surface.upsert_node(
&LayoutContext::TEST_CONTEXT,
params.root_node_entity,
&Style::default(),
None,
);
ui_surface.compute_camera_layout(
params.camera_entity,
UVec2::new(800, 600),
#[cfg(feature = "bevy_text")]
&mut computed_text_block_query,
#[cfg(feature = "bevy_text")]
&mut font_system.0,
);
}
let _ = world.run_system_once_with(
TestSystemParam {
camera_entity,
root_node_entity,
},
test_system,
);
let ui_surface = world.resource::<UiSurface>();
let taffy_node = ui_surface.entity_to_taffy.get(&root_node_entity).unwrap();
assert!(ui_surface.taffy.layout(*taffy_node).is_ok());
}
} }

View file

@ -33,6 +33,8 @@ pub struct UiSurface {
fn _assert_send_sync_ui_surface_impl_safe() { fn _assert_send_sync_ui_surface_impl_safe() {
fn _assert_send_sync<T: Send + Sync>() {} fn _assert_send_sync<T: Send + Sync>() {}
_assert_send_sync::<EntityHashMap<taffy::NodeId>>(); _assert_send_sync::<EntityHashMap<taffy::NodeId>>();
_assert_send_sync::<EntityHashMap<EntityHashMap<taffy::NodeId>>>();
_assert_send_sync::<EntityHashMap<Vec<RootNodePair>>>();
_assert_send_sync::<TaffyTree<NodeMeasure>>(); _assert_send_sync::<TaffyTree<NodeMeasure>>();
_assert_send_sync::<UiSurface>(); _assert_send_sync::<UiSurface>();
} }
@ -41,7 +43,9 @@ impl fmt::Debug for UiSurface {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("UiSurface") f.debug_struct("UiSurface")
.field("entity_to_taffy", &self.entity_to_taffy) .field("entity_to_taffy", &self.entity_to_taffy)
.field("camera_entity_to_taffy", &self.camera_entity_to_taffy)
.field("camera_roots", &self.camera_roots) .field("camera_roots", &self.camera_roots)
.field("taffy_children_scratch", &self.taffy_children_scratch)
.finish() .finish()
} }
} }
@ -312,3 +316,409 @@ fn get_text_buffer<'a>(
}; };
Some(computed.into_inner()) Some(computed.into_inner())
} }
#[cfg(test)]
mod tests {
use super::*;
use crate::{ContentSize, FixedMeasure};
use bevy_math::Vec2;
use taffy::TraversePartialTree;
/// Checks if the parent of the `user_root_node` in a `RootNodePair`
/// is correctly assigned as the `implicit_viewport_node`.
fn is_root_node_pair_valid(
taffy_tree: &TaffyTree<NodeMeasure>,
root_node_pair: &RootNodePair,
) -> bool {
taffy_tree.parent(root_node_pair.user_root_node)
== Some(root_node_pair.implicit_viewport_node)
}
/// Tries to get the root node pair for a given root node entity with the specified camera entity
fn get_root_node_pair_exact(
ui_surface: &UiSurface,
root_node_entity: Entity,
camera_entity: Entity,
) -> Option<&RootNodePair> {
let root_node_pairs = ui_surface.camera_roots.get(&camera_entity)?;
let root_node_taffy = ui_surface.entity_to_taffy.get(&root_node_entity)?;
root_node_pairs
.iter()
.find(|&root_node_pair| root_node_pair.user_root_node == *root_node_taffy)
}
#[test]
fn test_initialization() {
let ui_surface = UiSurface::default();
assert!(ui_surface.entity_to_taffy.is_empty());
assert!(ui_surface.camera_entity_to_taffy.is_empty());
assert!(ui_surface.camera_roots.is_empty());
assert_eq!(ui_surface.taffy.total_node_count(), 0);
}
#[test]
fn test_upsert() {
let mut ui_surface = UiSurface::default();
let camera_entity = Entity::from_raw(0);
let root_node_entity = Entity::from_raw(1);
let style = Style::default();
// standard upsert
ui_surface.upsert_node(&LayoutContext::TEST_CONTEXT, root_node_entity, &style, None);
// should be inserted into taffy
assert_eq!(ui_surface.taffy.total_node_count(), 1);
assert!(ui_surface.entity_to_taffy.contains_key(&root_node_entity));
// test duplicate insert 1
ui_surface.upsert_node(&LayoutContext::TEST_CONTEXT, root_node_entity, &style, None);
// node count should not have increased
assert_eq!(ui_surface.taffy.total_node_count(), 1);
// assign root node to camera
ui_surface.set_camera_children(camera_entity, vec![root_node_entity].into_iter());
// each root node will create 2 taffy nodes
assert_eq!(ui_surface.taffy.total_node_count(), 2);
// root node pair should now exist
let root_node_pair = get_root_node_pair_exact(&ui_surface, root_node_entity, camera_entity)
.expect("expected root node pair");
assert!(is_root_node_pair_valid(&ui_surface.taffy, root_node_pair));
// test duplicate insert 2
ui_surface.upsert_node(&LayoutContext::TEST_CONTEXT, root_node_entity, &style, None);
// node count should not have increased
assert_eq!(ui_surface.taffy.total_node_count(), 2);
// root node pair should be unaffected
let root_node_pair = get_root_node_pair_exact(&ui_surface, root_node_entity, camera_entity)
.expect("expected root node pair");
assert!(is_root_node_pair_valid(&ui_surface.taffy, root_node_pair));
}
#[test]
fn test_get_root_node_pair_exact() {
/// Attempts to find the camera entity that holds a reference to the given root node entity
fn get_associated_camera_entity(
ui_surface: &UiSurface,
root_node_entity: Entity,
) -> Option<Entity> {
for (&camera_entity, root_node_map) in ui_surface.camera_entity_to_taffy.iter() {
if root_node_map.contains_key(&root_node_entity) {
return Some(camera_entity);
}
}
None
}
/// Attempts to find the root node pair corresponding to the given root node entity
fn get_root_node_pair(
ui_surface: &UiSurface,
root_node_entity: Entity,
) -> Option<&RootNodePair> {
let camera_entity = get_associated_camera_entity(ui_surface, root_node_entity)?;
get_root_node_pair_exact(ui_surface, root_node_entity, camera_entity)
}
let mut ui_surface = UiSurface::default();
let camera_entity = Entity::from_raw(0);
let root_node_entity = Entity::from_raw(1);
let style = Style::default();
ui_surface.upsert_node(&LayoutContext::TEST_CONTEXT, root_node_entity, &style, None);
// assign root node to camera
ui_surface.set_camera_children(camera_entity, [root_node_entity].into_iter());
assert_eq!(
get_associated_camera_entity(&ui_surface, root_node_entity),
Some(camera_entity)
);
assert_eq!(
get_associated_camera_entity(&ui_surface, Entity::from_raw(2)),
None
);
let root_node_pair = get_root_node_pair(&ui_surface, root_node_entity);
assert!(root_node_pair.is_some());
assert_eq!(
Some(root_node_pair.unwrap().user_root_node).as_ref(),
ui_surface.entity_to_taffy.get(&root_node_entity)
);
assert_eq!(
get_root_node_pair_exact(&ui_surface, root_node_entity, camera_entity),
root_node_pair
);
}
#[allow(unreachable_code)]
#[test]
fn test_remove_camera_entities() {
let mut ui_surface = UiSurface::default();
let camera_entity = Entity::from_raw(0);
let root_node_entity = Entity::from_raw(1);
let style = Style::default();
ui_surface.upsert_node(&LayoutContext::TEST_CONTEXT, root_node_entity, &style, None);
// assign root node to camera
ui_surface.set_camera_children(camera_entity, [root_node_entity].into_iter());
assert!(ui_surface
.camera_entity_to_taffy
.contains_key(&camera_entity));
assert!(ui_surface
.camera_entity_to_taffy
.get(&camera_entity)
.unwrap()
.contains_key(&root_node_entity));
assert!(ui_surface.camera_roots.contains_key(&camera_entity));
let root_node_pair = get_root_node_pair_exact(&ui_surface, root_node_entity, camera_entity)
.expect("expected root node pair");
assert!(ui_surface
.camera_roots
.get(&camera_entity)
.unwrap()
.contains(root_node_pair));
ui_surface.remove_camera_entities([camera_entity]);
// should not affect `entity_to_taffy`
assert!(ui_surface.entity_to_taffy.contains_key(&root_node_entity));
// `camera_roots` and `camera_entity_to_taffy` should no longer contain entries for `camera_entity`
assert!(!ui_surface
.camera_entity_to_taffy
.contains_key(&camera_entity));
return; // TODO: can't pass the test if we continue - not implemented (remove allow(unreachable_code))
assert!(!ui_surface.camera_roots.contains_key(&camera_entity));
// root node pair should be removed
let root_node_pair = get_root_node_pair_exact(&ui_surface, root_node_entity, camera_entity);
assert_eq!(root_node_pair, None);
}
#[allow(unreachable_code)]
#[test]
fn test_remove_entities() {
let mut ui_surface = UiSurface::default();
let camera_entity = Entity::from_raw(0);
let root_node_entity = Entity::from_raw(1);
let style = Style::default();
ui_surface.upsert_node(&LayoutContext::TEST_CONTEXT, root_node_entity, &style, None);
ui_surface.set_camera_children(camera_entity, [root_node_entity].into_iter());
assert!(ui_surface.entity_to_taffy.contains_key(&root_node_entity));
assert!(ui_surface
.camera_entity_to_taffy
.get(&camera_entity)
.unwrap()
.contains_key(&root_node_entity));
let root_node_pair =
get_root_node_pair_exact(&ui_surface, root_node_entity, camera_entity).unwrap();
assert!(ui_surface
.camera_roots
.get(&camera_entity)
.unwrap()
.contains(root_node_pair));
ui_surface.remove_entities([root_node_entity]);
assert!(!ui_surface.entity_to_taffy.contains_key(&root_node_entity));
return; // TODO: can't pass the test if we continue - not implemented (remove allow(unreachable_code))
assert!(!ui_surface
.camera_entity_to_taffy
.get(&camera_entity)
.unwrap()
.contains_key(&root_node_entity));
assert!(!ui_surface
.camera_entity_to_taffy
.get(&camera_entity)
.unwrap()
.contains_key(&root_node_entity));
assert!(ui_surface
.camera_roots
.get(&camera_entity)
.unwrap()
.is_empty());
}
#[test]
fn test_try_update_measure() {
let mut ui_surface = UiSurface::default();
let root_node_entity = Entity::from_raw(1);
let style = Style::default();
ui_surface.upsert_node(&LayoutContext::TEST_CONTEXT, root_node_entity, &style, None);
let mut content_size = ContentSize::default();
content_size.set(NodeMeasure::Fixed(FixedMeasure { size: Vec2::ONE }));
let measure_func = content_size.measure.take().unwrap();
assert!(ui_surface
.update_node_context(root_node_entity, measure_func)
.is_some());
}
#[test]
fn test_update_children() {
let mut ui_surface = UiSurface::default();
let root_node_entity = Entity::from_raw(1);
let child_entity = Entity::from_raw(2);
let style = Style::default();
ui_surface.upsert_node(&LayoutContext::TEST_CONTEXT, root_node_entity, &style, None);
ui_surface.upsert_node(&LayoutContext::TEST_CONTEXT, child_entity, &style, None);
ui_surface.update_children(root_node_entity, vec![child_entity].into_iter());
let parent_node = *ui_surface.entity_to_taffy.get(&root_node_entity).unwrap();
let child_node = *ui_surface.entity_to_taffy.get(&child_entity).unwrap();
assert_eq!(ui_surface.taffy.parent(child_node), Some(parent_node));
}
#[allow(unreachable_code)]
#[test]
fn test_set_camera_children() {
let mut ui_surface = UiSurface::default();
let camera_entity = Entity::from_raw(0);
let root_node_entity = Entity::from_raw(1);
let child_entity = Entity::from_raw(2);
let style = Style::default();
ui_surface.upsert_node(&LayoutContext::TEST_CONTEXT, root_node_entity, &style, None);
ui_surface.upsert_node(&LayoutContext::TEST_CONTEXT, child_entity, &style, None);
let root_taffy_node = *ui_surface.entity_to_taffy.get(&root_node_entity).unwrap();
let child_taffy = *ui_surface.entity_to_taffy.get(&child_entity).unwrap();
// set up the relationship manually
ui_surface
.taffy
.add_child(root_taffy_node, child_taffy)
.unwrap();
ui_surface.set_camera_children(camera_entity, [root_node_entity].into_iter());
assert!(
ui_surface
.camera_entity_to_taffy
.get(&camera_entity)
.unwrap()
.contains_key(&root_node_entity),
"root node not associated with camera"
);
assert!(
!ui_surface
.camera_entity_to_taffy
.get(&camera_entity)
.unwrap()
.contains_key(&child_entity),
"child of root node should not be associated with camera"
);
let _root_node_pair =
get_root_node_pair_exact(&ui_surface, root_node_entity, camera_entity)
.expect("expected root node pair");
assert_eq!(ui_surface.taffy.parent(child_taffy), Some(root_taffy_node));
let root_taffy_children = ui_surface.taffy.children(root_taffy_node).unwrap();
assert!(
root_taffy_children.contains(&child_taffy),
"root node is not a parent of child node"
);
assert_eq!(
ui_surface.taffy.child_count(root_taffy_node),
1,
"expected root node child count to be 1"
);
// clear camera's root nodes
ui_surface.set_camera_children(camera_entity, Vec::<Entity>::new().into_iter());
return; // TODO: can't pass the test if we continue - not implemented (remove allow(unreachable_code))
assert!(
!ui_surface
.camera_entity_to_taffy
.get(&camera_entity)
.unwrap()
.contains_key(&root_node_entity),
"root node should have been unassociated with camera"
);
assert!(
!ui_surface
.camera_entity_to_taffy
.get(&camera_entity)
.unwrap()
.contains_key(&child_entity),
"child of root node should not be associated with camera"
);
let root_taffy_children = ui_surface.taffy.children(root_taffy_node).unwrap();
assert!(
root_taffy_children.contains(&child_taffy),
"root node is not a parent of child node"
);
assert_eq!(
ui_surface.taffy.child_count(root_taffy_node),
1,
"expected root node child count to be 1"
);
// re-associate root node with camera
ui_surface.set_camera_children(camera_entity, vec![root_node_entity].into_iter());
assert!(
ui_surface
.camera_entity_to_taffy
.get(&camera_entity)
.unwrap()
.contains_key(&root_node_entity),
"root node should have been re-associated with camera"
);
assert!(
!ui_surface
.camera_entity_to_taffy
.get(&camera_entity)
.unwrap()
.contains_key(&child_entity),
"child of root node should not be associated with camera"
);
let child_taffy = ui_surface.entity_to_taffy.get(&child_entity).unwrap();
let root_taffy_children = ui_surface.taffy.children(root_taffy_node).unwrap();
assert!(
root_taffy_children.contains(child_taffy),
"root node is not a parent of child node"
);
assert_eq!(
ui_surface.taffy.child_count(root_taffy_node),
1,
"expected root node child count to be 1"
);
}
#[test]
#[cfg(not(feature = "bevy_text"))]
fn test_compute_camera_layout() {
let mut ui_surface = UiSurface::default();
let camera_entity = Entity::from_raw(0);
let root_node_entity = Entity::from_raw(1);
let style = Style::default();
ui_surface.upsert_node(&LayoutContext::TEST_CONTEXT, root_node_entity, &style, None);
ui_surface.compute_camera_layout(camera_entity, UVec2::new(800, 600));
let taffy_node = ui_surface.entity_to_taffy.get(&root_node_entity).unwrap();
assert!(ui_surface.taffy.layout(*taffy_node).is_ok());
}
}

View file

@ -46,6 +46,7 @@ use widget::UiImageSize;
/// ///
/// This includes the most common types in this crate, re-exported for your convenience. /// This includes the most common types in this crate, re-exported for your convenience.
pub mod prelude { pub mod prelude {
#[cfg(feature = "bevy_text")]
#[allow(deprecated)] #[allow(deprecated)]
#[doc(hidden)] #[doc(hidden)]
pub use crate::widget::TextBundle; pub use crate::widget::TextBundle;
@ -178,17 +179,22 @@ impl Plugin for UiPlugin {
ui_focus_system.in_set(UiSystem::Focus).after(InputSystem), ui_focus_system.in_set(UiSystem::Focus).after(InputSystem),
); );
let ui_layout_system_config = ui_layout_system
.in_set(UiSystem::Layout)
.before(TransformSystem::TransformPropagate);
#[cfg(feature = "bevy_text")]
let ui_layout_system_config = ui_layout_system_config
// Text and Text2D operate on disjoint sets of entities
.ambiguous_with(bevy_text::update_text2d_layout)
.ambiguous_with(bevy_text::detect_text_needs_rerender::<bevy_text::Text2d>);
app.add_systems( app.add_systems(
PostUpdate, PostUpdate,
( (
check_visibility::<WithNode>.in_set(VisibilitySystems::CheckVisibility), check_visibility::<WithNode>.in_set(VisibilitySystems::CheckVisibility),
update_target_camera_system.in_set(UiSystem::Prepare), update_target_camera_system.in_set(UiSystem::Prepare),
ui_layout_system ui_layout_system_config,
.in_set(UiSystem::Layout)
.before(TransformSystem::TransformPropagate)
// Text and Text2D operate on disjoint sets of entities
.ambiguous_with(bevy_text::detect_text_needs_rerender::<bevy_text::Text2d>)
.ambiguous_with(bevy_text::update_text2d_layout),
ui_stack_system ui_stack_system
.in_set(UiSystem::Stack) .in_set(UiSystem::Stack)
// the systems don't care about stack index // the systems don't care about stack index