Skip to content

Commit c1ef7d0

Browse files
oceantumecart
authored andcommitted
Add z-index support with a predictable UI stack (bevyengine#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 <[email protected]>
1 parent 909d7a7 commit c1ef7d0

File tree

10 files changed

+539
-282
lines changed

10 files changed

+539
-282
lines changed

Cargo.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1462,6 +1462,16 @@ description = "Demonstrates transparency for UI"
14621462
category = "UI (User Interface)"
14631463
wasm = true
14641464

1465+
[[example]]
1466+
name = "z_index"
1467+
path = "examples/ui/z_index.rs"
1468+
1469+
[package.metadata.example.z_index]
1470+
name = "UI Z-Index"
1471+
description = "Demonstrates how to control the relative depth (z-position) of UI elements"
1472+
category = "UI (User Interface)"
1473+
wasm = true
1474+
14651475
[[example]]
14661476
name = "ui"
14671477
path = "examples/ui/ui.rs"

crates/bevy_ui/src/entity.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
33
use crate::{
44
widget::{Button, ImageMode},
5-
BackgroundColor, CalculatedSize, FocusPolicy, Interaction, Node, Style, UiImage,
5+
BackgroundColor, CalculatedSize, FocusPolicy, Interaction, Node, Style, UiImage, ZIndex,
66
};
77
use bevy_ecs::{
88
bundle::Bundle,
@@ -45,6 +45,8 @@ pub struct NodeBundle {
4545
pub visibility: Visibility,
4646
/// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering
4747
pub computed_visibility: ComputedVisibility,
48+
/// Indicates the depth at which the node should appear in the UI
49+
pub z_index: ZIndex,
4850
}
4951

5052
impl Default for NodeBundle {
@@ -60,6 +62,7 @@ impl Default for NodeBundle {
6062
global_transform: Default::default(),
6163
visibility: Default::default(),
6264
computed_visibility: Default::default(),
65+
z_index: Default::default(),
6366
}
6467
}
6568
}
@@ -97,6 +100,8 @@ pub struct ImageBundle {
97100
pub visibility: Visibility,
98101
/// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering
99102
pub computed_visibility: ComputedVisibility,
103+
/// Indicates the depth at which the node should appear in the UI
104+
pub z_index: ZIndex,
100105
}
101106

102107
/// A UI node that is text
@@ -126,6 +131,8 @@ pub struct TextBundle {
126131
pub visibility: Visibility,
127132
/// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering
128133
pub computed_visibility: ComputedVisibility,
134+
/// Indicates the depth at which the node should appear in the UI
135+
pub z_index: ZIndex,
129136
}
130137

131138
impl TextBundle {
@@ -174,6 +181,7 @@ impl Default for TextBundle {
174181
global_transform: Default::default(),
175182
visibility: Default::default(),
176183
computed_visibility: Default::default(),
184+
z_index: Default::default(),
177185
}
178186
}
179187
}
@@ -211,6 +219,8 @@ pub struct ButtonBundle {
211219
pub visibility: Visibility,
212220
/// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering
213221
pub computed_visibility: ComputedVisibility,
222+
/// Indicates the depth at which the node should appear in the UI
223+
pub z_index: ZIndex,
214224
}
215225

216226
impl Default for ButtonBundle {
@@ -227,6 +237,7 @@ impl Default for ButtonBundle {
227237
global_transform: Default::default(),
228238
visibility: Default::default(),
229239
computed_visibility: Default::default(),
240+
z_index: Default::default(),
230241
}
231242
}
232243
}

crates/bevy_ui/src/focus.rs

Lines changed: 54 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
use crate::{entity::UiCameraConfig, CalculatedClip, Node};
1+
use crate::{entity::UiCameraConfig, CalculatedClip, Node, UiStack};
22
use bevy_ecs::{
33
entity::Entity,
44
prelude::Component,
5+
query::WorldQuery,
56
reflect::ReflectComponent,
67
system::{Local, Query, Res},
78
};
@@ -11,7 +12,6 @@ use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize};
1112
use bevy_render::camera::{Camera, RenderTarget};
1213
use bevy_render::view::ComputedVisibility;
1314
use bevy_transform::components::GlobalTransform;
14-
use bevy_utils::FloatOrd;
1515
use bevy_window::Windows;
1616
use serde::{Deserialize, Serialize};
1717
use smallvec::SmallVec;
@@ -62,6 +62,19 @@ pub struct State {
6262
entities_to_reset: SmallVec<[Entity; 1]>,
6363
}
6464

