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

Pane Rework and Pane Group Functionality #170

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

Clicky02
Copy link

@Clicky02 Clicky02 commented Jan 14, 2025

This PR adds Pane groups and makes some pretty heavy modifications to how the Bevy Editor UI works internally. This PR is not ready to merged yet, but I wanted to make a PR to get feedback.

Pane Groups

Pane Groups allow multiple Panes to exist in the same area. You can switch between the Panes by clicking on the corresponding header tab. Unselected panes have their container's Node::display value set to Display::None. Selected panes have the SelectedPane component added to them.

image

Pane Changes

Additionally, I made some changes to how new Pane types are created. Before, new pane types were created in Plugins like this:

pub struct TestPanePlugin;

impl Plugin for TestPanePlugin {
    fn build(&self, app: &mut App) {
        app.register_pane("Test", setup_pane);
        ...
    }
}

fn setup_pane(pane: In<PaneStructure>, mut commands: Commands) {
    // Generally, you would add a component to the pane here so you can query the pane
    ...
}

And they would be referenced/spawned like this:

// spawn_pane(commands, theme, size, pane_name);
spawn_pane(&mut commands, &theme, 0.4, "Scene Tree");

After my changes, Panes are created by implementing a Pane trait like this:

#[derive(Component)]
pub struct TestPane {
    count: u32
};

impl Pane for TestPane {
    const NAME: &str = "Test"; // used for the Pane header
    const ID: &str = "test"; // used for serilization in the future

    fn build(app: &mut App) {
        // This is run like a Plugin
    }

    fn creation_system() -> impl System<In = In<PaneStructure>, Out = ()> {
        IntoSystem::into_system(|
            pane: In<PaneStructure>,
            mut commands: Commands,
            pane_query: Query<&TestPane>| {
            ...
        })
    }
}

And they are referenced/spawned like this:

pane_group.add_pane(&theme, TestPane {count: 5})

When panes are spawned, the corresponding Pane component (TestPane in the above example) will be added to the spawned Pane entity.

I believe this has a few advantages:

  • Panes are referenced by their type instead of a string
  • Panes can be parameterized/configured at spawn (through the Pane component)
  • The pane logic is more consolidated

UI Layout Creation Changes

These changes improve the interface for creating the editor layout. Before it looked like this:

    let divider = spawn_divider(&mut commands, Divider::Horizontal, 1.)
        .set_parent(*panes_root)
        .id();

    let sub_divider = spawn_divider(&mut commands, Divider::Vertical, 0.2)
        .set_parent(divider)
        .id();

    spawn_pane(&mut commands, &theme, 0.4, "Scene Tree").set_parent(sub_divider);
    spawn_resize_handle(&mut commands, Divider::Vertical).set_parent(sub_divider);
    spawn_pane(&mut commands, &theme, 0.6, "Properties").set_parent(sub_divider);

    spawn_resize_handle(&mut commands, Divider::Horizontal).set_parent(divider);

    let asset_browser_divider = spawn_divider(&mut commands, Divider::Vertical, 0.8)
        .set_parent(divider)
        .id();

    spawn_pane(&mut commands, &theme, 0.70, "Viewport 3D").set_parent(asset_browser_divider);
    spawn_resize_handle(&mut commands, Divider::Vertical).set_parent(asset_browser_divider);
    spawn_pane(&mut commands, &theme, 0.30, "Asset Browser").set_parent(asset_browser_divider);

After these changes, it works like this:

    let mut root_divider =
        spawn_root_divider(&mut commands, Divider::Horizontal, Some(*panes_root), 1.);

    let mut sidebar_divider = root_divider.add_divider(0.2);
    sidebar_divider
        .add_pane_group(&theme, 0.4)
        .add_pane(&theme, SceneTreePane);
    sidebar_divider
        .add_pane_group(&theme, 0.6)
        .add_pane(&theme, PropertiesPane);

    let mut asset_browser_divider = root_divider.add_divider(0.8);
    asset_browser_divider
        .add_pane_group(&theme, 0.7)
        .add_pane(&theme, Bevy3dViewportPane::default());
    asset_browser_divider
        .add_pane_group(&theme, 0.3)
        .add_pane(&theme, AssetBrowserPane);

The new version does not involve manually spawning the resize handles, and intellisense will tell you all the available actions for a divider or pane group.

This was accomplished by creating a DividerCommands struct and a PaneGroupCommands struct. These are wrappers around the Commands object that define the interface by which the layout can be interacted with. If you want to use these command objects in a system, you can use the system params DividerCommandsQuery and PaneGroupCommandsQuery respectively.

@Clicky02 Clicky02 marked this pull request as draft January 14, 2025 06:14

/// Removes and despawns the given content entity.
pub fn remove(&mut self, entity: Entity) {
let i = self.contents.iter().position(|val| *val == entity).unwrap();
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should I return an error here instead of unwrapping?


/// Removes and despawns a pane in this group.
pub fn remove_pane(&mut self, pane: Entity) -> &'a mut PaneGroupCommands {
let index = self.panes.iter().position(|val| val.root == pane).unwrap();
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should I return an error here instead of unwrapping?

}

/// Removes and despawns the pane in this group at the given index.
pub fn remove_pane_at(&mut self, index: usize) -> &'a mut PaneGroupCommands {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think moving the remove logic into a command could potentially simplify/improve this struct. Is that worth implementing?

)]),
))
.set_parent(group_header)
.observe(
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it better to query for elements or just move/copy the entity handles into the observer functions? Moving the values would simplify the logic of some of these observers.

});
}

/// This is temporary, until we can load maps from the asset browser
fn initial_layout(
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the layout belong in the bevy_editor crate or in another crate? I had to move it here so that I could reference the types of each Pane.

@IwantHAPPINESS
Copy link

Cool

@JMS55
Copy link
Contributor

JMS55 commented Jan 14, 2025

Is this structured in such a way that you could eventually pull out panes from groups and move them to other areas? I believe so, but want to double check.

fn build(app: &mut App);

/// A system that should be run when the Pane is added. Should create the pane's content.
fn creation_system() -> impl System<In = In<PaneStructure>, Out = ()>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using a system here is feels weird. I would just have a function that receives PaneStructure directly as an argument. Users could then access the world via system params defined in an associated type.

Something like this:

trait Pane {
    type Param: SystemParam;
    
    fn on_create(pane: PaneStructure, param: Self::Param);
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, using a system felt a bit weird to me too, I just wasn't sure how to give them arbitrary access to the world without it. I've looked into making this change, and (if I've done everything right), the types are bit messier in practice. Here's how it needs to be defined along with an example:

trait Pane {
    type Param<'w, 's>: SystemParam;

    fn on_create(pane: In<PaneStructure>, param: StaticSystemParam<Self::Param<'_, '_>>);
}

impl Pane for Bevy3dViewportPane {
    type Param<'w, 's> = (Commands<'w, 's>, ResMut<'w, Assets<Image>>, Res<'w, Theme>);
    
    fn on_create(structure: In<PaneStructure>, param: StaticSystemParam<Self::Param<'_, '_>>) {
        let (mut commands, mut images, theme) = param.into_inner();
        // ...
    }
}

I had to use StaticSystemParam in the on_create function because the function is used in generic contexts, and I had to add the lifetime specifiers to get the SystemParam types to work.

Does this look good?

@Clicky02
Copy link
Author

Is this structured in such a way that you could eventually pull out panes from groups and move them to other areas? I believe so, but want to double check.

Yeah, you can't pull them out right now, but adding it shouldn't be difficult at all

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants