mod convert; pub mod debug; use crate::{ContentSize, Node, Style, UiScale}; use bevy_ecs::{ change_detection::DetectChanges, 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::HashMap; use bevy_window::{PrimaryWindow, Window, WindowResolution, WindowScaleFactorChanged}; use std::fmt; use taffy::{prelude::Size, style_helpers::TaffyMaxContent, Taffy}; pub struct LayoutContext { pub scale_factor: f64, 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: f64, 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(Resource)] pub struct UiSurface { entity_to_taffy: HashMap, window_nodes: HashMap, 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_nodes) .finish() } } impl Default for UiSurface { fn default() -> Self { Self { entity_to_taffy: Default::default(), window_nodes: Default::default(), taffy: Taffy::new(), } } } 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(); } } /// Retrieve or insert the root layout node and update its size to match the size of the window. pub fn update_window(&mut self, window: Entity, window_resolution: &WindowResolution) { let taffy = &mut self.taffy; let node = self .window_nodes .entry(window) .or_insert_with(|| taffy.new_leaf(taffy::style::Style::default()).unwrap()); taffy .set_style( *node, taffy::style::Style { size: taffy::geometry::Size { width: taffy::style::Dimension::Points( window_resolution.physical_width() as f32 ), height: taffy::style::Dimension::Points( window_resolution.physical_height() as f32, ), }, ..Default::default() }, ) .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, parent_window: Entity, children: impl Iterator, ) { let taffy_node = self.window_nodes.get(&parent_window).unwrap(); let child_nodes = children .map(|e| *self.entity_to_taffy.get(&e).unwrap()) .collect::>(); self.taffy.set_children(*taffy_node, &child_nodes).unwrap(); } /// Compute the layout for each window entity's corresponding root node in the layout. pub fn compute_window_layouts(&mut self) { for window_node in self.window_nodes.values() { self.taffy .compute_layout(*window_node, Size::MAX_CONTENT) .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)] pub enum LayoutError { InvalidHierarchy, TaffyError(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