focustense / StardewUI

UI/widget library for Stardew modding
MIT License
5 stars 1 forks source link

Support tab groups in StarML #42

Open focustense opened 1 week ago

focustense commented 1 week ago

Tabs have Group and GroupKey properties they can use to "self-organize" and ensure only one tab is active at once. However, this requires a model type, SelectionGroup, that doesn't have an obvious conversion from any "standard" type.

At first it seemed like it would be nice to be able to just bind to a SelectedTab property (string, enum value, etc.) but a single value cannot convert to a shared SelectionGroup; for the feature to work, all tabs must refer to the same group instance.

One way to work around it could be to define a new type of IValueConverter and IAttributeBinder that works on the value source (IValueSource<T>) which, with a few little hacks, is technically capable of synchronizing both ways without a bunch of kludge in either the model or markup. However, it's ugly in terms of architectural design and also ugly in terms of user consistency; what happens when the group is a literal attribute instead of a context binding? It's going to "feel" like it "should" work just putting a hardcoded group name in there for a bunch of tabs, but it won't.

Other options could be to define a structural *attribute or pseudo-tag like <include> to set up a group. But these fall short as well, because they don't have any obvious or intuitive property to target. Maybe they target the string or enum property and then slap together something with ConditionalWeakTable, but again we run into the issue that the semantics are brittle and unintuitive; it won't work with literal values.

Final possibility I can think of, which is far less elegant internally but perhaps a lot friendlier to use on the outside, is an actual <group> widget that collects all groupable elements inside and sets their group. The implementation of this might not be pretty if it's meant to propagate more than one level deep, which it very likely should. Nevertheless, it could be the most intuitive markup-wise because the usage story is just not to set any group on the tabs, i.e.

<group selected-key={SelectedTab}>
    <lane>
        <tab group-key="Foo" />
        <tab group-key="Bar" />
    </lane>
</group>

etc. The hypothetical Group widget would simply be a DecoratorView for whatever goes inside it, usually a <lane>. The widget itself is responsible for creating the SelectionGroup, and propagating it down to descendants. Or, alternatively, setting some sort of ambient value that descendants in the update scope are able to pick up. And here it is very obvious that selected-key is supposed to be bound to something and obviously won't work correctly if it's just a literal.

That said, if we go with the "ambient selection group" model, which is a lot of spooky action at a distance and I don't love it, there is still a possibility of getting to something a little cleaner with a structural attribute:

<lane *group="GroupName">
    <tab group-key="Foo" />
    ...
</lane>

This works, or might work, because it's the ViewNodeFactory that handles all these structural attributes anyway and so the SelectionGroup can simply be attached to the node, and then passed down to descendants using whatever scoping mechanism. It's different from using CWT to attach to a literal string or enum value. It technically works regardless of binding type because in fact the value is meaningless; the group doesn't really have a name or target, really all we care about is the *group part but attributes have to have a value in the grammar.

So to summarize... no real obvious, elegant solution, just a lot of annoying tradeoffs, which is why it's not going into M3, but I'd like to pick it up at some point later. I don't think it's valuable enough to justify delaying M3 because it's not really that hard to implement exclusivity in the data model; but it would make things more convenient in the long run.