diff --git a/Cargo.toml b/Cargo.toml index 7f2d1fd25317b..3226a40152643 100644 --- a/Cargo.toml +++ b/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" diff --git a/crates/bevy_ui/src/entity.rs b/crates/bevy_ui/src/entity.rs index abb73fae70ec2..1e3491ad14d27 100644 --- a/crates/bevy_ui/src/entity.rs +++ b/crates/bevy_ui/src/entity.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(), } } } diff --git a/crates/bevy_ui/src/focus.rs b/crates/bevy_ui/src/focus.rs index 44ea324a12e90..423711d6e39ff 100644 --- a/crates/bevy_ui/src/focus.rs +++ b/crates/bevy_ui/src/focus.rs @@ -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, mouse_button_input: Res>, touches_input: Res, - mut node_query: Query<( - Entity, - &Node, - &GlobalTransform, - Option<&mut Interaction>, - Option<&FocusPolicy>, - Option<&CalculatedClip>, - Option<&ComputedVisibility>, - )>, + ui_stack: Res, + mut node_query: Query, ) { // 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::>(); - - moused_over_z_sorted_nodes.sort_by_key(|(_, _, _, z)| -*z); + } else { + None + } + }) + .collect::>() + .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 { @@ -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; diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index e1c73540c3ac0..39fae6f1761bf 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -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::::default()) .init_resource::() .init_resource::() + .init_resource::() .register_type::() .register_type::() .register_type::() @@ -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, diff --git a/crates/bevy_ui/src/render/mod.rs b/crates/bevy_ui/src/render/mod.rs index 9ef419e6bc82b..51418b6b448c4 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -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, images: Extract>>, + ui_stack: Extract>, windows: Extract>, uinode_query: Extract< Query<( @@ -215,31 +217,34 @@ 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() { - if !visibility.is_visible() { - continue; - } - let image = image.0.clone_weak(); - // Skip loading images - if !images.contains(&image) { - continue; - } - // Skip completely transparent nodes - if color.0.a() == 0.0 { - continue; + 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; + } + let image = image.0.clone_weak(); + // Skip loading images + if !images.contains(&image) { + continue; + } + // Skip completely transparent nodes + if color.0.a() == 0.0 { + continue; + } + extracted_uinodes.uinodes.push(ExtractedUiNode { + stack_index, + transform: transform.compute_matrix(), + background_color: color.0, + rect: Rect { + min: Vec2::ZERO, + max: uinode.calculated_size, + }, + image, + atlas_size: None, + clip: clip.map(|clip| clip.clip), + scale_factor, + }); } - extracted_uinodes.uinodes.push(ExtractedUiNode { - transform: transform.compute_matrix(), - background_color: color.0, - rect: Rect { - min: Vec2::ZERO, - max: uinode.calculated_size, - }, - image, - atlas_size: None, - clip: clip.map(|clip| clip.clip), - scale_factor, - }); } } @@ -303,6 +308,7 @@ pub fn extract_text_uinodes( mut extracted_uinodes: ResMut, texture_atlases: Extract>>, windows: Extract>, + ui_stack: Extract>, uinode_query: Extract< Query<( &Node, @@ -315,52 +321,56 @@ 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() - { - 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 { - continue; - } - let text_glyphs = &text_layout_info.glyphs; - let alignment_offset = (uinode.calculated_size / -2.0).extend(0.0); - - let mut color = Color::WHITE; - let mut current_section = usize::MAX; - for text_glyph in text_glyphs { - if text_glyph.section_index != current_section { - color = text.sections[text_glyph.section_index] - .style - .color - .as_rgba_linear(); - current_section = text_glyph.section_index; + 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.size() == Vec2::ZERO { + continue; + } + let text_glyphs = &text_layout_info.glyphs; + let alignment_offset = (uinode.size() / -2.0).extend(0.0); + + let mut color = Color::WHITE; + let mut current_section = usize::MAX; + for text_glyph in text_glyphs { + if text_glyph.section_index != current_section { + color = text.sections[text_glyph.section_index] + .style + .color + .as_rgba_linear(); + current_section = text_glyph.section_index; + } + let atlas = texture_atlases + .get(&text_glyph.atlas_info.texture_atlas) + .unwrap(); + let texture = atlas.texture.clone_weak(); + let index = text_glyph.atlas_info.glyph_index as usize; + let rect = atlas.textures[index]; + let atlas_size = Some(atlas.size); + + // NOTE: Should match `bevy_text::text2d::extract_text2d_sprite` + let extracted_transform = global_transform.compute_matrix() + * Mat4::from_scale(Vec3::splat(scale_factor.recip())) + * Mat4::from_translation( + alignment_offset * scale_factor + text_glyph.position.extend(0.), + ); + + extracted_uinodes.uinodes.push(ExtractedUiNode { + stack_index, + transform: extracted_transform, + background_color: color, + rect, + image: texture, + atlas_size, + clip: clip.map(|clip| clip.clip), + scale_factor, + }); } - let atlas = texture_atlases - .get(&text_glyph.atlas_info.texture_atlas) - .unwrap(); - let texture = atlas.texture.clone_weak(); - let index = text_glyph.atlas_info.glyph_index; - let rect = atlas.textures[index]; - let atlas_size = Some(atlas.size); - - // NOTE: Should match `bevy_text::text2d::extract_text2d_sprite` - let extracted_transform = global_transform.compute_matrix() - * Mat4::from_scale(Vec3::splat(scale_factor.recip())) - * Mat4::from_translation( - alignment_offset * scale_factor + text_glyph.position.extend(0.), - ); - - extracted_uinodes.uinodes.push(ExtractedUiNode { - transform: extracted_transform, - background_color: color, - rect, - image: texture, - atlas_size, - clip: clip.map(|clip| clip.clip), - scale_factor, - }); } } } @@ -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; diff --git a/crates/bevy_ui/src/stack.rs b/crates/bevy_ui/src/stack.rs new file mode 100644 index 0000000000000..4df23559b8d6a --- /dev/null +++ b/crates/bevy_ui/src/stack.rs @@ -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, +} + +#[derive(Default)] +struct StackingContext { + pub entries: Vec, +} + +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, + root_node_query: Query, Without)>, + zindex_query: Query<&ZIndex, With>, + 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>, + 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, 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::(); + + 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::(); + let actual_result = ui_stack + .uinodes + .iter() + .map(|entity| query.get(&world, *entity).unwrap().clone()) + .collect::>(); + 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); + } +} diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index 501a1604160ed..af7aec01b4e92 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -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; diff --git a/crates/bevy_ui/src/update.rs b/crates/bevy_ui/src/update.rs index aac455bb16335..623add4a6a283 100644 --- a/crates/bevy_ui/src/update.rs +++ b/crates/bevy_ui/src/update.rs @@ -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, Without)>, - mut node_query: Query<&mut Transform, With>, - 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>, - 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::>(); - 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); - } -} diff --git a/examples/README.md b/examples/README.md index c2d00514e0c40..026197ead0d3e 100644 --- a/examples/README.md +++ b/examples/README.md @@ -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 diff --git a/examples/ui/z_index.rs b/examples/ui/z_index.rs new file mode 100644 index 0000000000000..e469d2e30d107 --- /dev/null +++ b/examples/ui/z_index.rs @@ -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() + }); + }); +}