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:
parent
ea578415e1
commit
300fe4db4d
@ -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")]
|
||||
|
@ -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<State>,
|
||||
camera_query: Query<(Entity, &Camera)>,
|
||||
default_ui_camera: DefaultUiCamera,
|
||||
primary_window: Query<Entity, With<PrimaryWindow>>,
|
||||
windows: Query<&Window>,
|
||||
mouse_button_input: Res<ButtonInput<MouseButton>>,
|
||||
@ -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(),
|
||||
|
@ -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}");
|
||||
}
|
||||
}
|
||||
|
@ -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<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.
|
||||
pub fn ui_layout_system(
|
||||
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>,
|
||||
root_nodes: UiRootNodes,
|
||||
ui_root_node_query: UiRootNodes,
|
||||
mut node_query: Query<(
|
||||
Entity,
|
||||
Ref<Node>,
|
||||
Option<&mut ContentSize>,
|
||||
Option<&UiTargetCamera>,
|
||||
Ref<ComputedNodeTarget>,
|
||||
)>,
|
||||
computed_node_query: Query<(Entity, Option<Ref<ChildOf>>), With<ComputedNode>>,
|
||||
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<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.
|
||||
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();
|
||||
for ui_root_entity in ui_root_node_query.iter() {
|
||||
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(
|
||||
&mut commands,
|
||||
*root,
|
||||
&mut ui_surface,
|
||||
true,
|
||||
None,
|
||||
&mut node_transform_query,
|
||||
&ui_children,
|
||||
inverse_target_scale_factor,
|
||||
Vec2::ZERO,
|
||||
Vec2::ZERO,
|
||||
);
|
||||
}
|
||||
|
||||
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::<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]
|
||||
#[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::<UiSurface>();
|
||||
|
||||
|
@ -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<taffy::NodeId> for LayoutNode {
|
||||
|
||||
#[derive(Resource)]
|
||||
pub struct UiSurface {
|
||||
pub root_entity_to_viewport_node: EntityHashMap<taffy::NodeId>,
|
||||
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>,
|
||||
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<T: Send + Sync>() {}
|
||||
_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::<UiSurface>();
|
||||
}
|
||||
@ -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<NodeMeasure> = 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<Item = Entity>,
|
||||
) {
|
||||
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<Option<f32>>,
|
||||
available_space: taffy::Size<taffy::AvailableSpace>,
|
||||
_node_id: taffy::NodeId,
|
||||
context: Option<&mut NodeMeasure>,
|
||||
style: &taffy::Style|
|
||||
-> taffy::Size<f32> {
|
||||
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<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;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.taffy
|
||||
.compute_layout_with_measure(
|
||||
implicit_viewport_node,
|
||||
available_space,
|
||||
|known_dimensions: taffy::Size<Option<f32>>,
|
||||
available_space: taffy::Size<taffy::AvailableSpace>,
|
||||
_node_id: taffy::NodeId,
|
||||
context: Option<&mut NodeMeasure>,
|
||||
style: &taffy::Style|
|
||||
-> taffy::Size<f32> {
|
||||
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<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]
|
||||
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<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]
|
||||
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::<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))
|
||||
|
||||
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();
|
||||
|
@ -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::<bevy_text::Text2d>)
|
||||
// Potential conflict: `Assets<Image>`
|
||||
|
@ -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<IsDefaultUiCamera>)>,
|
||||
default_ui_camera: DefaultUiCamera,
|
||||
primary_window: Query<Entity, With<PrimaryWindow>>,
|
||||
ui_stack: Res<UiStack>,
|
||||
node_query: Query<NodeQuery>,
|
||||
@ -70,8 +69,6 @@ pub fn ui_picking(
|
||||
// For each camera, the pointer and its position
|
||||
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
|
||||
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;
|
||||
};
|
||||
|
||||
|
@ -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<ExtractedBoxShadows>,
|
||||
camera_query: Extract<Query<(Entity, &Camera)>>,
|
||||
box_shadow_query: Extract<
|
||||
Query<(
|
||||
Entity,
|
||||
@ -246,12 +244,12 @@ pub fn extract_shadows(
|
||||
&InheritedVisibility,
|
||||
&BoxShadow,
|
||||
Option<&CalculatedClip>,
|
||||
Option<&UiTargetCamera>,
|
||||
&ComputedNodeTarget,
|
||||
)>,
|
||||
>,
|
||||
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 {
|
||||
// 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<RenderDevice>,
|
||||
|
@ -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<UiCameraMap>,
|
||||
@ -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;
|
||||
};
|
||||
|
||||
|
@ -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<Entity>,
|
||||
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<Entity> {
|
||||
let camera_entity = camera
|
||||
.map(UiTargetCamera::entity)
|
||||
.or(self.default_camera_entity)?;
|
||||
pub fn map(&mut self, computed_target: &ComputedNodeTarget) -> Option<Entity> {
|
||||
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,
|
||||
)>,
|
||||
|
@ -376,7 +376,7 @@ pub fn extract_ui_material_nodes<M: UiMaterial>(
|
||||
&MaterialNode<M>,
|
||||
&InheritedVisibility,
|
||||
Option<&CalculatedClip>,
|
||||
Option<&UiTargetCamera>,
|
||||
&ComputedNodeTarget,
|
||||
)>,
|
||||
>,
|
||||
camera_map: Extract<UiCameraMap>,
|
||||
|
@ -256,7 +256,7 @@ pub fn extract_ui_texture_slices(
|
||||
&GlobalTransform,
|
||||
&InheritedVisibility,
|
||||
Option<&CalculatedClip>,
|
||||
Option<&UiTargetCamera>,
|
||||
&ComputedNodeTarget,
|
||||
&ImageNode,
|
||||
)>,
|
||||
>,
|
||||
|
@ -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<Vec2> for ScrollPosition {
|
||||
#[derive(Component, Clone, PartialEq, Debug, Reflect)]
|
||||
#[require(
|
||||
ComputedNode,
|
||||
ComputedNodeTarget,
|
||||
BackgroundColor,
|
||||
BorderColor,
|
||||
BorderRadius,
|
||||
@ -329,6 +331,7 @@ impl From<Vec2> 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<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
|
||||
#[derive(Component, Copy, Clone, Debug, Reflect)]
|
||||
#[reflect(Component, Default, Debug)]
|
||||
|
@ -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<Node>, Changed<UiTargetCamera>),
|
||||
>,
|
||||
node_query: Query<(Entity, Option<&UiTargetCamera>), With<Node>>,
|
||||
pub fn update_ui_context_system(
|
||||
default_ui_camera: DefaultUiCamera,
|
||||
ui_scale: Res<UiScale>,
|
||||
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<ChildOf>, With<ComputedNodeTarget>)>,
|
||||
mut visited: Local<EntityHashSet>,
|
||||
) {
|
||||
// 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 = <HashSet<_>>::default();
|
||||
visited.clear();
|
||||
let default_camera_entity = default_ui_camera.get();
|
||||
|
||||
// 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,
|
||||
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<Node>>,
|
||||
inherited_computed_target: ComputedNodeTarget,
|
||||
ui_children: &UiChildren,
|
||||
commands: &mut Commands,
|
||||
updated_entities: &mut HashSet<Entity>,
|
||||
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::<UiTargetCamera>();
|
||||
#[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,
|
||||
}
|
||||
);
|
||||
}
|
||||
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::<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.
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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<Node>, Without<crate::prelude::Text>);
|
||||
|
||||
/// Updates content size of the node based on the image provided
|
||||
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>>,
|
||||
|
||||
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
|
||||
.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;
|
||||
}
|
||||
|
@ -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<EntityHashMap<f32>>,
|
||||
mut last_scale_factors: Local<EntityHashMap<f32>>,
|
||||
fonts: Res<Assets<Font>>,
|
||||
camera_query: Query<&Camera>,
|
||||
default_ui_camera: DefaultUiCamera,
|
||||
ui_scale: Res<UiScale>,
|
||||
mut text_query: Query<
|
||||
(
|
||||
Entity,
|
||||
@ -261,7 +254,7 @@ pub fn measure_text_system(
|
||||
&mut ContentSize,
|
||||
&mut TextNodeFlags,
|
||||
&mut ComputedTextBlock,
|
||||
Option<&UiTargetCamera>,
|
||||
Ref<ComputedNodeTarget>,
|
||||
),
|
||||
With<Node>,
|
||||
>,
|
||||
@ -269,32 +262,9 @@ pub fn measure_text_system(
|
||||
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(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]
|
||||
|
@ -183,16 +183,19 @@ fn set_camera_viewports(
|
||||
|
||||
fn button_system(
|
||||
interaction_query: Query<
|
||||
(&Interaction, &UiTargetCamera, &RotateCamera),
|
||||
(&Interaction, &ComputedNodeTarget, &RotateCamera),
|
||||
(Changed<Interaction>, With<Button>),
|
||||
>,
|
||||
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 {
|
||||
// Since TargetCamera propagates to the children, we can use it to find
|
||||
// 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 {
|
||||
Direction::Left => -0.1,
|
||||
Direction::Right => 0.1,
|
||||
|
Loading…
Reference in New Issue
Block a user