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

Improve NodeBundle ergonomics #5503

Closed
wants to merge 6 commits into from

Conversation

IceSentry
Copy link
Contributor

@IceSentry IceSentry commented Jul 31, 2022

Objective

  • NodeBundles are useful when working with complex UIs but they are very verbose, especially when you just want to use them as a wrapper element since the defaults don't work well for those cases. There's also a lot of repetitions like style: Style and color: Color.

Solution

  • Add a few impls on NodeBundle to improve ergonomics. This was highly inspired by the recent changes to TextBundle
    • NodeBundle::new: most use of NodeBundle is done with a Style and a Color we should make this simpler to do.
    • NodeBundle::container: It's often useful to be able to wrap multiple element under a single node. To do that you always need a node with a transparent color and a focus policy that let's interaction go through it. The default values don't do that in this case so the best way to fix that in a non-breaking way is to have a new constructor.
    • Self::with_style and Self::with_color: This is mostly useful when you spawn a transparent node and you want to cchange a flex property on the style. the with_color is not as useful but I don't think there's any downsides in including it.

Changelog

  • Add a few impls on NodeBundle
    • NodeBundle::new(Style, Color), NodeBundle::container(), NodeBundle::with_style() and NodeBundle::with_color()

@IceSentry IceSentry added A-UI Graphical user interfaces, styles, layouts, and widgets C-Usability A targeted quality-of-life change that makes Bevy easier to use labels Jul 31, 2022
Copy link
Contributor

@Weibye Weibye left a comment

Choose a reason for hiding this comment

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

These are good changes, I think the only thing is to find a better name for "transparent" as it doesn't capture the fact that is allows passing through input, or at least that is my intuition when I see the name.

crates/bevy_ui/src/entity.rs Outdated Show resolved Hide resolved
Copy link
Contributor

@Weibye Weibye left a comment

Choose a reason for hiding this comment

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

👍

@alice-i-cecile alice-i-cecile added the S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it label Jul 31, 2022
Copy link
Contributor

@inodentry inodentry left a comment

Choose a reason for hiding this comment

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

I like this. Approved. Just left a note about a little tweak to the docs.

crates/bevy_ui/src/entity.rs Outdated Show resolved Hide resolved
Copy link
Contributor

@sixfold-origami sixfold-origami left a comment

Choose a reason for hiding this comment

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

I like these changes quite a bit, although I think it's a bit odd that there are with_X methods for style and color, but not the other fields. While I understand that style and color are by far the most common case, I don't like the inconsistency this brings to the API.

There is a second inconsistency introduced, as the following two things are now equivalent:

.spawn_bundle(NodeBundle {
	style: Style {
		todo!()
	},
	..NodeBundle::container()
})
.spawn_bundle(NodeBundle::container().with_style(Style {
	todo!()
}))

I definitely prefer the latter pattern, but again, I don't like the inconsistency introduced. We should probably pick a convention for this, although I'm willing to merge with it as is.

I also have a few naming quibbles :p.

crates/bevy_ui/src/entity.rs Outdated Show resolved Hide resolved
@@ -39,6 +42,38 @@ pub struct NodeBundle {
pub computed_visibility: ComputedVisibility,
}

