Path: blob/main/crates/bevy_ui/src/layout/ui_surface.rs
9367 views
use core::fmt;1use core::ops::{Deref, DerefMut};23use bevy_platform::collections::hash_map::Entry;4use taffy::TaffyTree;56use bevy_ecs::{7entity::{Entity, EntityHashMap},8prelude::Resource,9};10use bevy_math::{UVec2, Vec2};11use bevy_utils::default;1213use crate::{layout::convert, LayoutContext, LayoutError, Measure, MeasureArgs, Node, NodeMeasure};14use bevy_text::CosmicFontSystem;1516#[derive(Debug, Copy, Clone, PartialEq, Eq)]17pub struct LayoutNode {18// Implicit "viewport" node if this `LayoutNode` corresponds to a root UI node entity19pub(super) viewport_id: Option<taffy::NodeId>,20// The id of the node in the taffy tree21pub(super) id: taffy::NodeId,22}2324impl From<taffy::NodeId> for LayoutNode {25fn from(value: taffy::NodeId) -> Self {26LayoutNode {27viewport_id: None,28id: value,29}30}31}3233pub(crate) struct UiTree<T>(TaffyTree<T>);3435#[expect(unsafe_code, reason = "TaffyTree is safe as long as calc is not used")]36// SAFETY: Taffy Tree becomes thread unsafe when you use the calc feature, which we do not implement37unsafe impl Send for UiTree<NodeMeasure> {}3839#[expect(unsafe_code, reason = "TaffyTree is safe as long as calc is not used")]40// SAFETY: Taffy Tree becomes thread unsafe when you use the calc feature, which we do not implement41unsafe impl Sync for UiTree<NodeMeasure> {}4243impl<T> Deref for UiTree<T> {44type Target = TaffyTree<T>;45fn deref(&self) -> &Self::Target {46&self.047}48}4950impl<T> DerefMut for UiTree<T> {51fn deref_mut(&mut self) -> &mut Self::Target {52&mut self.053}54}5556#[derive(Resource)]57pub struct UiSurface {58pub root_entity_to_viewport_node: EntityHashMap<taffy::NodeId>,59pub(super) entity_to_taffy: EntityHashMap<LayoutNode>,60pub(super) taffy: UiTree<NodeMeasure>,61taffy_children_scratch: Vec<taffy::NodeId>,62}6364fn _assert_send_sync_ui_surface_impl_safe() {65fn _assert_send_sync<T: Send + Sync>() {}66_assert_send_sync::<EntityHashMap<taffy::NodeId>>();67_assert_send_sync::<UiTree<NodeMeasure>>();68_assert_send_sync::<UiSurface>();69}7071impl fmt::Debug for UiSurface {72fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {73f.debug_struct("UiSurface")74.field("entity_to_taffy", &self.entity_to_taffy)75.field("taffy_children_scratch", &self.taffy_children_scratch)76.finish()77}78}7980impl Default for UiSurface {81fn default() -> Self {82let taffy: UiTree<NodeMeasure> = UiTree(TaffyTree::new());83Self {84root_entity_to_viewport_node: Default::default(),85entity_to_taffy: Default::default(),86taffy,87taffy_children_scratch: Vec::new(),88}89}90}9192impl UiSurface {93/// Retrieves the Taffy node associated with the given UI node entity and updates its style.94/// If no associated Taffy node exists a new Taffy node is inserted into the Taffy layout.95pub fn upsert_node(96&mut self,97layout_context: &LayoutContext,98entity: Entity,99node: &Node,100mut new_node_context: Option<NodeMeasure>,101) {102let taffy = &mut self.taffy;103104match self.entity_to_taffy.entry(entity) {105Entry::Occupied(entry) => {106let taffy_node = *entry.get();107let has_measure = if new_node_context.is_some() {108taffy109.set_node_context(taffy_node.id, new_node_context)110.unwrap();111true112} else {113taffy.get_node_context(taffy_node.id).is_some()114};115116taffy117.set_style(118taffy_node.id,119convert::from_node(node, layout_context, has_measure),120)121.unwrap();122}123Entry::Vacant(entry) => {124let taffy_node = if let Some(measure) = new_node_context.take() {125taffy.new_leaf_with_context(126convert::from_node(node, layout_context, true),127measure,128)129} else {130taffy.new_leaf(convert::from_node(node, layout_context, false))131};132entry.insert(taffy_node.unwrap().into());133}134}135}136137/// Update the `MeasureFunc` of the taffy node corresponding to the given [`Entity`] if the node exists.138pub fn update_node_context(&mut self, entity: Entity, context: NodeMeasure) -> Option<()> {139let taffy_node = self.entity_to_taffy.get(&entity)?;140self.taffy141.set_node_context(taffy_node.id, Some(context))142.ok()143}144145/// Update the children of the taffy node corresponding to the given [`Entity`].146pub fn update_children(&mut self, entity: Entity, children: impl Iterator<Item = Entity>) {147self.taffy_children_scratch.clear();148149for child in children {150if let Some(taffy_node) = self.entity_to_taffy.get_mut(&child) {151self.taffy_children_scratch.push(taffy_node.id);152if let Some(viewport_id) = taffy_node.viewport_id.take() {153self.taffy.remove(viewport_id).ok();154}155}156}157158let taffy_node = self.entity_to_taffy.get(&entity).unwrap();159self.taffy160.set_children(taffy_node.id, &self.taffy_children_scratch)161.unwrap();162}163164/// Removes children from the entity's taffy node if it exists. Does nothing otherwise.165pub fn try_remove_children(&mut self, entity: Entity) {166if let Some(taffy_node) = self.entity_to_taffy.get(&entity) {167self.taffy.set_children(taffy_node.id, &[]).unwrap();168}169}170171/// Removes the measure from the entity's taffy node if it exists. Does nothing otherwise.172pub fn try_remove_node_context(&mut self, entity: Entity) {173if let Some(taffy_node) = self.entity_to_taffy.get(&entity) {174self.taffy.set_node_context(taffy_node.id, None).unwrap();175}176}177178/// Gets or inserts an implicit taffy viewport node corresponding to the given UI root entity179pub fn get_or_insert_taffy_viewport_node(&mut self, ui_root_entity: Entity) -> taffy::NodeId {180*self181.root_entity_to_viewport_node182.entry(ui_root_entity)183.or_insert_with(|| {184let root_node = self.entity_to_taffy.get_mut(&ui_root_entity).unwrap();185let implicit_root = self186.taffy187.new_leaf(taffy::style::Style {188display: taffy::style::Display::Grid,189// Note: Taffy percentages are floats ranging from 0.0 to 1.0.190// So this is setting width:100% and height:100%191size: taffy::geometry::Size {192width: taffy::style_helpers::percent(1.0),193height: taffy::style_helpers::percent(1.0),194},195align_items: Some(taffy::style::AlignItems::Start),196justify_items: Some(taffy::style::JustifyItems::Start),197..default()198})199.unwrap();200self.taffy.add_child(implicit_root, root_node.id).unwrap();201root_node.viewport_id = Some(implicit_root);202implicit_root203})204}205206/// Compute the layout for the given implicit taffy viewport node207pub fn compute_layout<'a>(208&mut self,209ui_root_entity: Entity,210render_target_resolution: UVec2,211buffer_query: &'a mut bevy_ecs::prelude::Query<&mut bevy_text::ComputedTextBlock>,212font_system: &'a mut CosmicFontSystem,213) {214let implicit_viewport_node = self.get_or_insert_taffy_viewport_node(ui_root_entity);215216let available_space = taffy::geometry::Size {217width: taffy::style::AvailableSpace::Definite(render_target_resolution.x as f32),218height: taffy::style::AvailableSpace::Definite(render_target_resolution.y as f32),219};220221self.taffy222.compute_layout_with_measure(223implicit_viewport_node,224available_space,225|known_dimensions: taffy::Size<Option<f32>>,226available_space: taffy::Size<taffy::AvailableSpace>,227_node_id: taffy::NodeId,228context: Option<&mut NodeMeasure>,229style: &taffy::Style|230-> taffy::Size<f32> {231context232.map(|ctx| {233let buffer = get_text_buffer(234crate::widget::TextMeasure::needs_buffer(235known_dimensions.height,236available_space.width,237),238ctx,239buffer_query,240);241let size = ctx.measure(242MeasureArgs {243width: known_dimensions.width,244height: known_dimensions.height,245available_width: available_space.width,246available_height: available_space.height,247font_system,248buffer,249},250style,251);252taffy::Size {253width: size.x,254height: size.y,255}256})257.unwrap_or(taffy::Size::ZERO)258},259)260.unwrap();261}262263/// Removes each entity from the internal map and then removes their associated nodes from taffy264pub fn remove_entities(&mut self, entities: impl IntoIterator<Item = Entity>) {265for entity in entities {266if let Some(node) = self.entity_to_taffy.remove(&entity) {267self.taffy.remove(node.id).unwrap();268if let Some(viewport_node) = node.viewport_id {269self.taffy.remove(viewport_node).ok();270}271}272}273}274275/// Get the layout geometry for the taffy node corresponding to the ui node [`Entity`].276/// Does not compute the layout geometry, `compute_window_layouts` should be run before using this function.277/// On success returns a pair consisting of the final resolved layout values after rounding278/// and the size of the node after layout resolution but before rounding.279pub fn get_layout(280&mut self,281entity: Entity,282use_rounding: bool,283) -> Result<(taffy::Layout, Vec2), LayoutError> {284let Some(taffy_node) = self.entity_to_taffy.get(&entity) else {285return Err(LayoutError::InvalidHierarchy);286};287288if use_rounding {289self.taffy.enable_rounding();290} else {291self.taffy.disable_rounding();292}293294let out = match self.taffy.layout(taffy_node.id).cloned() {295Ok(layout) => {296self.taffy.disable_rounding();297let taffy_size = self.taffy.layout(taffy_node.id).unwrap().size;298let unrounded_size = Vec2::new(taffy_size.width, taffy_size.height);299Ok((layout, unrounded_size))300}301Err(taffy_error) => Err(LayoutError::TaffyError(taffy_error)),302};303304self.taffy.enable_rounding();305out306}307}308309pub fn get_text_buffer<'a>(310needs_buffer: bool,311ctx: &mut NodeMeasure,312query: &'a mut bevy_ecs::prelude::Query<&mut bevy_text::ComputedTextBlock>,313) -> Option<&'a mut bevy_text::ComputedTextBlock> {314// We avoid a query lookup whenever the buffer is not required.315if !needs_buffer {316return None;317}318let NodeMeasure::Text(crate::widget::TextMeasure { info }) = ctx else {319return None;320};321let Ok(computed) = query.get_mut(info.entity) else {322return None;323};324Some(computed.into_inner())325}326327#[cfg(test)]328mod tests {329use super::*;330use crate::{ContentSize, FixedMeasure};331use bevy_math::Vec2;332use taffy::TraversePartialTree;333334#[test]335fn test_initialization() {336let ui_surface = UiSurface::default();337assert!(ui_surface.entity_to_taffy.is_empty());338assert_eq!(ui_surface.taffy.total_node_count(), 0);339}340341#[test]342fn test_upsert() {343let mut ui_surface = UiSurface::default();344let root_node_entity = Entity::from_raw_u32(1).unwrap();345let node = Node::default();346347// standard upsert348ui_surface.upsert_node(&LayoutContext::TEST_CONTEXT, root_node_entity, &node, None);349350// should be inserted into taffy351assert_eq!(ui_surface.taffy.total_node_count(), 1);352assert!(ui_surface.entity_to_taffy.contains_key(&root_node_entity));353354// test duplicate insert 1355ui_surface.upsert_node(&LayoutContext::TEST_CONTEXT, root_node_entity, &node, None);356357// node count should not have increased358assert_eq!(ui_surface.taffy.total_node_count(), 1);359360// assign root node to camera361ui_surface.get_or_insert_taffy_viewport_node(root_node_entity);362363// each root node will create 2 taffy nodes364assert_eq!(ui_surface.taffy.total_node_count(), 2);365366// test duplicate insert 2367ui_surface.upsert_node(&LayoutContext::TEST_CONTEXT, root_node_entity, &node, None);368369// node count should not have increased370assert_eq!(ui_surface.taffy.total_node_count(), 2);371}372373#[test]374fn test_remove_entities() {375let mut ui_surface = UiSurface::default();376let root_node_entity = Entity::from_raw_u32(1).unwrap();377let node = Node::default();378379ui_surface.upsert_node(&LayoutContext::TEST_CONTEXT, root_node_entity, &node, None);380381ui_surface.get_or_insert_taffy_viewport_node(root_node_entity);382383assert!(ui_surface.entity_to_taffy.contains_key(&root_node_entity));384385ui_surface.remove_entities([root_node_entity]);386assert!(!ui_surface.entity_to_taffy.contains_key(&root_node_entity));387}388389#[test]390fn test_try_update_measure() {391let mut ui_surface = UiSurface::default();392let root_node_entity = Entity::from_raw_u32(1).unwrap();393let node = Node::default();394395ui_surface.upsert_node(&LayoutContext::TEST_CONTEXT, root_node_entity, &node, None);396let mut content_size = ContentSize::default();397content_size.set(NodeMeasure::Fixed(FixedMeasure { size: Vec2::ONE }));398let measure_func = content_size.measure.take().unwrap();399assert!(ui_surface400.update_node_context(root_node_entity, measure_func)401.is_some());402}403404#[test]405fn test_update_children() {406let mut ui_surface = UiSurface::default();407let root_node_entity = Entity::from_raw_u32(1).unwrap();408let child_entity = Entity::from_raw_u32(2).unwrap();409let node = Node::default();410411ui_surface.upsert_node(&LayoutContext::TEST_CONTEXT, root_node_entity, &node, None);412ui_surface.upsert_node(&LayoutContext::TEST_CONTEXT, child_entity, &node, None);413414ui_surface.update_children(root_node_entity, vec![child_entity].into_iter());415416let parent_node = *ui_surface.entity_to_taffy.get(&root_node_entity).unwrap();417let child_node = *ui_surface.entity_to_taffy.get(&child_entity).unwrap();418assert_eq!(ui_surface.taffy.parent(child_node.id), Some(parent_node.id));419}420421#[expect(422unreachable_code,423reason = "Certain pieces of code tested here cause the test to fail if made reachable; see #16362 for progress on fixing this"424)]425#[test]426fn test_set_camera_children() {427let mut ui_surface = UiSurface::default();428let root_node_entity = Entity::from_raw_u32(1).unwrap();429let child_entity = Entity::from_raw_u32(2).unwrap();430let node = Node::default();431432ui_surface.upsert_node(&LayoutContext::TEST_CONTEXT, root_node_entity, &node, None);433ui_surface.upsert_node(&LayoutContext::TEST_CONTEXT, child_entity, &node, None);434435let root_taffy_node = *ui_surface.entity_to_taffy.get(&root_node_entity).unwrap();436let child_taffy = *ui_surface.entity_to_taffy.get(&child_entity).unwrap();437438// set up the relationship manually439ui_surface440.taffy441.add_child(root_taffy_node.id, child_taffy.id)442.unwrap();443444ui_surface.get_or_insert_taffy_viewport_node(root_node_entity);445446assert_eq!(447ui_surface.taffy.parent(child_taffy.id),448Some(root_taffy_node.id)449);450let root_taffy_children = ui_surface.taffy.children(root_taffy_node.id).unwrap();451assert!(452root_taffy_children.contains(&child_taffy.id),453"root node is not a parent of child node"454);455assert_eq!(456ui_surface.taffy.child_count(root_taffy_node.id),4571,458"expected root node child count to be 1"459);460461// clear camera's root nodes462ui_surface.get_or_insert_taffy_viewport_node(root_node_entity);463464return; // TODO: can't pass the test if we continue - not implemented (remove allow(unreachable_code))465466let root_taffy_children = ui_surface.taffy.children(root_taffy_node.id).unwrap();467assert!(468root_taffy_children.contains(&child_taffy.id),469"root node is not a parent of child node"470);471assert_eq!(472ui_surface.taffy.child_count(root_taffy_node.id),4731,474"expected root node child count to be 1"475);476477// re-associate root node with viewport node478ui_surface.get_or_insert_taffy_viewport_node(root_node_entity);479480let child_taffy = ui_surface.entity_to_taffy.get(&child_entity).unwrap();481let root_taffy_children = ui_surface.taffy.children(root_taffy_node.id).unwrap();482assert!(483root_taffy_children.contains(&child_taffy.id),484"root node is not a parent of child node"485);486assert_eq!(487ui_surface.taffy.child_count(root_taffy_node.id),4881,489"expected root node child count to be 1"490);491}492}493494495