Store UI render target info locally per node (#17579)

# Objective

It's difficult to understand or make changes to the UI systems because
of how each system needs to individually track changes to scale factor,
windows and camera targets in local hashmaps, particularly for new
contributors. Any major change inevitably introduces new scale factor
bugs.

Instead of per-system resolution we can resolve the camera target info
for all UI nodes in a system at the start of `PostUpdate` and then store
it per-node in components that can be queried with change detection.

Fixes #17578
Fixes #15143

## Solution

Store the UI render target's data locally per node in a component that
is updated in `PostUpdate` before any other UI systems run.

This component can be then be queried with change detection so that UI
systems no longer need to have knowledge of cameras and windows and
don't require fragile custom change detection solutions using local
hashmaps.

## Showcase
Compare `measure_text_system` from main (which has a bug the causes it
to use the wrong scale factor when a node's camera target changes):
```
pub fn measure_text_system(
    mut scale_factors_buffer: Local<EntityHashMap<f32>>,
    mut last_scale_factors: Local<EntityHashMap<f32>>,
    fonts: Res<Assets<Font>>,
    camera_query: Query<(Entity, &Camera)>,
    default_ui_camera: DefaultUiCamera,
    ui_scale: Res<UiScale>,
    mut text_query: Query<
        (
            Entity,
            Ref<TextLayout>,
            &mut ContentSize,
            &mut TextNodeFlags,
            &mut ComputedTextBlock,
            Option<&UiTargetCamera>,
        ),
        With<Node>,
    >,
    mut text_reader: TextUiReader,
    mut text_pipeline: ResMut<TextPipeline>,
    mut font_system: ResMut<CosmicFontSystem>,
) {
    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(|(_, c)| c.target_scaling_factor())
                    .unwrap_or(1.0)
                    * ui_scale.0,
            ),
        };

        if last_scale_factors.get(&camera_entity) != Some(&scale_factor)
            || computed.needs_rerender()
            || text_flags.needs_measure_fn
            || content_size.is_added()
        {
            create_text_measure(
                entity,
                &fonts,
                scale_factor.into(),
                text_reader.iter(entity),
                block,
                &mut text_pipeline,
                content_size,
                text_flags,
                computed,
                &mut font_system,
            );
        }
    }
    core::mem::swap(&mut *last_scale_factors, &mut *scale_factors_buffer);
}
```

with `measure_text_system` from this PR (which always uses the correct
scale factor):
```
pub fn measure_text_system(
    fonts: Res<Assets<Font>>,
    mut text_query: Query<
        (
            Entity,
            Ref<TextLayout>,
            &mut ContentSize,
            &mut TextNodeFlags,
            &mut ComputedTextBlock,
            Ref<ComputedNodeTarget>,
        ),
        With<Node>,
    >,
    mut text_reader: TextUiReader,
    mut text_pipeline: ResMut<TextPipeline>,
    mut font_system: ResMut<CosmicFontSystem>,
) {
    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 computed_target.is_changed()
            || computed.needs_rerender()
            || text_flags.needs_measure_fn
            || content_size.is_added()
        {
            create_text_measure(
                entity,
                &fonts,
                computed_target.scale_factor.into(),
                text_reader.iter(entity),
                block,
                &mut text_pipeline,
                content_size,
                text_flags,
                computed,
                &mut font_system,
            );
        }
    }
}
```

## Testing

I removed an alarming number of tests from the `layout` module but they
were mostly to do with the deleted camera synchronisation logic. The
remaining tests should all pass now.

The most relevant examples are `multiple_windows` and `split_screen`,
the behaviour of both should be unchanged from main.

---------

Co-authored-by: UkoeHB <37489173+UkoeHB@users.noreply.github.com>
Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
This commit is contained in:
ickshonpe 2025-02-10 07:27:58 +00:00 committed by GitHub
parent ea578415e1
commit 300fe4db4d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 754 additions and 814 deletions

View File

@ -1,8 +1,9 @@
//! This module contains [`GhostNode`] and utilities to flatten the UI hierarchy, traversing past ghost nodes. //! 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 crate::Node;
use bevy_ecs::{prelude::*, system::SystemParam}; use bevy_ecs::{prelude::*, system::SystemParam};
#[cfg(feature = "ghost_nodes")] #[cfg(feature = "ghost_nodes")]
use bevy_reflect::prelude::*; use bevy_reflect::prelude::*;
#[cfg(feature = "ghost_nodes")] #[cfg(feature = "ghost_nodes")]
@ -11,7 +12,6 @@ use bevy_render::view::Visibility;
use bevy_transform::prelude::Transform; use bevy_transform::prelude::Transform;
#[cfg(feature = "ghost_nodes")] #[cfg(feature = "ghost_nodes")]
use smallvec::SmallVec; use smallvec::SmallVec;
/// Marker component for entities that should be ignored within UI hierarchies. /// 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. /// 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)] #[derive(Component, Debug, Copy, Clone, Reflect)]
#[cfg_attr(feature = "ghost_nodes", derive(Default))] #[cfg_attr(feature = "ghost_nodes", derive(Default))]
#[reflect(Component, Debug)] #[reflect(Component, Debug)]
#[require(Visibility, Transform)] #[require(Visibility, Transform, ComputedNodeTarget)]
pub struct GhostNode; pub struct GhostNode;
#[cfg(feature = "ghost_nodes")] #[cfg(feature = "ghost_nodes")]

View File

@ -1,6 +1,4 @@
use crate::{ use crate::{CalculatedClip, ComputedNode, ComputedNodeTarget, ResolvedBorderRadius, UiStack};
CalculatedClip, ComputedNode, DefaultUiCamera, ResolvedBorderRadius, UiStack, UiTargetCamera,
};
use bevy_ecs::{ use bevy_ecs::{
change_detection::DetectChangesMut, change_detection::DetectChangesMut,
entity::{Entity, EntityBorrow}, entity::{Entity, EntityBorrow},
@ -141,7 +139,7 @@ pub struct NodeQuery {
focus_policy: Option<&'static FocusPolicy>, focus_policy: Option<&'static FocusPolicy>,
calculated_clip: Option<&'static CalculatedClip>, calculated_clip: Option<&'static CalculatedClip>,
inherited_visibility: Option<&'static InheritedVisibility>, 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 /// 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( pub fn ui_focus_system(
mut state: Local<State>, mut state: Local<State>,
camera_query: Query<(Entity, &Camera)>, camera_query: Query<(Entity, &Camera)>,
default_ui_camera: DefaultUiCamera,
primary_window: Query<Entity, With<PrimaryWindow>>, primary_window: Query<Entity, With<PrimaryWindow>>,
windows: Query<&Window>, windows: Query<&Window>,
mouse_button_input: Res<ButtonInput<MouseButton>>, mouse_button_input: Res<ButtonInput<MouseButton>>,
@ -212,8 +209,6 @@ pub fn ui_focus_system(
}) })
.collect(); .collect();
let default_camera_entity = default_ui_camera.get();
// prepare an iterator that contains all the nodes that have the cursor in their rect, // 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` // 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. // for all nodes encountered that are no longer hovered.
@ -237,10 +232,7 @@ pub fn ui_focus_system(
} }
return None; return None;
} }
let camera_entity = node let camera_entity = node.target_camera.camera()?;
.target_camera
.map(UiTargetCamera::entity)
.or(default_camera_entity)?;
let node_rect = Rect::from_center_size( let node_rect = Rect::from_center_size(
node.global_transform.translation().truncate(), node.global_transform.translation().truncate(),

View File

@ -14,19 +14,18 @@ pub fn print_ui_layout_tree(ui_surface: &UiSurface) {
.iter() .iter()
.map(|(entity, node)| (node.id, *entity)) .map(|(entity, node)| (node.id, *entity))
.collect(); .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(); let mut out = String::new();
for root in roots {
print_node( print_node(
ui_surface, ui_surface,
&taffy_to_entity, &taffy_to_entity,
entity, entity,
root.implicit_viewport_node, viewport_node,
false, false,
String::new(), String::new(),
&mut out, &mut out,
); );
}
tracing::info!("Layout tree for camera entity: {entity}\n{out}"); tracing::info!("Layout tree for camera entity: {entity}\n{out}");
} }
} }

