diff --git a/crates/bevy_ui/src/render/mod.rs b/crates/bevy_ui/src/render/mod.rs index 951f602e35..40985ad237 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -13,7 +13,7 @@ pub use render_pass::*; use crate::Outline; use crate::{ prelude::UiCameraConfig, BackgroundColor, BorderColor, CalculatedClip, ContentSize, Node, - Style, UiImage, UiScale, UiStack, UiTextureAtlasImage, Val, + Style, UiImage, UiScale, UiTextureAtlasImage, Val, }; use bevy_app::prelude::*; @@ -152,7 +152,7 @@ fn get_ui_graph(render_app: &mut App) -> RenderGraph { } pub struct ExtractedUiNode { - pub stack_index: usize, + pub stack_index: u32, pub transform: Mat4, pub color: Color, pub rect: Rect, @@ -172,7 +172,6 @@ pub fn extract_atlas_uinodes( mut extracted_uinodes: ResMut, images: Extract>>, texture_atlases: Extract>>, - ui_stack: Extract>, uinode_query: Extract< Query< ( @@ -189,70 +188,68 @@ pub fn extract_atlas_uinodes( >, >, ) { - for (stack_index, entity) in ui_stack.uinodes.iter().enumerate() { - if let Ok(( - entity, - uinode, - transform, - color, - view_visibility, - clip, - texture_atlas_handle, - atlas_image, - )) = uinode_query.get(*entity) - { - // Skip invisible and completely transparent nodes - if !view_visibility.get() || color.0.is_fully_transparent() { - continue; - } - - let (mut atlas_rect, mut atlas_size, image) = - if let Some(texture_atlas) = texture_atlases.get(texture_atlas_handle) { - let atlas_rect = *texture_atlas - .textures - .get(atlas_image.index) - .unwrap_or_else(|| { - panic!( - "Atlas index {:?} does not exist for texture atlas handle {:?}.", - atlas_image.index, - texture_atlas_handle.id(), - ) - }); - ( - atlas_rect, - texture_atlas.size, - texture_atlas.texture.clone(), - ) - } else { - // Atlas not present in assets resource (should this warn the user?) - continue; - }; - - // Skip loading images - if !images.contains(&image) { - continue; - } - - let scale = uinode.size() / atlas_rect.size(); - atlas_rect.min *= scale; - atlas_rect.max *= scale; - atlas_size *= scale; - - extracted_uinodes.uinodes.insert( - entity, - ExtractedUiNode { - stack_index, - transform: transform.compute_matrix(), - color: color.0, - rect: atlas_rect, - clip: clip.map(|clip| clip.clip), - image: image.id(), - atlas_size: Some(atlas_size), - flip_x: atlas_image.flip_x, - flip_y: atlas_image.flip_y, - }, - ); + for ( + entity, + uinode, + transform, + color, + view_visibility, + clip, + texture_atlas_handle, + atlas_image, + ) in uinode_query.iter() + { + // Skip invisible and completely transparent nodes + if !view_visibility.get() || color.0.is_fully_transparent() { + continue; } + + let (mut atlas_rect, mut atlas_size, image) = + if let Some(texture_atlas) = texture_atlases.get(texture_atlas_handle) { + let atlas_rect = *texture_atlas + .textures + .get(atlas_image.index) + .unwrap_or_else(|| { + panic!( + "Atlas index {:?} does not exist for texture atlas handle {:?}.", + atlas_image.index, + texture_atlas_handle.id(), + ) + }); + ( + atlas_rect, + texture_atlas.size, + texture_atlas.texture.clone(), + ) + } else { + // Atlas not present in assets resource (should this warn the user?) + continue; + }; + + // Skip loading images + if !images.contains(&image) { + continue; + } + + let scale = uinode.size() / atlas_rect.size(); + atlas_rect.min *= scale; + atlas_rect.max *= scale; + atlas_size *= scale; + + extracted_uinodes.uinodes.insert( + entity, + ExtractedUiNode { + stack_index: uinode.stack_index, + transform: transform.compute_matrix(), + color: color.0, + rect: atlas_rect, + clip: clip.map(|clip| clip.clip), + image: image.id(), + atlas_size: Some(atlas_size), + flip_x: atlas_image.flip_x, + flip_y: atlas_image.flip_y, + }, + ); } } @@ -273,7 +270,6 @@ pub fn extract_uinode_borders( mut extracted_uinodes: ResMut, windows: Extract>>, ui_scale: Extract>, - ui_stack: Extract>, uinode_query: Extract< Query< ( @@ -300,92 +296,84 @@ pub fn extract_uinode_borders( // so we have to divide by `UiScale` to get the size of the UI viewport. / ui_scale.0 as f32; - for (stack_index, entity) in ui_stack.uinodes.iter().enumerate() { - if let Ok((node, global_transform, style, border_color, parent, view_visibility, clip)) = - uinode_query.get(*entity) + for (node, global_transform, style, border_color, parent, view_visibility, clip) in + uinode_query.iter() + { + // Skip invisible borders + if !view_visibility.get() + || border_color.0.is_fully_transparent() + || node.size().x <= 0. + || node.size().y <= 0. { - // Skip invisible borders - if !view_visibility.get() - || border_color.0.is_fully_transparent() - || node.size().x <= 0. - || node.size().y <= 0. - { - continue; - } + continue; + } - // Both vertical and horizontal percentage border values are calculated based on the width of the parent node - // - let parent_width = parent - .and_then(|parent| node_query.get(parent.get()).ok()) - .map(|parent_node| parent_node.size().x) - .unwrap_or(ui_logical_viewport_size.x); - let left = - resolve_border_thickness(style.border.left, parent_width, ui_logical_viewport_size); - let right = resolve_border_thickness( - style.border.right, - parent_width, - ui_logical_viewport_size, - ); - let top = - resolve_border_thickness(style.border.top, parent_width, ui_logical_viewport_size); - let bottom = resolve_border_thickness( - style.border.bottom, - parent_width, - ui_logical_viewport_size, - ); + // Both vertical and horizontal percentage border values are calculated based on the width of the parent node + // + let parent_width = parent + .and_then(|parent| node_query.get(parent.get()).ok()) + .map(|parent_node| parent_node.size().x) + .unwrap_or(ui_logical_viewport_size.x); + let left = + resolve_border_thickness(style.border.left, parent_width, ui_logical_viewport_size); + let right = + resolve_border_thickness(style.border.right, parent_width, ui_logical_viewport_size); + let top = + resolve_border_thickness(style.border.top, parent_width, ui_logical_viewport_size); + let bottom = + resolve_border_thickness(style.border.bottom, parent_width, ui_logical_viewport_size); - // Calculate the border rects, ensuring no overlap. - // The border occupies the space between the node's bounding rect and the node's bounding rect inset in each direction by the node's corresponding border value. - let max = 0.5 * node.size(); - let min = -max; - let inner_min = min + Vec2::new(left, top); - let inner_max = (max - Vec2::new(right, bottom)).max(inner_min); - let border_rects = [ - // Left border - Rect { - min, - max: Vec2::new(inner_min.x, max.y), - }, - // Right border - Rect { - min: Vec2::new(inner_max.x, min.y), - max, - }, - // Top border - Rect { - min: Vec2::new(inner_min.x, min.y), - max: Vec2::new(inner_max.x, inner_min.y), - }, - // Bottom border - Rect { - min: Vec2::new(inner_min.x, inner_max.y), - max: Vec2::new(inner_max.x, max.y), - }, - ]; + // Calculate the border rects, ensuring no overlap. + // The border occupies the space between the node's bounding rect and the node's bounding rect inset in each direction by the node's corresponding border value. + let max = 0.5 * node.size(); + let min = -max; + let inner_min = min + Vec2::new(left, top); + let inner_max = (max - Vec2::new(right, bottom)).max(inner_min); + let border_rects = [ + // Left border + Rect { + min, + max: Vec2::new(inner_min.x, max.y), + }, + // Right border + Rect { + min: Vec2::new(inner_max.x, min.y), + max, + }, + // Top border + Rect { + min: Vec2::new(inner_min.x, min.y), + max: Vec2::new(inner_max.x, inner_min.y), + }, + // Bottom border + Rect { + min: Vec2::new(inner_min.x, inner_max.y), + max: Vec2::new(inner_max.x, max.y), + }, + ]; - let transform = global_transform.compute_matrix(); + let transform = global_transform.compute_matrix(); - for edge in border_rects { - if edge.min.x < edge.max.x && edge.min.y < edge.max.y { - extracted_uinodes.uinodes.insert( - commands.spawn_empty().id(), - ExtractedUiNode { - stack_index, - // This translates the uinode's transform to the center of the current border rectangle - transform: transform * Mat4::from_translation(edge.center().extend(0.)), - color: border_color.0, - rect: Rect { - max: edge.size(), - ..Default::default() - }, - image, - atlas_size: None, - clip: clip.map(|clip| clip.clip), - flip_x: false, - flip_y: false, + for edge in border_rects { + if edge.min.x < edge.max.x && edge.min.y < edge.max.y { + extracted_uinodes.uinodes.insert( + commands.spawn_empty().id(), + ExtractedUiNode { + stack_index: node.stack_index, + // This translates the uinode's transform to the center of the current border rectangle + transform: transform * Mat4::from_translation(edge.center().extend(0.)), + color: border_color.0, + rect: Rect { + max: edge.size(), + ..Default::default() }, - ); - } + image, + atlas_size: None, + clip: clip.map(|clip| clip.clip), + flip_x: false, + flip_y: false, + }, + ); } } } @@ -394,7 +382,6 @@ pub fn extract_uinode_borders( pub fn extract_uinode_outlines( mut commands: Commands, mut extracted_uinodes: ResMut, - ui_stack: Extract>, uinode_query: Extract< Query<( &Node, @@ -407,81 +394,75 @@ pub fn extract_uinode_outlines( clip_query: Query<&CalculatedClip>, ) { let image = AssetId::::default(); - - for (stack_index, entity) in ui_stack.uinodes.iter().enumerate() { - if let Ok((node, global_transform, outline, view_visibility, maybe_parent)) = - uinode_query.get(*entity) + for (node, global_transform, outline, view_visibility, maybe_parent) in uinode_query.iter() { + // Skip invisible outlines + if !view_visibility.get() + || outline.color.is_fully_transparent() + || node.outline_width == 0. { - // Skip invisible outlines - if !view_visibility.get() - || outline.color.is_fully_transparent() - || node.outline_width == 0. - { - continue; - } + continue; + } - // Outline's are drawn outside of a node's borders, so they are clipped using the clipping Rect of their UI node entity's parent. - let clip = maybe_parent - .and_then(|parent| clip_query.get(parent.get()).ok().map(|clip| clip.clip)); + // Outline's are drawn outside of a node's borders, so they are clipped using the clipping Rect of their UI node entity's parent. + let clip = + maybe_parent.and_then(|parent| clip_query.get(parent.get()).ok().map(|clip| clip.clip)); - // Calculate the outline rects. - let inner_rect = - Rect::from_center_size(Vec2::ZERO, node.size() + 2. * node.outline_offset); - let outer_rect = inner_rect.inset(node.outline_width()); - let outline_edges = [ - // Left edge - Rect::new( - outer_rect.min.x, - outer_rect.min.y, - inner_rect.min.x, - outer_rect.max.y, - ), - // Right edge - Rect::new( - inner_rect.max.x, - outer_rect.min.y, - outer_rect.max.x, - outer_rect.max.y, - ), - // Top edge - Rect::new( - inner_rect.min.x, - outer_rect.min.y, - inner_rect.max.x, - inner_rect.min.y, - ), - // Bottom edge - Rect::new( - inner_rect.min.x, - inner_rect.max.y, - inner_rect.max.x, - outer_rect.max.y, - ), - ]; + // Calculate the outline rects. + let inner_rect = Rect::from_center_size(Vec2::ZERO, node.size() + 2. * node.outline_offset); + let outer_rect = inner_rect.inset(node.outline_width()); + let outline_edges = [ + // Left edge + Rect::new( + outer_rect.min.x, + outer_rect.min.y, + inner_rect.min.x, + outer_rect.max.y, + ), + // Right edge + Rect::new( + inner_rect.max.x, + outer_rect.min.y, + outer_rect.max.x, + outer_rect.max.y, + ), + // Top edge + Rect::new( + inner_rect.min.x, + outer_rect.min.y, + inner_rect.max.x, + inner_rect.min.y, + ), + // Bottom edge + Rect::new( + inner_rect.min.x, + inner_rect.max.y, + inner_rect.max.x, + outer_rect.max.y, + ), + ]; - let transform = global_transform.compute_matrix(); + let transform = global_transform.compute_matrix(); - for edge in outline_edges { - if edge.min.x < edge.max.x && edge.min.y < edge.max.y { - extracted_uinodes.uinodes.insert( - commands.spawn_empty().id(), - ExtractedUiNode { - stack_index, - // This translates the uinode's transform to the center of the current border rectangle - transform: transform * Mat4::from_translation(edge.center().extend(0.)), - color: outline.color, - rect: Rect { - max: edge.size(), - ..Default::default() - }, - image, - atlas_size: None, - clip, - flip_x: false, - flip_y: false, + for edge in outline_edges { + if edge.min.x < edge.max.x && edge.min.y < edge.max.y { + extracted_uinodes.uinodes.insert( + commands.spawn_empty().id(), + ExtractedUiNode { + stack_index: node.stack_index, + // This translates the uinode's transform to the center of the current border rectangle + transform: transform * Mat4::from_translation(edge.center().extend(0.)), + color: outline.color, + rect: Rect { + max: edge.size(), + ..Default::default() }, - ); - } + image, + atlas_size: None, + clip, + flip_x: false, + flip_y: false, + }, + ); } } } @@ -490,7 +471,6 @@ pub fn extract_uinode_outlines( pub fn extract_uinodes( mut extracted_uinodes: ResMut, images: Extract>>, - ui_stack: Extract>, uinode_query: Extract< Query< ( @@ -506,43 +486,41 @@ pub fn extract_uinodes( >, >, ) { - for (stack_index, entity) in ui_stack.uinodes.iter().enumerate() { - if let Ok((entity, uinode, transform, color, maybe_image, view_visibility, clip)) = - uinode_query.get(*entity) - { - // Skip invisible and completely transparent nodes - if !view_visibility.get() || color.0.is_fully_transparent() { + for (entity, uinode, transform, color, maybe_image, view_visibility, clip) in + uinode_query.iter() + { + // Skip invisible and completely transparent nodes + if !view_visibility.get() || color.0.is_fully_transparent() { + continue; + } + + let (image, flip_x, flip_y) = if let Some(image) = maybe_image { + // Skip loading images + if !images.contains(&image.texture) { continue; } - - let (image, flip_x, flip_y) = if let Some(image) = maybe_image { - // Skip loading images - if !images.contains(&image.texture) { - continue; - } - (image.texture.id(), image.flip_x, image.flip_y) - } else { - (AssetId::default(), false, false) - }; - - extracted_uinodes.uinodes.insert( - entity, - ExtractedUiNode { - stack_index, - transform: transform.compute_matrix(), - color: color.0, - rect: Rect { - min: Vec2::ZERO, - max: uinode.calculated_size, - }, - clip: clip.map(|clip| clip.clip), - image, - atlas_size: None, - flip_x, - flip_y, - }, - ); + (image.texture.id(), image.flip_x, image.flip_y) + } else { + (AssetId::default(), false, false) }; + + extracted_uinodes.uinodes.insert( + entity, + ExtractedUiNode { + stack_index: uinode.stack_index, + transform: transform.compute_matrix(), + color: color.0, + rect: Rect { + min: Vec2::ZERO, + max: uinode.calculated_size, + }, + clip: clip.map(|clip| clip.clip), + image, + atlas_size: None, + flip_x, + flip_y, + }, + ); } } @@ -625,7 +603,6 @@ pub fn extract_text_uinodes( mut extracted_uinodes: ResMut, texture_atlases: Extract>>, windows: Extract>>, - ui_stack: Extract>, ui_scale: Extract>, uinode_query: Extract< Query<( @@ -647,51 +624,49 @@ pub fn extract_text_uinodes( let inverse_scale_factor = (scale_factor as f32).recip(); - for (stack_index, entity) in ui_stack.uinodes.iter().enumerate() { - if let Ok((uinode, global_transform, text, text_layout_info, view_visibility, clip)) = - uinode_query.get(*entity) + for (uinode, global_transform, text, text_layout_info, view_visibility, clip) in + uinode_query.iter() + { + // Skip if not visible or if size is set to zero (e.g. when a parent is set to `Display::None`) + if !view_visibility.get() || uinode.size().x == 0. || uinode.size().y == 0. { + continue; + } + let transform = global_transform.compute_matrix() + * Mat4::from_translation(-0.5 * uinode.size().extend(0.)); + + let mut color = Color::WHITE; + let mut current_section = usize::MAX; + for PositionedGlyph { + position, + atlas_info, + section_index, + .. + } in &text_layout_info.glyphs { - // Skip if not visible or if size is set to zero (e.g. when a parent is set to `Display::None`) - if !view_visibility.get() || uinode.size().x == 0. || uinode.size().y == 0. { - continue; + if *section_index != current_section { + color = text.sections[*section_index].style.color.as_rgba_linear(); + current_section = *section_index; } - let transform = global_transform.compute_matrix() - * Mat4::from_translation(-0.5 * uinode.size().extend(0.)); + let atlas = texture_atlases.get(&atlas_info.texture_atlas).unwrap(); - let mut color = Color::WHITE; - let mut current_section = usize::MAX; - for PositionedGlyph { - position, - atlas_info, - section_index, - .. - } in &text_layout_info.glyphs - { - if *section_index != current_section { - color = text.sections[*section_index].style.color.as_rgba_linear(); - current_section = *section_index; - } - let atlas = texture_atlases.get(&atlas_info.texture_atlas).unwrap(); - - let mut rect = atlas.textures[atlas_info.glyph_index]; - rect.min *= inverse_scale_factor; - rect.max *= inverse_scale_factor; - extracted_uinodes.uinodes.insert( - commands.spawn_empty().id(), - ExtractedUiNode { - stack_index, - transform: transform - * Mat4::from_translation(position.extend(0.) * inverse_scale_factor), - color, - rect, - image: atlas.texture.id(), - atlas_size: Some(atlas.size * inverse_scale_factor), - clip: clip.map(|clip| clip.clip), - flip_x: false, - flip_y: false, - }, - ); - } + let mut rect = atlas.textures[atlas_info.glyph_index]; + rect.min *= inverse_scale_factor; + rect.max *= inverse_scale_factor; + extracted_uinodes.uinodes.insert( + commands.spawn_empty().id(), + ExtractedUiNode { + stack_index: uinode.stack_index, + transform: transform + * Mat4::from_translation(position.extend(0.) * inverse_scale_factor), + color, + rect, + image: atlas.texture.id(), + atlas_size: Some(atlas.size * inverse_scale_factor), + clip: clip.map(|clip| clip.clip), + flip_x: false, + flip_y: false, + }, + ); } } } diff --git a/crates/bevy_ui/src/stack.rs b/crates/bevy_ui/src/stack.rs index c24a8aad66..60ab0aad52 100644 --- a/crates/bevy_ui/src/stack.rs +++ b/crates/bevy_ui/src/stack.rs @@ -35,6 +35,7 @@ pub fn ui_stack_system( root_node_query: Query, Without)>, zindex_query: Query<&ZIndex, With>, children_query: Query<&Children>, + mut update_query: Query<&mut Node>, ) { // Generate `StackingContext` tree let mut global_context = StackingContext::default(); @@ -55,6 +56,14 @@ pub fn ui_stack_system( ui_stack.uinodes.clear(); ui_stack.uinodes.reserve(total_entry_count); fill_stack_recursively(&mut ui_stack.uinodes, &mut global_context); + + for (i, entity) in ui_stack.uinodes.iter().enumerate() { + update_query + .get_mut(*entity) + .unwrap() + .bypass_change_detection() + .stack_index = i as u32; + } } /// Generate z-index based UI node tree diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index 1240066840..a02308566b 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -14,9 +14,12 @@ use thiserror::Error; #[derive(Component, Debug, Copy, Clone, Reflect)] #[reflect(Component, Default)] pub struct Node { - /// The size of the node as width and height in logical pixels. + /// The order of the node in the UI layout. + /// Nodes with a higher stack index are drawn on top of and recieve interactions before nodes with lower stack indices. + pub(crate) stack_index: u32, + /// The size of the node as width and height in logical pixels /// - /// Automatically calculated by [`super::layout::ui_layout_system`]. + /// automatically calculated by [`super::layout::ui_layout_system`] pub(crate) calculated_size: Vec2, /// The width of this node's outline. /// If this value is `Auto`, negative or `0.` then no outline will be rendered. @@ -39,6 +42,12 @@ impl Node { self.calculated_size } + /// The order of the node in the UI layout. + /// Nodes with a higher stack index are drawn on top of and recieve interactions before nodes with lower stack indices. + pub const fn stack_index(&self) -> u32 { + self.stack_index + } + /// The calculated node size as width and height in logical pixels before rounding. /// /// Automatically calculated by [`super::layout::ui_layout_system`]. @@ -92,6 +101,7 @@ impl Node { impl Node { pub const DEFAULT: Self = Self { + stack_index: 0, calculated_size: Vec2::ZERO, outline_width: 0., outline_offset: 0.,