Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Merged by Bors] - Add z-index support with a predictable UI stack #5877

Closed
wants to merge 30 commits into from
Closed
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
bf32dbe
Add ZIndex component and consistent UI stack
oceantume Sep 4, 2022
f5ac1e4
Merge branch 'main' into feature/zindex-and-uistack
oceantume Sep 4, 2022
1bd3fe1
Fix small issues after merging
oceantume Sep 4, 2022
04e5d7a
Run fmt and clippy
oceantume Sep 4, 2022
321de09
Fix clippy errors
oceantume Sep 4, 2022
12cc199
Fix example error
oceantume Sep 4, 2022
0c1bbab
Simplify the z_index example
oceantume Sep 7, 2022
2d2d335
Replace one color in z-index example
oceantume Sep 7, 2022
97205c7
Add ZIndex to UI bundles and change example to reflect that
oceantume Sep 8, 2022
c4a3f48
Add unit test for the UI Stack system
oceantume Sep 8, 2022
37a8c40
Merge branch 'main' into feature/zindex-and-uistack
oceantume Sep 8, 2022
34fd3a2
Fix clippy errors
oceantume Sep 8, 2022
e0690a6
Improvements based on review comments
oceantume Sep 9, 2022
fff9b33
Use iter_many in focus system to improve perfs and readability
oceantume Sep 9, 2022
4e6a193
Use iter_many in UI render extraction
oceantume Sep 9, 2022
bc7c725
Revert "Use iter_many in UI render extraction"
oceantume Sep 9, 2022
d11dbb5
Fix hover state by restoring the mutable iterator logic
oceantume Sep 9, 2022
29bf401
Fix small issues from comments
oceantume Sep 9, 2022
e1d58fe
Fix clippy error 💦💦💦
oceantume Sep 9, 2022
775ebc6
Add comments to clarify the interaction & focus system
oceantume Sep 20, 2022
2b58677
Update examples/ui/z_index.rs
oceantume Sep 20, 2022
791b677
Update examples/README.md
oceantume Sep 20, 2022
6a70d96
Update crates/bevy_ui/src/ui_node.rs
oceantume Sep 20, 2022
a4d8e7b
Update examples readme
oceantume Sep 20, 2022
34f9790
Properly update example description
oceantume Sep 20, 2022
c0ff6f5
Remove unused component from z_index example
oceantume Sep 21, 2022
bd18d78
Merge branch 'main' into feature/zindex-and-uistack
oceantume Oct 31, 2022
36dfd25
Fix format
oceantume Oct 31, 2022
32766b3
Fix example and reuse uinodes vector
oceantume Oct 31, 2022
2f793cc
Fix merge errors
cart Nov 2, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -1442,6 +1442,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"
Expand Down
12 changes: 11 additions & 1 deletion crates/bevy_ui/src/entity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

use crate::{
widget::{Button, ImageMode},
CalculatedSize, FocusPolicy, Interaction, Node, Style, UiColor, UiImage,
CalculatedSize, FocusPolicy, Interaction, Node, Style, UiColor, UiImage, ZIndex,
};
use bevy_ecs::{
bundle::Bundle,
Expand Down Expand Up @@ -37,6 +37,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,
}

/// A UI node that is an image
Expand Down Expand Up @@ -64,6 +66,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
Expand All @@ -87,6 +91,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 {
Expand Down Expand Up @@ -135,6 +141,7 @@ impl Default for TextBundle {
global_transform: Default::default(),
visibility: Default::default(),
computed_visibility: Default::default(),
z_index: Default::default(),
}
}
}
Expand Down Expand Up @@ -164,6 +171,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 {
Expand All @@ -180,6 +189,7 @@ impl Default for ButtonBundle {
global_transform: Default::default(),
visibility: Default::default(),
computed_visibility: Default::default(),
z_index: Default::default(),
}
}
}
Expand Down
94 changes: 54 additions & 40 deletions crates/bevy_ui/src/focus.rs
Original file line number Diff line number Diff line change
@@ -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},
};
Expand All @@ -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;
Expand Down Expand Up @@ -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.
Expand All @@ -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(..) {
Expand All @@ -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;
}
Expand Down Expand Up @@ -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()
oceantume marked this conversation as resolved.
Show resolved Hide resolved
.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;
Expand All @@ -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.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);
}
Expand All @@ -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)
{
Expand All @@ -172,41 +182,45 @@ pub fn ui_focus_system(
}
None
}
},
)
.collect::<Vec<_>>();

moused_over_z_sorted_nodes.sort_by_key(|(_, _, _, z)| -*z);
} else {
None
}
})
.collect::<Vec<Entity>>()
.into_iter();

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 {
*interaction = Interaction::Clicked;
// 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 {
*interaction = Interaction::Hovered;
}
}

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);
oceantume marked this conversation as resolved.
Show resolved Hide resolved
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;
Expand Down
12 changes: 8 additions & 4 deletions crates/bevy_ui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ mod flex;
mod focus;
mod geometry;
mod render;
mod stack;
mod ui_node;

pub mod entity;
Expand Down Expand Up @@ -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;

Expand All @@ -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.
Expand All @@ -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>()
Expand Down Expand Up @@ -119,9 +125,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,
Expand Down
Loading