Path: blob/main/crates/bevy_ui/src/layout/ui_surface.rs
6599 views
use core::fmt;12use bevy_platform::collections::hash_map::Entry;3use taffy::TaffyTree;45use bevy_ecs::{6entity::{Entity, EntityHashMap},7prelude::Resource,8};9use bevy_math::{UVec2, Vec2};10use bevy_utils::default;1112use crate::{layout::convert, LayoutContext, LayoutError, Measure, MeasureArgs, Node, NodeMeasure};13use bevy_text::CosmicFontSystem;1415#[derive(Debug, Copy, Clone, PartialEq, Eq)]16pub struct LayoutNode {17// Implicit "viewport" node if this `LayoutNode` corresponds to a root UI node entity18pub(super) viewport_id: Option<taffy::NodeId>,19// The id of the node in the taffy tree20pub(super) id: taffy::NodeId,21}2223impl From<taffy::NodeId> for LayoutNode {24fn from(value: taffy::NodeId) -> Self {25LayoutNode {26viewport_id: None,27id: value,28}29}30}3132#[derive(Resource)]33pub struct UiSurface {34pub root_entity_to_viewport_node: EntityHashMap<taffy::NodeId>,35pub(super) entity_to_taffy: EntityHashMap<LayoutNode>,36pub(super) taffy: TaffyTree<NodeMeasure>,37taffy_children_scratch: Vec<taffy::NodeId>,38}3940fn _assert_send_sync_ui_surface_impl_safe() {41fn _assert_send_sync<T: Send + Sync>() {}42_assert_send_sync::<EntityHashMap<taffy::NodeId>>();43_assert_send_sync::<TaffyTree<NodeMeasure>>();44_assert_send_sync::<UiSurface>();45}4647impl fmt::Debug for UiSurface {48fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {49f.debug_struct("UiSurface")50.field("entity_to_taffy", &self.entity_to_taffy)51.field("taffy_children_scratch", &self.taffy_children_scratch)52.finish()53}54}5556impl Default for UiSurface {57fn default() -> Self {58let taffy: TaffyTree<NodeMeasure> = TaffyTree::new();59Self {60root_entity_to_viewport_node: Default::default(),61entity_to_taffy: Default::default(),62taffy,63taffy_children_scratch: Vec::new(),64}65}66}6768impl UiSurface {69/// Retrieves the Taffy node associated with the given UI node entity and updates its style.70/// If no associated Taffy node exists a new Taffy node is inserted into the Taffy layout.71pub fn upsert_node(72&mut self,73layout_context: &LayoutContext,74entity: Entity,75node: &Node,76mut new_node_context: Option<NodeMeasure>,77) {78let taffy = &mut self.taffy;7980match self.entity_to_taffy.entry(entity) {81Entry::Occupied(entry) => {82let taffy_node = *entry.get();83let has_measure = if new_node_context.is_some() {84taffy85.set_node_context(taffy_node.id, new_node_context)86.unwrap();87true88} else {89taffy.get_node_context(taffy_node.id).is_some()90};9192taffy93.set_style(94taffy_node.id,95convert::from_node(node, layout_context, has_measure),96)97.unwrap();98}99Entry::Vacant(entry) => {100let taffy_node = if let Some(measure) = new_node_context.take() {101taffy.new_leaf_with_context(102convert::from_node(node, layout_context, true),103measure,104)105} else {106taffy.new_leaf(convert::from_node(node, layout_context, false))107};108entry.insert(taffy_node.unwrap().into());109}110}111}112113/// Update the `MeasureFunc` of the taffy node corresponding to the given [`Entity`] if the node exists.114pub fn update_node_context(&mut self, entity: Entity, context: NodeMeasure) -> Option<()> {115let taffy_node = self.entity_to_taffy.get(&entity)?;116self.taffy117.set_node_context(taffy_node.id, Some(context))118.ok()119}120121/// Update the children of the taffy node corresponding to the given [`Entity`].122pub fn update_children(&mut self, entity: Entity, children: impl Iterator<Item = Entity>) {123self.taffy_children_scratch.clear();124125for child in children {126if let Some(taffy_node) = self.entity_to_taffy.get_mut(&child) {127self.taffy_children_scratch.push(taffy_node.id);128if let Some(viewport_id) = taffy_node.viewport_id.take() {129self.taffy.remove(viewport_id).ok();130}131}132}133134let taffy_node = self.entity_to_taffy.get(&entity).unwrap();135self.taffy136.set_children(taffy_node.id, &self.taffy_children_scratch)137.unwrap();138}139140/// Removes children from the entity's taffy node if it exists. Does nothing otherwise.141pub fn try_remove_children(&mut self, entity: Entity) {142if let Some(taffy_node) = self.entity_to_taffy.get(&entity) {143self.taffy.set_children(taffy_node.id, &[]).unwrap();144}145}146147/// Removes the measure from the entity's taffy node if it exists. Does nothing otherwise.148pub fn try_remove_node_context(&mut self, entity: Entity) {149if let Some(taffy_node) = self.entity_to_taffy.get(&entity) {150self.taffy.set_node_context(taffy_node.id, None).unwrap();151}152}153154/// Gets or inserts an implicit taffy viewport node corresponding to the given UI root entity155pub fn get_or_insert_taffy_viewport_node(&mut self, ui_root_entity: Entity) -> taffy::NodeId {156*self157.root_entity_to_viewport_node158.entry(ui_root_entity)159.or_insert_with(|| {160let root_node = self.entity_to_taffy.get_mut(&ui_root_entity).unwrap();161let implicit_root = self162.taffy163.new_leaf(taffy::style::Style {164display: taffy::style::Display::Grid,165// Note: Taffy percentages are floats ranging from 0.0 to 1.0.166// So this is setting width:100% and height:100%167size: taffy::geometry::Size {168width: taffy::style::Dimension::Percent(1.0),169height: taffy::style::Dimension::Percent(1.0),170},171align_items: Some(taffy::style::AlignItems::Start),172justify_items: Some(taffy::style::JustifyItems::Start),173..default()174})175.unwrap();176self.taffy.add_child(implicit_root, root_node.id).unwrap();177root_node.viewport_id = Some(implicit_root);178implicit_root179})180}181182/// Compute the layout for the given implicit taffy viewport node183pub fn compute_layout<'a>(184&mut self,185ui_root_entity: Entity,186render_target_resolution: UVec2,187buffer_query: &'a mut bevy_ecs::prelude::Query<&mut bevy_text::ComputedTextBlock>,188font_system: &'a mut CosmicFontSystem,189) {190let implicit_viewport_node = self.get_or_insert_taffy_viewport_node(ui_root_entity);191192let available_space = taffy::geometry::Size {193width: taffy::style::AvailableSpace::Definite(render_target_resolution.x as f32),194height: taffy::style::AvailableSpace::Definite(render_target_resolution.y as f32),195};196197self.taffy198.compute_layout_with_measure(199implicit_viewport_node,200available_space,201|known_dimensions: taffy::Size<Option<f32>>,202available_space: taffy::Size<taffy::AvailableSpace>,203_node_id: taffy::NodeId,204context: Option<&mut NodeMeasure>,205style: &taffy::Style|206-> taffy::Size<f32> {207context208.map(|ctx| {209let buffer = get_text_buffer(210crate::widget::TextMeasure::needs_buffer(211known_dimensions.height,212available_space.width,213),214ctx,215buffer_query,216);217let size = ctx.measure(218MeasureArgs {219width: known_dimensions.width,220height: known_dimensions.height,221available_width: available_space.width,222available_height: available_space.height,223font_system,224buffer,225},226style,227);228taffy::Size {229width: size.x,230height: size.y,231}232})233.unwrap_or(taffy::Size::ZERO)234},235)236.unwrap();237}238239/// Removes each entity from the internal map and then removes their associated nodes from taffy240pub fn remove_entities(&mut self, entities: impl IntoIterator<Item = Entity>) {241for entity in entities {242if let Some(node) = self.entity_to_taffy.remove(&entity) {243self.taffy.remove(node.id).unwrap();244if let Some(viewport_node) = node.viewport_id {245self.taffy.remove(viewport_node).ok();246}247}248}249}250251/// Get the layout geometry for the taffy node corresponding to the ui node [`Entity`].252/// Does not compute the layout geometry, `compute_window_layouts` should be run before using this function.253/// On success returns a pair consisting of the final resolved layout values after rounding254/// and the size of the node after layout resolution but before rounding.255pub fn get_layout(256&mut self,257entity: Entity,258use_rounding: bool,259) -> Result<(taffy::Layout, Vec2), LayoutError> {260let Some(taffy_node) = self.entity_to_taffy.get(&entity) else {261return Err(LayoutError::InvalidHierarchy);262};263264if use_rounding {265self.taffy.enable_rounding();266} else {267self.taffy.disable_rounding();268}269270let out = match self.taffy.layout(taffy_node.id).cloned() {271Ok(layout) => {272self.taffy.disable_rounding();273let taffy_size = self.taffy.layout(taffy_node.id).unwrap().size;274let unrounded_size = Vec2::new(taffy_size.width, taffy_size.height);275Ok((layout, unrounded_size))276}277Err(taffy_error) => Err(LayoutError::TaffyError(taffy_error)),278};279280self.taffy.enable_rounding();281out282}283}284285pub fn get_text_buffer<'a>(286needs_buffer: bool,287ctx: &mut NodeMeasure,288query: &'a mut bevy_ecs::prelude::Query<&mut bevy_text::ComputedTextBlock>,289) -> Option<&'a mut bevy_text::ComputedTextBlock> {290// We avoid a query lookup whenever the buffer is not required.291if !needs_buffer {292return None;293}294let NodeMeasure::Text(crate::widget::TextMeasure { info }) = ctx else {295return None;296};297let Ok(computed) = query.get_mut(info.entity) else {298return None;299};300Some(computed.into_inner())301}302303#[cfg(test)]304mod tests {305use super::*;306use crate::{ContentSize, FixedMeasure};307use bevy_math::Vec2;308use taffy::TraversePartialTree;309310#[test]311fn test_initialization() {312let ui_surface = UiSurface::default();313assert!(ui_surface.entity_to_taffy.is_empty());314assert_eq!(ui_surface.taffy.total_node_count(), 0);315}316317#[test]318fn test_upsert() {319let mut ui_surface = UiSurface::default();320let root_node_entity = Entity::from_raw_u32(1).unwrap();321let node = Node::default();322323// standard upsert324ui_surface.upsert_node(&LayoutContext::TEST_CONTEXT, root_node_entity, &node, None);325326// should be inserted into taffy327assert_eq!(ui_surface.taffy.total_node_count(), 1);328assert!(ui_surface.entity_to_taffy.contains_key(&root_node_entity));329330// test duplicate insert 1331ui_surface.upsert_node(&LayoutContext::TEST_CONTEXT, root_node_entity, &node, None);332333// node count should not have increased334assert_eq!(ui_surface.taffy.total_node_count(), 1);335336// assign root node to camera337ui_surface.get_or_insert_taffy_viewport_node(root_node_entity);338339// each root node will create 2 taffy nodes340assert_eq!(ui_surface.taffy.total_node_count(), 2);341342// test duplicate insert 2343ui_surface.upsert_node(&LayoutContext::TEST_CONTEXT, root_node_entity, &node, None);344345// node count should not have increased346assert_eq!(ui_surface.taffy.total_node_count(), 2);347}348349#[test]350fn test_remove_entities() {351let mut ui_surface = UiSurface::default();352let root_node_entity = Entity::from_raw_u32(1).unwrap();353let node = Node::default();354355ui_surface.upsert_node(&LayoutContext::TEST_CONTEXT, root_node_entity, &node, None);356357ui_surface.get_or_insert_taffy_viewport_node(root_node_entity);358359assert!(ui_surface.entity_to_taffy.contains_key(&root_node_entity));360361ui_surface.remove_entities([root_node_entity]);362assert!(!ui_surface.entity_to_taffy.contains_key(&root_node_entity));363}364365#[test]366fn test_try_update_measure() {367let mut ui_surface = UiSurface::default();368let root_node_entity = Entity::from_raw_u32(1).unwrap();369let node = Node::default();370371ui_surface.upsert_node(&LayoutContext::TEST_CONTEXT, root_node_entity, &node, None);372let mut content_size = ContentSize::default();373content_size.set(NodeMeasure::Fixed(FixedMeasure { size: Vec2::ONE }));374let measure_func = content_size.measure.take().unwrap();375assert!(ui_surface376.update_node_context(root_node_entity, measure_func)377.is_some());378}379380#[test]381fn test_update_children() {382let mut ui_surface = UiSurface::default();383let root_node_entity = Entity::from_raw_u32(1).unwrap();384let child_entity = Entity::from_raw_u32(2).unwrap();385let node = Node::default();386387ui_surface.upsert_node(&LayoutContext::TEST_CONTEXT, root_node_entity, &node, None);388ui_surface.upsert_node(&LayoutContext::TEST_CONTEXT, child_entity, &node, None);389390ui_surface.update_children(root_node_entity, vec![child_entity].into_iter());391392let parent_node = *ui_surface.entity_to_taffy.get(&root_node_entity).unwrap();393let child_node = *ui_surface.entity_to_taffy.get(&child_entity).unwrap();394assert_eq!(ui_surface.taffy.parent(child_node.id), Some(parent_node.id));395}396397#[expect(398unreachable_code,399reason = "Certain pieces of code tested here cause the test to fail if made reachable; see #16362 for progress on fixing this"400)]401#[test]402fn test_set_camera_children() {403let mut ui_surface = UiSurface::default();404let root_node_entity = Entity::from_raw_u32(1).unwrap();405let child_entity = Entity::from_raw_u32(2).unwrap();406let node = Node::default();407408ui_surface.upsert_node(&LayoutContext::TEST_CONTEXT, root_node_entity, &node, None);409ui_surface.upsert_node(&LayoutContext::TEST_CONTEXT, child_entity, &node, None);410411let root_taffy_node = *ui_surface.entity_to_taffy.get(&root_node_entity).unwrap();412let child_taffy = *ui_surface.entity_to_taffy.get(&child_entity).unwrap();413414// set up the relationship manually415ui_surface416.taffy417.add_child(root_taffy_node.id, child_taffy.id)418.unwrap();419420ui_surface.get_or_insert_taffy_viewport_node(root_node_entity);421422assert_eq!(423ui_surface.taffy.parent(child_taffy.id),424Some(root_taffy_node.id)425);426let root_taffy_children = ui_surface.taffy.children(root_taffy_node.id).unwrap();427assert!(428root_taffy_children.contains(&child_taffy.id),429"root node is not a parent of child node"430);431assert_eq!(432ui_surface.taffy.child_count(root_taffy_node.id),4331,434"expected root node child count to be 1"435);436437// clear camera's root nodes438ui_surface.get_or_insert_taffy_viewport_node(root_node_entity);439440return; // TODO: can't pass the test if we continue - not implemented (remove allow(unreachable_code))441442let root_taffy_children = ui_surface.taffy.children(root_taffy_node.id).unwrap();443assert!(444root_taffy_children.contains(&child_taffy.id),445"root node is not a parent of child node"446);447assert_eq!(448ui_surface.taffy.child_count(root_taffy_node.id),4491,450"expected root node child count to be 1"451);452453// re-associate root node with viewport node454ui_surface.get_or_insert_taffy_viewport_node(root_node_entity);455456let child_taffy = ui_surface.entity_to_taffy.get(&child_entity).unwrap();457let root_taffy_children = ui_surface.taffy.children(root_taffy_node.id).unwrap();458assert!(459root_taffy_children.contains(&child_taffy.id),460"root node is not a parent of child node"461);462assert_eq!(463ui_surface.taffy.child_count(root_taffy_node.id),4641,465"expected root node child count to be 1"466);467}468}469470471