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

Implement ValueSource #3012

Open
mhsmith opened this issue Dec 3, 2024 · 5 comments
Open

Implement ValueSource #3012

mhsmith opened this issue Dec 3, 2024 · 5 comments
Labels
enhancement New features, or improvements to existing features.

Comments

@mhsmith
Copy link
Member

mhsmith commented Dec 3, 2024

ValueSource itself is already implemented, documented and tested, but there's no way to link it to any other component.

This would be required to support the Invent datastore. There's a sketch of how that might work here:

Any implementation of this feature is likely to involve replacing all the widget @properties with a custom class. Such a class could also be used to store metadata for an interactive GUI builder, such as:

  • A list of possible choices for the property, to generate a drop-down menu
  • An icon to represent the property

Related:

@mhsmith mhsmith added the enhancement New features, or improvements to existing features. label Dec 3, 2024
@HalfWhitt
Copy link
Contributor

An alternative would be to leave widget properties essentially as they are, but give a value source the ability to link to and modify any arbitrary attribute of any object, or at least of any widget.

Something along the lines of

a_source.link_to(a_widget, "attribute_name")

Or perhaps more like

link(a_source, a_widget, "attribute_name")

Would probably be most ergonomic to give widget a convenience method that then calls the above, like

a_widget.link_source("attribute_name", a_source)

However it's phrased, the source would keep a list of things it's connected to, and update them whenever it's changed.

@freakboy3742
Copy link
Member

The root of the issue is the source of truth.

In other source-based widgets (tree, table), the source is the source of truth, and the widget renders that source of truth. Change the source and the widget is informed of the need to re-render. If there are multiple widgets using that source, they're all notified.

However, in other widgets (TextInput is a good example), the widget's value is the source of truth. That's problematic for a source-based implementation, because you can't tie a second widget to the first widget's internal state (or, if you do, you risk notification cascades - changing one widget changes the source, which is an event that changes the second widget which is an event that changes the source, and so on).

The other key difference is that Toga currently differentiates between the "value" of a widget (i.e., the data actually being displayed) and all other properties of the widget. As I understand it, Invent doesn't make this distinction - all properties of the widget are essentially treated as being "source controlled". So, as an example, the "enabled" status of a widget is a property that could be controlled by the source of another widget. A switch widget could be used literally to control the enabled status of a text input with nothing more than "plugging the wires together", as it were.

The risk with these sorts of changes is that you end up with the possibility of diverged state - where the physical state of the widget doesn't reflect the value that the source thinks the widget has. This should be resolvable; we just need to be careful to get the details right. And most of the details will be exactly the same - the main difference is that any get_* property (e.g., get_enabled()) on a backend implementation of a widget needs to be replaced with a signal monitoring that property which notifies the source of the change, with some mechanism to prevent being "self-notified" of a change.

Ironically, Toga has been down this path in the past - one of the big changes in the 0.3 release was moving to "widget as source of truth" for most widget; with a handful of widgets (like NumberInput) retaining some core-side representation of value because of issues with round-tripping values in the backend.

@mhsmith
Copy link
Member Author

mhsmith commented Dec 4, 2024

you can't tie a second widget to the first widget's internal state (or, if you do, you risk notification cascades - changing one widget changes the source, which is an event that changes the second widget which is an event that changes the source, and so on).

Invent currently avoids this because JavaScript events are not fired on programmatic changes. But Toga consistently fires events on programmatic changes on all platforms, with the backend generating the event itself if the native layer doesn't already do so.

Toga currently differentiates between the "value" of a widget (i.e., the data actually being displayed) and all other properties of the widget.

I'm not sure what you mean by this: how is the "value" property different from the others?

the main difference is that any get_* property (e.g., get_enabled()) on a backend implementation of a widget needs to be replaced with a signal monitoring that property which notifies the source of the change, with some mechanism to prevent being "self-notified" of a change.

Since the native widget always exists but the ValueSource is optional, I'd prefer to keep the native widget as the source of truth for its own properties if we can, with the ValueSource being a layer on top. In that case, the "self-notification" could be prevented by making the ValueSource take no action if set to its existing value.

