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

[bevy_ui/layout] extra hierarchy change handling #16383

Open
wants to merge 12 commits into
base: main
Choose a base branch
from

Conversation

StrikeForceZero
Copy link
Contributor

supersedes #13360

Objective

  • Handle UI Nodes becoming a child of another UI node Added<Parent>
  • Handle UI Nodes who lose their parent to become a root UI node RemovedComponent<Parent>

Solution

  • Extends the queries for the layout_system and various system params to capture and handle these events.

Testing

  • Did you test these changes? If so, how?

Unit tests and example

  • Are there any parts that need more testing?

Unknown

  • How can other people (reviewers) test your changes? Is there anything specific they need to know?

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

Click to view 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
//! 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::<Vec<_>>();

    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<ButtonInput<KeyCode>>,
    mut marker_q: Query<PlaceOrMarkerQueryData, With<Marker>>,
    mut places_q: Query<PlaceOrMarkerQueryData, Without<Marker>>,
) {
    let sorted_places = (0..=3)
        .map(|ix| {
            places_q.iter().find_map(|place| {
                if place.pos() == ix {
                    Some(place.entity)
                } else {
                    None
                }
            })
        })
        .collect::<Vec<_>>();

    let mut update = |place: usize| {
        println!(
            "update {:?}",
            sorted_places
                .iter()
                .map(|entity| entity.map(|entity| places_q.get(entity).unwrap().id))
                .collect::<Vec<_>>()
        );

        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);
    }
}

@StrikeForceZero StrikeForceZero force-pushed the dev/bevy_ui/extra_hierarchy_change_handling branch from 3b08b3c to a2ff2c6 Compare November 14, 2024 01:41
@kristoff3r kristoff3r added A-UI Graphical user interfaces, styles, layouts, and widgets S-Needs-Review Needs reviewer attention (from anyone!) to move forward D-Modest A "normal" level of difficulty; suitable for simple features or challenging fixes labels Nov 14, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-UI Graphical user interfaces, styles, layouts, and widgets D-Modest A "normal" level of difficulty; suitable for simple features or challenging fixes S-Needs-Review Needs reviewer attention (from anyone!) to move forward
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants