diff --git a/crates/bevy_ui/src/experimental/ghost_hierarchy.rs b/crates/bevy_ui/src/experimental/ghost_hierarchy.rs index a343ec87de135..8e663964f7eb9 100644 --- a/crates/bevy_ui/src/experimental/ghost_hierarchy.rs +++ b/crates/bevy_ui/src/experimental/ghost_hierarchy.rs @@ -1,8 +1,9 @@ //! This module contains [`GhostNode`] and utilities to flatten the UI hierarchy, traversing past ghost nodes. +#[cfg(feature = "ghost_nodes")] +use crate::ui_node::ComputedNodeTarget; use crate::Node; use bevy_ecs::{prelude::*, system::SystemParam}; - #[cfg(feature = "ghost_nodes")] use bevy_reflect::prelude::*; #[cfg(feature = "ghost_nodes")] @@ -11,7 +12,6 @@ use bevy_render::view::Visibility; use bevy_transform::prelude::Transform; #[cfg(feature = "ghost_nodes")] use smallvec::SmallVec; - /// Marker component for entities that should be ignored within UI hierarchies. /// /// The UI systems will traverse past these and treat their first non-ghost descendants as direct children of their first non-ghost ancestor. @@ -21,7 +21,7 @@ use smallvec::SmallVec; #[derive(Component, Debug, Copy, Clone, Reflect)] #[cfg_attr(feature = "ghost_nodes", derive(Default))] #[reflect(Component, Debug)] -#[require(Visibility, Transform)] +#[require(Visibility, Transform, ComputedNodeTarget)] pub struct GhostNode; #[cfg(feature = "ghost_nodes")] diff --git a/crates/bevy_ui/src/focus.rs b/crates/bevy_ui/src/focus.rs index 126f0d48ff35f..4093b81d55347 100644 --- a/crates/bevy_ui/src/focus.rs +++ b/crates/bevy_ui/src/focus.rs @@ -1,6 +1,4 @@ -use crate::{ - CalculatedClip, ComputedNode, DefaultUiCamera, ResolvedBorderRadius, UiStack, UiTargetCamera, -}; +use crate::{CalculatedClip, ComputedNode, ComputedNodeTarget, ResolvedBorderRadius, UiStack}; use bevy_ecs::{ change_detection::DetectChangesMut, entity::{Entity, EntityBorrow}, @@ -141,7 +139,7 @@ pub struct NodeQuery { focus_policy: Option<&'static FocusPolicy>, calculated_clip: Option<&'static CalculatedClip>, inherited_visibility: Option<&'static InheritedVisibility>, - target_camera: Option<&'static UiTargetCamera>, + target_camera: &'static ComputedNodeTarget, } /// The system that sets Interaction for all UI elements based on the mouse cursor activity @@ -150,7 +148,6 @@ pub struct NodeQuery { pub fn ui_focus_system( mut state: Local, camera_query: Query<(Entity, &Camera)>, - default_ui_camera: DefaultUiCamera, primary_window: Query>, windows: Query<&Window>, mouse_button_input: Res>, @@ -212,8 +209,6 @@ pub fn ui_focus_system( }) .collect(); - let default_camera_entity = default_ui_camera.get(); - // prepare an iterator that contains all the nodes that have the cursor in their rect, // from the top node to the bottom one. this will also reset the interaction to `None` // for all nodes encountered that are no longer hovered. @@ -237,10 +232,7 @@ pub fn ui_focus_system( } return None; } - let camera_entity = node - .target_camera - .map(UiTargetCamera::entity) - .or(default_camera_entity)?; + let camera_entity = node.target_camera.camera()?; let node_rect = Rect::from_center_size( node.global_transform.translation().truncate(), diff --git a/crates/bevy_ui/src/layout/debug.rs b/crates/bevy_ui/src/layout/debug.rs index e83f2a3b23e99..2a8b371320482 100644 --- a/crates/bevy_ui/src/layout/debug.rs +++ b/crates/bevy_ui/src/layout/debug.rs @@ -14,19 +14,18 @@ pub fn print_ui_layout_tree(ui_surface: &UiSurface) { .iter() .map(|(entity, node)| (node.id, *entity)) .collect(); - for (&entity, roots) in &ui_surface.camera_roots { + for (&entity, &viewport_node) in &ui_surface.root_entity_to_viewport_node { let mut out = String::new(); - for root in roots { - print_node( - ui_surface, - &taffy_to_entity, - entity, - root.implicit_viewport_node, - false, - String::new(), - &mut out, - ); - } + print_node( + ui_surface, + &taffy_to_entity, + entity, + viewport_node, + false, + String::new(), + &mut out, + ); + tracing::info!("Layout tree for camera entity: {entity}\n{out}"); } } diff --git a/crates/bevy_ui/src/layout/mod.rs b/crates/bevy_ui/src/layout/mod.rs index 7d1860e3b4fc5..912079fdaeac8 100644 --- a/crates/bevy_ui/src/layout/mod.rs +++ b/crates/bevy_ui/src/layout/mod.rs @@ -1,19 +1,20 @@ use crate::{ experimental::{UiChildren, UiRootNodes}, - BorderRadius, ComputedNode, ContentSize, DefaultUiCamera, Display, LayoutConfig, Node, Outline, - OverflowAxis, ScrollPosition, UiScale, UiTargetCamera, Val, + BorderRadius, ComputedNode, ComputedNodeTarget, ContentSize, Display, LayoutConfig, Node, + Outline, OverflowAxis, ScrollPosition, Val, }; use bevy_ecs::{ - entity::{hash_map::EntityHashMap, hash_set::EntityHashSet}, - prelude::*, - system::SystemParam, + change_detection::{DetectChanges, DetectChangesMut}, + entity::Entity, + hierarchy::{ChildOf, Children}, + query::With, + removal_detection::RemovedComponents, + system::{Commands, Query, ResMut}, + world::Ref, }; -use bevy_math::{UVec2, Vec2}; -use bevy_render::camera::{Camera, NormalizedRenderTarget}; +use bevy_math::Vec2; use bevy_sprite::BorderRect; use bevy_transform::components::Transform; -use bevy_utils::once; -use bevy_window::{PrimaryWindow, Window, WindowScaleFactorChanged}; use thiserror::Error; use tracing::warn; use ui_surface::UiSurface; @@ -67,50 +68,19 @@ pub enum LayoutError { TaffyError(taffy::TaffyError), } -#[doc(hidden)] -#[derive(SystemParam)] -pub struct UiLayoutSystemRemovedComponentParam<'w, 's> { - removed_cameras: RemovedComponents<'w, 's, Camera>, - removed_children: RemovedComponents<'w, 's, Children>, - removed_content_sizes: RemovedComponents<'w, 's, ContentSize>, - removed_nodes: RemovedComponents<'w, 's, Node>, -} - -#[doc(hidden)] -#[derive(Default)] -pub struct UiLayoutSystemBuffers { - interned_root_nodes: Vec>, - resized_windows: EntityHashSet, - camera_layout_info: EntityHashMap, -} - -struct CameraLayoutInfo { - size: UVec2, - resized: bool, - scale_factor: f32, - root_nodes: Vec, -} - /// Updates the UI's layout tree, computes the new layout geometry and then updates the sizes and transforms of all the UI nodes. pub fn ui_layout_system( mut commands: Commands, - mut buffers: Local, - primary_window: Query<(Entity, &Window), With>, - camera_data: (Query<(Entity, &Camera)>, DefaultUiCamera), - ui_scale: Res, - mut scale_factor_events: EventReader, - mut resize_events: EventReader, mut ui_surface: ResMut, - root_nodes: UiRootNodes, + ui_root_node_query: UiRootNodes, mut node_query: Query<( Entity, Ref, Option<&mut ContentSize>, - Option<&UiTargetCamera>, + Ref, )>, computed_node_query: Query<(Entity, Option>), With>, ui_children: UiChildren, - mut removed_components: UiLayoutSystemRemovedComponentParam, mut node_transform_query: Query<( &mut ComputedNode, &mut Transform, @@ -120,127 +90,38 @@ pub fn ui_layout_system( Option<&Outline>, Option<&ScrollPosition>, )>, - mut buffer_query: Query<&mut ComputedTextBlock>, mut font_system: ResMut, + mut removed_children: RemovedComponents, + mut removed_content_sizes: RemovedComponents, + mut removed_nodes: RemovedComponents, ) { - let UiLayoutSystemBuffers { - interned_root_nodes, - resized_windows, - camera_layout_info, - } = &mut *buffers; - - let (cameras, default_ui_camera) = camera_data; - - let default_camera = default_ui_camera.get(); - let camera_with_default = |target_camera: Option<&UiTargetCamera>| { - target_camera.map(UiTargetCamera::entity).or(default_camera) - }; - - resized_windows.clear(); - resized_windows.extend(resize_events.read().map(|event| event.window)); - let mut calculate_camera_layout_info = |camera: &Camera| { - let size = camera.physical_viewport_size().unwrap_or(UVec2::ZERO); - let scale_factor = camera.target_scaling_factor().unwrap_or(1.0); - let camera_target = camera - .target - .normalize(primary_window.get_single().map(|(e, _)| e).ok()); - let resized = matches!(camera_target, - Some(NormalizedRenderTarget::Window(window_ref)) if resized_windows.contains(&window_ref.entity()) - ); - CameraLayoutInfo { - size, - resized, - scale_factor: scale_factor * ui_scale.0, - root_nodes: interned_root_nodes.pop().unwrap_or_default(), - } - }; - - // Precalculate the layout info for each camera, so we have fast access to it for each node - camera_layout_info.clear(); - - node_query - .iter_many(root_nodes.iter()) - .for_each(|(entity, _, _, target_camera)| { - match camera_with_default(target_camera) { - Some(camera_entity) => { - let Ok((_, camera)) = cameras.get(camera_entity) else { - once!(warn!( - "UiTargetCamera (of root UI node {entity}) is pointing to a camera {} which doesn't exist", - camera_entity - )); - return; - }; - let layout_info = camera_layout_info - .entry(camera_entity) - .or_insert_with(|| calculate_camera_layout_info(camera)); - layout_info.root_nodes.push(entity); - } - None => { - if cameras.is_empty() { - once!(warn!("No camera found to render UI to. To fix this, add at least one camera to the scene.")); - } else { - once!(warn!( - "Multiple cameras found, causing UI target ambiguity. \ - To fix this, add an explicit `UiTargetCamera` component to the root UI node {}", - entity - )); - } - } - } - - } - ); - // When a `ContentSize` component is removed from an entity, we need to remove the measure from the corresponding taffy node. - for entity in removed_components.removed_content_sizes.read() { + for entity in removed_content_sizes.read() { ui_surface.try_remove_node_context(entity); } // Sync Node and ContentSize to Taffy for all nodes node_query .iter_mut() - .for_each(|(entity, node, content_size, target_camera)| { - if let Some(camera) = - camera_with_default(target_camera).and_then(|c| camera_layout_info.get(&c)) + .for_each(|(entity, node, content_size, computed_target)| { + if computed_target.is_changed() + || node.is_changed() + || content_size + .as_ref() + .is_some_and(|c| c.is_changed() || c.measure.is_some()) { - if camera.resized - || !scale_factor_events.is_empty() - || ui_scale.is_changed() - || node.is_changed() - || content_size - .as_ref() - .is_some_and(|c| c.is_changed() || c.measure.is_some()) - { - let layout_context = LayoutContext::new( - camera.scale_factor, - [camera.size.x as f32, camera.size.y as f32].into(), - ); - let measure = content_size.and_then(|mut c| c.measure.take()); - ui_surface.upsert_node(&layout_context, entity, &node, measure); - } - } else { - ui_surface.upsert_node(&LayoutContext::DEFAULT, entity, &Node::default(), None); + let layout_context = LayoutContext::new( + computed_target.scale_factor, + computed_target.physical_size.as_vec2(), + ); + let measure = content_size.and_then(|mut c| c.measure.take()); + ui_surface.upsert_node(&layout_context, entity, &node, measure); } }); - scale_factor_events.clear(); - - // clean up removed cameras - ui_surface.remove_camera_entities(removed_components.removed_cameras.read()); - - // update camera children - for (camera_id, _) in cameras.iter() { - let root_nodes = - if let Some(CameraLayoutInfo { root_nodes, .. }) = camera_layout_info.get(&camera_id) { - root_nodes.iter().cloned() - } else { - [].iter().cloned() - }; - ui_surface.set_camera_children(camera_id, root_nodes); - } // update and remove children - for entity in removed_components.removed_children.read() { + for entity in removed_children.read() { ui_surface.try_remove_children(entity); } @@ -264,11 +145,9 @@ with UI components as a child of an entity without UI components, your UI layout } }); - let text_buffers = &mut buffer_query; // clean up removed nodes after syncing children to avoid potential panic (invalid SlotMap key used) ui_surface.remove_entities( - removed_components - .removed_nodes + removed_nodes .read() .filter(|entity| !node_query.contains(*entity)), ); @@ -280,28 +159,28 @@ with UI components as a child of an entity without UI components, your UI layout } }); - for (camera_id, mut camera) in camera_layout_info.drain() { - let inverse_target_scale_factor = camera.scale_factor.recip(); - - ui_surface.compute_camera_layout(camera_id, camera.size, text_buffers, &mut font_system); + for ui_root_entity in ui_root_node_query.iter() { + let (_, _, _, computed_target) = node_query.get(ui_root_entity).unwrap(); - for root in &camera.root_nodes { - update_uinode_geometry_recursive( - &mut commands, - *root, - &mut ui_surface, - true, - None, - &mut node_transform_query, - &ui_children, - inverse_target_scale_factor, - Vec2::ZERO, - Vec2::ZERO, - ); - } + ui_surface.compute_layout( + ui_root_entity, + computed_target.physical_size, + &mut buffer_query, + &mut font_system, + ); - camera.root_nodes.clear(); - interned_root_nodes.push(camera.root_nodes); + update_uinode_geometry_recursive( + &mut commands, + ui_root_entity, + &mut ui_surface, + true, + None, + &mut node_transform_query, + &ui_children, + computed_target.scale_factor.recip(), + Vec2::ZERO, + Vec2::ZERO, + ); } // Returns the combined bounding box of the node and any of its overflowing children. @@ -486,7 +365,7 @@ mod tests { use crate::{ layout::ui_surface::UiSurface, prelude::*, ui_layout_system, - update::update_target_camera_system, ContentSize, LayoutContext, + update::update_ui_context_system, ContentSize, LayoutContext, }; // these window dimensions are easy to convert to and from percentage values @@ -526,7 +405,7 @@ mod tests { ( // UI is driven by calculated camera target info, so we need to run the camera system first bevy_render::camera::camera_system, - update_target_camera_system, + update_ui_context_system, ApplyDeferred, ui_layout_system, sync_simple_transforms, @@ -600,54 +479,6 @@ mod tests { assert!(ui_surface.entity_to_taffy.is_empty()); } - #[test] - fn ui_surface_tracks_camera_entities() { - let (mut world, mut ui_schedule) = setup_ui_test_world(); - - // despawn all cameras so we can reset ui_surface back to a fresh state - let camera_entities = world - .query_filtered::>() - .iter(&world) - .collect::>(); - for camera_entity in camera_entities { - world.despawn(camera_entity); - } - - ui_schedule.run(&mut world); - - // no UI entities in world, none in UiSurface - let ui_surface = world.resource::(); - assert!(ui_surface.camera_entity_to_taffy.is_empty()); - - // respawn camera - let camera_entity = world.spawn(Camera2d).id(); - - let ui_entity = world - .spawn((Node::default(), UiTargetCamera(camera_entity))) - .id(); - - // `ui_layout_system` should map `camera_entity` to a ui node in `UiSurface::camera_entity_to_taffy` - ui_schedule.run(&mut world); - - let ui_surface = world.resource::(); - assert!(ui_surface - .camera_entity_to_taffy - .contains_key(&camera_entity)); - assert_eq!(ui_surface.camera_entity_to_taffy.len(), 1); - - world.despawn(ui_entity); - world.despawn(camera_entity); - - // `ui_layout_system` should remove `camera_entity` from `UiSurface::camera_entity_to_taffy` - ui_schedule.run(&mut world); - - let ui_surface = world.resource::(); - assert!(!ui_surface - .camera_entity_to_taffy - .contains_key(&camera_entity)); - assert!(ui_surface.camera_entity_to_taffy.is_empty()); - } - #[test] #[should_panic] fn despawning_a_ui_entity_should_remove_its_corresponding_ui_node() { @@ -1172,7 +1003,7 @@ mod tests { ( // UI is driven by calculated camera target info, so we need to run the camera system first bevy_render::camera::camera_system, - update_target_camera_system, + update_ui_context_system, ApplyDeferred, ui_layout_system, ) @@ -1206,11 +1037,9 @@ mod tests { 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, } @@ -1227,21 +1056,15 @@ mod tests { None, ); - ui_surface.compute_camera_layout( - params.camera_entity, + ui_surface.compute_layout( + params.root_node_entity, UVec2::new(800, 600), &mut computed_text_block_query, &mut font_system, ); } - let _ = world.run_system_once_with( - test_system, - TestSystemParam { - camera_entity, - root_node_entity, - }, - ); + let _ = world.run_system_once_with(test_system, TestSystemParam { root_node_entity }); let ui_surface = world.resource::(); diff --git a/crates/bevy_ui/src/layout/ui_surface.rs b/crates/bevy_ui/src/layout/ui_surface.rs index 661924229efd8..e0333763b83d2 100644 --- a/crates/bevy_ui/src/layout/ui_surface.rs +++ b/crates/bevy_ui/src/layout/ui_surface.rs @@ -13,14 +13,6 @@ use bevy_utils::default; use crate::{layout::convert, LayoutContext, LayoutError, Measure, MeasureArgs, Node, NodeMeasure}; use bevy_text::CosmicFontSystem; -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct RootNodePair { - // The implicit "viewport" node created by Bevy - pub(super) implicit_viewport_node: taffy::NodeId, - // The root (parentless) node specified by the user - pub(super) user_root_node: taffy::NodeId, -} - #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub struct LayoutNode { // Implicit "viewport" node if this `LayoutNode` corresponds to a root UI node entity @@ -40,9 +32,8 @@ impl From for LayoutNode { #[derive(Resource)] pub struct UiSurface { + pub root_entity_to_viewport_node: EntityHashMap, pub(super) entity_to_taffy: EntityHashMap, - pub(super) camera_entity_to_taffy: EntityHashMap>, - pub(super) camera_roots: EntityHashMap>, pub(super) taffy: TaffyTree, taffy_children_scratch: Vec, } @@ -50,8 +41,6 @@ pub struct UiSurface { fn _assert_send_sync_ui_surface_impl_safe() { fn _assert_send_sync() {} _assert_send_sync::>(); - _assert_send_sync::>>(); - _assert_send_sync::>>(); _assert_send_sync::>(); _assert_send_sync::(); } @@ -60,8 +49,6 @@ 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("camera_entity_to_taffy", &self.camera_entity_to_taffy) - .field("camera_roots", &self.camera_roots) .field("taffy_children_scratch", &self.taffy_children_scratch) .finish() } @@ -71,9 +58,8 @@ impl Default for UiSurface { fn default() -> Self { let taffy: TaffyTree = TaffyTree::new(); Self { + root_entity_to_viewport_node: Default::default(), entity_to_taffy: Default::default(), - camera_entity_to_taffy: Default::default(), - camera_roots: Default::default(), taffy, taffy_children_scratch: Vec::new(), } @@ -166,127 +152,89 @@ impl UiSurface { } } - /// Sets the ui root node entities as children to the root node in the taffy layout. - pub fn set_camera_children( - &mut self, - camera_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 camera_root_node_map = self.camera_entity_to_taffy.entry(camera_id).or_default(); - let existing_roots = self.camera_roots.entry(camera_id).or_default(); - let mut new_roots = Vec::new(); - for entity in children { - let node = self.entity_to_taffy.get_mut(&entity).unwrap(); - let root_node = existing_roots - .iter() - .find(|n| n.user_root_node == node.id) - .cloned() - .unwrap_or_else(|| { - if let Some(previous_parent) = self.taffy.parent(node.id) { - // remove the root node from the previous implicit node's children - self.taffy.remove_child(previous_parent, node.id).unwrap(); - } - - let viewport_node = *camera_root_node_map.entry(entity).or_insert_with(|| { - node.viewport_id - .unwrap_or_else(|| self.taffy.new_leaf(viewport_style.clone()).unwrap()) - }); - node.viewport_id = Some(viewport_node); - self.taffy.add_child(viewport_node, node.id).unwrap(); - RootNodePair { - implicit_viewport_node: viewport_node, - user_root_node: node.id, - } - }); - new_roots.push(root_node); - } - - self.camera_roots.insert(camera_id, new_roots); + /// Gets or inserts an implicit taffy viewport node corresponding to the given UI root entity + pub fn get_or_insert_taffy_viewport_node(&mut self, ui_root_entity: Entity) -> taffy::NodeId { + *self + .root_entity_to_viewport_node + .entry(ui_root_entity) + .or_insert_with(|| { + let root_node = self.entity_to_taffy.get_mut(&ui_root_entity).unwrap(); + let implicit_root = self + .taffy + .new_leaf(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() + }) + .unwrap(); + self.taffy.add_child(implicit_root, root_node.id).unwrap(); + root_node.viewport_id = Some(implicit_root); + implicit_root + }) } - /// Compute the layout for each window entity's corresponding root node in the layout. - pub fn compute_camera_layout<'a>( + /// Compute the layout for the given implicit taffy viewport node + pub fn compute_layout<'a>( &mut self, - camera: Entity, + ui_root_entity: Entity, render_target_resolution: UVec2, buffer_query: &'a mut bevy_ecs::prelude::Query<&mut bevy_text::ComputedTextBlock>, font_system: &'a mut CosmicFontSystem, ) { - let Some(camera_root_nodes) = self.camera_roots.get(&camera) else { - return; - }; + let implicit_viewport_node = self.get_or_insert_taffy_viewport_node(ui_root_entity); let available_space = taffy::geometry::Size { width: taffy::style::AvailableSpace::Definite(render_target_resolution.x as f32), height: taffy::style::AvailableSpace::Definite(render_target_resolution.y as f32), }; - for root_nodes in camera_root_nodes { - self.taffy - .compute_layout_with_measure( - root_nodes.implicit_viewport_node, - available_space, - |known_dimensions: taffy::Size>, - available_space: taffy::Size, - _node_id: taffy::NodeId, - context: Option<&mut NodeMeasure>, - style: &taffy::Style| - -> taffy::Size { - context - .map(|ctx| { - let buffer = get_text_buffer( - crate::widget::TextMeasure::needs_buffer( - known_dimensions.height, - available_space.width, - ), - ctx, - buffer_query, - ); - let size = ctx.measure( - MeasureArgs { - width: known_dimensions.width, - height: known_dimensions.height, - available_width: available_space.width, - available_height: available_space.height, - font_system, - buffer, - }, - style, - ); - taffy::Size { - width: size.x, - height: size.y, - } - }) - .unwrap_or(taffy::Size::ZERO) - }, - ) - .unwrap(); - } - } - /// Removes each camera entity from the internal map and then removes their associated node from taffy - pub fn remove_camera_entities(&mut self, entities: impl IntoIterator) { - for entity in entities { - if let Some(camera_root_node_map) = self.camera_entity_to_taffy.remove(&entity) { - for (entity, node) in camera_root_node_map.iter() { - self.taffy.remove(*node).unwrap(); - self.entity_to_taffy.get_mut(entity).unwrap().viewport_id = None; - } - } - } + self.taffy + .compute_layout_with_measure( + implicit_viewport_node, + available_space, + |known_dimensions: taffy::Size>, + available_space: taffy::Size, + _node_id: taffy::NodeId, + context: Option<&mut NodeMeasure>, + style: &taffy::Style| + -> taffy::Size { + context + .map(|ctx| { + let buffer = get_text_buffer( + crate::widget::TextMeasure::needs_buffer( + known_dimensions.height, + available_space.width, + ), + ctx, + buffer_query, + ); + let size = ctx.measure( + MeasureArgs { + width: known_dimensions.width, + height: known_dimensions.height, + available_width: available_space.width, + available_height: available_space.height, + font_system, + buffer, + }, + style, + ); + taffy::Size { + width: size.x, + height: size.y, + } + }) + .unwrap_or(taffy::Size::ZERO) + }, + ) + .unwrap(); } /// Removes each entity from the internal map and then removes their associated nodes from taffy @@ -335,7 +283,7 @@ impl UiSurface { } } -fn get_text_buffer<'a>( +pub fn get_text_buffer<'a>( needs_buffer: bool, ctx: &mut NodeMeasure, query: &'a mut bevy_ecs::prelude::Query<&mut bevy_text::ComputedTextBlock>, @@ -360,42 +308,16 @@ mod tests { 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, - 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.id) - } - #[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 node = Node::default(); @@ -413,188 +335,32 @@ mod tests { 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()); + ui_surface.get_or_insert_taffy_viewport_node(root_node_entity); // 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, &node, 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 { - 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 node = Node::default(); - - ui_surface.upsert_node(&LayoutContext::TEST_CONTEXT, root_node_entity, &node, 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) - .map(|taffy_node| &taffy_node.id) - ); - - assert_eq!( - get_root_node_pair_exact(&ui_surface, root_node_entity, camera_entity), - root_node_pair - ); - } - - #[expect( - unreachable_code, - reason = "Certain pieces of code tested here cause the test to fail if made reachable; see #16362 for progress on fixing this" - )] - #[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 node = Node::default(); - - ui_surface.upsert_node(&LayoutContext::TEST_CONTEXT, root_node_entity, &node, 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); - } - - #[expect( - unreachable_code, - reason = "Certain pieces of code tested here cause the test to fail if made reachable; see #16362 for progress on fixing this" - )] #[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 node = Node::default(); ui_surface.upsert_node(&LayoutContext::TEST_CONTEXT, root_node_entity, &node, None); - ui_surface.set_camera_children(camera_entity, [root_node_entity].into_iter()); + ui_surface.get_or_insert_taffy_viewport_node(root_node_entity); 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] @@ -636,7 +402,6 @@ mod tests { #[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 node = Node::default(); @@ -653,28 +418,7 @@ mod tests { .add_child(root_taffy_node.id, child_taffy.id) .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"); + ui_surface.get_or_insert_taffy_viewport_node(root_node_entity); assert_eq!( ui_surface.taffy.parent(child_taffy.id), @@ -692,27 +436,10 @@ mod tests { ); // clear camera's root nodes - ui_surface.set_camera_children(camera_entity, Vec::::new().into_iter()); + ui_surface.get_or_insert_taffy_viewport_node(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), - "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.id).unwrap(); assert!( root_taffy_children.contains(&child_taffy.id), @@ -724,25 +451,8 @@ mod tests { "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" - ); + // re-associate root node with viewport node + ui_surface.get_or_insert_taffy_viewport_node(root_node_entity); let child_taffy = ui_surface.entity_to_taffy.get(&child_entity).unwrap(); let root_taffy_children = ui_surface.taffy.children(root_taffy_node.id).unwrap(); diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index 272d79a4b0a60..fd573f1c0c54a 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -72,7 +72,7 @@ use bevy_transform::TransformSystem; use layout::ui_surface::UiSurface; use stack::ui_stack_system; pub use stack::UiStack; -use update::{update_clipping_system, update_target_camera_system}; +use update::{update_clipping_system, update_ui_context_system}; /// The basic plugin for Bevy UI pub struct UiPlugin { @@ -103,6 +103,8 @@ pub enum UiSystem { Focus, /// All UI systems in [`PostUpdate`] will run in or after this label. Prepare, + /// Update content requirements before layout. + Content, /// After this label, the ui layout state has been updated. /// /// Runs in [`PostUpdate`]. @@ -172,7 +174,8 @@ impl Plugin for UiPlugin { PostUpdate, ( CameraUpdateSystem, - UiSystem::Prepare.before(UiSystem::Stack).after(Animation), + UiSystem::Prepare.after(Animation), + UiSystem::Content, UiSystem::Layout, UiSystem::PostLayout, ) @@ -195,7 +198,7 @@ impl Plugin for UiPlugin { app.add_systems( PostUpdate, ( - update_target_camera_system.in_set(UiSystem::Prepare), + update_ui_context_system.in_set(UiSystem::Prepare), ui_layout_system_config, ui_stack_system .in_set(UiSystem::Stack) @@ -209,7 +212,7 @@ impl Plugin for UiPlugin { // its own ImageNode, and `widget::text_system` & `bevy_text::update_text2d_layout` // will never modify a pre-existing `Image` asset. widget::update_image_content_size_system - .in_set(UiSystem::Prepare) + .in_set(UiSystem::Content) .in_set(AmbiguousWithTextSystem) .in_set(AmbiguousWithUpdateText2DLayout), ), @@ -261,7 +264,7 @@ fn build_text_interop(app: &mut App) { widget::measure_text_system, ) .chain() - .in_set(UiSystem::Prepare) + .in_set(UiSystem::Content) // Text and Text2d are independent. .ambiguous_with(bevy_text::detect_text_needs_rerender::) // Potential conflict: `Assets` diff --git a/crates/bevy_ui/src/picking_backend.rs b/crates/bevy_ui/src/picking_backend.rs index 7f2b973b4e4c1..1db91893c43a3 100644 --- a/crates/bevy_ui/src/picking_backend.rs +++ b/crates/bevy_ui/src/picking_backend.rs @@ -51,7 +51,7 @@ pub struct NodeQuery { pickable: Option<&'static Pickable>, calculated_clip: Option<&'static CalculatedClip>, inherited_visibility: Option<&'static InheritedVisibility>, - target_camera: Option<&'static UiTargetCamera>, + target_camera: &'static ComputedNodeTarget, } /// Computes the UI node entities under each pointer. @@ -61,7 +61,6 @@ pub struct NodeQuery { pub fn ui_picking( pointers: Query<(&PointerId, &PointerLocation)>, camera_query: Query<(Entity, &Camera, Has)>, - default_ui_camera: DefaultUiCamera, primary_window: Query>, ui_stack: Res, node_query: Query, @@ -70,8 +69,6 @@ pub fn ui_picking( // For each camera, the pointer and its position let mut pointer_pos_by_camera = HashMap::>::default(); - let default_camera_entity = default_ui_camera.get(); - for (pointer_id, pointer_location) in pointers.iter().filter_map(|(pointer, pointer_location)| { Some(*pointer).zip(pointer_location.location().cloned()) @@ -130,11 +127,7 @@ pub fn ui_picking( { continue; } - let Some(camera_entity) = node - .target_camera - .map(UiTargetCamera::entity) - .or(default_camera_entity) - else { + let Some(camera_entity) = node.target_camera.camera() else { continue; }; @@ -186,11 +179,7 @@ pub fn ui_picking( let mut depth = 0.0; for node in node_query.iter_many(hovered_nodes) { - let Some(camera_entity) = node - .target_camera - .map(UiTargetCamera::entity) - .or(default_camera_entity) - else { + let Some(camera_entity) = node.target_camera.camera() else { continue; }; diff --git a/crates/bevy_ui/src/render/box_shadow.rs b/crates/bevy_ui/src/render/box_shadow.rs index d3687bd07baa2..0e501783564a4 100644 --- a/crates/bevy_ui/src/render/box_shadow.rs +++ b/crates/bevy_ui/src/render/box_shadow.rs @@ -3,8 +3,8 @@ use core::{hash::Hash, ops::Range}; use crate::{ - BoxShadow, BoxShadowSamples, CalculatedClip, ComputedNode, RenderUiSystem, - ResolvedBorderRadius, TransparentUi, UiTargetCamera, Val, + BoxShadow, BoxShadowSamples, CalculatedClip, ComputedNode, ComputedNodeTarget, RenderUiSystem, + ResolvedBorderRadius, TransparentUi, Val, }; use bevy_app::prelude::*; use bevy_asset::*; @@ -22,7 +22,6 @@ use bevy_math::{vec2, FloatOrd, Mat4, Rect, Vec2, Vec3Swizzles, Vec4Swizzles}; use bevy_render::sync_world::MainEntity; use bevy_render::RenderApp; use bevy_render::{ - camera::Camera, render_phase::*, render_resource::{binding_types::uniform_buffer, *}, renderer::{RenderDevice, RenderQueue}, @@ -237,7 +236,6 @@ pub struct ExtractedBoxShadows { pub fn extract_shadows( mut commands: Commands, mut extracted_box_shadows: ResMut, - camera_query: Extract>, box_shadow_query: Extract< Query<( Entity, @@ -246,12 +244,12 @@ pub fn extract_shadows( &InheritedVisibility, &BoxShadow, Option<&CalculatedClip>, - Option<&UiTargetCamera>, + &ComputedNodeTarget, )>, >, camera_map: Extract, ) { - let mut camera_mapper = camera_map.get_mapper(); + let mut mapping = camera_map.get_mapper(); for (entity, uinode, transform, visibility, box_shadow, clip, camera) in &box_shadow_query { // Skip if no visible shadows @@ -259,18 +257,11 @@ pub fn extract_shadows( continue; } - let Some(extracted_camera_entity) = camera_mapper.map(camera) else { + let Some(extracted_camera_entity) = mapping.map(camera) else { continue; }; - let ui_physical_viewport_size = camera_query - .get(camera_mapper.current_camera()) - .ok() - .and_then(|(_, c)| { - c.physical_viewport_size() - .map(|size| Vec2::new(size.x as f32, size.y as f32)) - }) - .unwrap_or(Vec2::ZERO); + let ui_physical_viewport_size = camera.physical_size.as_vec2(); let scale_factor = uinode.inverse_scale_factor.recip(); @@ -386,10 +377,6 @@ pub fn queue_shadows( } } -#[expect( - clippy::too_many_arguments, - reason = "Could be rewritten with less arguments using a QueryData-implementing struct, but doesn't need to be." -)] pub fn prepare_shadows( mut commands: Commands, render_device: Res, diff --git a/crates/bevy_ui/src/render/debug_overlay.rs b/crates/bevy_ui/src/render/debug_overlay.rs index 67d527946a529..79001f3ba1982 100644 --- a/crates/bevy_ui/src/render/debug_overlay.rs +++ b/crates/bevy_ui/src/render/debug_overlay.rs @@ -1,6 +1,6 @@ +use crate::ui_node::ComputedNodeTarget; use crate::CalculatedClip; use crate::ComputedNode; -use crate::UiTargetCamera; use bevy_asset::AssetId; use bevy_color::Hsla; use bevy_ecs::entity::Entity; @@ -64,7 +64,7 @@ pub fn extract_debug_overlay( &InheritedVisibility, Option<&CalculatedClip>, &GlobalTransform, - Option<&UiTargetCamera>, + &ComputedNodeTarget, )>, >, camera_map: Extract, @@ -75,12 +75,12 @@ pub fn extract_debug_overlay( let mut camera_mapper = camera_map.get_mapper(); - for (entity, uinode, visibility, maybe_clip, transform, camera) in &uinode_query { + for (entity, uinode, visibility, maybe_clip, transform, computed_target) in &uinode_query { if !debug_options.show_hidden && !visibility.get() { continue; } - let Some(extracted_camera_entity) = camera_mapper.map(camera) else { + let Some(extracted_camera_entity) = camera_mapper.map(computed_target) else { continue; }; diff --git a/crates/bevy_ui/src/render/mod.rs b/crates/bevy_ui/src/render/mod.rs index 89c258fc53058..d7eebc35b032e 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -9,8 +9,9 @@ mod debug_overlay; use crate::widget::ImageNode; use crate::{ - BackgroundColor, BorderColor, BoxShadowSamples, CalculatedClip, ComputedNode, DefaultUiCamera, - Outline, ResolvedBorderRadius, TextShadow, UiAntiAlias, UiTargetCamera, + BackgroundColor, BorderColor, BoxShadowSamples, CalculatedClip, ComputedNode, + ComputedNodeTarget, DefaultUiCamera, Outline, ResolvedBorderRadius, TextShadow, UiAntiAlias, + UiTargetCamera, }; use bevy_app::prelude::*; use bevy_asset::{load_internal_asset, weak_handle, AssetEvent, AssetId, Assets, Handle}; @@ -257,17 +258,14 @@ impl ExtractedUiNodes { #[derive(SystemParam)] pub struct UiCameraMap<'w, 's> { - default: DefaultUiCamera<'w, 's>, mapping: Query<'w, 's, RenderEntity>, } impl<'w, 's> UiCameraMap<'w, 's> { /// Get the default camera and create the mapper pub fn get_mapper(&'w self) -> UiCameraMapper<'w, 's> { - let default_camera_entity = self.default.get(); UiCameraMapper { mapping: &self.mapping, - default_camera_entity, camera_entity: Entity::PLACEHOLDER, render_entity: Entity::PLACEHOLDER, } @@ -276,17 +274,14 @@ impl<'w, 's> UiCameraMap<'w, 's> { pub struct UiCameraMapper<'w, 's> { mapping: &'w Query<'w, 's, RenderEntity>, - default_camera_entity: Option, camera_entity: Entity, render_entity: Entity, } impl<'w, 's> UiCameraMapper<'w, 's> { /// Returns the render entity corresponding to the given `UiTargetCamera` or the default camera if `None`. - pub fn map(&mut self, camera: Option<&UiTargetCamera>) -> Option { - let camera_entity = camera - .map(UiTargetCamera::entity) - .or(self.default_camera_entity)?; + pub fn map(&mut self, computed_target: &ComputedNodeTarget) -> Option { + let camera_entity = computed_target.camera; if self.camera_entity != camera_entity { let Ok(new_render_camera_entity) = self.mapping.get(camera_entity) else { return None; @@ -338,7 +333,7 @@ pub fn extract_uinode_background_colors( &GlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, - Option<&UiTargetCamera>, + &ComputedNodeTarget, &BackgroundColor, )>, >, @@ -397,7 +392,7 @@ pub fn extract_uinode_images( &GlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, - Option<&UiTargetCamera>, + &ComputedNodeTarget, &ImageNode, )>, >, @@ -481,7 +476,7 @@ pub fn extract_uinode_borders( &GlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, - Option<&UiTargetCamera>, + &ComputedNodeTarget, AnyOf<(&BorderColor, &Outline)>, )>, >, @@ -497,7 +492,7 @@ pub fn extract_uinode_borders( global_transform, inherited_visibility, maybe_clip, - maybe_camera, + camera, (maybe_border_color, maybe_outline), ) in &uinode_query { @@ -506,7 +501,7 @@ pub fn extract_uinode_borders( continue; } - let Some(extracted_camera_entity) = camera_mapper.map(maybe_camera) else { + let Some(extracted_camera_entity) = camera_mapper.map(camera) else { continue; }; @@ -709,7 +704,7 @@ pub fn extract_text_sections( &GlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, - Option<&UiTargetCamera>, + &ComputedNodeTarget, &ComputedTextBlock, &TextLayoutInfo, )>, diff --git a/crates/bevy_ui/src/render/ui_material_pipeline.rs b/crates/bevy_ui/src/render/ui_material_pipeline.rs index e939c9a281c65..202f3749222f5 100644 --- a/crates/bevy_ui/src/render/ui_material_pipeline.rs +++ b/crates/bevy_ui/src/render/ui_material_pipeline.rs @@ -376,7 +376,7 @@ pub fn extract_ui_material_nodes( &MaterialNode, &InheritedVisibility, Option<&CalculatedClip>, - Option<&UiTargetCamera>, + &ComputedNodeTarget, )>, >, camera_map: Extract, diff --git a/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs b/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs index 8a946e8e570de..fd9799b679149 100644 --- a/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs +++ b/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs @@ -256,7 +256,7 @@ pub fn extract_ui_texture_slices( &GlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, - Option<&UiTargetCamera>, + &ComputedNodeTarget, &ImageNode, )>, >, diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index 330ad5facdd3e..96d9d2aeb2410 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -2,11 +2,12 @@ use crate::{FocusPolicy, UiRect, Val}; use bevy_color::Color; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{prelude::*, system::SystemParam}; -use bevy_math::{vec4, Rect, Vec2, Vec4Swizzles}; +use bevy_math::{vec4, Rect, UVec2, Vec2, Vec4Swizzles}; use bevy_reflect::prelude::*; use bevy_render::{ camera::{Camera, RenderTarget}, view::Visibility, + view::VisibilityClass, }; use bevy_sprite::BorderRect; use bevy_transform::components::Transform; @@ -322,6 +323,7 @@ impl From for ScrollPosition { #[derive(Component, Clone, PartialEq, Debug, Reflect)] #[require( ComputedNode, + ComputedNodeTarget, BackgroundColor, BorderColor, BorderRadius, @@ -329,6 +331,7 @@ impl From for ScrollPosition { ScrollPosition, Transform, Visibility, + VisibilityClass, ZIndex )] #[reflect(Component, Default, PartialEq, Debug)] @@ -2763,6 +2766,43 @@ impl Default for BoxShadowSamples { } } +/// Derived information about the camera target for this UI node. +#[derive(Component, Clone, Copy, Debug, Reflect, PartialEq)] +#[reflect(Component, Default)] +pub struct ComputedNodeTarget { + pub(crate) camera: Entity, + pub(crate) scale_factor: f32, + pub(crate) physical_size: UVec2, +} + +impl Default for ComputedNodeTarget { + fn default() -> Self { + Self { + camera: Entity::PLACEHOLDER, + scale_factor: 1., + physical_size: UVec2::ZERO, + } + } +} + +impl ComputedNodeTarget { + pub fn camera(&self) -> Option { + Some(self.camera).filter(|&entity| entity != Entity::PLACEHOLDER) + } + + pub const fn scale_factor(&self) -> f32 { + self.scale_factor + } + + pub const fn physical_size(&self) -> UVec2 { + self.physical_size + } + + pub fn logical_size(&self) -> Vec2 { + self.physical_size.as_vec2() / self.scale_factor + } +} + /// Adds a shadow behind text #[derive(Component, Copy, Clone, Debug, Reflect)] #[reflect(Component, Default, Debug)] diff --git a/crates/bevy_ui/src/update.rs b/crates/bevy_ui/src/update.rs index 390ddc6913b54..b139b591c0e55 100644 --- a/crates/bevy_ui/src/update.rs +++ b/crates/bevy_ui/src/update.rs @@ -2,17 +2,20 @@ use crate::{ experimental::{UiChildren, UiRootNodes}, - CalculatedClip, Display, Node, OverflowAxis, UiTargetCamera, + CalculatedClip, ComputedNodeTarget, DefaultUiCamera, Display, Node, OverflowAxis, UiScale, + UiTargetCamera, }; use super::ComputedNode; use bevy_ecs::{ - entity::Entity, + change_detection::DetectChangesMut, + entity::{hash_set::EntityHashSet, Entity}, + hierarchy::ChildOf, query::{Changed, With}, - system::{Commands, Query}, + system::{Commands, Local, Query, Res}, }; -use bevy_math::Rect; -use bevy_platform_support::collections::HashSet; +use bevy_math::{Rect, UVec2}; +use bevy_render::camera::Camera; use bevy_sprite::BorderRect; use bevy_transform::components::GlobalTransform; @@ -134,85 +137,520 @@ fn update_clipping( } } -pub fn update_target_camera_system( - mut commands: Commands, - changed_root_nodes_query: Query< - (Entity, Option<&UiTargetCamera>), - (With, Changed), - >, - node_query: Query<(Entity, Option<&UiTargetCamera>), With>, +pub fn update_ui_context_system( + default_ui_camera: DefaultUiCamera, + ui_scale: Res, + camera_query: Query<&Camera>, + target_camera_query: Query<&UiTargetCamera>, ui_root_nodes: UiRootNodes, + mut computed_target_query: Query<&mut ComputedNodeTarget>, ui_children: UiChildren, + reparented_nodes: Query<(Entity, &ChildOf), (Changed, With)>, + mut visited: Local, ) { - // Track updated entities to prevent redundant updates, as `Commands` changes are deferred, - // and updates done for changed_children_query can overlap with itself or with root_node_query - let mut updated_entities = >::default(); - - // Assuming that UiTargetCamera is manually set on the root node only, - // update root nodes first, since it implies the biggest change - for (root_node, target_camera) in changed_root_nodes_query.iter_many(ui_root_nodes.iter()) { - update_children_target_camera( - root_node, - target_camera, - &node_query, + visited.clear(); + let default_camera_entity = default_ui_camera.get(); + + for root_entity in ui_root_nodes.iter() { + let camera = target_camera_query + .get(root_entity) + .ok() + .map(UiTargetCamera::entity) + .or(default_camera_entity) + .unwrap_or(Entity::PLACEHOLDER); + + let (scale_factor, physical_size) = camera_query + .get(camera) + .ok() + .map(|camera| { + ( + camera.target_scaling_factor().unwrap_or(1.) * ui_scale.0, + camera.physical_viewport_size().unwrap_or(UVec2::ZERO), + ) + }) + .unwrap_or((1., UVec2::ZERO)); + + update_contexts_recursively( + root_entity, + ComputedNodeTarget { + camera, + scale_factor, + physical_size, + }, &ui_children, - &mut commands, - &mut updated_entities, + &mut computed_target_query, + &mut visited, ); } - // If the root node UiTargetCamera was changed, then every child is updated - // by this point, and iteration will be skipped. - // Otherwise, update changed children - for (parent, target_camera) in &node_query { - if !ui_children.is_changed(parent) { + for (entity, child_of) in reparented_nodes.iter() { + let Ok(computed_target) = computed_target_query.get(child_of.0) else { continue; - } + }; - update_children_target_camera( - parent, - target_camera, - &node_query, + update_contexts_recursively( + entity, + *computed_target, &ui_children, - &mut commands, - &mut updated_entities, + &mut computed_target_query, + &mut visited, ); } } -fn update_children_target_camera( +fn update_contexts_recursively( entity: Entity, - camera_to_set: Option<&UiTargetCamera>, - node_query: &Query<(Entity, Option<&UiTargetCamera>), With>, + inherited_computed_target: ComputedNodeTarget, ui_children: &UiChildren, - commands: &mut Commands, - updated_entities: &mut HashSet, + query: &mut Query<&mut ComputedNodeTarget>, + visited: &mut EntityHashSet, ) { - for child in ui_children.iter_ui_children(entity) { - // Skip if the child has already been updated or update is not needed - if updated_entities.contains(&child) - || camera_to_set == node_query.get(child).ok().and_then(|(_, camera)| camera) - { - continue; + if !visited.insert(entity) { + return; + } + if query + .get_mut(entity) + .map(|mut computed_target| computed_target.set_if_neq(inherited_computed_target)) + .unwrap_or(false) + { + for child in ui_children.iter_ui_children(entity) { + update_contexts_recursively( + child, + inherited_computed_target, + ui_children, + query, + visited, + ); } + } +} - match camera_to_set { - Some(camera) => { - commands.entity(child).try_insert(camera.clone()); - } - None => { - commands.entity(child).remove::(); +#[cfg(test)] +mod tests { + use bevy_asset::AssetEvent; + use bevy_asset::Assets; + use bevy_core_pipeline::core_2d::Camera2d; + use bevy_ecs::event::Events; + use bevy_ecs::hierarchy::ChildOf; + use bevy_ecs::schedule::IntoSystemConfigs; + use bevy_ecs::schedule::Schedule; + use bevy_ecs::world::World; + use bevy_image::Image; + use bevy_math::UVec2; + use bevy_render::camera::Camera; + use bevy_render::camera::ManualTextureViews; + use bevy_render::camera::RenderTarget; + use bevy_utils::default; + use bevy_window::PrimaryWindow; + use bevy_window::Window; + use bevy_window::WindowCreated; + use bevy_window::WindowRef; + use bevy_window::WindowResized; + use bevy_window::WindowResolution; + use bevy_window::WindowScaleFactorChanged; + + use crate::ComputedNodeTarget; + use crate::IsDefaultUiCamera; + use crate::Node; + use crate::UiScale; + use crate::UiTargetCamera; + + fn setup_test_world_and_schedule() -> (World, Schedule) { + let mut world = World::new(); + + world.init_resource::(); + + // init resources required by `camera_system` + world.init_resource::>(); + world.init_resource::>(); + world.init_resource::>(); + world.init_resource::>>(); + world.init_resource::>(); + world.init_resource::(); + + let mut schedule = Schedule::default(); + + schedule.add_systems( + ( + bevy_render::camera::camera_system, + super::update_ui_context_system, + ) + .chain(), + ); + + (world, schedule) + } + + #[test] + fn update_context_for_single_ui_root() { + let (mut world, mut schedule) = setup_test_world_and_schedule(); + + let scale_factor = 10.; + let physical_size = UVec2::new(1000, 500); + + world.spawn(( + Window { + resolution: WindowResolution::new(physical_size.x as f32, physical_size.y as f32) + .with_scale_factor_override(10.), + ..Default::default() + }, + PrimaryWindow, + )); + + let camera = world.spawn(Camera2d).id(); + + let uinode = world.spawn(Node::default()).id(); + + schedule.run(&mut world); + + assert_eq!( + *world.get::(uinode).unwrap(), + ComputedNodeTarget { + camera, + physical_size, + scale_factor, } + ); + } + + #[test] + fn update_multiple_context_for_multiple_ui_roots() { + let (mut world, mut schedule) = setup_test_world_and_schedule(); + + let scale1 = 1.; + let size1 = UVec2::new(100, 100); + let scale2 = 2.; + let size2 = UVec2::new(200, 200); + + world.spawn(( + Window { + resolution: WindowResolution::new(size1.x as f32, size1.y as f32) + .with_scale_factor_override(scale1), + ..Default::default() + }, + PrimaryWindow, + )); + + let window_2 = world + .spawn((Window { + resolution: WindowResolution::new(size2.x as f32, size2.y as f32) + .with_scale_factor_override(scale2), + ..Default::default() + },)) + .id(); + + let camera1 = world.spawn((Camera2d, IsDefaultUiCamera)).id(); + let camera2 = world + .spawn(( + Camera2d, + Camera { + target: RenderTarget::Window(WindowRef::Entity(window_2)), + ..default() + }, + )) + .id(); + + let uinode1a = world.spawn(Node::default()).id(); + let uinode2a = world.spawn((Node::default(), UiTargetCamera(camera2))).id(); + let uinode2b = world.spawn((Node::default(), UiTargetCamera(camera2))).id(); + let uinode2c = world.spawn((Node::default(), UiTargetCamera(camera2))).id(); + let uinode1b = world.spawn(Node::default()).id(); + + schedule.run(&mut world); + + for (uinode, camera, scale_factor, physical_size) in [ + (uinode1a, camera1, scale1, size1), + (uinode1b, camera1, scale1, size1), + (uinode2a, camera2, scale2, size2), + (uinode2b, camera2, scale2, size2), + (uinode2c, camera2, scale2, size2), + ] { + assert_eq!( + *world.get::(uinode).unwrap(), + ComputedNodeTarget { + camera, + scale_factor, + physical_size, + } + ); } - updated_entities.insert(child); - - update_children_target_camera( - child, - camera_to_set, - node_query, - ui_children, - commands, - updated_entities, + } + + #[test] + fn update_context_on_changed_camera() { + let (mut world, mut schedule) = setup_test_world_and_schedule(); + + let scale1 = 1.; + let size1 = UVec2::new(100, 100); + let scale2 = 2.; + let size2 = UVec2::new(200, 200); + + world.spawn(( + Window { + resolution: WindowResolution::new(size1.x as f32, size1.y as f32) + .with_scale_factor_override(scale1), + ..Default::default() + }, + PrimaryWindow, + )); + + let window_2 = world + .spawn((Window { + resolution: WindowResolution::new(size2.x as f32, size2.y as f32) + .with_scale_factor_override(scale2), + ..Default::default() + },)) + .id(); + + let camera1 = world.spawn((Camera2d, IsDefaultUiCamera)).id(); + let camera2 = world + .spawn(( + Camera2d, + Camera { + target: RenderTarget::Window(WindowRef::Entity(window_2)), + ..default() + }, + )) + .id(); + + let uinode = world.spawn(Node::default()).id(); + + schedule.run(&mut world); + + assert_eq!( + world + .get::(uinode) + .unwrap() + .scale_factor, + scale1 + ); + + assert_eq!( + world + .get::(uinode) + .unwrap() + .physical_size, + size1 + ); + + assert_eq!( + world + .get::(uinode) + .unwrap() + .camera() + .unwrap(), + camera1 + ); + + world.entity_mut(uinode).insert(UiTargetCamera(camera2)); + + schedule.run(&mut world); + + assert_eq!( + world + .get::(uinode) + .unwrap() + .scale_factor, + scale2 + ); + + assert_eq!( + world + .get::(uinode) + .unwrap() + .physical_size, + size2 + ); + + assert_eq!( + world + .get::(uinode) + .unwrap() + .camera() + .unwrap(), + camera2 + ); + } + + #[test] + fn update_context_after_parent_removed() { + let (mut world, mut schedule) = setup_test_world_and_schedule(); + + let scale1 = 1.; + let size1 = UVec2::new(100, 100); + let scale2 = 2.; + let size2 = UVec2::new(200, 200); + + world.spawn(( + Window { + resolution: WindowResolution::new(size1.x as f32, size1.y as f32) + .with_scale_factor_override(scale1), + ..Default::default() + }, + PrimaryWindow, + )); + + let window_2 = world + .spawn((Window { + resolution: WindowResolution::new(size2.x as f32, size2.y as f32) + .with_scale_factor_override(scale2), + ..Default::default() + },)) + .id(); + + let camera1 = world.spawn((Camera2d, IsDefaultUiCamera)).id(); + let camera2 = world + .spawn(( + Camera2d, + Camera { + target: RenderTarget::Window(WindowRef::Entity(window_2)), + ..default() + }, + )) + .id(); + + // `UiTargetCamera` is ignored on non-root UI nodes + let uinode1 = world.spawn((Node::default(), UiTargetCamera(camera2))).id(); + let uinode2 = world.spawn(Node::default()).add_child(uinode1).id(); + + schedule.run(&mut world); + + assert_eq!( + world + .get::(uinode1) + .unwrap() + .scale_factor(), + scale1 + ); + + assert_eq!( + world + .get::(uinode1) + .unwrap() + .physical_size(), + size1 + ); + + assert_eq!( + world + .get::(uinode1) + .unwrap() + .camera() + .unwrap(), + camera1 + ); + + assert_eq!( + world + .get::(uinode2) + .unwrap() + .camera() + .unwrap(), + camera1 + ); + + // Now `uinode1` is a root UI node its `UiTargetCamera` component will be used and its camera target set to `camera2`. + world.entity_mut(uinode1).remove::(); + + schedule.run(&mut world); + + assert_eq!( + world + .get::(uinode1) + .unwrap() + .scale_factor(), + scale2 + ); + + assert_eq!( + world + .get::(uinode1) + .unwrap() + .physical_size(), + size2 + ); + + assert_eq!( + world + .get::(uinode1) + .unwrap() + .camera() + .unwrap(), + camera2 + ); + + assert_eq!( + world + .get::(uinode2) + .unwrap() + .camera() + .unwrap(), + camera1 + ); + } + + #[test] + fn update_great_grandchild() { + let (mut world, mut schedule) = setup_test_world_and_schedule(); + + let scale = 1.; + let size = UVec2::new(100, 100); + + world.spawn(( + Window { + resolution: WindowResolution::new(size.x as f32, size.y as f32) + .with_scale_factor_override(scale), + ..Default::default() + }, + PrimaryWindow, + )); + + let camera = world.spawn(Camera2d).id(); + + let uinode = world.spawn(Node::default()).id(); + world.spawn(Node::default()).with_children(|builder| { + builder.spawn(Node::default()).with_children(|builder| { + builder.spawn(Node::default()).add_child(uinode); + }); + }); + + schedule.run(&mut world); + + assert_eq!( + world + .get::(uinode) + .unwrap() + .scale_factor, + scale + ); + + assert_eq!( + world + .get::(uinode) + .unwrap() + .physical_size, + size + ); + + assert_eq!( + world + .get::(uinode) + .unwrap() + .camera() + .unwrap(), + camera + ); + + world.resource_mut::().0 = 2.; + + schedule.run(&mut world); + + assert_eq!( + world + .get::(uinode) + .unwrap() + .scale_factor(), + 2. ); } } diff --git a/crates/bevy_ui/src/widget/image.rs b/crates/bevy_ui/src/widget/image.rs index e34efdc715103..9c6abbbf82809 100644 --- a/crates/bevy_ui/src/widget/image.rs +++ b/crates/bevy_ui/src/widget/image.rs @@ -1,4 +1,4 @@ -use crate::{ContentSize, Measure, MeasureArgs, Node, NodeMeasure, UiScale}; +use crate::{ComputedNodeTarget, ContentSize, Measure, MeasureArgs, Node, NodeMeasure}; use bevy_asset::{Assets, Handle}; use bevy_color::Color; use bevy_ecs::prelude::*; @@ -7,7 +7,6 @@ use bevy_math::{Rect, UVec2, Vec2}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::texture::TRANSPARENT_IMAGE_HANDLE; use bevy_sprite::TextureSlicer; -use bevy_window::{PrimaryWindow, Window}; use taffy::{MaybeMath, MaybeResolve}; /// A UI Node that renders an image. @@ -254,21 +253,19 @@ type UpdateImageFilter = (With, Without); /// Updates content size of the node based on the image provided pub fn update_image_content_size_system( - mut previous_combined_scale_factor: Local, - windows: Query<&Window, With>, - ui_scale: Res, textures: Res>, - atlases: Res>, - mut query: Query<(&mut ContentSize, Ref, &mut ImageNodeSize), UpdateImageFilter>, + mut query: Query< + ( + &mut ContentSize, + Ref, + &mut ImageNodeSize, + Ref, + ), + UpdateImageFilter, + >, ) { - let combined_scale_factor = windows - .get_single() - .map(|window| window.resolution.scale_factor()) - .unwrap_or(1.) - * ui_scale.0; - - for (mut content_size, image, mut image_size) in &mut query { + for (mut content_size, image, mut image_size, computed_target) in &mut query { if !matches!(image.image_mode, NodeImageMode::Auto) || image.image.id() == TRANSPARENT_IMAGE_HANDLE.id() { @@ -289,18 +286,13 @@ pub fn update_image_content_size_system( }) { // Update only if size or scale factor has changed to avoid needless layout calculations - if size != image_size.size - || combined_scale_factor != *previous_combined_scale_factor - || content_size.is_added() - { + if size != image_size.size || computed_target.is_changed() || content_size.is_added() { image_size.size = size; content_size.set(NodeMeasure::Image(ImageMeasure { // multiply the image size by the scale factor to get the physical size - size: size.as_vec2() * combined_scale_factor, + size: size.as_vec2() * computed_target.scale_factor(), })); } } } - - *previous_combined_scale_factor = combined_scale_factor; } diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index 9dec47103751d..e1be210950a0b 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -1,24 +1,22 @@ use crate::{ - ComputedNode, ContentSize, DefaultUiCamera, FixedMeasure, Measure, MeasureArgs, Node, - NodeMeasure, UiScale, UiTargetCamera, + ComputedNode, ComputedNodeTarget, ContentSize, FixedMeasure, Measure, MeasureArgs, Node, + NodeMeasure, }; use bevy_asset::Assets; use bevy_color::Color; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ change_detection::DetectChanges, - entity::{hash_map::EntityHashMap, Entity}, + entity::Entity, prelude::{require, Component}, query::With, reflect::ReflectComponent, - system::{Local, Query, Res, ResMut}, + system::{Query, Res, ResMut}, world::{Mut, Ref}, }; use bevy_image::prelude::*; use bevy_math::Vec2; -use bevy_platform_support::collections::hash_map::Entry; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; -use bevy_render::camera::Camera; use bevy_text::{ scale_value, ComputedTextBlock, CosmicFontSystem, Font, FontAtlasSets, LineBreak, SwashCache, TextBounds, TextColor, TextError, TextFont, TextLayout, TextLayoutInfo, TextMeasureInfo, @@ -242,18 +240,13 @@ fn create_text_measure<'a>( /// A `Measure` is used by the UI's layout algorithm to determine the appropriate amount of space /// to provide for the text given the fonts, the text itself and the constraints of the layout. /// -/// * Measures are regenerated if the target camera's scale factor (or primary window if no specific target) or [`UiScale`] is changed. +/// * Measures are regenerated on changes to either [`ComputedTextBlock`] or [`ComputedNodeTarget`]. /// * Changes that only modify the colors of a `Text` do not require a new `Measure`. This system /// is only able to detect that a `Text` component has changed and will regenerate the `Measure` on /// color changes. This can be expensive, particularly for large blocks of text, and the [`bypass_change_detection`](bevy_ecs::change_detection::DetectChangesMut::bypass_change_detection) /// method should be called when only changing the `Text`'s colors. pub fn measure_text_system( - mut scale_factors_buffer: Local>, - mut last_scale_factors: Local>, fonts: Res>, - camera_query: Query<&Camera>, - default_ui_camera: DefaultUiCamera, - ui_scale: Res, mut text_query: Query< ( Entity, @@ -261,7 +254,7 @@ pub fn measure_text_system( &mut ContentSize, &mut TextNodeFlags, &mut ComputedTextBlock, - Option<&UiTargetCamera>, + Ref, ), With, >, @@ -269,32 +262,9 @@ pub fn measure_text_system( mut text_pipeline: ResMut, mut font_system: ResMut, ) { - scale_factors_buffer.clear(); - - let default_camera_entity = default_ui_camera.get(); - - for (entity, block, content_size, text_flags, computed, maybe_camera) in &mut text_query { - let Some(camera_entity) = maybe_camera - .map(UiTargetCamera::entity) - .or(default_camera_entity) - else { - continue; - }; - - let scale_factor = match scale_factors_buffer.entry(camera_entity) { - Entry::Occupied(entry) => *entry.get(), - Entry::Vacant(entry) => *entry.insert( - camera_query - .get(camera_entity) - .ok() - .and_then(Camera::target_scaling_factor) - .unwrap_or(1.0) - * ui_scale.0, - ), - }; - + for (entity, block, content_size, text_flags, computed, computed_target) in &mut text_query { // Note: the ComputedTextBlock::needs_rerender bool is cleared in create_text_measure(). - if last_scale_factors.get(&camera_entity) != Some(&scale_factor) + if computed_target.is_changed() || computed.needs_rerender() || text_flags.needs_measure_fn || content_size.is_added() @@ -302,7 +272,7 @@ pub fn measure_text_system( create_text_measure( entity, &fonts, - scale_factor.into(), + computed_target.scale_factor.into(), text_reader.iter(entity), block, &mut text_pipeline, @@ -313,7 +283,6 @@ pub fn measure_text_system( ); } } - core::mem::swap(&mut *last_scale_factors, &mut *scale_factors_buffer); } #[inline] diff --git a/examples/3d/split_screen.rs b/examples/3d/split_screen.rs index 2a46fa5a65eda..c11fb2e3ccd07 100644 --- a/examples/3d/split_screen.rs +++ b/examples/3d/split_screen.rs @@ -183,16 +183,19 @@ fn set_camera_viewports( fn button_system( interaction_query: Query< - (&Interaction, &UiTargetCamera, &RotateCamera), + (&Interaction, &ComputedNodeTarget, &RotateCamera), (Changed, With