View File

@ -1,19 +1,20 @@
use crate::{ use crate::{
experimental::{UiChildren, UiRootNodes}, experimental::{UiChildren, UiRootNodes},
BorderRadius, ComputedNode, ContentSize, DefaultUiCamera, Display, LayoutConfig, Node, Outline, BorderRadius, ComputedNode, ComputedNodeTarget, ContentSize, Display, LayoutConfig, Node,
OverflowAxis, ScrollPosition, UiScale, UiTargetCamera, Val, Outline, OverflowAxis, ScrollPosition, Val,
}; };
use bevy_ecs::{ use bevy_ecs::{
entity::{hash_map::EntityHashMap, hash_set::EntityHashSet}, change_detection::{DetectChanges, DetectChangesMut},
prelude::*, entity::Entity,
system::SystemParam, hierarchy::{ChildOf, Children},
query::With,
removal_detection::RemovedComponents,
system::{Commands, Query, ResMut},
world::Ref,
}; };
use bevy_math::{UVec2, Vec2}; use bevy_math::Vec2;
use bevy_render::camera::{Camera, NormalizedRenderTarget};
use bevy_sprite::BorderRect; use bevy_sprite::BorderRect;
use bevy_transform::components::Transform; use bevy_transform::components::Transform;
use bevy_utils::once;
use bevy_window::{PrimaryWindow, Window, WindowScaleFactorChanged};
use thiserror::Error; use thiserror::Error;
use tracing::warn; use tracing::warn;
use ui_surface::UiSurface; use ui_surface::UiSurface;
@ -67,50 +68,19 @@ pub enum LayoutError {
TaffyError(taffy::TaffyError), 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<Vec<Entity>>,
resized_windows: EntityHashSet,
camera_layout_info: EntityHashMap<CameraLayoutInfo>,
}
struct CameraLayoutInfo {
size: UVec2,
resized: bool,
scale_factor: f32,
root_nodes: Vec<Entity>,
}
/// Updates the UI's layout tree, computes the new layout geometry and then updates the sizes and transforms of all the UI nodes. /// 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( pub fn ui_layout_system(
mut commands: Commands, mut commands: Commands,
mut buffers: Local<UiLayoutSystemBuffers>,
primary_window: Query<(Entity, &Window), With<PrimaryWindow>>,
camera_data: (Query<(Entity, &Camera)>, DefaultUiCamera),
ui_scale: Res<UiScale>,
mut scale_factor_events: EventReader<WindowScaleFactorChanged>,
mut resize_events: EventReader<bevy_window::WindowResized>,
mut ui_surface: ResMut<UiSurface>, mut ui_surface: ResMut<UiSurface>,
root_nodes: UiRootNodes, ui_root_node_query: UiRootNodes,
mut node_query: Query<( mut node_query: Query<(
Entity, Entity,
Ref<Node>, Ref<Node>,
Option<&mut ContentSize>, Option<&mut ContentSize>,
Option<&UiTargetCamera>, Ref<ComputedNodeTarget>,
)>, )>,
computed_node_query: Query<(Entity, Option<Ref<ChildOf>>), With<ComputedNode>>, computed_node_query: Query<(Entity, Option<Ref<ChildOf>>), With<ComputedNode>>,
ui_children: UiChildren, ui_children: UiChildren,
mut removed_components: UiLayoutSystemRemovedComponentParam,
mut node_transform_query: Query<( mut node_transform_query: Query<(
&mut ComputedNode, &mut ComputedNode,
&mut Transform, &mut Transform,
@ -120,127 +90,38 @@ pub fn ui_layout_system(
Option<&Outline>, Option<&Outline>,
Option<&ScrollPosition>, Option<&ScrollPosition>,
)>, )>,
mut buffer_query: Query<&mut ComputedTextBlock>, mut buffer_query: Query<&mut ComputedTextBlock>,
mut font_system: ResMut<CosmicFontSystem>, mut font_system: ResMut<CosmicFontSystem>,
mut removed_children: RemovedComponents<Children>,
mut removed_content_sizes: RemovedComponents<ContentSize>,
mut removed_nodes: RemovedComponents<Node>,
) { ) {
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. // 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); ui_surface.try_remove_node_context(entity);
} }
// Sync Node and ContentSize to Taffy for all nodes // Sync Node and ContentSize to Taffy for all nodes
node_query node_query
.iter_mut() .iter_mut()
.for_each(|(entity, node, content_size, target_camera)| { .for_each(|(entity, node, content_size, computed_target)| {
if let Some(camera) = if computed_target.is_changed()
camera_with_default(target_camera).and_then(|c| camera_layout_info.get(&c))
{
if camera.resized
|| !scale_factor_events.is_empty()
|| ui_scale.is_changed()
|| node.is_changed() || node.is_changed()
|| content_size || content_size
.as_ref() .as_ref()
.is_some_and(|c| c.is_changed() || c.measure.is_some()) .is_some_and(|c| c.is_changed() || c.measure.is_some())
{ {
let layout_context = LayoutContext::new( let layout_context = LayoutContext::new(
camera.scale_factor, computed_target.scale_factor,
[camera.size.x as f32, camera.size.y as f32].into(), computed_target.physical_size.as_vec2(),
); );
let measure = content_size.and_then(|mut c| c.measure.take()); let measure = content_size.and_then(|mut c| c.measure.take());
ui_surface.upsert_node(&layout_context, entity, &node, measure); ui_surface.upsert_node(&layout_context, entity, &node, measure);
} }
} else {
ui_surface.upsert_node(&LayoutContext::DEFAULT, entity, &Node::default(), None);
}
}); });
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 // update and remove children
for entity in removed_components.removed_children.read() { for entity in removed_children.read() {
ui_surface.try_remove_children(entity); 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) // clean up removed nodes after syncing children to avoid potential panic (invalid SlotMap key used)
ui_surface.remove_entities( ui_surface.remove_entities(
removed_components removed_nodes
.removed_nodes
.read() .read()
.filter(|entity| !node_query.contains(*entity)), .filter(|entity| !node_query.contains(*entity)),
); );
@ -280,30 +159,30 @@ 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() { for ui_root_entity in ui_root_node_query.iter() {
let inverse_target_scale_factor = camera.scale_factor.recip(); let (_, _, _, computed_target) = node_query.get(ui_root_entity).unwrap();
ui_surface.compute_camera_layout(camera_id, camera.size, text_buffers, &mut font_system); ui_surface.compute_layout(
ui_root_entity,
computed_target.physical_size,
&mut buffer_query,
&mut font_system,
);
for root in &camera.root_nodes {
update_uinode_geometry_recursive( update_uinode_geometry_recursive(
&mut commands, &mut commands,
*root, ui_root_entity,
&mut ui_surface, &mut ui_surface,
true, true,
None, None,
&mut node_transform_query, &mut node_transform_query,
&ui_children, &ui_children,
inverse_target_scale_factor, computed_target.scale_factor.recip(),
Vec2::ZERO, Vec2::ZERO,
Vec2::ZERO, Vec2::ZERO,
); );
} }
camera.root_nodes.clear();
interned_root_nodes.push(camera.root_nodes);
}
// Returns the combined bounding box of the node and any of its overflowing children. // Returns the combined bounding box of the node and any of its overflowing children.
fn update_uinode_geometry_recursive( fn update_uinode_geometry_recursive(
commands: &mut Commands, commands: &mut Commands,
@ -486,7 +365,7 @@ mod tests {
use crate::{ use crate::{
layout::ui_surface::UiSurface, prelude::*, ui_layout_system, 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 // 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 // UI is driven by calculated camera target info, so we need to run the camera system first
bevy_render::camera::camera_system, bevy_render::camera::camera_system,
update_target_camera_system, update_ui_context_system,
ApplyDeferred, ApplyDeferred,
ui_layout_system, ui_layout_system,
sync_simple_transforms, sync_simple_transforms,
@ -600,54 +479,6 @@ mod tests {
assert!(ui_surface.entity_to_taffy.is_empty()); 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::<Entity, With<Camera>>()
.iter(&world)
.collect::<Vec<_>>();
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::<UiSurface>();
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::<UiSurface>();
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::<UiSurface>();
assert!(!ui_surface
.camera_entity_to_taffy
.contains_key(&camera_entity));
assert!(ui_surface.camera_entity_to_taffy.is_empty());
}
#[test] #[test]
#[should_panic] #[should_panic]
fn despawning_a_ui_entity_should_remove_its_corresponding_ui_node() { 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 // UI is driven by calculated camera target info, so we need to run the camera system first
bevy_render::camera::camera_system, bevy_render::camera::camera_system,
update_target_camera_system, update_ui_context_system,
ApplyDeferred, ApplyDeferred,
ui_layout_system, ui_layout_system,
) )
@ -1206,11 +1037,9 @@ mod tests {
let (mut world, ..) = setup_ui_test_world(); let (mut world, ..) = setup_ui_test_world();
let camera_entity = Entity::from_raw(0);
let root_node_entity = Entity::from_raw(1); let root_node_entity = Entity::from_raw(1);
struct TestSystemParam { struct TestSystemParam {
camera_entity: Entity,
root_node_entity: Entity, root_node_entity: Entity,
} }
@ -1227,21 +1056,15 @@ mod tests {
None, None,
); );
ui_surface.compute_camera_layout( ui_surface.compute_layout(
params.camera_entity, params.root_node_entity,
UVec2::new(800, 600), UVec2::new(800, 600),
&mut computed_text_block_query, &mut computed_text_block_query,
&mut font_system, &mut font_system,
); );
} }
let _ = world.run_system_once_with( let _ = world.run_system_once_with(test_system, TestSystemParam { root_node_entity });
test_system,
TestSystemParam {
camera_entity,
root_node_entity,
},
);
let ui_surface = world.resource::<UiSurface>(); let ui_surface = world.resource::<UiSurface>();

View File

@ -13,14 +13,6 @@ use bevy_utils::default;
use crate::{layout::convert, LayoutContext, LayoutError, Measure, MeasureArgs, Node, NodeMeasure}; use crate::{layout::convert, LayoutContext, LayoutError, Measure, MeasureArgs, Node, NodeMeasure};
use bevy_text::CosmicFontSystem; 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)] #[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct LayoutNode { pub struct LayoutNode {
// Implicit "viewport" node if this `LayoutNode` corresponds to a root UI node entity // Implicit "viewport" node if this `LayoutNode` corresponds to a root UI node entity
@ -40,9 +32,8 @@ impl From<taffy::NodeId> for LayoutNode {
#[derive(Resource)] #[derive(Resource)]
pub struct UiSurface { pub struct UiSurface {
pub root_entity_to_viewport_node: EntityHashMap<taffy::NodeId>,
pub(super) entity_to_taffy: EntityHashMap<LayoutNode>, pub(super) entity_to_taffy: EntityHashMap<LayoutNode>,
pub(super) camera_entity_to_taffy: EntityHashMap<EntityHashMap<taffy::NodeId>>,
pub(super) camera_roots: EntityHashMap<Vec<RootNodePair>>,
pub(super) taffy: TaffyTree<NodeMeasure>, pub(super) taffy: TaffyTree<NodeMeasure>,
taffy_children_scratch: Vec<taffy::NodeId>, taffy_children_scratch: Vec<taffy::NodeId>,
} }
@ -50,8 +41,6 @@ pub struct UiSurface {
fn _assert_send_sync_ui_surface_impl_safe() { fn _assert_send_sync_ui_surface_impl_safe() {
fn _assert_send_sync<T: Send + Sync>() {} fn _assert_send_sync<T: Send + Sync>() {}
_assert_send_sync::<EntityHashMap<taffy::NodeId>>(); _assert_send_sync::<EntityHashMap<taffy::NodeId>>();
_assert_send_sync::<EntityHashMap<EntityHashMap<taffy::NodeId>>>();
_assert_send_sync::<EntityHashMap<Vec<RootNodePair>>>();
_assert_send_sync::<TaffyTree<NodeMeasure>>(); _assert_send_sync::<TaffyTree<NodeMeasure>>();
_assert_send_sync::<UiSurface>(); _assert_send_sync::<UiSurface>();
} }
@ -60,8 +49,6 @@ impl fmt::Debug for UiSurface {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("UiSurface") f.debug_struct("UiSurface")
.field("entity_to_taffy", &self.entity_to_taffy) .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) .field("taffy_children_scratch", &self.taffy_children_scratch)
.finish() .finish()
} }
@ -71,9 +58,8 @@ impl Default for UiSurface {
fn default() -> Self { fn default() -> Self {
let taffy: TaffyTree<NodeMeasure> = TaffyTree::new(); let taffy: TaffyTree<NodeMeasure> = TaffyTree::new();
Self { Self {
root_entity_to_viewport_node: Default::default(),
entity_to_taffy: Default::default(), entity_to_taffy: Default::default(),
camera_entity_to_taffy: Default::default(),
camera_roots: Default::default(),
taffy, taffy,
taffy_children_scratch: Vec::new(), taffy_children_scratch: Vec::new(),
} }
@ -166,13 +152,16 @@ impl UiSurface {
} }
} }
/// Sets the ui root node entities as children to the root node in the taffy layout. /// Gets or inserts an implicit taffy viewport node corresponding to the given UI root entity
pub fn set_camera_children( pub fn get_or_insert_taffy_viewport_node(&mut self, ui_root_entity: Entity) -> taffy::NodeId {
&mut self, *self
camera_id: Entity, .root_entity_to_viewport_node
children: impl Iterator<Item = Entity>, .entry(ui_root_entity)
) { .or_insert_with(|| {
let viewport_style = taffy::style::Style { 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, display: taffy::style::Display::Grid,
// Note: Taffy percentages are floats ranging from 0.0 to 1.0. // Note: Taffy percentages are floats ranging from 0.0 to 1.0.
// So this is setting width:100% and height:100% // So this is setting width:100% and height:100%
@ -183,60 +172,32 @@ impl UiSurface {
align_items: Some(taffy::style::AlignItems::Start), align_items: Some(taffy::style::AlignItems::Start),
justify_items: Some(taffy::style::JustifyItems::Start), justify_items: Some(taffy::style::JustifyItems::Start),
..default() ..default()
}; })
.unwrap();
let camera_root_node_map = self.camera_entity_to_taffy.entry(camera_id).or_default(); self.taffy.add_child(implicit_root, root_node.id).unwrap();
let existing_roots = self.camera_roots.entry(camera_id).or_default(); root_node.viewport_id = Some(implicit_root);
let mut new_roots = Vec::new(); implicit_root
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(|| { /// Compute the layout for the given implicit taffy viewport node
node.viewport_id pub fn compute_layout<'a>(
.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);
}
/// Compute the layout for each window entity's corresponding root node in the layout.
pub fn compute_camera_layout<'a>(
&mut self, &mut self,
camera: Entity, ui_root_entity: Entity,
render_target_resolution: UVec2, render_target_resolution: UVec2,
buffer_query: &'a mut bevy_ecs::prelude::Query<&mut bevy_text::ComputedTextBlock>, buffer_query: &'a mut bevy_ecs::prelude::Query<&mut bevy_text::ComputedTextBlock>,
font_system: &'a mut CosmicFontSystem, font_system: &'a mut CosmicFontSystem,
) { ) {
let Some(camera_root_nodes) = self.camera_roots.get(&camera) else { let implicit_viewport_node = self.get_or_insert_taffy_viewport_node(ui_root_entity);
return;
};
let available_space = taffy::geometry::Size { let available_space = taffy::geometry::Size {
width: taffy::style::AvailableSpace::Definite(render_target_resolution.x as f32), width: taffy::style::AvailableSpace::Definite(render_target_resolution.x as f32),
height: taffy::style::AvailableSpace::Definite(render_target_resolution.y as f32), height: taffy::style::AvailableSpace::Definite(render_target_resolution.y as f32),
}; };
for root_nodes in camera_root_nodes {
self.taffy self.taffy
.compute_layout_with_measure( .compute_layout_with_measure(
root_nodes.implicit_viewport_node, implicit_viewport_node,
available_space, available_space,
|known_dimensions: taffy::Size<Option<f32>>, |known_dimensions: taffy::Size<Option<f32>>,
available_space: taffy::Size<taffy::AvailableSpace>, available_space: taffy::Size<taffy::AvailableSpace>,
@ -275,19 +236,6 @@ impl UiSurface {
) )
.unwrap(); .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<Item = Entity>) {
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;
}
}
}
}
/// Removes each entity from the internal map and then removes their associated nodes from taffy /// Removes each entity from the internal map and then removes their associated nodes from taffy
pub fn remove_entities(&mut self, entities: impl IntoIterator<Item = Entity>) { pub fn remove_entities(&mut self, entities: impl IntoIterator<Item = Entity>) {
@ -335,7 +283,7 @@ impl UiSurface {
} }
} }
fn get_text_buffer<'a>( pub fn get_text_buffer<'a>(
needs_buffer: bool, needs_buffer: bool,
ctx: &mut NodeMeasure, ctx: &mut NodeMeasure,
query: &'a mut bevy_ecs::prelude::Query<&mut bevy_text::ComputedTextBlock>, query: &'a mut bevy_ecs::prelude::Query<&mut bevy_text::ComputedTextBlock>,
@ -360,42 +308,16 @@ mod tests {
use bevy_math::Vec2; use bevy_math::Vec2;
use taffy::TraversePartialTree; 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<NodeMeasure>,
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] #[test]
fn test_initialization() { fn test_initialization() {
let ui_surface = UiSurface::default(); let ui_surface = UiSurface::default();
assert!(ui_surface.entity_to_taffy.is_empty()); 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); assert_eq!(ui_surface.taffy.total_node_count(), 0);
} }
#[test] #[test]
fn test_upsert() { fn test_upsert() {
let mut ui_surface = UiSurface::default(); let mut ui_surface = UiSurface::default();
let camera_entity = Entity::from_raw(0);
let root_node_entity = Entity::from_raw(1); let root_node_entity = Entity::from_raw(1);
let node = Node::default(); let node = Node::default();
@ -413,188 +335,32 @@ mod tests {
assert_eq!(ui_surface.taffy.total_node_count(), 1); assert_eq!(ui_surface.taffy.total_node_count(), 1);
// assign root node to camera // 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 // each root node will create 2 taffy nodes
assert_eq!(ui_surface.taffy.total_node_count(), 2); 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 // test duplicate insert 2
ui_surface.upsert_node(&LayoutContext::TEST_CONTEXT, root_node_entity, &node, None); ui_surface.upsert_node(&LayoutContext::TEST_CONTEXT, root_node_entity, &node, None);
// node count should not have increased // node count should not have increased
assert_eq!(ui_surface.taffy.total_node_count(), 2); 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<Entity> {
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] #[test]
fn test_remove_entities() { fn test_remove_entities() {
let mut ui_surface = UiSurface::default(); let mut ui_surface = UiSurface::default();
let camera_entity = Entity::from_raw(0);
let root_node_entity = Entity::from_raw(1); let root_node_entity = Entity::from_raw(1);
let node = Node::default(); let node = Node::default();
ui_surface.upsert_node(&LayoutContext::TEST_CONTEXT, root_node_entity, &node, None); 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.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]); ui_surface.remove_entities([root_node_entity]);
assert!(!ui_surface.entity_to_taffy.contains_key(&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] #[test]
@ -636,7 +402,6 @@ mod tests {
#[test] #[test]
fn test_set_camera_children() { fn test_set_camera_children() {
let mut ui_surface = UiSurface::default(); let mut ui_surface = UiSurface::default();
let camera_entity = Entity::from_raw(0);
let root_node_entity = Entity::from_raw(1); let root_node_entity = Entity::from_raw(1);
let child_entity = Entity::from_raw(2); let child_entity = Entity::from_raw(2);
let node = Node::default(); let node = Node::default();
@ -653,28 +418,7 @@ mod tests {
.add_child(root_taffy_node.id, child_taffy.id) .add_child(root_taffy_node.id, child_taffy.id)
.unwrap(); .unwrap();
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
.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");
assert_eq!( assert_eq!(
ui_surface.taffy.parent(child_taffy.id), ui_surface.taffy.parent(child_taffy.id),
@ -692,27 +436,10 @@ mod tests {
); );
// clear camera's root nodes // clear camera's root nodes
ui_surface.set_camera_children(camera_entity, Vec::<Entity>::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)) 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(); let root_taffy_children = ui_surface.taffy.children(root_taffy_node.id).unwrap();
assert!( assert!(
root_taffy_children.contains(&child_taffy.id), root_taffy_children.contains(&child_taffy.id),
@ -724,25 +451,8 @@ mod tests {
"expected root node child count to be 1" "expected root node child count to be 1"
); );
// re-associate root node with camera // re-associate root node with viewport node
ui_surface.set_camera_children(camera_entity, vec![root_node_entity].into_iter()); ui_surface.get_or_insert_taffy_viewport_node(root_node_entity);
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"
);
let child_taffy = ui_surface.entity_to_taffy.get(&child_entity).unwrap(); 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(); let root_taffy_children = ui_surface.taffy.children(root_taffy_node.id).unwrap();

View File

@ -72,7 +72,7 @@ use bevy_transform::TransformSystem;
use layout::ui_surface::UiSurface; use layout::ui_surface::UiSurface;
use stack::ui_stack_system; use stack::ui_stack_system;
pub use stack::UiStack; 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 /// The basic plugin for Bevy UI
pub struct UiPlugin { pub struct UiPlugin {
@ -103,6 +103,8 @@ pub enum UiSystem {
Focus, Focus,
/// All UI systems in [`PostUpdate`] will run in or after this label. /// All UI systems in [`PostUpdate`] will run in or after this label.
Prepare, Prepare,
/// Update content requirements before layout.
Content,
/// After this label, the ui layout state has been updated. /// After this label, the ui layout state has been updated.
/// ///
/// Runs in [`PostUpdate`]. /// Runs in [`PostUpdate`].
@ -172,7 +174,8 @@ impl Plugin for UiPlugin {
PostUpdate, PostUpdate,
( (
CameraUpdateSystem, CameraUpdateSystem,
UiSystem::Prepare.before(UiSystem::Stack).after(Animation), UiSystem::Prepare.after(Animation),
UiSystem::Content,
UiSystem::Layout, UiSystem::Layout,
UiSystem::PostLayout, UiSystem::PostLayout,
) )
@ -195,7 +198,7 @@ impl Plugin for UiPlugin {
app.add_systems( app.add_systems(
PostUpdate, PostUpdate,
( (
update_target_camera_system.in_set(UiSystem::Prepare), update_ui_context_system.in_set(UiSystem::Prepare),
ui_layout_system_config, ui_layout_system_config,
ui_stack_system ui_stack_system
.in_set(UiSystem::Stack) .in_set(UiSystem::Stack)
@ -209,7 +212,7 @@ impl Plugin for UiPlugin {
// its own ImageNode, and `widget::text_system` & `bevy_text::update_text2d_layout` // its own ImageNode, and `widget::text_system` & `bevy_text::update_text2d_layout`
// will never modify a pre-existing `Image` asset. // will never modify a pre-existing `Image` asset.
widget::update_image_content_size_system widget::update_image_content_size_system
.in_set(UiSystem::Prepare) .in_set(UiSystem::Content)
.in_set(AmbiguousWithTextSystem) .in_set(AmbiguousWithTextSystem)
.in_set(AmbiguousWithUpdateText2DLayout), .in_set(AmbiguousWithUpdateText2DLayout),
), ),
@ -261,7 +264,7 @@ fn build_text_interop(app: &mut App) {
widget::measure_text_system, widget::measure_text_system,
) )
.chain() .chain()
.in_set(UiSystem::Prepare) .in_set(UiSystem::Content)
// Text and Text2d are independent. // Text and Text2d are independent.
.ambiguous_with(bevy_text::detect_text_needs_rerender::<bevy_text::Text2d>) .ambiguous_with(bevy_text::detect_text_needs_rerender::<bevy_text::Text2d>)
// Potential conflict: `Assets<Image>` // Potential conflict: `Assets<Image>`

View File

@ -51,7 +51,7 @@ pub struct NodeQuery {
pickable: Option<&'static Pickable>, pickable: Option<&'static Pickable>,
calculated_clip: Option<&'static CalculatedClip>, calculated_clip: Option<&'static CalculatedClip>,
inherited_visibility: Option<&'static InheritedVisibility>, inherited_visibility: Option<&'static InheritedVisibility>,
target_camera: Option<&'static UiTargetCamera>, target_camera: &'static ComputedNodeTarget,
} }
/// Computes the UI node entities under each pointer. /// Computes the UI node entities under each pointer.
@ -61,7 +61,6 @@ pub struct NodeQuery {
pub fn ui_picking( pub fn ui_picking(
pointers: Query<(&PointerId, &PointerLocation)>, pointers: Query<(&PointerId, &PointerLocation)>,
camera_query: Query<(Entity, &Camera, Has<IsDefaultUiCamera>)>, camera_query: Query<(Entity, &Camera, Has<IsDefaultUiCamera>)>,
default_ui_camera: DefaultUiCamera,
primary_window: Query<Entity, With<PrimaryWindow>>, primary_window: Query<Entity, With<PrimaryWindow>>,
ui_stack: Res<UiStack>, ui_stack: Res<UiStack>,
node_query: Query<NodeQuery>, node_query: Query<NodeQuery>,
@ -70,8 +69,6 @@ pub fn ui_picking(
// For each camera, the pointer and its position // For each camera, the pointer and its position
let mut pointer_pos_by_camera = HashMap::<Entity, HashMap<PointerId, Vec2>>::default(); let mut pointer_pos_by_camera = HashMap::<Entity, HashMap<PointerId, Vec2>>::default();
let default_camera_entity = default_ui_camera.get();
for (pointer_id, pointer_location) in for (pointer_id, pointer_location) in
pointers.iter().filter_map(|(pointer, pointer_location)| { pointers.iter().filter_map(|(pointer, pointer_location)| {
Some(*pointer).zip(pointer_location.location().cloned()) Some(*pointer).zip(pointer_location.location().cloned())
@ -130,11 +127,7 @@ pub fn ui_picking(
{ {
continue; continue;
} }
let Some(camera_entity) = node let Some(camera_entity) = node.target_camera.camera() else {
.target_camera
.map(UiTargetCamera::entity)
.or(default_camera_entity)
else {
continue; continue;
}; };
@ -186,11 +179,7 @@ pub fn ui_picking(
let mut depth = 0.0; let mut depth = 0.0;
for node in node_query.iter_many(hovered_nodes) { for node in node_query.iter_many(hovered_nodes) {
let Some(camera_entity) = node let Some(camera_entity) = node.target_camera.camera() else {
.target_camera
.map(UiTargetCamera::entity)
.or(default_camera_entity)
else {
continue; continue;
}; };

View File

@ -3,8 +3,8 @@
use core::{hash::Hash, ops::Range}; use core::{hash::Hash, ops::Range};
use crate::{ use crate::{
BoxShadow, BoxShadowSamples, CalculatedClip, ComputedNode, RenderUiSystem, BoxShadow, BoxShadowSamples, CalculatedClip, ComputedNode, ComputedNodeTarget, RenderUiSystem,
ResolvedBorderRadius, TransparentUi, UiTargetCamera, Val, ResolvedBorderRadius, TransparentUi, Val,
}; };
use bevy_app::prelude::*; use bevy_app::prelude::*;
use bevy_asset::*; 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::sync_world::MainEntity;
use bevy_render::RenderApp; use bevy_render::RenderApp;
use bevy_render::{ use bevy_render::{
camera::Camera,
render_phase::*, render_phase::*,
render_resource::{binding_types::uniform_buffer, *}, render_resource::{binding_types::uniform_buffer, *},
renderer::{RenderDevice, RenderQueue}, renderer::{RenderDevice, RenderQueue},
@ -237,7 +236,6 @@ pub struct ExtractedBoxShadows {
pub fn extract_shadows( pub fn extract_shadows(
mut commands: Commands, mut commands: Commands,
mut extracted_box_shadows: ResMut<ExtractedBoxShadows>, mut extracted_box_shadows: ResMut<ExtractedBoxShadows>,
camera_query: Extract<Query<(Entity, &Camera)>>,
box_shadow_query: Extract< box_shadow_query: Extract<
Query<( Query<(
Entity, Entity,
@ -246,12 +244,12 @@ pub fn extract_shadows(
&InheritedVisibility, &InheritedVisibility,
&BoxShadow, &BoxShadow,
Option<&CalculatedClip>, Option<&CalculatedClip>,
Option<&UiTargetCamera>, &ComputedNodeTarget,
)>, )>,
>, >,
camera_map: Extract<UiCameraMap>, camera_map: Extract<UiCameraMap>,
) { ) {
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 { for (entity, uinode, transform, visibility, box_shadow, clip, camera) in &box_shadow_query {
// Skip if no visible shadows // Skip if no visible shadows
@ -259,18 +257,11 @@ pub fn extract_shadows(
continue; continue;
} }
let Some(extracted_camera_entity) = camera_mapper.map(camera) else { let Some(extracted_camera_entity) = mapping.map(camera) else {
continue; continue;
}; };
let ui_physical_viewport_size = camera_query let ui_physical_viewport_size = camera.physical_size.as_vec2();
.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 scale_factor = uinode.inverse_scale_factor.recip(); 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( pub fn prepare_shadows(
mut commands: Commands, mut commands: Commands,
render_device: Res<RenderDevice>, render_device: Res<RenderDevice>,

View File

@ -1,6 +1,6 @@
use crate::ui_node::ComputedNodeTarget;
use crate::CalculatedClip; use crate::CalculatedClip;
use crate::ComputedNode; use crate::ComputedNode;
use crate::UiTargetCamera;
use bevy_asset::AssetId; use bevy_asset::AssetId;
use bevy_color::Hsla; use bevy_color::Hsla;
use bevy_ecs::entity::Entity; use bevy_ecs::entity::Entity;
@ -64,7 +64,7 @@ pub fn extract_debug_overlay(
&InheritedVisibility, &InheritedVisibility,
Option<&CalculatedClip>, Option<&CalculatedClip>,
&GlobalTransform, &GlobalTransform,
Option<&UiTargetCamera>, &ComputedNodeTarget,
)>, )>,
>, >,
camera_map: Extract<UiCameraMap>, camera_map: Extract<UiCameraMap>,
@ -75,12 +75,12 @@ pub fn extract_debug_overlay(
let mut camera_mapper = camera_map.get_mapper(); 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() { if !debug_options.show_hidden && !visibility.get() {
continue; continue;
} }
let Some(extracted_camera_entity) = camera_mapper.map(camera) else { let Some(extracted_camera_entity) = camera_mapper.map(computed_target) else {
continue; continue;
}; };

View File

@ -9,8 +9,9 @@ mod debug_overlay;
use crate::widget::ImageNode; use crate::widget::ImageNode;
use crate::{ use crate::{
BackgroundColor, BorderColor, BoxShadowSamples, CalculatedClip, ComputedNode, DefaultUiCamera, BackgroundColor, BorderColor, BoxShadowSamples, CalculatedClip, ComputedNode,
Outline, ResolvedBorderRadius, TextShadow, UiAntiAlias, UiTargetCamera, ComputedNodeTarget, DefaultUiCamera, Outline, ResolvedBorderRadius, TextShadow, UiAntiAlias,
UiTargetCamera,
}; };
use bevy_app::prelude::*; use bevy_app::prelude::*;
use bevy_asset::{load_internal_asset, weak_handle, AssetEvent, AssetId, Assets, Handle}; use bevy_asset::{load_internal_asset, weak_handle, AssetEvent, AssetId, Assets, Handle};
@ -257,17 +258,14 @@ impl ExtractedUiNodes {
#[derive(SystemParam)] #[derive(SystemParam)]
pub struct UiCameraMap<'w, 's> { pub struct UiCameraMap<'w, 's> {
default: DefaultUiCamera<'w, 's>,
mapping: Query<'w, 's, RenderEntity>, mapping: Query<'w, 's, RenderEntity>,
} }
impl<'w, 's> UiCameraMap<'w, 's> { impl<'w, 's> UiCameraMap<'w, 's> {
/// Get the default camera and create the mapper /// Get the default camera and create the mapper
pub fn get_mapper(&'w self) -> UiCameraMapper<'w, 's> { pub fn get_mapper(&'w self) -> UiCameraMapper<'w, 's> {
let default_camera_entity = self.default.get();
UiCameraMapper { UiCameraMapper {
mapping: &self.mapping, mapping: &self.mapping,
default_camera_entity,
camera_entity: Entity::PLACEHOLDER, camera_entity: Entity::PLACEHOLDER,
render_entity: Entity::PLACEHOLDER, render_entity: Entity::PLACEHOLDER,
} }
@ -276,17 +274,14 @@ impl<'w, 's> UiCameraMap<'w, 's> {
pub struct UiCameraMapper<'w, 's> { pub struct UiCameraMapper<'w, 's> {
mapping: &'w Query<'w, 's, RenderEntity>, mapping: &'w Query<'w, 's, RenderEntity>,
default_camera_entity: Option<Entity>,
camera_entity: Entity, camera_entity: Entity,
render_entity: Entity, render_entity: Entity,
} }
impl<'w, 's> UiCameraMapper<'w, 's> { impl<'w, 's> UiCameraMapper<'w, 's> {
/// Returns the render entity corresponding to the given `UiTargetCamera` or the default camera if `None`. /// Returns the render entity corresponding to the given `UiTargetCamera` or the default camera if `None`.
pub fn map(&mut self, camera: Option<&UiTargetCamera>) -> Option<Entity> { pub fn map(&mut self, computed_target: &ComputedNodeTarget) -> Option<Entity> {
let camera_entity = camera let camera_entity = computed_target.camera;
.map(UiTargetCamera::entity)
.or(self.default_camera_entity)?;
if self.camera_entity != camera_entity { if self.camera_entity != camera_entity {
let Ok(new_render_camera_entity) = self.mapping.get(camera_entity) else { let Ok(new_render_camera_entity) = self.mapping.get(camera_entity) else {
return None; return None;
@ -338,7 +333,7 @@ pub fn extract_uinode_background_colors(
&GlobalTransform, &GlobalTransform,
&InheritedVisibility, &InheritedVisibility,
Option<&CalculatedClip>, Option<&CalculatedClip>,
Option<&UiTargetCamera>, &ComputedNodeTarget,
&BackgroundColor, &BackgroundColor,
)>, )>,
>, >,
@ -397,7 +392,7 @@ pub fn extract_uinode_images(
&GlobalTransform, &GlobalTransform,
&InheritedVisibility, &InheritedVisibility,
Option<&CalculatedClip>, Option<&CalculatedClip>,
Option<&UiTargetCamera>, &ComputedNodeTarget,
&ImageNode, &ImageNode,
)>, )>,
>, >,
@ -481,7 +476,7 @@ pub fn extract_uinode_borders(
&GlobalTransform, &GlobalTransform,
&InheritedVisibility, &InheritedVisibility,
Option<&CalculatedClip>, Option<&CalculatedClip>,
Option<&UiTargetCamera>, &ComputedNodeTarget,
AnyOf<(&BorderColor, &Outline)>, AnyOf<(&BorderColor, &Outline)>,
)>, )>,
>, >,
@ -497,7 +492,7 @@ pub fn extract_uinode_borders(
global_transform, global_transform,
inherited_visibility, inherited_visibility,
maybe_clip, maybe_clip,
maybe_camera, camera,
(maybe_border_color, maybe_outline), (maybe_border_color, maybe_outline),
) in &uinode_query ) in &uinode_query
{ {
@ -506,7 +501,7 @@ pub fn extract_uinode_borders(
continue; continue;
} }
let Some(extracted_camera_entity) = camera_mapper.map(maybe_camera) else { let Some(extracted_camera_entity) = camera_mapper.map(camera) else {
continue; continue;
}; };
@ -709,7 +704,7 @@ pub fn extract_text_sections(
&GlobalTransform, &GlobalTransform,
&InheritedVisibility, &InheritedVisibility,
Option<&CalculatedClip>, Option<&CalculatedClip>,
Option<&UiTargetCamera>, &ComputedNodeTarget,
&ComputedTextBlock, &ComputedTextBlock,
&TextLayoutInfo, &TextLayoutInfo,
)>, )>,

View File

@ -376,7 +376,7 @@ pub fn extract_ui_material_nodes<M: UiMaterial>(
&MaterialNode<M>, &MaterialNode<M>,
&InheritedVisibility, &InheritedVisibility,
Option<&CalculatedClip>, Option<&CalculatedClip>,
Option<&UiTargetCamera>, &ComputedNodeTarget,
)>, )>,
>, >,
camera_map: Extract<UiCameraMap>, camera_map: Extract<UiCameraMap>,

View File

@ -256,7 +256,7 @@ pub fn extract_ui_texture_slices(
&GlobalTransform, &GlobalTransform,
&InheritedVisibility, &InheritedVisibility,
Option<&CalculatedClip>, Option<&CalculatedClip>,
Option<&UiTargetCamera>, &ComputedNodeTarget,
&ImageNode, &ImageNode,
)>, )>,
>, >,

View File

@ -2,11 +2,12 @@ use crate::{FocusPolicy, UiRect, Val};
use bevy_color::Color; use bevy_color::Color;
use bevy_derive::{Deref, DerefMut}; use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{prelude::*, system::SystemParam}; 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_reflect::prelude::*;
use bevy_render::{ use bevy_render::{
camera::{Camera, RenderTarget}, camera::{Camera, RenderTarget},
view::Visibility, view::Visibility,
view::VisibilityClass,
}; };
use bevy_sprite::BorderRect; use bevy_sprite::BorderRect;
use bevy_transform::components::Transform; use bevy_transform::components::Transform;
@ -322,6 +323,7 @@ impl From<Vec2> for ScrollPosition {
#[derive(Component, Clone, PartialEq, Debug, Reflect)] #[derive(Component, Clone, PartialEq, Debug, Reflect)]
#[require( #[require(
ComputedNode, ComputedNode,
ComputedNodeTarget,
BackgroundColor, BackgroundColor,
BorderColor, BorderColor,
BorderRadius, BorderRadius,
@ -329,6 +331,7 @@ impl From<Vec2> for ScrollPosition {
ScrollPosition, ScrollPosition,
Transform, Transform,
Visibility, Visibility,
VisibilityClass,
ZIndex ZIndex
)] )]
#[reflect(Component, Default, PartialEq, Debug)] #[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<Entity> {
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 /// Adds a shadow behind text
#[derive(Component, Copy, Clone, Debug, Reflect)] #[derive(Component, Copy, Clone, Debug, Reflect)]
#[reflect(Component, Default, Debug)] #[reflect(Component, Default, Debug)]

View File

@ -2,17 +2,20 @@
use crate::{ use crate::{
experimental::{UiChildren, UiRootNodes}, experimental::{UiChildren, UiRootNodes},
CalculatedClip, Display, Node, OverflowAxis, UiTargetCamera, CalculatedClip, ComputedNodeTarget, DefaultUiCamera, Display, Node, OverflowAxis, UiScale,
UiTargetCamera,
}; };
use super::ComputedNode; use super::ComputedNode;
use bevy_ecs::{ use bevy_ecs::{
entity::Entity, change_detection::DetectChangesMut,
entity::{hash_set::EntityHashSet, Entity},
hierarchy::ChildOf,
query::{Changed, With}, query::{Changed, With},
system::{Commands, Query}, system::{Commands, Local, Query, Res},
}; };
use bevy_math::Rect; use bevy_math::{Rect, UVec2};
use bevy_platform_support::collections::HashSet; use bevy_render::camera::Camera;
use bevy_sprite::BorderRect; use bevy_sprite::BorderRect;
use bevy_transform::components::GlobalTransform; use bevy_transform::components::GlobalTransform;
@ -134,85 +137,520 @@ fn update_clipping(
} }
} }
pub fn update_target_camera_system( pub fn update_ui_context_system(
mut commands: Commands, default_ui_camera: DefaultUiCamera,
changed_root_nodes_query: Query< ui_scale: Res<UiScale>,
(Entity, Option<&UiTargetCamera>), camera_query: Query<&Camera>,
(With<Node>, Changed<UiTargetCamera>), target_camera_query: Query<&UiTargetCamera>,
>,
node_query: Query<(Entity, Option<&UiTargetCamera>), With<Node>>,
ui_root_nodes: UiRootNodes, ui_root_nodes: UiRootNodes,
mut computed_target_query: Query<&mut ComputedNodeTarget>,
ui_children: UiChildren, ui_children: UiChildren,
reparented_nodes: Query<(Entity, &ChildOf), (Changed<ChildOf>, With<ComputedNodeTarget>)>,
mut visited: Local<EntityHashSet>,
) { ) {
// Track updated entities to prevent redundant updates, as `Commands` changes are deferred, visited.clear();
// and updates done for changed_children_query can overlap with itself or with root_node_query let default_camera_entity = default_ui_camera.get();
let mut updated_entities = <HashSet<_>>::default();
// Assuming that UiTargetCamera is manually set on the root node only, for root_entity in ui_root_nodes.iter() {
// update root nodes first, since it implies the biggest change let camera = target_camera_query
for (root_node, target_camera) in changed_root_nodes_query.iter_many(ui_root_nodes.iter()) { .get(root_entity)
update_children_target_camera( .ok()
root_node, .map(UiTargetCamera::entity)
target_camera, .or(default_camera_entity)
&node_query, .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, &ui_children,
&mut commands, &mut computed_target_query,
&mut updated_entities, &mut visited,
); );
} }
// If the root node UiTargetCamera was changed, then every child is updated for (entity, child_of) in reparented_nodes.iter() {
// by this point, and iteration will be skipped. let Ok(computed_target) = computed_target_query.get(child_of.0) else {
// Otherwise, update changed children
for (parent, target_camera) in &node_query {
if !ui_children.is_changed(parent) {
continue; continue;
} };
update_children_target_camera( update_contexts_recursively(
parent, entity,
target_camera, *computed_target,
&node_query,
&ui_children, &ui_children,
&mut commands, &mut computed_target_query,
&mut updated_entities, &mut visited,
); );
} }
} }
fn update_children_target_camera( fn update_contexts_recursively(
entity: Entity, entity: Entity,
camera_to_set: Option<&UiTargetCamera>, inherited_computed_target: ComputedNodeTarget,
node_query: &Query<(Entity, Option<&UiTargetCamera>), With<Node>>,
ui_children: &UiChildren, ui_children: &UiChildren,
commands: &mut Commands, query: &mut Query<&mut ComputedNodeTarget>,
updated_entities: &mut HashSet<Entity>, visited: &mut EntityHashSet,
) { ) {
for child in ui_children.iter_ui_children(entity) { if !visited.insert(entity) {
// Skip if the child has already been updated or update is not needed return;
if updated_entities.contains(&child) }
|| camera_to_set == node_query.get(child).ok().and_then(|(_, camera)| camera) if query
.get_mut(entity)
.map(|mut computed_target| computed_target.set_if_neq(inherited_computed_target))
.unwrap_or(false)
{ {
continue; for child in ui_children.iter_ui_children(entity) {
} update_contexts_recursively(
match camera_to_set {
Some(camera) => {
commands.entity(child).try_insert(camera.clone());
}
None => {
commands.entity(child).remove::<UiTargetCamera>();
}
}
updated_entities.insert(child);
update_children_target_camera(
child, child,
camera_to_set, inherited_computed_target,
node_query,
ui_children, ui_children,
commands, query,
updated_entities, visited,
);
}
}
}
#[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::<UiScale>();
// init resources required by `camera_system`
world.init_resource::<Events<WindowScaleFactorChanged>>();
world.init_resource::<Events<WindowResized>>();
world.init_resource::<Events<WindowCreated>>();
world.init_resource::<Events<AssetEvent<Image>>>();
world.init_resource::<Assets<Image>>();
world.init_resource::<ManualTextureViews>();
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::<ComputedNodeTarget>(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::<ComputedNodeTarget>(uinode).unwrap(),
ComputedNodeTarget {
camera,
scale_factor,
physical_size,
}
);
}
}
#[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::<ComputedNodeTarget>(uinode)
.unwrap()
.scale_factor,
scale1
);
assert_eq!(
world
.get::<ComputedNodeTarget>(uinode)
.unwrap()
.physical_size,
size1
);
assert_eq!(
world
.get::<ComputedNodeTarget>(uinode)
.unwrap()
.camera()
.unwrap(),
camera1
);
world.entity_mut(uinode).insert(UiTargetCamera(camera2));
schedule.run(&mut world);
assert_eq!(
world
.get::<ComputedNodeTarget>(uinode)
.unwrap()
.scale_factor,
scale2
);
assert_eq!(
world
.get::<ComputedNodeTarget>(uinode)
.unwrap()
.physical_size,
size2
);
assert_eq!(
world
.get::<ComputedNodeTarget>(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::<ComputedNodeTarget>(uinode1)
.unwrap()
.scale_factor(),
scale1
);
assert_eq!(
world
.get::<ComputedNodeTarget>(uinode1)
.unwrap()
.physical_size(),
size1
);
assert_eq!(
world
.get::<ComputedNodeTarget>(uinode1)
.unwrap()
.camera()
.unwrap(),
camera1
);
assert_eq!(
world
.get::<ComputedNodeTarget>(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::<ChildOf>();
schedule.run(&mut world);
assert_eq!(
world
.get::<ComputedNodeTarget>(uinode1)
.unwrap()
.scale_factor(),
scale2
);
assert_eq!(
world
.get::<ComputedNodeTarget>(uinode1)
.unwrap()
.physical_size(),
size2
);
assert_eq!(
world
.get::<ComputedNodeTarget>(uinode1)
.unwrap()
.camera()
.unwrap(),
camera2
);
assert_eq!(
world
.get::<ComputedNodeTarget>(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::<ComputedNodeTarget>(uinode)
.unwrap()
.scale_factor,
scale
);
assert_eq!(
world
.get::<ComputedNodeTarget>(uinode)
.unwrap()
.physical_size,
size
);
assert_eq!(
world
.get::<ComputedNodeTarget>(uinode)
.unwrap()
.camera()
.unwrap(),
camera
);
world.resource_mut::<UiScale>().0 = 2.;
schedule.run(&mut world);
assert_eq!(
world
.get::<ComputedNodeTarget>(uinode)
.unwrap()
.scale_factor(),
2.
); );
} }
} }

View File

@ -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_asset::{Assets, Handle};
use bevy_color::Color; use bevy_color::Color;
use bevy_ecs::prelude::*; use bevy_ecs::prelude::*;
@ -7,7 +7,6 @@ use bevy_math::{Rect, UVec2, Vec2};
use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::texture::TRANSPARENT_IMAGE_HANDLE; use bevy_render::texture::TRANSPARENT_IMAGE_HANDLE;
use bevy_sprite::TextureSlicer; use bevy_sprite::TextureSlicer;
use bevy_window::{PrimaryWindow, Window};
use taffy::{MaybeMath, MaybeResolve}; use taffy::{MaybeMath, MaybeResolve};
/// A UI Node that renders an image. /// A UI Node that renders an image.
@ -254,21 +253,19 @@ type UpdateImageFilter = (With<Node>, Without<crate::prelude::Text>);
/// Updates content size of the node based on the image provided /// Updates content size of the node based on the image provided
pub fn update_image_content_size_system( pub fn update_image_content_size_system(
mut previous_combined_scale_factor: Local<f32>,
windows: Query<&Window, With<PrimaryWindow>>,
ui_scale: Res<UiScale>,
textures: Res<Assets<Image>>, textures: Res<Assets<Image>>,
atlases: Res<Assets<TextureAtlasLayout>>, atlases: Res<Assets<TextureAtlasLayout>>,
mut query: Query<(&mut ContentSize, Ref<ImageNode>, &mut ImageNodeSize), UpdateImageFilter>, mut query: Query<
(
&mut ContentSize,
Ref<ImageNode>,
&mut ImageNodeSize,
Ref<ComputedNodeTarget>,
),
UpdateImageFilter,
>,
) { ) {
let combined_scale_factor = windows for (mut content_size, image, mut image_size, computed_target) in &mut query {
.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 {
if !matches!(image.image_mode, NodeImageMode::Auto) if !matches!(image.image_mode, NodeImageMode::Auto)
|| image.image.id() == TRANSPARENT_IMAGE_HANDLE.id() || 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 // Update only if size or scale factor has changed to avoid needless layout calculations
if size != image_size.size if size != image_size.size || computed_target.is_changed() || content_size.is_added() {
|| combined_scale_factor != *previous_combined_scale_factor
|| content_size.is_added()
{
image_size.size = size; image_size.size = size;
content_size.set(NodeMeasure::Image(ImageMeasure { content_size.set(NodeMeasure::Image(ImageMeasure {
// multiply the image size by the scale factor to get the physical size // 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;
} }

View File

@ -1,24 +1,22 @@
use crate::{ use crate::{
ComputedNode, ContentSize, DefaultUiCamera, FixedMeasure, Measure, MeasureArgs, Node, ComputedNode, ComputedNodeTarget, ContentSize, FixedMeasure, Measure, MeasureArgs, Node,
NodeMeasure, UiScale, UiTargetCamera, NodeMeasure,
}; };
use bevy_asset::Assets; use bevy_asset::Assets;
use bevy_color::Color; use bevy_color::Color;
use bevy_derive::{Deref, DerefMut}; use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{ use bevy_ecs::{
change_detection::DetectChanges, change_detection::DetectChanges,
entity::{hash_map::EntityHashMap, Entity}, entity::Entity,
prelude::{require, Component}, prelude::{require, Component},
query::With, query::With,
reflect::ReflectComponent, reflect::ReflectComponent,
system::{Local, Query, Res, ResMut}, system::{Query, Res, ResMut},
world::{Mut, Ref}, world::{Mut, Ref},
}; };
use bevy_image::prelude::*; use bevy_image::prelude::*;
use bevy_math::Vec2; use bevy_math::Vec2;
use bevy_platform_support::collections::hash_map::Entry;
use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::camera::Camera;
use bevy_text::{ use bevy_text::{
scale_value, ComputedTextBlock, CosmicFontSystem, Font, FontAtlasSets, LineBreak, SwashCache, scale_value, ComputedTextBlock, CosmicFontSystem, Font, FontAtlasSets, LineBreak, SwashCache,
TextBounds, TextColor, TextError, TextFont, TextLayout, TextLayoutInfo, TextMeasureInfo, 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 /// 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. /// 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 /// * 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 /// 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) /// 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. /// method should be called when only changing the `Text`'s colors.
pub fn measure_text_system( pub fn measure_text_system(
mut scale_factors_buffer: Local<EntityHashMap<f32>>,
mut last_scale_factors: Local<EntityHashMap<f32>>,
fonts: Res<Assets<Font>>, fonts: Res<Assets<Font>>,
camera_query: Query<&Camera>,
default_ui_camera: DefaultUiCamera,
ui_scale: Res<UiScale>,
mut text_query: Query< mut text_query: Query<
( (
Entity, Entity,
@ -261,7 +254,7 @@ pub fn measure_text_system(
&mut ContentSize, &mut ContentSize,
&mut TextNodeFlags, &mut TextNodeFlags,
&mut ComputedTextBlock, &mut ComputedTextBlock,
Option<&UiTargetCamera>, Ref<ComputedNodeTarget>,
), ),
With<Node>, With<Node>,
>, >,
@ -269,32 +262,9 @@ pub fn measure_text_system(
mut text_pipeline: ResMut<TextPipeline>, mut text_pipeline: ResMut<TextPipeline>,
mut font_system: ResMut<CosmicFontSystem>, mut font_system: ResMut<CosmicFontSystem>,
) { ) {
scale_factors_buffer.clear(); for (entity, block, content_size, text_flags, computed, computed_target) in &mut text_query {
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,
),
};
// Note: the ComputedTextBlock::needs_rerender bool is cleared in create_text_measure(). // 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() || computed.needs_rerender()
|| text_flags.needs_measure_fn || text_flags.needs_measure_fn
|| content_size.is_added() || content_size.is_added()
@ -302,7 +272,7 @@ pub fn measure_text_system(
create_text_measure( create_text_measure(
entity, entity,
&fonts, &fonts,
scale_factor.into(), computed_target.scale_factor.into(),
text_reader.iter(entity), text_reader.iter(entity),
block, block,
&mut text_pipeline, &mut text_pipeline,
@ -313,7 +283,6 @@ pub fn measure_text_system(
); );
} }
} }
core::mem::swap(&mut *last_scale_factors, &mut *scale_factors_buffer);
} }
#[inline] #[inline]

View File

@ -183,16 +183,19 @@ fn set_camera_viewports(
fn button_system( fn button_system(
interaction_query: Query< interaction_query: Query<
(&Interaction, &UiTargetCamera, &RotateCamera), (&Interaction, &ComputedNodeTarget, &RotateCamera),
(Changed<Interaction>, With<Button>), (Changed<Interaction>, With<Button>),
>, >,
mut camera_query: Query<&mut Transform, With<Camera>>, mut camera_query: Query<&mut Transform, With<Camera>>,
) { ) {
for (interaction, target_camera, RotateCamera(direction)) in &interaction_query { for (interaction, computed_target, RotateCamera(direction)) in &interaction_query {
if let Interaction::Pressed = *interaction { if let Interaction::Pressed = *interaction {
// Since TargetCamera propagates to the children, we can use it to find // Since TargetCamera propagates to the children, we can use it to find
// which side of the screen the button is on. // which side of the screen the button is on.
if let Ok(mut camera_transform) = camera_query.get_mut(target_camera.entity()) { if let Some(mut camera_transform) = computed_target
.camera()
.and_then(|camera| camera_query.get_mut(camera).ok())
{
let angle = match direction { let angle = match direction {
Direction::Left => -0.1, Direction::Left => -0.1,
Direction::Right => 0.1, Direction::Right => 0.1,