mod convert; pub mod debug; use crate::{ContentSize, Node, Outline, Style, UiScale}; use bevy_ecs::{ change_detection::{DetectChanges, DetectChangesMut}, entity::Entity, event::EventReader, query::{With, Without}, removal_detection::RemovedComponents, system::{Query, Res, ResMut, Resource}, world::Ref, }; use bevy_hierarchy::{Children, Parent}; use bevy_log::warn; use bevy_math::Vec2; use bevy_transform::components::Transform; use bevy_utils::{default, EntityHashMap}; use bevy_window::{PrimaryWindow, Window, WindowResolution, WindowScaleFactorChanged}; use std::fmt; use taffy::Taffy; use thiserror::Error; pub struct LayoutContext { pub scale_factor: f32, pub physical_size: Vec2, pub min_size: f32, pub max_size: f32, } impl LayoutContext { /// create new a [`LayoutContext`] from the window's physical size and scale factor fn new(scale_factor: f32, physical_size: Vec2) -> Self { Self { scale_factor, physical_size, min_size: physical_size.x.min(physical_size.y), max_size: physical_size.x.max(physical_size.y), } } } #[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, window_roots: EntityHashMap>, taffy: Taffy, } fn _assert_send_sync_ui_surface_impl_safe() { fn _assert_send_sync() {} _assert_send_sync::>(); _assert_send_sync::(); _assert_send_sync::(); } 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_roots) .finish() } } impl Default for UiSurface { fn default() -> Self { let mut taffy = Taffy::new(); taffy.disable_rounding(); Self { entity_to_taffy: Default::default(), window_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`]. pub fn update_measure(&mut self, entity: Entity, measure_func: taffy::node::MeasureFunc) { let taffy_node = self.entity_to_taffy.get(&entity).unwrap(); 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_window_children( &mut self, window_id: Entity, children: impl Iterator, ) { 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_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(root_nodes.implicit_viewport_node, available_space) .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) { 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)] pub enum LayoutError { #[error("Invalid hierarchy")] InvalidHierarchy, #[error("Taffy error: {0}")] TaffyError(#[from] taffy::error::TaffyError), } /// Updates the UI's layout tree, computes the new layout geometry and then updates the sizes and transforms of all the UI nodes. #[allow(clippy::too_many_arguments)] pub fn ui_layout_system( primary_window: Query<(Entity, &Window), With>, windows: Query<(Entity, &Window)>, ui_scale: Res, mut scale_factor_events: EventReader, mut resize_events: EventReader, mut ui_surface: ResMut, root_node_query: Query, Without)>, style_query: Query<(Entity, Ref