A couple of other notes:

  • The change notification mechanism can be implemented on top of Toga's existing event handlers, as long as we impose the restriction that any given property can either have an explicit event handler or a ValueSource binding, but not both. Since the ValueSource is effectively an event handler already, I think this is acceptable.

  • There are issues with binding properties of different types to the same ValueSource. For example, Invent already supports binding a Slider and a Label to the same source, and the Label will display the value of the Slider. But this is only possible because the Label doesn't generate any events of its own. If you instead linked a Slider to a TextInput, it would be unclear whether the type of the value was a number or a string. I guess this would require a custom ValueSource subclass to convert the types as desired.

@HalfWhitt
Copy link
Contributor

HalfWhitt commented Dec 4, 2024

If you instead linked a Slider to a TextInput, it would be unclear whether the type of the value was a number or a string. I guess this would require a custom ValueSource subclass to convert the types as desired.

One of my (not terribly common) fond memories of using QML in Qt was the ability to set the value of basically anything as dynamic. Like something as simple as width = 2 * height would mean that width would remain twice the height, updating automatically when height was changed. Of course, it had the advantage of being an entire DSL.

Anyway, I doubt Toga could feasibly do anything quite so magical (especially not that example, since I don't think actual, calculated sizes are even exposed), but my point in this case is that the ability to modify the received value when the source updates is useful, and if there were a way to supply a callable when setting up a binding, one could supply something like lambda x: 2 * x. Or, in the situation you mention, str or int. Or even lambda x: f"It's {x}º outside" for a label, say.

@freakboy3742
Copy link
Member

you can't tie a second widget to the first widget's internal state (or, if you do, you risk notification cascades - changing one widget changes the source, which is an event that changes the second widget which is an event that changes the source, and so on).

Invent currently avoids this because JavaScript events are not fired on programmatic changes. But Toga consistently fires events on programmatic changes on all platforms, with the backend generating the event itself if the native layer doesn't already do so.

Agreed - I suspect we may need to add something like a "notification source" to the notification mechanism, with notifications not being reflected back to the widget that created them.

Toga currently differentiates between the "value" of a widget (i.e., the data actually being displayed) and all other properties of the widget.

I'm not sure what you mean by this: how is the "value" property different from the others?

I mean in the sense of things like places where source can be used. For example, you can use a source for the value of a table, but readonly is a property of the widget.

I guess you could argue that distinction is mostly in my head, and that this is really a manifestation of "not using ValueSource everywhere it could be".

Since the native widget always exists but the ValueSource is optional, I'd prefer to keep the native widget as the source of truth for its own properties if we can, with the ValueSource being a layer on top. In that case, the "self-notification" could be prevented by making the ValueSource take no action if set to its existing value.

I agree that this would minimise the number of changes required; my concern is that it will be prone to "Widget has a different value to the source"-style bugs, because the "source of truth" becomes a concept, rather than literally the only place you can get the value. I guess we can protect against this with testing, but it still makes me nervous.

A couple of other notes:

  • The change notification mechanism can be implemented on top of Toga's existing event handlers, as long as we impose the restriction that any given property can either have an explicit event handler or a ValueSource binding, but not both. Since the ValueSource is effectively an event handler already, I think this is acceptable.

I think I can see what you're describing here; from a functionality perspective, it concerns me a little, as it seems like the sort of thing that could be easy to misunderstand/misconfigure. From an external API perspective, it won't necessarily be obvious that because you've added a source, custom event handlers will no longer work (or vice versa).

This is possibly ratholing on the implementation, but I suspect that in order to manage this, we'll need to put the tools in place so that every property is implicitly a source under the hood; that will also mean that we can rely on a source existing for every data value, so we should be in a position to tie user-defined event handling and data-source handling into the same wrapper.

  • There are issues with binding properties of different types to the same ValueSource. For example, Invent already supports binding a Slider and a Label to the same source, and the Label will display the value of the Slider. But this is only possible because the Label doesn't generate any events of its own. If you instead linked a Slider to a TextInput, it would be unclear whether the type of the value was a number or a string. I guess this would require a custom ValueSource subclass to convert the types as desired.

Agreed - I suspect "value" will need to have some basic typed variants that also do validation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New features, or improvements to existing features.
Projects
None yet
Development

No branches or pull requests

3 participants