bevyengine / bevy

A refreshingly simple data-driven game engine built in Rust
https://bevyengine.org
Apache License 2.0
36.46k stars 3.6k forks source link

[bevy_ui/layout] extra hierarchy change handling #16383

Open StrikeForceZero opened 2 weeks ago

StrikeForceZero commented 2 weeks ago

supersedes https://github.com/bevyengine/bevy/pull/13360

Objective

Solution

Testing

Unit tests and example

Unknown

Run example code below, apply diff to see before and after behavior

Click to view diff ```diff Index: crates/bevy_ui/src/layout/mod.rs IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/crates/bevy_ui/src/layout/mod.rs b/crates/bevy_ui/src/layout/mod.rs --- a/crates/bevy_ui/src/layout/mod.rs (revision Staged) +++ b/crates/bevy_ui/src/layout/mod.rs (date 1731545949320) @@ -210,11 +210,11 @@ .filter(|&entity| root_nodes.is_root_node(entity)); for entity in promoted_root_ui_nodes { - ui_surface.promote_ui_node(entity); + // ui_surface.promote_ui_node(entity); } for (entity, parent) in root_nodes.iter_demoted_root_nodes() { - ui_surface.demote_ui_node(entity, parent.get()); + // ui_surface.demote_ui_node(entity, parent.get()); } // When a `ContentSize` component is removed from an entity, we need to remove the measure from the corresponding taffy node. ```

Example