impl NodeBundle {
pub fn new(style: Style, color: impl Into<UiColor>) -> Self {
Copy link
Contributor

@sixfold-origami sixfold-origami Aug 1, 2022

Choose a reason for hiding this comment

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

While I like this method a lot, I think using the name new here is confusing. I'm used to new being the generic constructor, but this constructor is clearly meant as a "simpler, common-case constructor". Maybe a name like basic would make more sense?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I wasn't sure about that one either. It's pretty common in languages with variadics to just have a bunch of new with different overloads, but in this case the rust way would probably be something like NodeBundle::from_style_and_color() but that's way too long for something that common. I'm not a big fan of basic but right now I don't have any better idea. Maybe NodeBundle::styled().

Copy link
Contributor

Choose a reason for hiding this comment

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

I like NodeBundle::styled()

Copy link
Contributor

Choose a reason for hiding this comment

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

Me too!

@alice-i-cecile alice-i-cecile removed the S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it label Aug 1, 2022
@IceSentry
Copy link
Contributor Author

IceSentry commented Aug 1, 2022

The main reason I only used with_style and with_color is that, honestly, nobody should even be touching the other components 99% of the time. They are pretty much just implementation details at this point. Maybe the focus_policy could be added, but I don't think specifying a custom transforrms on a node should be encouraged. Also, just to be clear, I did this to copy how the TextBundle is currently handled where only a few components have a with_*

@sixfold-origami
Copy link
Contributor

The main reason I only used with_style and with_color is that, honestly, nobody should even be touching the other components 99% of the time. They are pretty much just implementation details at this point. Maybe the focus_policy could be added, but I don't think specifying a custom transforrms on a node should be encouraged. Also, just to be clear, I did this to copy how the TextBundle is currently handled where only a few components have a with_*

Ah, that makes sense. If this is how TextBundle does it, then I'm ok merging this as is. (Although it looks like both of those are going to be replaced with a generic bundle builder pattern soon.)

@IceSentry
Copy link
Contributor Author

I renamed new to styled and added a with_focus_policy for the rare cases where you would want a styled node with pass-through.

@IceSentry
Copy link
Contributor Author

bors try

bors bot added a commit that referenced this pull request Aug 1, 2022
Copy link
Contributor

@sixfold-origami sixfold-origami left a comment

Choose a reason for hiding this comment

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

Looks good to me. Nice work!

@alice-i-cecile alice-i-cecile added the S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it label Aug 2, 2022
@cart
Copy link
Member

cart commented Aug 5, 2022

There's also a lot of repetitions like style: Style and color: Color.

with_style(Style {}) not only still repeats in the same way as style: Style {}, it does it with more characters and more rust syntax.

[builder pattern]

First, the impl Default for Foo / Foo { bar: Bar, ..default()} pattern is currently the official way to define and use Bundles. A few exceptions exist, but I'm of the mind that these should be reined in. Pattern consistency is important.

I don't think we should go down the "bundle builder" road without more careful consideration. It complicates things for a number of reasons:

  • It complicates bundles conceptually: makes them more than just a flat list of fields to be set. For a given bundle, creates the question "do I use a builder pattern here"? What method should I call? Setting a field with a builder is no longer necessarily setting a field, as it could do arbitrary logic.
  • Doesn't translate to scene editors: scene editors won't give you a constructor choice (or at least, this is not a pattern you see much in other engines). You will say "give this entity this bundle" and then you will set those fields using bevy reflect / ui that ties into it. Of course, what role bundles will play in scenes is TBD, but in the event we don't use bundles for this use case, we will almost certainly adapt code based workflows to use the new concept too. Maintaining NodeBundle and NodePrefab / making people switch between them contextually is probably a bad idea.
  • More boilerplate for implementors: bundles using this pattern now need to implement builder methods for every component. Failing to do so puts limits on the construction abstraction, which imo is a non-starter. It needs to be all or nothing. Of course UX comes first and I'm happy to eat internal boilerplate in the interest of that. But Bundles are a pattern that users will use too / Bevy internals should look like user code. Should we be encouraging builder patterns in user defined Bundles? Open question, but one we should discuss.
  • Inconsistency might burn us: consistent pattern in bundles makes abstractions easier to build. Assuming Default is "the" way for bundles to be defined means that we (or the ecosystem) could build some future "entity construction DSL" in the interest of ergonomics. If we've fully embraced the builder pattern for some cases, those might not be compatible. Of course this PR doesn't remove the Default impl, so it wouldn't necessarily break things. But small things like the implicit with_color(c: impl Into<Color>) means that code written with builders wouldn't translate.

Of course, UI ergonomics and UX are a hot / high priority topic. Builders might be the solution. But given that nobody else has pushed back / tried to have this conversation, I'm going to force it :)

Some alternatives to consider (please try to come up with more):

  1. A generically useful "entity construction DSL macro". Obviously macros should be avoided as a matter of policy, but it could be opt-in, doesn't require the builder pattern / extra declaration boilerplate, builds on the existing pattern, and could yield significant wins (beyond what we're winning here). This could either be paired with (2) or support constructors (maybe parameterless only?).
  2. Different Bundles for each variant. ex: ContainerNodeBundle with their own Default impls. (see rationale for avoiding multiple constructors above). Of course, this involves more declaration boilerplate / redundancy than might be acceptable. But builders introduce lots of boilerplate and redundancy too!
  3. Embrace the builder pattern (maybe everywhere), but auto-generate the boilerplate with derives.

Note that some of these ideas are not mutually exclusive / could be adopted in combination. I also have no strong opinions yet, other than that this is a conversation worth having.

This was highly inspired by the recent changes to TextBundle

These thoughts apply to that PR as well / I would have left them there if I had been able to review it.

@cart cart added the X-Controversial There is active debate or serious implications around merging this PR label Aug 5, 2022
@IceSentry
Copy link
Contributor Author

IceSentry commented Aug 5, 2022

I have to admit, I was a bit too focused on working around the current status quo than actually make a more deliberate change for the better.

I think the biggest issue is that NodeBundle is too generic and makes some use case, like a container, much harder than it needs to be. It's also flawed because I believe the defaults are just not good for a container.

Here's a few changes and new bundles I believe would help alleviate the need for specialized constructor.

  • Yeet NodeBundle in favor of all the other bundles or keep a NodeBundle with only the bare minimum.
  • ContainerBundle:
    • No color component or at least default to Color::NONE
    • FocusPolicy::Pass
    • Only layout options, no images
  • PanelBundle:
    • Default to a solid color, I believe this default color should be a color that contrast with the default font color
    • FocusPolicy::Block
    • Optional Background image
    • This one is pretty similar to the current NodeBundle
  • ImageBundle:
    • I'm not sure how the color component interacts with images in this case, but default to Color::NONE or Color::WHITE
    • For now, should just be the same as PanelBundle, I'd like it if it was possible to not add children to an image but it's not something that bevy_ecs supports.
    • Add an alt-text component for accessibility reasons
  • BunttonBundle:
    • Should either have a Text component or be renamed to InteractiveNode
  • TextBundle:
    • We need a system to globally configure default values for TextStyle. I believe my FontRef PR is part of the solution, but we need something for the default color and size too.
    • I also think it should be possible to do TextBundle::from_str("hello bevy"). Declaring text is way too verbose right now.

These bundles having smart defaults will certainly help a bit, but it still has various issues that also needs to be addressed at some point. Using ...default() is fine, but all these bundles always need to use it because user shouldn't be touching all the Transform and visibility stuff, but it still needs to exist on the entity. This contributes a lot to the "verticalness" of ui code but I currently don't know how to fix this. Wrapping these with utility functions would be nice, but the lack of variadics or default parameter makes it really painful.

I believe there are also some issues with introducing even more bundles that contain almost the same things. It will become easier and easier to forget to remove or add a component to each of these bundles whenever we start needing even more components on those bundles. For example, some people are thinking about splitting Style in multiple components. One solution to that problem could be some sort of derive that lets you do something like this, which feels a bit like inheritance, but from a users perspective it would still be flat and they could use the same mechanism for their own complex bundle.

#[flat_bundle(NodeBundle)]
struct ImageBundle {
    image: Image,
    alt_text: String
}

Another small note, using parent in with_children forces rustfmt to add a new line. Using a single letter variable like p removes that new line and honestly feels nicer to read since it makes it easier to ignore but that's a lot more controversial and doesn't need any change in bevy.

A few more future possibilities that would make UI code simpler to build:

  • Make sure every thing ui related can be defined as an asset file and use a text format that's terser than rust to define those templates. Unfortunately, this isn't nearly enough because you generally want some form of scripting when declaring interactive UI.
  • A solution to that problem would be the ability to declare an asset file inside a rust macro. This would let you create templates of smaller reusable components directly in the code. At this point we pretty much got react in bevy. You can use rust for any behaviour and just use that macro for declaring small trees of ui nodes.
  • If we go that route, it would be really nice if that macro could somehow be tied to the hot reload feature of the asset server and be able to be reloaded when the rust file is saved without recompiling everything if only the content of the string changed. This is either very complicated or impossible, but I think it's worth investigating.
  • As for a DSL macro, I wanted to go that route, but I assumed it was a complete no-go even if it was optional. Since you at least talked about it it makes me interested to at least try to create a third party macro to experiment with that.

This got a bit out of scope of the original PR, but as I said at the start, I was admittedly too focused on improving a very limited subset of the overall problem. I have a bunch more issues that makes abstracting bundle construction in user code hard, but this is too out of scope.

@Weibye
Copy link
Contributor

Weibye commented Aug 8, 2022

Of course, UI ergonomics and UX are a hot / high priority topic. Builders might be the solution. But given that nobody else has pushed back / tried to have this conversation, I'm going to force it :)
@cart

The feedback is extremely valuable and the push-back is valid, so thank you :)

I have to admit, I was a bit too focused on working around the current status quo than actually make a more deliberate change for the better.
@IceSentry

I got to admit I'm guilty of the same. @IceSentry, your ideas of improving the bundle situation is good I think, and we should extract them into an issue for later.

I do believe that in order to verify the approach and confirm if this is indeed the best approach we need to step back and get a better understanding of our high level targets and where we want to end up.

I've opened the discussion #5604 for this reason and I would love both of your input there.

inodentry pushed a commit to BroovyJammy/bevy that referenced this pull request Aug 19, 2022
@cart
Copy link
Member

cart commented Sep 13, 2022

I think I'm going to close this out until we come to a consensus on direction. Not trying to shut down the conversation, just trying to filter attention. Feel free to respond here if you want this reopened.

@cart cart closed this Sep 13, 2022
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 C-Usability A targeted quality-of-life change that makes Bevy easier to use S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it X-Controversial There is active debate or serious implications around merging this PR
Projects
Status: Done
Development

Successfully merging this pull request may close these issues.

6 participants