bevy
bevy copied to clipboard
Improve NodeBundle ergonomics
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 likestyle: Styleandcolor: Color.
Solution
- Add a few impls on
NodeBundleto improve ergonomics. This was highly inspired by the recent changes toTextBundleNodeBundle::new: most use ofNodeBundleis done with aStyleand aColorwe 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_styleandSelf::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
NodeBundleNodeBundle::new(Style, Color),NodeBundle::container(),NodeBundle::with_style()andNodeBundle::with_color()
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_*
The main reason I only used
with_styleandwith_coloris 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 thefocus_policycould 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 theTextBundleis currently handled where only a few components have awith_*
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.)
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.
bors try
try
Build succeeded:
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):
- 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?).
- Different Bundles for each variant. ex:
ContainerNodeBundlewith 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! - 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.
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
NodeBundlewith only the bare minimum. ContainerBundle:- No color component or at least default to
Color::NONE FocusPolicy::Pass- Only layout options, no images
- No color component or at least default to
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::NONEorColor::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
- I'm not sure how the color component interacts with images in this case, but default to
BunttonBundle:- Should either have a
Textcomponent or be renamed toInteractiveNode
- Should either have a
TextBundle:- We need a system to globally configure default values for
TextStyle. I believe myFontRefPR 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.
- We need a system to globally configure default values for
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.
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.
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.