
# Objective Sometimes when despawning a ui node in the PostUpdate schedule it panics. This is because both a despawn command and insert command are being run on the same entity. See this example code: ```rs use bevy::{prelude::*, ui::UiSystem}; #[derive(Resource)] struct SliceSquare(Handle<Image>); fn main() { App::new() .add_plugins(DefaultPlugins) .add_systems(Startup, setup) .add_systems(Update, create_ui) .add_systems(PostUpdate, despawn_nine_slice.after(UiSystem::Layout)) .run(); } fn setup(mut commands: Commands, asset_server: Res<AssetServer>) { commands.spawn(Camera2dBundle::default()); commands.insert_resource(SliceSquare(asset_server.load("textures/slice_square.png"))); } fn create_ui(mut commands: Commands, slice_square: Res<SliceSquare>) { commands.spawn(( NodeBundle { style: Style { width: Val::Px(200.), height: Val::Px(200.), ..default() }, background_color: Color::WHITE.into(), ..default() }, UiImage::new(slice_square.0.clone()), ImageScaleMode::Sliced(TextureSlicer { border: BorderRect::square(220.), center_scale_mode: SliceScaleMode::Stretch, sides_scale_mode: SliceScaleMode::Stretch, max_corner_scale: 1., }), )); } fn despawn_nine_slice(mut commands: Commands, mut slices: Query<Entity, With<ImageScaleMode>>) { for entity in slices.iter_mut() { commands.entity(entity).despawn_recursive(); } } ``` This code spawns a UiNode with a sliced image scale mode, and despawns it in the same frame. The bevy_ui::texture_slice::compute_slices_on_image_change system tries to insert the ComputedTextureSlices component on that node, but that entity is already despawned causing this error: ```md error[B0003]: Could not insert a bundle (of type `bevy_ui::texture_slice::ComputedTextureSlices`) for entity Entity { index: 2, generation: 3 } because it doesn't exist in this World. See: https://bevyengine.org/learn/errors/#b0003 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace Encountered a panic when applying buffers for system `bevy_ui::texture_slice::compute_slices_on_image_change`! Encountered a panic in system `bevy_ecs::schedule::executor::apply_deferred`! Encountered a panic in system `bevy_app::main_schedule::Main::run_main`! ``` Note that you might have to run the code a few times before this error appears. ## Solution Use try_insert instead of insert for non critical inserts in the bevy_ui crate. ## Some notes In a lot of cases it does not makes much sense to despawn ui nodes after the layout system has finished. Except maybe if you delete the root ui node of a tree. I personally encountered this issue in bevy `0.13.2` with a system that was running before the layout system. And in `0.13.2` the `compute_slices_on_image_change` system was also running before the layout system. But now it runs after the layout system. So the only way that this bug appears now is if you despawn ui nodes after the layout system. So I am not 100% sure if using try_insert in this system is the best option. But personally I still think it is better then the program panicking. However the `update_children_target_camera` system does still run before the layout system. So I do think it might still be able to panic when ui nodes are despawned before the layout system. Though I haven't been able to verify that.
184 lines
6.3 KiB
Rust
184 lines
6.3 KiB
Rust
//! This module contains systems that update the UI when something changes
|
|
|
|
use crate::{CalculatedClip, Display, OverflowAxis, Style, TargetCamera};
|
|
|
|
use super::Node;
|
|
use bevy_ecs::{
|
|
entity::Entity,
|
|
query::{Changed, With, Without},
|
|
system::{Commands, Query},
|
|
};
|
|
use bevy_hierarchy::{Children, Parent};
|
|
use bevy_math::Rect;
|
|
use bevy_transform::components::GlobalTransform;
|
|
use bevy_utils::HashSet;
|
|
|
|
/// Updates clipping for all nodes
|
|
pub fn update_clipping_system(
|
|
mut commands: Commands,
|
|
root_node_query: Query<Entity, (With<Node>, Without<Parent>)>,
|
|
mut node_query: Query<(&Node, &GlobalTransform, &Style, Option<&mut CalculatedClip>)>,
|
|
children_query: Query<&Children>,
|
|
) {
|
|
for root_node in &root_node_query {
|
|
update_clipping(
|
|
&mut commands,
|
|
&children_query,
|
|
&mut node_query,
|
|
root_node,
|
|
None,
|
|
);
|
|
}
|
|
}
|
|
|
|
fn update_clipping(
|
|
commands: &mut Commands,
|
|
children_query: &Query<&Children>,
|
|
node_query: &mut Query<(&Node, &GlobalTransform, &Style, Option<&mut CalculatedClip>)>,
|
|
entity: Entity,
|
|
mut maybe_inherited_clip: Option<Rect>,
|
|
) {
|
|
let Ok((node, global_transform, style, maybe_calculated_clip)) = node_query.get_mut(entity)
|
|
else {
|
|
return;
|
|
};
|
|
|
|
// If `display` is None, clip the entire node and all its descendants by replacing the inherited clip with a default rect (which is empty)
|
|
if style.display == Display::None {
|
|
maybe_inherited_clip = Some(Rect::default());
|
|
}
|
|
|
|
// Update this node's CalculatedClip component
|
|
if let Some(mut calculated_clip) = maybe_calculated_clip {
|
|
if let Some(inherited_clip) = maybe_inherited_clip {
|
|
// Replace the previous calculated clip with the inherited clipping rect
|
|
if calculated_clip.clip != inherited_clip {
|
|
*calculated_clip = CalculatedClip {
|
|
clip: inherited_clip,
|
|
};
|
|
}
|
|
} else {
|
|
// No inherited clipping rect, remove the component
|
|
commands.entity(entity).remove::<CalculatedClip>();
|
|
}
|
|
} else if let Some(inherited_clip) = maybe_inherited_clip {
|
|
// No previous calculated clip, add a new CalculatedClip component with the inherited clipping rect
|
|
commands.entity(entity).try_insert(CalculatedClip {
|
|
clip: inherited_clip,
|
|
});
|
|
}
|
|
|
|
// Calculate new clip rectangle for children nodes
|
|
let children_clip = if style.overflow.is_visible() {
|
|
// When `Visible`, children might be visible even when they are outside
|
|
// the current node's boundaries. In this case they inherit the current
|
|
// node's parent clip. If an ancestor is set as `Hidden`, that clip will
|
|
// be used; otherwise this will be `None`.
|
|
maybe_inherited_clip
|
|
} else {
|
|
// If `maybe_inherited_clip` is `Some`, use the intersection between
|
|
// current node's clip and the inherited clip. This handles the case
|
|
// of nested `Overflow::Hidden` nodes. If parent `clip` is not
|
|
// defined, use the current node's clip.
|
|
let mut node_rect = node.logical_rect(global_transform);
|
|
if style.overflow.x == OverflowAxis::Visible {
|
|
node_rect.min.x = -f32::INFINITY;
|
|
node_rect.max.x = f32::INFINITY;
|
|
}
|
|
if style.overflow.y == OverflowAxis::Visible {
|
|
node_rect.min.y = -f32::INFINITY;
|
|
node_rect.max.y = f32::INFINITY;
|
|
}
|
|
Some(maybe_inherited_clip.map_or(node_rect, |c| c.intersect(node_rect)))
|
|
};
|
|
|
|
if let Ok(children) = children_query.get(entity) {
|
|
for &child in children {
|
|
update_clipping(commands, children_query, node_query, child, children_clip);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn update_target_camera_system(
|
|
mut commands: Commands,
|
|
changed_root_nodes_query: Query<
|
|
(Entity, Option<&TargetCamera>),
|
|
(With<Node>, Without<Parent>, Changed<TargetCamera>),
|
|
>,
|
|
changed_children_query: Query<(Entity, Option<&TargetCamera>), (With<Node>, Changed<Children>)>,
|
|
children_query: Query<&Children, With<Node>>,
|
|
node_query: Query<Option<&TargetCamera>, With<Node>>,
|
|
) {
|
|
// Track updated entities to prevent redundant updates, as `Commands` changes are deferred,
|
|
// and updates done for changed_children_query can overlap with itself or with root_node_query
|
|
let mut updated_entities = HashSet::new();
|
|
|
|
// Assuming that TargetCamera is manually set on the root node only,
|
|
// update root nodes first, since it implies the biggest change
|
|
for (root_node, target_camera) in &changed_root_nodes_query {
|
|
update_children_target_camera(
|
|
root_node,
|
|
target_camera,
|
|
&node_query,
|
|
&children_query,
|
|
&mut commands,
|
|
&mut updated_entities,
|
|
);
|
|
}
|
|
|
|
// If the root node TargetCamera was changed, then every child is updated
|
|
// by this point, and iteration will be skipped.
|
|
// Otherwise, update changed children
|
|
for (parent, target_camera) in &changed_children_query {
|
|
update_children_target_camera(
|
|
parent,
|
|
target_camera,
|
|
&node_query,
|
|
&children_query,
|
|
&mut commands,
|
|
&mut updated_entities,
|
|
);
|
|
}
|
|
}
|
|
|
|
fn update_children_target_camera(
|
|
entity: Entity,
|
|
camera_to_set: Option<&TargetCamera>,
|
|
node_query: &Query<Option<&TargetCamera>, With<Node>>,
|
|
children_query: &Query<&Children, With<Node>>,
|
|
commands: &mut Commands,
|
|
updated_entities: &mut HashSet<Entity>,
|
|
) {
|
|
let Ok(children) = children_query.get(entity) else {
|
|
return;
|
|
};
|
|
|
|
for &child in children {
|
|
// Skip if the child has already been updated or update is not needed
|
|
if updated_entities.contains(&child)
|
|
|| camera_to_set == node_query.get(child).ok().flatten()
|
|
{
|
|
continue;
|
|
}
|
|
|
|
match camera_to_set {
|
|
Some(camera) => {
|
|
commands.entity(child).try_insert(camera.clone());
|
|
}
|
|
None => {
|
|
commands.entity(child).remove::<TargetCamera>();
|
|
}
|
|
}
|
|
updated_entities.insert(child);
|
|
|
|
update_children_target_camera(
|
|
child,
|
|
camera_to_set,
|
|
node_query,
|
|
children_query,
|
|
commands,
|
|
updated_entities,
|
|
);
|
|
}
|
|
}
|