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

Remove ZIndex::Global #9877

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
242 changes: 41 additions & 201 deletions crates/bevy_ui/src/stack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,217 +15,57 @@ 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.
///
/// First generate a UI node tree (`StackingContext`) based on z-index.
/// Then flatten that tree into back-to-front ordered `UiStack`.
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>,
node_query: Query<Option<&Children>, With<Node>>,
zindex_query: Query<&ZIndex>,
) {
// Generate `StackingContext` tree
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,
);
}

// Flatten `StackingContext` into `UiStack`
ui_stack.uinodes.clear();
ui_stack.uinodes.reserve(total_entry_count);
fill_stack_recursively(&mut ui_stack.uinodes, &mut global_context);
}

/// Generate z-index based UI node tree
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 since
// nodes with `ZIndex::Global` are pushed to the global (root) context.
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 uinodes = &mut ui_stack.uinodes;

fn update_uistack_recursively(
entity: Entity,
uinodes: &mut Vec<Entity>,
node_query: &Query<Option<&Children>, With<Node>>,
zindex_query: &Query<&ZIndex>,
) {
let Ok(children) = node_query.get(entity) else {
return;
};

uinodes.push(entity);

if let Some(children) = children {
let mut z_children: Vec<(Entity, i32)> = children
.iter()
.map(|&child_id| {
(
child_id,
match zindex_query.get(child_id) {
Ok(ZIndex(z)) => *z,
_ => 0,
},
)
})
.collect();
z_children.sort_by_key(|k| k.1);
for (child_id, _) in z_children {
update_uistack_recursively(child_id, uinodes, node_query, zindex_query);
}
}
}

// The node will be added either to global/parent based on its z-index type: global/local.
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,
let mut root_nodes: Vec<_> = root_node_query.iter().collect();
root_nodes.sort_by_cached_key(|entity| {
zindex_query
.get(*entity)
.map(|zindex| zindex.0)
.unwrap_or(0)
});
}

/// Flatten `StackingContext` (z-index based UI node tree) into back-to-front entities list
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. This results
// in `back-to-front` ordering, low z_index = back; high z_index = front.
stack.entries.sort_by_key(|e| e.z_index);

for entry in &mut stack.entries {
// Parent node renders before/behind child nodes
result.push(entry.entity);
fill_stack_recursively(result, &mut entry.stack);
}
}

#[cfg(test)]
mod tests {
use bevy_ecs::{
component::Component,
schedule::Schedule,
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();
schedule.add_systems(ui_stack_system);
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);
for entity in root_nodes {
update_uistack_recursively(entity, uinodes, &node_query, &zindex_query);
}
}
22 changes: 3 additions & 19 deletions crates/bevy_ui/src/ui_node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1505,26 +1505,10 @@ pub struct CalculatedClip {
/// 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)]
/// Nodes without this component will be treated as if they had a value of [`ZIndex(0)`].
#[derive(Component, Copy, Clone, Debug, Default, Reflect)]
#[reflect(Component)]
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)
}
}
pub struct ZIndex(pub i32);

#[cfg(test)]
mod tests {
Expand Down
2 changes: 1 addition & 1 deletion examples/stress_tests/bevymark.rs
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ fn setup(
padding: UiRect::all(Val::Px(5.0)),
..default()
},
z_index: ZIndex::Global(i32::MAX),
z_index: ZIndex(i32::MAX),
background_color: Color::BLACK.with_a(0.75).into(),
..default()
})
Expand Down
41 changes: 3 additions & 38 deletions examples/ui/z_index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@ 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.
// the default z-index value is `ZIndex(0)`.
commands
.spawn(NodeBundle {
style: Style {
Expand Down Expand Up @@ -59,7 +58,7 @@ fn setup(mut commands: Commands) {
// spawn a node with a positive local z-index of 2.
// it will show above other nodes in the gray container.
parent.spawn(NodeBundle {
z_index: ZIndex::Local(2),
z_index: ZIndex(2),
background_color: Color::BLUE.into(),
style: Style {
position_type: PositionType::Absolute,
Expand All @@ -75,7 +74,7 @@ fn setup(mut commands: Commands) {
// spawn a node with a negative local z-index.
// it will show under other nodes in the gray container.
parent.spawn(NodeBundle {
z_index: ZIndex::Local(-1),
z_index: ZIndex(-1),
background_color: Color::GREEN.into(),
style: Style {
position_type: PositionType::Absolute,
Expand All @@ -87,40 +86,6 @@ fn setup(mut commands: Commands) {
},
..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 gray container is added to.
parent.spawn(NodeBundle {
z_index: ZIndex::Global(1),
background_color: Color::PURPLE.into(),
style: Style {
position_type: PositionType::Absolute,
left: Val::Px(15.0),
bottom: Val::Px(10.0),
width: Val::Px(100.),
height: Val::Px(60.),
..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,
left: Val::Px(-15.0),
bottom: Val::Px(-15.0),
width: Val::Px(100.),
height: Val::Px(125.),
..default()
},
..default()
});
});
});
}