Click to view example ```rust //! Demonstrates how UI can handle non-standard hierarchy changes use bevy::ecs::query::QueryData; use bevy::{color::palettes::css::*, prelude::*}; fn main() { App::new() .add_plugins(DefaultPlugins) .add_systems(Startup, spawn_layout) .add_systems(Update, input) .run(); } #[derive(Component, Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)] struct Id(usize); #[derive(Component, Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)] struct Pos(usize); impl Pos { fn set(&mut self, pos: usize) { self.0 = pos; } fn get(&self) -> usize { self.0 } } #[derive(Component, Debug, Copy, Clone)] struct Marker; const COLS: usize = 3; fn spawn_layout(mut commands: Commands) { commands.spawn(Camera2d); let grid_template_columns = (0..COLS).map(|_| GridTrack::flex(1.0)).collect::>(); fn create_place(ix: usize) -> impl Bundle { ( Id(ix), Pos(ix), Node { margin: UiRect::all(Val::Px(10.0)), padding: UiRect::all(Val::Px(10.0)), display: Display::Flex, justify_content: JustifyContent::Center, ..default() }, BackgroundColor(BLUE.into()), ) } commands .spawn(( Node { display: Display::Grid, width: Val::Percent(100.0), grid_template_columns: vec![GridTrack::auto()], grid_template_rows: vec![GridTrack::auto()], justify_content: JustifyContent::Center, top: Val::Px(65.0), ..default() }, TextLayout::new_with_justify(JustifyText::Center), BackgroundColor(Color::WHITE), Transform::from_translation(Vec3::Z * 1.0), )) .with_children(|builder| { builder.spawn(( Node { display: Display::Grid, width: Val::Percent(100.0), grid_template_columns: grid_template_columns.clone(), grid_template_rows: vec![GridTrack::auto(), GridTrack::auto(), GridTrack::auto()], justify_content: JustifyContent::Center, ..default() }, TextLayout::new_with_justify(JustifyText::Center), BackgroundColor(Color::WHITE), Transform::from_translation(Vec3::Z * 1.0), )).with_children(|builder| { builder .spawn(( Node { display: Display::Grid, grid_column: GridPlacement::span(COLS as u16), grid_row: GridPlacement::span(1), justify_content: JustifyContent::Center, ..default() }, BackgroundColor(BLACK.into()), )) .with_children(|builder| { builder.spawn(( Text::new("Press 0 to move ^ here (root)"), TextFont { font_size: 20.0, ..default() }, TextLayout::new_with_justify(JustifyText::Center), TextColor(WHITE.into()), )); }); builder .spawn(( Node { display: Display::Grid, width: Val::Percent(100.0), grid_template_columns, grid_template_rows: vec![GridTrack::auto()], grid_column: GridPlacement::span(3), grid_row: GridPlacement::span(1), justify_content: JustifyContent::Center, ..default() }, TextLayout::new_with_justify(JustifyText::Center), BackgroundColor(Color::WHITE), Transform::from_translation(Vec3::Z * 1.0), )) .with_children(|builder| { for ix in 0..COLS { let ix = ix + 1; builder.spawn(create_place(ix)).with_children(|builder| { builder.spawn(( Text::new(format!("{:?}", Id(ix))), TextFont { font_size: 20.0, ..default() }, TextLayout::new_with_justify(JustifyText::Center), TextColor(WHITE.into()), )); }); } }); for ix in 0..COLS { let ix = ix + 1; // Header builder .spawn(( Node { display: Display::Grid, padding: UiRect::all(Val::Px(6.0)), grid_column: GridPlacement::span(1), justify_content: JustifyContent::Center, ..default() }, BackgroundColor(BLACK.into()), )) .with_children(|builder| { builder.spawn(( Text::new(format!("Press {ix} to move ^ here (child)")), TextFont { font_size: 20.0, ..default() }, TextColor(WHITE.into()), )); }); } }); }); commands .spawn(create_place(0)) .insert(Marker) .with_children(|builder| { builder.spawn(( Text::new("Target"), TextFont { font_size: 20.0, ..default() }, TextLayout::new_with_justify(JustifyText::Center), TextColor(RED.into()), )); }); } #[derive(QueryData)] #[query_data(mutable)] struct PlaceOrMarkerQueryData { entity: Entity, parent: Option<&'static Parent>, id: &'static Id, pos: &'static mut Pos, } impl PlaceOrMarkerQueryDataItem<'_> { fn swap_pos(&mut self, other: &mut Self) { let other_pos = other.pos.0; other.pos.set(self.pos.0); self.pos.set(other_pos); } fn pos(&self) -> usize { self.pos.get() } } impl PlaceOrMarkerQueryDataReadOnlyItem<'_> { fn pos(&self) -> usize { self.pos.get() } } fn input( mut commands: Commands, keys: Res>, mut marker_q: Query>, mut places_q: Query>, ) { let sorted_places = (0..=3) .map(|ix| { places_q.iter().find_map(|place| { if place.pos() == ix { Some(place.entity) } else { None } }) }) .collect::>(); let mut update = |place: usize| { println!( "update {:?}", sorted_places .iter() .map(|entity| entity.map(|entity| places_q.get(entity).unwrap().id)) .collect::>() ); let mut marker = marker_q.single_mut(); if marker.pos() == place { return; } let Some(target_place_entity) = sorted_places[place] else { return; }; let mut target_place = places_q.get_mut(target_place_entity).unwrap(); println!("{} -> {}", marker.pos(), target_place.pos()); let (remove_target_place_parent, remove_marker_parent) = (marker.pos() == 0, target_place.pos() == 0); if remove_marker_parent && remove_target_place_parent { unreachable!(); } marker.swap_pos(&mut target_place); let marker_parent = if remove_marker_parent { let marker_parent = marker.parent.expect("marker missing parent").get(); commands.entity(marker.entity).remove_parent(); Some(marker_parent) } else { marker.parent.map(Parent::get) }; let target_place_parent = if remove_target_place_parent { let target_place_parent = target_place .parent .expect("target_place missing parent") .get(); commands.entity(target_place.entity).remove_parent(); Some(target_place_parent) } else { target_place.parent.map(Parent::get) }; if let Some(marker_parent) = marker_parent { commands .entity(marker_parent) .insert_children(target_place.pos() - 1, &[target_place.entity]); } if let Some(target_place_parent) = target_place_parent { commands .entity(target_place_parent) .insert_children(marker.pos() - 1, &[marker.entity]); } }; if keys.just_pressed(KeyCode::Digit0) { update(0); } if keys.just_pressed(KeyCode::Digit1) { update(1); } if keys.just_pressed(KeyCode::Digit2) { update(2); } if keys.just_pressed(KeyCode::Digit3) { update(3); } } ```