65+
/// Main query for [`ui_focus_system`]
66+
#[derive(WorldQuery)]
67+
#[world_query(mutable)]
68+
pub struct NodeQuery {
69+
entity: Entity,
70+
node: &'static Node,
71+
global_transform: &'static GlobalTransform,
72+
interaction: Option<&'static mut Interaction>,
73+
focus_policy: Option<&'static FocusPolicy>,
74+
calculated_clip: Option<&'static CalculatedClip>,
75+
computed_visibility: Option<&'static ComputedVisibility>,
76+
}
77+
6578
/// The system that sets Interaction for all UI elements based on the mouse cursor activity
6679
///
6780
/// Entities with a hidden [`ComputedVisibility`] are always treated as released.
@@ -71,15 +84,8 @@ pub fn ui_focus_system(
7184
windows: Res<Windows>,
7285
mouse_button_input: Res<Input<MouseButton>>,
7386
touches_input: Res<Touches>,
74-
mut node_query: Query<(
75-
Entity,
76-
&Node,
77-
&GlobalTransform,
78-
Option<&mut Interaction>,
79-
Option<&FocusPolicy>,
80-
Option<&CalculatedClip>,
81-
Option<&ComputedVisibility>,
82-
)>,
87+
ui_stack: Res<UiStack>,
88+
mut node_query: Query<NodeQuery>,
8389
) {
8490
// reset entities that were both clicked and released in the last frame
8591
for entity in state.entities_to_reset.drain(..) {
@@ -91,10 +97,8 @@ pub fn ui_focus_system(
9197
let mouse_released =
9298
mouse_button_input.just_released(MouseButton::Left) || touches_input.any_just_released();
9399
if mouse_released {
94-
for (_entity, _node, _global_transform, interaction, _focus_policy, _clip, _visibility) in
95-
node_query.iter_mut()
96-
{
97-
if let Some(mut interaction) = interaction {
100+
for node in node_query.iter_mut() {
101+
if let Some(mut interaction) = node.interaction {
98102
if *interaction == Interaction::Clicked {
99103
*interaction = Interaction::None;
100104
}
@@ -123,15 +127,21 @@ pub fn ui_focus_system(
123127
.find_map(|window| window.cursor_position())
124128
.or_else(|| touches_input.first_pressed_position());
125129

126-
let mut moused_over_z_sorted_nodes = node_query
127-
.iter_mut()
128-
.filter_map(
129-
|(entity, node, global_transform, interaction, focus_policy, clip, visibility)| {
130+
// prepare an iterator that contains all the nodes that have the cursor in their rect,
131+
// from the top node to the bottom one. this will also reset the interaction to `None`
132+
// for all nodes encountered that are no longer hovered.
133+
let mut moused_over_nodes = ui_stack
134+
.uinodes
135+
.iter()
136+
// reverse the iterator to traverse the tree from closest nodes to furthest
137+
.rev()
138+
.filter_map(|entity| {
139+
if let Ok(node) = node_query.get_mut(*entity) {
130140
// Nodes that are not rendered should not be interactable
131-
if let Some(computed_visibility) = visibility {
141+
if let Some(computed_visibility) = node.computed_visibility {
132142
if !computed_visibility.is_visible() {
133143
// Reset their interaction to None to avoid strange stuck state
134-
if let Some(mut interaction) = interaction {
144+
if let Some(mut interaction) = node.interaction {
135145
// We cannot simply set the interaction to None, as that will trigger change detection repeatedly
136146
if *interaction != Interaction::None {
137147
*interaction = Interaction::None;
@@ -142,12 +152,12 @@ pub fn ui_focus_system(
142152
}
143153
}
144154

145-
let position = global_transform.translation();
155+
let position = node.global_transform.translation();
146156
let ui_position = position.truncate();
147-
let extents = node.calculated_size / 2.0;
157+
let extents = node.node.size() / 2.0;
148158
let mut min = ui_position - extents;
149159
let mut max = ui_position + extents;
150-
if let Some(clip) = clip {
160+
if let Some(clip) = node.calculated_clip {
151161
min = Vec2::max(min, clip.clip.min);
152162
max = Vec2::min(max, clip.clip.max);
153163
}
@@ -161,9 +171,9 @@ pub fn ui_focus_system(
161171
};
162172

163173
if contains_cursor {
164-
Some((entity, focus_policy, interaction, FloatOrd(position.z)))
174+
Some(*entity)
165175
} else {
166-
if let Some(mut interaction) = interaction {
176+
if let Some(mut interaction) = node.interaction {
167177
if *interaction == Interaction::Hovered
168178
|| (cursor_position.is_none() && *interaction != Interaction::None)
169179
{
@@ -172,41 +182,45 @@ pub fn ui_focus_system(
172182
}
173183
None
174184
}
175-
},
176-
)
177-
.collect::<Vec<_>>();
178-
179-
moused_over_z_sorted_nodes.sort_by_key(|(_, _, _, z)| -*z);
185+
} else {
186+
None
187+
}
188+
})
189+
.collect::<Vec<Entity>>()
190+
.into_iter();
180191

181-
let mut moused_over_z_sorted_nodes = moused_over_z_sorted_nodes.into_iter();
182-
// set Clicked or Hovered on top nodes
183-
for (entity, focus_policy, interaction, _) in moused_over_z_sorted_nodes.by_ref() {
184-
if let Some(mut interaction) = interaction {
192+
// set Clicked or Hovered on top nodes. as soon as a node with a `Block` focus policy is detected,
193+
// the iteration will stop on it because it "captures" the interaction.
194+
let mut iter = node_query.iter_many_mut(moused_over_nodes.by_ref());
195+
while let Some(node) = iter.fetch_next() {
196+
if let Some(mut interaction) = node.interaction {
185197
if mouse_clicked {
186198
// only consider nodes with Interaction "clickable"
187199
if *interaction != Interaction::Clicked {
188200
*interaction = Interaction::Clicked;
189201
// if the mouse was simultaneously released, reset this Interaction in the next
190202
// frame
191203
if mouse_released {
192-
state.entities_to_reset.push(entity);
204+
state.entities_to_reset.push(node.entity);
193205
}
194206
}
195207
} else if *interaction == Interaction::None {
196208
*interaction = Interaction::Hovered;
197209
}
198210
}
199211

200-
match focus_policy.cloned().unwrap_or(FocusPolicy::Block) {
212+
match node.focus_policy.unwrap_or(&FocusPolicy::Block) {
201213
FocusPolicy::Block => {
202214
break;
203215
}
204216
FocusPolicy::Pass => { /* allow the next node to be hovered/clicked */ }
205217
}
206218
}
207-
// reset lower nodes to None
208-
for (_entity, _focus_policy, interaction, _) in moused_over_z_sorted_nodes {
209-
if let Some(mut interaction) = interaction {
219+
// reset `Interaction` for the remaining lower nodes to `None`. those are the nodes that remain in
220+
// `moused_over_nodes` after the previous loop is exited.
221+
let mut iter = node_query.iter_many_mut(moused_over_nodes);
222+
while let Some(node) = iter.fetch_next() {
223+
if let Some(mut interaction) = node.interaction {
210224
// don't reset clicked nodes because they're handled separately
211225
if *interaction != Interaction::Clicked && *interaction != Interaction::None {
212226
*interaction = Interaction::None;

crates/bevy_ui/src/lib.rs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ mod flex;
66
mod focus;
77
mod geometry;
88
mod render;
9+
mod stack;
910
mod ui_node;
1011

1112
pub mod entity;
@@ -33,7 +34,9 @@ use bevy_ecs::{
3334
use bevy_input::InputSystem;
3435
use bevy_transform::TransformSystem;
3536
use bevy_window::ModifiesWindows;
36-
use update::{ui_z_system, update_clipping_system};
37+
use stack::ui_stack_system;
38+
pub use stack::UiStack;
39+
use update::update_clipping_system;
3740

3841
use crate::prelude::UiCameraConfig;
3942

@@ -48,6 +51,8 @@ pub enum UiSystem {
4851
Flex,
4952
/// After this label, input interactions with UI entities have been updated for this frame
5053
Focus,
54+
/// After this label, the [`UiStack`] resource has been updated
55+
Stack,
5156
}
5257

5358
/// The current scale of the UI.
@@ -71,6 +76,7 @@ impl Plugin for UiPlugin {
7176
app.add_plugin(ExtractComponentPlugin::<UiCameraConfig>::default())
7277
.init_resource::<FlexSurface>()
7378
.init_resource::<UiScale>()
79+
.init_resource::<UiStack>()
7480
.register_type::<AlignContent>()
7581
.register_type::<AlignItems>()
7682
.register_type::<AlignSelf>()
@@ -135,9 +141,7 @@ impl Plugin for UiPlugin {
135141
)
136142
.add_system_to_stage(
137143
CoreStage::PostUpdate,
138-
ui_z_system
139-
.after(UiSystem::Flex)
140-
.before(TransformSystem::TransformPropagate),
144+
ui_stack_system.label(UiSystem::Stack),
141145
)
142146
.add_system_to_stage(
143147
CoreStage::PostUpdate,

0 commit comments

Comments
 (0)