mirror of
https://github.com/bevyengine/bevy
synced 2024-11-25 14:10:19 +00:00
Add z-index support with a predictable UI stack (#5877)
# Objective Add consistent UI rendering and interaction where deep nodes inside two different hierarchies will never render on top of one-another by default and offer an escape hatch (z-index) for nodes to change their depth. ## The problem with current implementation The current implementation of UI rendering is broken in that regard, mainly because [it sets the Z value of the `Transform` component based on a "global Z" space](https://github.com/bevyengine/bevy/blob/main/crates/bevy_ui/src/update.rs#L43) shared by all nodes in the UI. This doesn't account for the fact that each node's final `GlobalTransform` value will be relative to its parent. This effectively makes the depth unpredictable when two deep trees are rendered on top of one-another. At the moment, it's also up to each part of the UI code to sort all of the UI nodes. The solution that's offered here does the full sorting of UI node entities once and offers the result through a resource so that all systems can use it. ## Solution ### New ZIndex component This adds a new optional `ZIndex` enum component for nodes which offers two mechanism: - `ZIndex::Local(i32)`: Overrides the depth of the node relative to its siblings. - `ZIndex::Global(i32)`: Overrides the depth of the node relative to the UI root. This basically allows any node in the tree to "escape" the parent and be ordered relative to the entire UI. Note that in the current implementation, omitting `ZIndex` on a node has the same result as adding `ZIndex::Local(0)`. Additionally, the "global" stacking context is essentially a way to add your node to the root stacking context, so using `ZIndex::Local(n)` on a root node (one without parent) will share that space with all nodes using `Index::Global(n)`. ### New UiStack resource This adds a new `UiStack` resource which is calculated from both hierarchy and `ZIndex` during UI update and contains a vector of all node entities in the UI, ordered by depth (from farthest from camera to closest). This is exposed publicly by the bevy_ui crate with the hope that it can be used for consistent ordering and to reduce the amount of sorting that needs to be done by UI systems (i.e. instead of sorting everything by `global_transform.z` in every system, this array can be iterated over). ### New z_index example This also adds a new z_index example that showcases the new `ZIndex` component. It's also a good general demo of the new UI stack system, because making this kind of UI was very broken with the old system (e.g. nodes would render on top of each other, not respecting hierarchy or insert order at all). ![image](https://user-images.githubusercontent.com/1060971/189015985-8ea8f989-0e9d-4601-a7e0-4a27a43a53f9.png) --- ## Changelog - Added the `ZIndex` component to bevy_ui. - Added the `UiStack` resource to bevy_ui, and added implementation in a new `stack.rs` module. - Removed the previous Z updating system from bevy_ui, because it was replaced with the above. - Changed bevy_ui rendering to use UiStack instead of z ordering. - Changed bevy_ui focus/interaction system to use UiStack instead of z ordering. - Added a new z_index example. ## ZIndex demo Here's a demo I wrote to test these features https://user-images.githubusercontent.com/1060971/188329295-d7beebd6-9aee-43ab-821e-d437df5dbe8a.mp4 Co-authored-by: Carter Anderson <mcanders1@gmail.com>
This commit is contained in:
parent
334e09892b
commit
4b5a33d970
10 changed files with 537 additions and 280 deletions
10
Cargo.toml
10
Cargo.toml
|
@ -1462,6 +1462,16 @@ description = "Demonstrates transparency for UI"
|
|||
category = "UI (User Interface)"
|
||||
wasm = true
|
||||
|
||||
[[example]]
|
||||
name = "z_index"
|
||||
path = "examples/ui/z_index.rs"
|
||||
|
||||
[package.metadata.example.z_index]
|
||||
name = "UI Z-Index"
|
||||
description = "Demonstrates how to control the relative depth (z-position) of UI elements"
|
||||
category = "UI (User Interface)"
|
||||
wasm = true
|
||||
|
||||
[[example]]
|
||||
name = "ui"
|
||||
path = "examples/ui/ui.rs"
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
use crate::{
|
||||
widget::{Button, ImageMode},
|
||||
BackgroundColor, CalculatedSize, FocusPolicy, Interaction, Node, Style, UiImage,
|
||||
BackgroundColor, CalculatedSize, FocusPolicy, Interaction, Node, Style, UiImage, ZIndex,
|
||||
};
|
||||
use bevy_ecs::{
|
||||
bundle::Bundle,
|
||||
|
@ -45,6 +45,8 @@ pub struct NodeBundle {
|
|||
pub visibility: Visibility,
|
||||
/// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering
|
||||
pub computed_visibility: ComputedVisibility,
|
||||
/// Indicates the depth at which the node should appear in the UI
|
||||
pub z_index: ZIndex,
|
||||
}
|
||||
|
||||
impl Default for NodeBundle {
|
||||
|
@ -60,6 +62,7 @@ impl Default for NodeBundle {
|
|||
global_transform: Default::default(),
|
||||
visibility: Default::default(),
|
||||
computed_visibility: Default::default(),
|
||||
z_index: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -97,6 +100,8 @@ pub struct ImageBundle {
|
|||
pub visibility: Visibility,
|
||||
/// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering
|
||||
pub computed_visibility: ComputedVisibility,
|
||||
/// Indicates the depth at which the node should appear in the UI
|
||||
pub z_index: ZIndex,
|
||||
}
|
||||
|
||||
/// A UI node that is text
|
||||
|
@ -126,6 +131,8 @@ pub struct TextBundle {
|
|||
pub visibility: Visibility,
|
||||
/// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering
|
||||
pub computed_visibility: ComputedVisibility,
|
||||
/// Indicates the depth at which the node should appear in the UI
|
||||
pub z_index: ZIndex,
|
||||
}
|
||||
|
||||
impl TextBundle {
|
||||
|
@ -174,6 +181,7 @@ impl Default for TextBundle {
|
|||
global_transform: Default::default(),
|
||||
visibility: Default::default(),
|
||||
computed_visibility: Default::default(),
|
||||
z_index: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -211,6 +219,8 @@ pub struct ButtonBundle {
|
|||
pub visibility: Visibility,
|
||||
/// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering
|
||||
pub computed_visibility: ComputedVisibility,
|
||||
/// Indicates the depth at which the node should appear in the UI
|
||||
pub z_index: ZIndex,
|
||||
}
|
||||
|
||||
impl Default for ButtonBundle {
|
||||
|
@ -227,6 +237,7 @@ impl Default for ButtonBundle {
|
|||
global_transform: Default::default(),
|
||||
visibility: Default::default(),
|
||||
computed_visibility: Default::default(),
|
||||
z_index: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
use crate::{entity::UiCameraConfig, CalculatedClip, Node};
|
||||
use crate::{entity::UiCameraConfig, CalculatedClip, Node, UiStack};
|
||||
use bevy_ecs::{
|
||||
entity::Entity,
|
||||
prelude::Component,
|
||||
query::WorldQuery,
|
||||
reflect::ReflectComponent,
|
||||
system::{Local, Query, Res},
|
||||
};
|
||||
|
@ -11,7 +12,6 @@ use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize};
|
|||
use bevy_render::camera::{Camera, RenderTarget};
|
||||
use bevy_render::view::ComputedVisibility;
|
||||
use bevy_transform::components::GlobalTransform;
|
||||
use bevy_utils::FloatOrd;
|
||||
use bevy_window::Windows;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use smallvec::SmallVec;
|
||||
|
@ -62,6 +62,19 @@ pub struct State {
|
|||
entities_to_reset: SmallVec<[Entity; 1]>,
|
||||
}
|
||||
|
||||
/// Main query for [`ui_focus_system`]
|
||||
#[derive(WorldQuery)]
|
||||
#[world_query(mutable)]
|
||||
pub struct NodeQuery {
|
||||
entity: Entity,
|
||||
node: &'static Node,
|
||||
global_transform: &'static GlobalTransform,
|
||||
interaction: Option<&'static mut Interaction>,
|
||||
focus_policy: Option<&'static FocusPolicy>,
|
||||
calculated_clip: Option<&'static CalculatedClip>,
|
||||
computed_visibility: Option<&'static ComputedVisibility>,
|
||||
}
|
||||
|
||||
/// The system that sets Interaction for all UI elements based on the mouse cursor activity
|
||||
///
|
||||
/// Entities with a hidden [`ComputedVisibility`] are always treated as released.
|
||||
|
@ -71,15 +84,8 @@ pub fn ui_focus_system(
|
|||
windows: Res<Windows>,
|
||||
mouse_button_input: Res<Input<MouseButton>>,
|
||||
touches_input: Res<Touches>,
|
||||
mut node_query: Query<(
|
||||
Entity,
|
||||
&Node,
|
||||
&GlobalTransform,
|
||||
Option<&mut Interaction>,
|
||||
Option<&FocusPolicy>,
|
||||
Option<&CalculatedClip>,
|
||||
Option<&ComputedVisibility>,
|
||||
)>,
|
||||
ui_stack: Res<UiStack>,
|
||||
mut node_query: Query<NodeQuery>,
|
||||
) {
|
||||
// reset entities that were both clicked and released in the last frame
|
||||
for entity in state.entities_to_reset.drain(..) {
|
||||
|
@ -91,10 +97,8 @@ pub fn ui_focus_system(
|
|||
let mouse_released =
|
||||
mouse_button_input.just_released(MouseButton::Left) || touches_input.any_just_released();
|
||||
if mouse_released {
|
||||
for (_entity, _node, _global_transform, interaction, _focus_policy, _clip, _visibility) in
|
||||
node_query.iter_mut()
|
||||
{
|
||||
if let Some(mut interaction) = interaction {
|
||||
for node in node_query.iter_mut() {
|
||||
if let Some(mut interaction) = node.interaction {
|
||||
if *interaction == Interaction::Clicked {
|
||||
*interaction = Interaction::None;
|
||||
}
|
||||
|
@ -123,15 +127,21 @@ pub fn ui_focus_system(
|
|||
.find_map(|window| window.cursor_position())
|
||||
.or_else(|| touches_input.first_pressed_position());
|
||||
|
||||
let mut moused_over_z_sorted_nodes = node_query
|
||||
.iter_mut()
|
||||
.filter_map(
|
||||
|(entity, node, global_transform, interaction, focus_policy, clip, visibility)| {
|
||||
// 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.
|
||||
let mut moused_over_nodes = ui_stack
|
||||
.uinodes
|
||||
.iter()
|
||||
// reverse the iterator to traverse the tree from closest nodes to furthest
|
||||
.rev()
|
||||
.filter_map(|entity| {
|
||||
if let Ok(node) = node_query.get_mut(*entity) {
|
||||
// Nodes that are not rendered should not be interactable
|
||||
if let Some(computed_visibility) = visibility {
|
||||
if let Some(computed_visibility) = node.computed_visibility {
|
||||
if !computed_visibility.is_visible() {
|
||||
// Reset their interaction to None to avoid strange stuck state
|
||||
if let Some(mut interaction) = interaction {
|
||||
if let Some(mut interaction) = node.interaction {
|
||||
// We cannot simply set the interaction to None, as that will trigger change detection repeatedly
|
||||
if *interaction != Interaction::None {
|
||||
*interaction = Interaction::None;
|
||||
|
@ -142,12 +152,12 @@ pub fn ui_focus_system(
|
|||
}
|
||||
}
|
||||
|
||||
let position = global_transform.translation();
|
||||
let position = node.global_transform.translation();
|
||||
let ui_position = position.truncate();
|
||||
let extents = node.calculated_size / 2.0;
|
||||
let extents = node.node.size() / 2.0;
|
||||
let mut min = ui_position - extents;
|
||||
let mut max = ui_position + extents;
|
||||
if let Some(clip) = clip {
|
||||
if let Some(clip) = node.calculated_clip {
|
||||
min = Vec2::max(min, clip.clip.min);
|
||||
max = Vec2::min(max, clip.clip.max);
|
||||
}
|
||||
|
@ -161,9 +171,9 @@ pub fn ui_focus_system(
|
|||
};
|
||||
|
||||
if contains_cursor {
|
||||
Some((entity, focus_policy, interaction, FloatOrd(position.z)))
|
||||
Some(*entity)
|
||||
} else {
|
||||
if let Some(mut interaction) = interaction {
|
||||
if let Some(mut interaction) = node.interaction {
|
||||
if *interaction == Interaction::Hovered
|
||||
|| (cursor_position.is_none() && *interaction != Interaction::None)
|
||||
{
|
||||
|
@ -172,16 +182,18 @@ pub fn ui_focus_system(
|
|||
}
|
||||
None
|
||||
}
|
||||
},
|
||||
)
|
||||
.collect::<Vec<_>>();
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<Entity>>()
|
||||
.into_iter();
|
||||
|
||||
moused_over_z_sorted_nodes.sort_by_key(|(_, _, _, z)| -*z);
|
||||
|
||||
let mut moused_over_z_sorted_nodes = moused_over_z_sorted_nodes.into_iter();
|
||||
// set Clicked or Hovered on top nodes
|
||||
for (entity, focus_policy, interaction, _) in moused_over_z_sorted_nodes.by_ref() {
|
||||
if let Some(mut interaction) = interaction {
|
||||
// set Clicked or Hovered on top nodes. as soon as a node with a `Block` focus policy is detected,
|
||||
// the iteration will stop on it because it "captures" the interaction.
|
||||
let mut iter = node_query.iter_many_mut(moused_over_nodes.by_ref());
|
||||
while let Some(node) = iter.fetch_next() {
|
||||
if let Some(mut interaction) = node.interaction {
|
||||
if mouse_clicked {
|
||||
// only consider nodes with Interaction "clickable"
|
||||
if *interaction != Interaction::Clicked {
|
||||
|
@ -189,7 +201,7 @@ pub fn ui_focus_system(
|
|||
// if the mouse was simultaneously released, reset this Interaction in the next
|
||||
// frame
|
||||
if mouse_released {
|
||||
state.entities_to_reset.push(entity);
|
||||
state.entities_to_reset.push(node.entity);
|
||||
}
|
||||
}
|
||||
} else if *interaction == Interaction::None {
|
||||
|
@ -197,16 +209,18 @@ pub fn ui_focus_system(
|
|||
}
|
||||
}
|
||||
|
||||
match focus_policy.cloned().unwrap_or(FocusPolicy::Block) {
|
||||
match node.focus_policy.unwrap_or(&FocusPolicy::Block) {
|
||||
FocusPolicy::Block => {
|
||||
break;
|
||||
}
|
||||
FocusPolicy::Pass => { /* allow the next node to be hovered/clicked */ }
|
||||
}
|
||||
}
|
||||
// reset lower nodes to None
|
||||
for (_entity, _focus_policy, interaction, _) in moused_over_z_sorted_nodes {
|
||||
if let Some(mut interaction) = interaction {
|
||||
// reset `Interaction` for the remaining lower nodes to `None`. those are the nodes that remain in
|
||||
// `moused_over_nodes` after the previous loop is exited.
|
||||
let mut iter = node_query.iter_many_mut(moused_over_nodes);
|
||||
while let Some(node) = iter.fetch_next() {
|
||||
if let Some(mut interaction) = node.interaction {
|
||||
// don't reset clicked nodes because they're handled separately
|
||||
if *interaction != Interaction::Clicked && *interaction != Interaction::None {
|
||||
*interaction = Interaction::None;
|
||||
|
|
|
@ -6,6 +6,7 @@ mod flex;
|
|||
mod focus;
|
||||
mod geometry;
|
||||
mod render;
|
||||
mod stack;
|
||||
mod ui_node;
|
||||
|
||||
pub mod entity;
|
||||
|
@ -33,7 +34,9 @@ use bevy_ecs::{
|
|||
use bevy_input::InputSystem;
|
||||
use bevy_transform::TransformSystem;
|
||||
use bevy_window::ModifiesWindows;
|
||||
use update::{ui_z_system, update_clipping_system};
|
||||
use stack::ui_stack_system;
|
||||
pub use stack::UiStack;
|
||||
use update::update_clipping_system;
|
||||
|
||||
use crate::prelude::UiCameraConfig;
|
||||
|
||||
|
@ -48,6 +51,8 @@ pub enum UiSystem {
|
|||
Flex,
|
||||
/// After this label, input interactions with UI entities have been updated for this frame
|
||||
Focus,
|
||||
/// After this label, the [`UiStack`] resource has been updated
|
||||
Stack,
|
||||
}
|
||||
|
||||
/// The current scale of the UI.
|
||||
|
@ -71,6 +76,7 @@ impl Plugin for UiPlugin {
|
|||
app.add_plugin(ExtractComponentPlugin::<UiCameraConfig>::default())
|
||||
.init_resource::<FlexSurface>()
|
||||
.init_resource::<UiScale>()
|
||||
.init_resource::<UiStack>()
|
||||
.register_type::<AlignContent>()
|
||||
.register_type::<AlignItems>()
|
||||
.register_type::<AlignSelf>()
|
||||
|
@ -135,9 +141,7 @@ impl Plugin for UiPlugin {
|
|||
)
|
||||
.add_system_to_stage(
|
||||
CoreStage::PostUpdate,
|
||||
ui_z_system
|
||||
.after(UiSystem::Flex)
|
||||
.before(TransformSystem::TransformPropagate),
|
||||
ui_stack_system.label(UiSystem::Stack),
|
||||
)
|
||||
.add_system_to_stage(
|
||||
CoreStage::PostUpdate,
|
||||
|
|
|
@ -5,7 +5,7 @@ use bevy_core_pipeline::{core_2d::Camera2d, core_3d::Camera3d};
|
|||
pub use pipeline::*;
|
||||
pub use render_pass::*;
|
||||
|
||||
use crate::{prelude::UiCameraConfig, BackgroundColor, CalculatedClip, Node, UiImage};
|
||||
use crate::{prelude::UiCameraConfig, BackgroundColor, CalculatedClip, Node, UiImage, UiStack};
|
||||
use bevy_app::prelude::*;
|
||||
use bevy_asset::{load_internal_asset, AssetEvent, Assets, Handle, HandleUntyped};
|
||||
use bevy_ecs::prelude::*;
|
||||
|
@ -184,6 +184,7 @@ fn get_ui_graph(render_app: &mut App) -> RenderGraph {
|
|||
}
|
||||
|
||||
pub struct ExtractedUiNode {
|
||||
pub stack_index: usize,
|
||||
pub transform: Mat4,
|
||||
pub background_color: Color,
|
||||
pub rect: Rect,
|
||||
|
@ -201,6 +202,7 @@ pub struct ExtractedUiNodes {
|
|||
pub fn extract_uinodes(
|
||||
mut extracted_uinodes: ResMut<ExtractedUiNodes>,
|
||||
images: Extract<Res<Assets<Image>>>,
|
||||
ui_stack: Extract<Res<UiStack>>,
|
||||
windows: Extract<Res<Windows>>,
|
||||
uinode_query: Extract<
|
||||
Query<(
|
||||
|
@ -215,7 +217,8 @@ pub fn extract_uinodes(
|
|||
) {
|
||||
let scale_factor = windows.scale_factor(WindowId::primary()) as f32;
|
||||
extracted_uinodes.uinodes.clear();
|
||||
for (uinode, transform, color, image, visibility, clip) in uinode_query.iter() {
|
||||
for (stack_index, entity) in ui_stack.uinodes.iter().enumerate() {
|
||||
if let Ok((uinode, transform, color, image, visibility, clip)) = uinode_query.get(*entity) {
|
||||
if !visibility.is_visible() {
|
||||
continue;
|
||||
}
|
||||
|
@ -229,6 +232,7 @@ pub fn extract_uinodes(
|
|||
continue;
|
||||
}
|
||||
extracted_uinodes.uinodes.push(ExtractedUiNode {
|
||||
stack_index,
|
||||
transform: transform.compute_matrix(),
|
||||
background_color: color.0,
|
||||
rect: Rect {
|
||||
|
@ -241,6 +245,7 @@ pub fn extract_uinodes(
|
|||
scale_factor,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The UI camera is "moved back" by this many units (plus the [`UI_CAMERA_TRANSFORM_OFFSET`]) and also has a view
|
||||
|
@ -303,6 +308,7 @@ pub fn extract_text_uinodes(
|
|||
mut extracted_uinodes: ResMut<ExtractedUiNodes>,
|
||||
texture_atlases: Extract<Res<Assets<TextureAtlas>>>,
|
||||
windows: Extract<Res<Windows>>,
|
||||
ui_stack: Extract<Res<UiStack>>,
|
||||
uinode_query: Extract<
|
||||
Query<(
|
||||
&Node,
|
||||
|
@ -315,17 +321,19 @@ pub fn extract_text_uinodes(
|
|||
>,
|
||||
) {
|
||||
let scale_factor = windows.scale_factor(WindowId::primary()) as f32;
|
||||
for (uinode, global_transform, text, text_layout_info, visibility, clip) in uinode_query.iter()
|
||||
for (stack_index, entity) in ui_stack.uinodes.iter().enumerate() {
|
||||
if let Ok((uinode, global_transform, text, text_layout_info, visibility, clip)) =
|
||||
uinode_query.get(*entity)
|
||||
{
|
||||
if !visibility.is_visible() {
|
||||
continue;
|
||||
}
|
||||
// Skip if size is set to zero (e.g. when a parent is set to `Display::None`)
|
||||
if uinode.calculated_size == Vec2::ZERO {
|
||||
if uinode.size() == Vec2::ZERO {
|
||||
continue;
|
||||
}
|
||||
let text_glyphs = &text_layout_info.glyphs;
|
||||
let alignment_offset = (uinode.calculated_size / -2.0).extend(0.0);
|
||||
let alignment_offset = (uinode.size() / -2.0).extend(0.0);
|
||||
|
||||
let mut color = Color::WHITE;
|
||||
let mut current_section = usize::MAX;
|
||||
|
@ -341,7 +349,7 @@ pub fn extract_text_uinodes(
|
|||
.get(&text_glyph.atlas_info.texture_atlas)
|
||||
.unwrap();
|
||||
let texture = atlas.texture.clone_weak();
|
||||
let index = text_glyph.atlas_info.glyph_index;
|
||||
let index = text_glyph.atlas_info.glyph_index as usize;
|
||||
let rect = atlas.textures[index];
|
||||
let atlas_size = Some(atlas.size);
|
||||
|
||||
|
@ -353,6 +361,7 @@ pub fn extract_text_uinodes(
|
|||
);
|
||||
|
||||
extracted_uinodes.uinodes.push(ExtractedUiNode {
|
||||
stack_index,
|
||||
transform: extracted_transform,
|
||||
background_color: color,
|
||||
rect,
|
||||
|
@ -363,6 +372,7 @@ pub fn extract_text_uinodes(
|
|||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
|
@ -413,10 +423,10 @@ pub fn prepare_uinodes(
|
|||
) {
|
||||
ui_meta.vertices.clear();
|
||||
|
||||
// sort by increasing z for correct transparency
|
||||
// sort by ui stack index, starting from the deepest node
|
||||
extracted_uinodes
|
||||
.uinodes
|
||||
.sort_by(|a, b| FloatOrd(a.transform.w_axis[2]).cmp(&FloatOrd(b.transform.w_axis[2])));
|
||||
.sort_by_key(|node| node.stack_index);
|
||||
|
||||
let mut start = 0;
|
||||
let mut end = 0;
|
||||
|
|
220
crates/bevy_ui/src/stack.rs
Normal file
220
crates/bevy_ui/src/stack.rs
Normal file
|
@ -0,0 +1,220 @@
|
|||
//! This module contains the systems that update the stored UI nodes stack
|
||||
|
||||
use bevy_ecs::prelude::*;
|
||||
use bevy_hierarchy::prelude::*;
|
||||
|
||||
use crate::{Node, ZIndex};
|
||||
|
||||
/// The current UI stack, which contains all UI nodes ordered by their depth.
|
||||
///
|
||||
/// The first entry is the furthest node from the camera and is the first one to get rendered
|
||||
/// while the last entry is the first node to receive interactions.
|
||||
#[derive(Debug, Resource, Default)]
|
||||
pub struct UiStack {
|
||||
pub uinodes: Vec<Entity>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct StackingContext {
|
||||
pub entries: Vec<StackingContextEntry>,
|
||||
}
|
||||
|
||||
struct StackingContextEntry {
|
||||
pub z_index: i32,
|
||||
pub entity: Entity,
|
||||
pub stack: StackingContext,
|
||||
}
|
||||
|
||||
/// Generates the render stack for UI nodes.
|
||||
pub fn ui_stack_system(
|
||||
mut ui_stack: ResMut<UiStack>,
|
||||
root_node_query: Query<Entity, (With<Node>, Without<Parent>)>,
|
||||
zindex_query: Query<&ZIndex, With<Node>>,
|
||||
children_query: Query<&Children>,
|
||||
) {
|
||||
let mut global_context = StackingContext::default();
|
||||
|
||||
let mut total_entry_count: usize = 0;
|
||||
for entity in &root_node_query {
|
||||
insert_context_hierarchy(
|
||||
&zindex_query,
|
||||
&children_query,
|
||||
entity,
|
||||
&mut global_context,
|
||||
None,
|
||||
&mut total_entry_count,
|
||||
);
|
||||
}
|
||||
|
||||
ui_stack.uinodes.clear();
|
||||
ui_stack.uinodes.reserve(total_entry_count);
|
||||
fill_stack_recursively(&mut ui_stack.uinodes, &mut global_context);
|
||||
}
|
||||
|
||||
fn insert_context_hierarchy(
|
||||
zindex_query: &Query<&ZIndex, With<Node>>,
|
||||
children_query: &Query<&Children>,
|
||||
entity: Entity,
|
||||
global_context: &mut StackingContext,
|
||||
parent_context: Option<&mut StackingContext>,
|
||||
total_entry_count: &mut usize,
|
||||
) {
|
||||
let mut new_context = StackingContext::default();
|
||||
if let Ok(children) = children_query.get(entity) {
|
||||
// reserve space for all children. in practice, some may not get pushed.
|
||||
new_context.entries.reserve_exact(children.len());
|
||||
|
||||
for entity in children {
|
||||
insert_context_hierarchy(
|
||||
zindex_query,
|
||||
children_query,
|
||||
*entity,
|
||||
global_context,
|
||||
Some(&mut new_context),
|
||||
total_entry_count,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let z_index = zindex_query.get(entity).unwrap_or(&ZIndex::Local(0));
|
||||
let (entity_context, z_index) = match z_index {
|
||||
ZIndex::Local(value) => (parent_context.unwrap_or(global_context), *value),
|
||||
ZIndex::Global(value) => (global_context, *value),
|
||||
};
|
||||
|
||||
*total_entry_count += 1;
|
||||
entity_context.entries.push(StackingContextEntry {
|
||||
z_index,
|
||||
entity,
|
||||
stack: new_context,
|
||||
});
|
||||
}
|
||||
|
||||
fn fill_stack_recursively(result: &mut Vec<Entity>, stack: &mut StackingContext) {
|
||||
// sort entries by ascending z_index, while ensuring that siblings
|
||||
// with the same local z_index will keep their ordering.
|
||||
stack.entries.sort_by_key(|e| e.z_index);
|
||||
|
||||
for entry in &mut stack.entries {
|
||||
result.push(entry.entity);
|
||||
fill_stack_recursively(result, &mut entry.stack);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use bevy_ecs::{
|
||||
component::Component,
|
||||
schedule::{Schedule, Stage, SystemStage},
|
||||
system::{CommandQueue, Commands},
|
||||
world::World,
|
||||
};
|
||||
use bevy_hierarchy::BuildChildren;
|
||||
|
||||
use crate::{Node, UiStack, ZIndex};
|
||||
|
||||
use super::ui_stack_system;
|
||||
|
||||
#[derive(Component, PartialEq, Debug, Clone)]
|
||||
struct Label(&'static str);
|
||||
|
||||
fn node_with_zindex(name: &'static str, z_index: ZIndex) -> (Label, Node, ZIndex) {
|
||||
(Label(name), Node::default(), z_index)
|
||||
}
|
||||
|
||||
fn node_without_zindex(name: &'static str) -> (Label, Node) {
|
||||
(Label(name), Node::default())
|
||||
}
|
||||
|
||||
/// Tests the UI Stack system.
|
||||
///
|
||||
/// This tests for siblings default ordering according to their insertion order, but it
|
||||
/// can't test the same thing for UI roots. UI roots having no parents, they do not have
|
||||
/// a stable ordering that we can test against. If we test it, it may pass now and start
|
||||
/// failing randomly in the future because of some unrelated `bevy_ecs` change.
|
||||
#[test]
|
||||
fn test_ui_stack_system() {
|
||||
let mut world = World::default();
|
||||
world.init_resource::<UiStack>();
|
||||
|
||||
let mut queue = CommandQueue::default();
|
||||
let mut commands = Commands::new(&mut queue, &world);
|
||||
commands.spawn(node_with_zindex("0", ZIndex::Global(2)));
|
||||
|
||||
commands
|
||||
.spawn(node_with_zindex("1", ZIndex::Local(1)))
|
||||
.with_children(|parent| {
|
||||
parent
|
||||
.spawn(node_without_zindex("1-0"))
|
||||
.with_children(|parent| {
|
||||
parent.spawn(node_without_zindex("1-0-0"));
|
||||
parent.spawn(node_without_zindex("1-0-1"));
|
||||
parent.spawn(node_with_zindex("1-0-2", ZIndex::Local(-1)));
|
||||
});
|
||||
parent.spawn(node_without_zindex("1-1"));
|
||||
parent
|
||||
.spawn(node_with_zindex("1-2", ZIndex::Global(-1)))
|
||||
.with_children(|parent| {
|
||||
parent.spawn(node_without_zindex("1-2-0"));
|
||||
parent.spawn(node_with_zindex("1-2-1", ZIndex::Global(-3)));
|
||||
parent
|
||||
.spawn(node_without_zindex("1-2-2"))
|
||||
.with_children(|_| ());
|
||||
parent.spawn(node_without_zindex("1-2-3"));
|
||||
});
|
||||
parent.spawn(node_without_zindex("1-3"));
|
||||
});
|
||||
|
||||
commands
|
||||
.spawn(node_without_zindex("2"))
|
||||
.with_children(|parent| {
|
||||
parent
|
||||
.spawn(node_without_zindex("2-0"))
|
||||
.with_children(|_parent| ());
|
||||
parent
|
||||
.spawn(node_without_zindex("2-1"))
|
||||
.with_children(|parent| {
|
||||
parent.spawn(node_without_zindex("2-1-0"));
|
||||
});
|
||||
});
|
||||
|
||||
commands.spawn(node_with_zindex("3", ZIndex::Global(-2)));
|
||||
|
||||
queue.apply(&mut world);
|
||||
|
||||
let mut schedule = Schedule::default();
|
||||
let mut update_stage = SystemStage::parallel();
|
||||
update_stage.add_system(ui_stack_system);
|
||||
schedule.add_stage("update", update_stage);
|
||||
schedule.run(&mut world);
|
||||
|
||||
let mut query = world.query::<&Label>();
|
||||
let ui_stack = world.resource::<UiStack>();
|
||||
let actual_result = ui_stack
|
||||
.uinodes
|
||||
.iter()
|
||||
.map(|entity| query.get(&world, *entity).unwrap().clone())
|
||||
.collect::<Vec<_>>();
|
||||
let expected_result = vec![
|
||||
(Label("1-2-1")), // ZIndex::Global(-3)
|
||||
(Label("3")), // ZIndex::Global(-2)
|
||||
(Label("1-2")), // ZIndex::Global(-1)
|
||||
(Label("1-2-0")),
|
||||
(Label("1-2-2")),
|
||||
(Label("1-2-3")),
|
||||
(Label("2")),
|
||||
(Label("2-0")),
|
||||
(Label("2-1")),
|
||||
(Label("2-1-0")),
|
||||
(Label("1")), // ZIndex::Local(1)
|
||||
(Label("1-0")),
|
||||
(Label("1-0-2")), // ZIndex::Local(-1)
|
||||
(Label("1-0-0")),
|
||||
(Label("1-0-1")),
|
||||
(Label("1-1")),
|
||||
(Label("1-3")),
|
||||
(Label("0")), // ZIndex::Global(2)
|
||||
];
|
||||
assert_eq!(actual_result, expected_result);
|
||||
}
|
||||
}
|
|
@ -479,6 +479,34 @@ pub struct CalculatedClip {
|
|||
pub clip: Rect,
|
||||
}
|
||||
|
||||
/// Indicates that this [`Node`] entity's front-to-back ordering is not controlled solely
|
||||
/// by its location in the UI hierarchy. A node with a higher z-index will appear on top
|
||||
/// of other nodes with a lower z-index.
|
||||
///
|
||||
/// UI nodes that have the same z-index will appear according to the order in which they
|
||||
/// appear in the UI hierarchy. In such a case, the last node to be added to its parent
|
||||
/// will appear in front of this parent's other children.
|
||||
///
|
||||
/// Internally, nodes with a global z-index share the stacking context of root UI nodes
|
||||
/// (nodes that have no parent). Because of this, there is no difference between using
|
||||
/// [`ZIndex::Local(n)`] and [`ZIndex::Global(n)`] for root nodes.
|
||||
///
|
||||
/// Nodes without this component will be treated as if they had a value of [`ZIndex::Local(0)`].
|
||||
#[derive(Component, Copy, Clone, Debug, Reflect)]
|
||||
pub enum ZIndex {
|
||||
/// Indicates the order in which this node should be rendered relative to its siblings.
|
||||
Local(i32),
|
||||
/// Indicates the order in which this node should be rendered relative to root nodes and
|
||||
/// all other nodes that have a global z-index.
|
||||
Global(i32),
|
||||
}
|
||||
|
||||
impl Default for ZIndex {
|
||||
fn default() -> Self {
|
||||
Self::Local(0)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::ValArithmeticError;
|
||||
|
|
|
@ -10,58 +10,7 @@ use bevy_ecs::{
|
|||
};
|
||||
use bevy_hierarchy::{Children, Parent};
|
||||
use bevy_math::Rect;
|
||||
use bevy_transform::components::{GlobalTransform, Transform};
|
||||
|
||||
/// The resolution of `Z` values for UI
|
||||
pub const UI_Z_STEP: f32 = 0.001;
|
||||
|
||||
/// Updates transforms of nodes to fit with the `Z` system
|
||||
pub fn ui_z_system(
|
||||
root_node_query: Query<Entity, (With<Node>, Without<Parent>)>,
|
||||
mut node_query: Query<&mut Transform, With<Node>>,
|
||||
children_query: Query<&Children>,
|
||||
) {
|
||||
let mut current_global_z = 0.0;
|
||||
for entity in &root_node_query {
|
||||
current_global_z = update_hierarchy(
|
||||
&children_query,
|
||||
&mut node_query,
|
||||
entity,
|
||||
current_global_z,
|
||||
current_global_z,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn update_hierarchy(
|
||||
children_query: &Query<&Children>,
|
||||
node_query: &mut Query<&mut Transform, With<Node>>,
|
||||
entity: Entity,
|
||||
parent_global_z: f32,
|
||||
mut current_global_z: f32,
|
||||
) -> f32 {
|
||||
current_global_z += UI_Z_STEP;
|
||||
if let Ok(mut transform) = node_query.get_mut(entity) {
|
||||
let new_z = current_global_z - parent_global_z;
|
||||
// only trigger change detection when the new value is different
|
||||
if transform.translation.z != new_z {
|
||||
transform.translation.z = new_z;
|
||||
}
|
||||
}
|
||||
if let Ok(children) = children_query.get(entity) {
|
||||
let current_parent_global_z = current_global_z;
|
||||
for child in children.iter().cloned() {
|
||||
current_global_z = update_hierarchy(
|
||||
children_query,
|
||||
node_query,
|
||||
child,
|
||||
current_parent_global_z,
|
||||
current_global_z,
|
||||
);
|
||||
}
|
||||
}
|
||||
current_global_z
|
||||
}
|
||||
use bevy_transform::components::GlobalTransform;
|
||||
|
||||
/// Updates clipping for all nodes
|
||||
pub fn update_clipping_system(
|
||||
|
@ -121,116 +70,3 @@ fn update_clipping(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use bevy_ecs::{
|
||||
component::Component,
|
||||
schedule::{Schedule, Stage, StageLabel, SystemStage},
|
||||
system::{CommandQueue, Commands},
|
||||
world::World,
|
||||
};
|
||||
use bevy_hierarchy::BuildChildren;
|
||||
use bevy_transform::components::Transform;
|
||||
|
||||
use crate::Node;
|
||||
|
||||
use super::{ui_z_system, UI_Z_STEP};
|
||||
|
||||
#[derive(Component, PartialEq, Debug, Clone)]
|
||||
struct Label(&'static str);
|
||||
|
||||
fn node_with_transform(name: &'static str) -> (Label, Node, Transform) {
|
||||
(Label(name), Node::default(), Transform::IDENTITY)
|
||||
}
|
||||
|
||||
fn node_without_transform(name: &'static str) -> (Label, Node) {
|
||||
(Label(name), Node::default())
|
||||
}
|
||||
|
||||
fn get_steps(transform: &Transform) -> u32 {
|
||||
(transform.translation.z / UI_Z_STEP).round() as u32
|
||||
}
|
||||
|
||||
#[derive(StageLabel)]
|
||||
struct Update;
|
||||
|
||||
#[test]
|
||||
fn test_ui_z_system() {
|
||||
let mut world = World::default();
|
||||
let mut queue = CommandQueue::default();
|
||||
let mut commands = Commands::new(&mut queue, &world);
|
||||
commands.spawn(node_with_transform("0"));
|
||||
|
||||
commands
|
||||
.spawn(node_with_transform("1"))
|
||||
.with_children(|parent| {
|
||||
parent
|
||||
.spawn(node_with_transform("1-0"))
|
||||
.with_children(|parent| {
|
||||
parent.spawn(node_with_transform("1-0-0"));
|
||||
parent.spawn(node_without_transform("1-0-1"));
|
||||
parent.spawn(node_with_transform("1-0-2"));
|
||||
});
|
||||
parent.spawn(node_with_transform("1-1"));
|
||||
parent
|
||||
.spawn(node_without_transform("1-2"))
|
||||
.with_children(|parent| {
|
||||
parent.spawn(node_with_transform("1-2-0"));
|
||||
parent.spawn(node_with_transform("1-2-1"));
|
||||
parent
|
||||
.spawn(node_with_transform("1-2-2"))
|
||||
.with_children(|_| ());
|
||||
parent.spawn(node_with_transform("1-2-3"));
|
||||
});
|
||||
parent.spawn(node_with_transform("1-3"));
|
||||
});
|
||||
|
||||
commands
|
||||
.spawn(node_without_transform("2"))
|
||||
.with_children(|parent| {
|
||||
parent
|
||||
.spawn(node_with_transform("2-0"))
|
||||
.with_children(|_parent| ());
|
||||
parent
|
||||
.spawn(node_with_transform("2-1"))
|
||||
.with_children(|parent| {
|
||||
parent.spawn(node_with_transform("2-1-0"));
|
||||
});
|
||||
});
|
||||
queue.apply(&mut world);
|
||||
|
||||
let mut schedule = Schedule::default();
|
||||
let mut update_stage = SystemStage::parallel();
|
||||
update_stage.add_system(ui_z_system);
|
||||
schedule.add_stage(Update, update_stage);
|
||||
schedule.run(&mut world);
|
||||
|
||||
let mut actual_result = world
|
||||
.query::<(&Label, &Transform)>()
|
||||
.iter(&world)
|
||||
.map(|(name, transform)| (name.clone(), get_steps(transform)))
|
||||
.collect::<Vec<(Label, u32)>>();
|
||||
actual_result.sort_unstable_by_key(|(name, _)| name.0);
|
||||
let expected_result = vec![
|
||||
(Label("0"), 1),
|
||||
(Label("1"), 1),
|
||||
(Label("1-0"), 1),
|
||||
(Label("1-0-0"), 1),
|
||||
// 1-0-1 has no transform
|
||||
(Label("1-0-2"), 3),
|
||||
(Label("1-1"), 5),
|
||||
// 1-2 has no transform
|
||||
(Label("1-2-0"), 1),
|
||||
(Label("1-2-1"), 2),
|
||||
(Label("1-2-2"), 3),
|
||||
(Label("1-2-3"), 4),
|
||||
(Label("1-3"), 11),
|
||||
// 2 has no transform
|
||||
(Label("2-0"), 1),
|
||||
(Label("2-1"), 2),
|
||||
(Label("2-1-0"), 1),
|
||||
];
|
||||
assert_eq!(actual_result, expected_result);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -316,6 +316,7 @@ Example | Description
|
|||
[Transparency UI](../examples/ui/transparency_ui.rs) | Demonstrates transparency for UI
|
||||
[UI](../examples/ui/ui.rs) | Illustrates various features of Bevy UI
|
||||
[UI Scaling](../examples/ui/ui_scaling.rs) | Illustrates how to scale the UI
|
||||
[UI Z-Index](../examples/ui/z_index.rs) | Demonstrates how to control the relative depth (z-position) of UI elements
|
||||
|
||||
## Window
|
||||
|
||||
|
|
123
examples/ui/z_index.rs
Normal file
123
examples/ui/z_index.rs
Normal file
|
@ -0,0 +1,123 @@
|
|||
//! Demonstrates how to use the z-index component on UI nodes to control their relative depth
|
||||
//!
|
||||
//! It uses colored boxes with different z-index values to demonstrate how it can affect the order of
|
||||
//! depth of nodes compared to their siblings, but also compared to the entire UI.
|
||||
|
||||
use bevy::prelude::*;
|
||||
|
||||
fn main() {
|
||||
App::new()
|
||||
.insert_resource(ClearColor(Color::BLACK))
|
||||
.add_plugins(DefaultPlugins)
|
||||
.add_startup_system(setup)
|
||||
.run();
|
||||
}
|
||||
|
||||
fn setup(mut commands: Commands) {
|
||||
commands.spawn(Camera2dBundle::default());
|
||||
|
||||
// spawn the container with default z-index.
|
||||
// the default z-index value is `ZIndex::Local(0)`.
|
||||
// because this is a root UI node, using local or global values will do the same thing.
|
||||
commands
|
||||
.spawn(NodeBundle {
|
||||
background_color: Color::GRAY.into(),
|
||||
style: Style {
|
||||
size: Size::new(Val::Px(180.0), Val::Px(100.0)),
|
||||
margin: UiRect::all(Val::Auto),
|
||||
..default()
|
||||
},
|
||||
..default()
|
||||
})
|
||||
.with_children(|parent| {
|
||||
// spawn a node with default z-index.
|
||||
parent.spawn(NodeBundle {
|
||||
background_color: Color::RED.into(),
|
||||
style: Style {
|
||||
position_type: PositionType::Absolute,
|
||||
position: UiRect {
|
||||
left: Val::Px(10.0),
|
||||
bottom: Val::Px(40.0),
|
||||
..default()
|
||||
},
|
||||
size: Size::new(Val::Px(100.0), Val::Px(50.0)),
|
||||
..default()
|
||||
},
|
||||
..default()
|
||||
});
|
||||
|
||||
// spawn a node with a positive local z-index of 2.
|
||||
// it will show above other nodes in the grey container.
|
||||
parent.spawn(NodeBundle {
|
||||
z_index: ZIndex::Local(2),
|
||||
background_color: Color::BLUE.into(),
|
||||
style: Style {
|
||||
position_type: PositionType::Absolute,
|
||||
position: UiRect {
|
||||
left: Val::Px(45.0),
|
||||
bottom: Val::Px(30.0),
|
||||
..default()
|
||||
},
|
||||
size: Size::new(Val::Px(100.0), Val::Px(50.0)),
|
||||
..default()
|
||||
},
|
||||
..default()
|
||||
});
|
||||
|
||||
// spawn a node with a negative local z-index.
|
||||
// it will show under other nodes in the grey container.
|
||||
parent.spawn(NodeBundle {
|
||||
z_index: ZIndex::Local(-1),
|
||||
background_color: Color::GREEN.into(),
|
||||
style: Style {
|
||||
position_type: PositionType::Absolute,
|
||||
position: UiRect {
|
||||
left: Val::Px(70.0),
|
||||
bottom: Val::Px(20.0),
|
||||
..default()
|
||||
},
|
||||
size: Size::new(Val::Px(100.0), Val::Px(75.0)),
|
||||
..default()
|
||||
},
|
||||
..default()
|
||||
});
|
||||
|
||||
// spawn a node with a positive global z-index of 1.
|
||||
// it will show above all other nodes, because it's the highest global z-index in this example.
|
||||
// by default, boxes all share the global z-index of 0 that the grey container is added to.
|
||||
parent.spawn(NodeBundle {
|
||||
z_index: ZIndex::Global(1),
|
||||
background_color: Color::PURPLE.into(),
|
||||
style: Style {
|
||||
position_type: PositionType::Absolute,
|
||||
position: UiRect {
|
||||
left: Val::Px(15.0),
|
||||
bottom: Val::Px(10.0),
|
||||
..default()
|
||||
},
|
||||
size: Size::new(Val::Px(100.0), Val::Px(60.0)),
|
||||
..default()
|
||||
},
|
||||
..default()
|
||||
});
|
||||
|
||||
// spawn a node with a negative global z-index of -1.
|
||||
// this will show under all other nodes including its parent, because it's the lowest global z-index
|
||||
// in this example.
|
||||
parent.spawn(NodeBundle {
|
||||
z_index: ZIndex::Global(-1),
|
||||
background_color: Color::YELLOW.into(),
|
||||
style: Style {
|
||||
position_type: PositionType::Absolute,
|
||||
position: UiRect {
|
||||
left: Val::Px(-15.0),
|
||||
bottom: Val::Px(-15.0),
|
||||
..default()
|
||||
},
|
||||
size: Size::new(Val::Px(100.0), Val::Px(125.0)),
|
||||
..default()
|
||||
},
|
||||
..default()
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Reference in a new issue