Replies: 8 comments 6 replies
-
I like this! I think the emphasis on headless + controlled are both pretty good call outs!
Another benefit I've found (working with Remix.js on the web) is that when you're using forms, uncontrolled components can basically "look after" themselves, as the form does all the heavy lifting in terms of using the data from inputs. That being said, I don't see a way Bevy could easily do this or any practical use cases compared to the controlled variants. |
Beta Was this translation helpful? Give feedback.
-
I'm looking forward to this :) Would Are core widgets mutually exclusive or can you combine them? |
Beta Was this translation helpful? Give feedback.
-
When talking about headless, does it mean a visually basic widget with knobs to change appearance or having "abstract" widgets (widget logic without UI)? The controlled/uncontrolled part confuses me a little bit… I think I understand the difference: source-of-truth resides outside the widget vs. the widget is the source-of-truth. So for a checkbox, something like this?
I suppose that what confuses me is why/when would someone want uncontrolled widgets by default? uncontrolled sounds like to problems to me… and you loose one very nice property of the controlled widgets model: having several widgets in different parts of the widgets tree update to the same source of truth. This sounds like reactive vs. imperative debate in disguise. ^_^ |
Beta Was this translation helpful? Give feedback.
-
I really like the idea of headless widgets, it will give the developers a lot of freedom on how to render their UI elements if they want to do some unique things (like Persona levels of creativity).
|
Beta Was this translation helpful? Give feedback.
-
@MiniaczQ Certainly
Core widgets are building-blocks for constructing more elaborate widgets. In the example you give, the "combo slider" widget is a third-party widget that normally has a slider, but spawns a text edit widget when it's needed. These "combo" widgets would often as not be implemented as BSN templates in a library. @doup The way I envision this is that the text widget emits "10a23" (to use your example), and the observer callback runs a validation on it. Since it's not a valid number, it can do one of several things:
Now, I have to admit that there's one aspect about this that I don't know: how to handle cursor movement in the error case. That is, inserting a key not only changes the text but also changes the cursor location. When the user types an invalid digit which is then ignored, what happens to the cursor? React handles this, but I'm not sure how - when you get an "on_change" message from a text input field, that contains the new text, but it does not contain a new cursor location. Nor would you want it to; cursor location and selection range is a complexity that callers should not have to care about.
Uncontrolled is potentially simpler, especially to a novice user, and if you aren't doing any kind of validation on the input. The widget is self-sustaining, and you only get notification events after a change has occurred. The difference is really a difference in mindset, I think, and if you are in the habit of using controlled widgets you don't need uncontrolled. |
Beta Was this translation helpful? Give feedback.
-
I want to talk about my vision for Bevy UI in general. A while back, I did an evaluation of a bunch of different React-based widget libraries for a project I was working on:
As well as a bunch of "headless" frameworks:
That's a lot of choices, and one might make the argument that it's too many, that there's chaos and confusion in having so many competing solutions. However, I think that having a robust set of choices is a healthy sign, that there should be no "one widget library to rule them all". Each of these libraries has a different aesthetic - some consumer-oriented like MUI, some enterprisey like Fluent, others funky and offbeat like Rebass. And yet, every one of these toolkits is works with the same underlying framework (React), and many of them share common tooling like the CSS-in-JS framework Emotion. Of course, games thrive on novelty, and there's a tendency for every game to define it's own unique aesthetic, which argues against any kind of standard widget toolkit. But I don't think this necessarily has to be the case for all games, and Bevy is used for more than just games. Having a set of standard headless widgets makes it easier to support this kind of rich ecosystem. |
Beta Was this translation helpful? Give feedback.
-
I have a demo of the "core slider" widget. Both the "combo slider" and "gradient slider" widgets in the screenshot below are using the same core slider component: Here's the code: https://github.com/viridia/bevy_reactor/blob/main/crates/bevy_reactor_obsidian/src/controls/core_slider.rs |
Beta Was this translation helpful? Give feedback.
-
I did a bit more research on how React handles the updating of selection on text input. The basic problem is that React's on_change message contains only a text payload, not a cursor position or selection range, so when you set the text on a controlled widget React has to guess where the cursor is going to go. Apparently, React assumes that when it emits an on_change event, the receiver is going to update the text right away (like, synchronously), and makes certain assumptions around that - to the extent that if you wait and set the state later, it puts the cursor at the end of the text, which is a footgun that some people have run into. This is a bit too kludgey for my taste, so I propose something different: when the controlled text input emits a text change event, the event contains both the updated text and a selection range (start index..end index), where a zero-length range is a normal cursor. The receiver is responsible for passing both of these pieces of information back to the widget. Note that even though selection range is included in the change event, merely moving the cursor around does not trigger an event; you only get a snapshot of the cursor position when the text actually changes. |
Beta Was this translation helpful? Give feedback.
-
As many of you know, I've been involved with a lot of discussions w/r/t reactivity on the Discord in the past year, and more recently have been having a lot of discussions with @cart about the future of reactivity in BSN. One topic that has come up is the idea that Bevy should have a core set of widgets (
Button
,TextInput
,Slider
, etc.) that are able to mesh seamlessly with BSN.At the same time, I recognize that not everyone is sold on the idea of reactivity. That's fine. In fact, I think that these core widgets be written in such a way that they can be used non-reactively; the rest of this post outlines how that would work.
Each widget starts with a key component (
Button
,Slider
,TextInput
and so on), which holds the state of the widget. These components are unopinionated about the visible appearance of the widget - theSlider
component doesn't care whether the "thumb" is a sprite, a customUiMaterial
, or an absolutely-positioned child. What the slider component does provide is the mechanics of pointer events and the math for computing track positions. Actually drawing the slider can be done by a user-supplied (or more likely, library-supplied) system or observer which examines the current widget state.Core widgets are the equivalent of "headless" widgets in the web world. The one exception is the text input widget, which will render editable text (in a configurable color, with configurable cursor and selection colors), but will be unopinionated about border, background and other adornments such as a "search" or "clear" button. The one other feature it will need to support is password mode, since that requires changing how text drawing works.
Each of these core widget components "communicates" with the outside world using two channels:
Note that reactive frameworks may use different methods of communication, such as signals and callbacks. This can be accommodated by adding an additional "adapter" component which bridges between the reactive and non-reactive worlds. For example, a reactive slider will subscribe to the on_change event and trigger the appropriate callbacks or one-shot systems; it will also react to changes in signal data and update the slider state accordingly.
Core widgets are meant to be the basic building-blocks of more complex widgets with additional features. There's only a handful of core widgets, maybe a half dozen total. Third-party crates will be responsible for supplying a rich set of advanced, opinionated widgets that are consistent with particular visual styles and themes, which are built on top of the core widgets. Widgets that are purely aesthetic, like page headers or avatars, will only live in third-party crates and not be part of the core widget set.
Take for example a "blender slider": a slider widget that works like the one in Blender. This has a number of features on top of a basic slider:
All of these features can be built as a layer on top of the core slider component; none of them need to be built in to the core component directly.
Similarly, the core text input widget does not need to support integer / float / email / phone number / hex color formatting and validation, or click-drag to increment & decrement - all of these can be layered on top, so long as the widget allows the right set of events to propagate upward to its parent. For example, a single-line text input will normally call propagate(false) on most keystrokes, but arrow up and arrow down are allowed to propagate; this allows a higher-level widget to implement something like increment / decrement on a numeric value.
Controlled vs. uncontrolled: In the React world, a "controlled" widget is one that does not update its own state. For example, a controlled checkbox emits an "on_change" message when clicked, but does not change itself. Instead, it's the responsibility of the caller to receive the "on_change" event and update the source of truth for the checkbox state. Since the "on_change" event contains a copy of the proposed new state, updating the widget is simply a matter of copying the state from the event to the widget.
An uncontrolled widget, on the other hand, is one where the widget has its own state which is updated automatically by the widget when the user interacts with it.
React allows the option of both controlled and uncontrolled widgets, but many UI developers prefer to use controlled widgets exclusively because of their versatility. Controlled widgets are especially good for situations such as a text input field which can be edited manually but also changes programmatically: for example, a hex color field which also changes when the color sliders are dragged. It means that instead of having to synchronize the state of the hex color field with the color sliders, there is always a single source of truth for the current color.
Uncontrolled widgets, on the other hand, can have better performance, especially when the widget state is large and complex (such as a widget intended to edit large documents).
For small, simple widgets such as sliders and checkboxes, it is relatively easy to transform a controlled widget to an uncontrolled once, simply by adding an observer which listens for the
on_change
event. Converting an uncontrolled widget into a controlled one is much more difficult, because it requires subtracting functionality.My proposal is that all of the Bevy core widgets be controlled by default, or a least have an option to be controlled: so for example, when you drag the slider, it emits change events but does not actually modify the slider value.
Disabled widgets: All of the core widgets will support a "disabled" state which is indicated by inserting an "InteractionDisabled" marker component (so as to distinguish it from the existing "Disabled" component already in Bevy, which has a different effect). This will be a signal that the widget should be rendered in a grayed-out state.
Core widgets will have built-in support for accessibility, and will automatically insert the appropriate ARIA components, and dynamically update them in response to state changes.
@alice-i-cecile @UkoeHB @NthTensor
Beta Was this translation helpful? Give feedback.
All reactions