How does one create a group of Radio Controls using UIElements? IMGUI has this feature and is able to represent a Toggle as either a Radio or a Checkbox. It also supports the ability to logically group checkboxes together.
There is nothing out of the box, however here is an implementation of a element that will scan all its descendant toggle elements and create a radio button behavior.
// This will catch all toggle change events and add a radio button behavior to them
// It also provides setting/getting the index of the currently selected toggle.
// No styling provided
public class RadioButtonGroup : BindableElement, INotifyValueChanged<int>
{
public new class UxmlFactory : UxmlFactory<RadioButtonGroup, UxmlTraits> {}
private UQueryState<Toggle> toggleQuery;
public RadioButtonGroup()
{
//we cache this query to avoid gc allocations each time we need to run it.
toggleQuery = this.Query<Toggle>().Build();
//This means we get notified of all ChangeEvents<bool> in our descendants
RegisterCallback<ChangeEvent<bool>>((evt) => OnToggleChanged(evt));
}
public int SelectedIndex
{
get
{
//We find the first Toggle
int index = -1;
bool found = false;
toggleQuery.ForEach((toggle) =>
{
if (!found)
{
++index;
found = toggle.value;
}
});
if(found)
return index;
return -1;
}
set
{
int index = -1;
toggleQuery.ForEach((toggle) =>
{
++index;
if (index == value)
{
toggle.value = true;
}
});
}
}
void OnToggleChanged(ChangeEvent<bool> evt)
{
Toggle t = evt.target as Toggle;
if (t != null)
{
if (evt.newValue)
{
//User selected a new toggle, we need to disable all the others
int index = -1;
int newValue = -1;
int previousValue = -1;
toggleQuery.ForEach((toggle) =>
{
++index;
if (ReferenceEquals(t, toggle))
{
newValue = index;
}else
{
if (toggle.value)
{
previousValue = index;
}
toggle.SetValueWithoutNotify(false);
}
});
evt.StopPropagation();
using (var newEvent = ChangeEvent<int>.GetPooled(previousValue, newValue))
{
newEvent.target = this;
SendEvent(newEvent);
}
}
else if(evt.previousValue)
{
//You can't unselect the currently selected toggle
evt.StopPropagation();
t.SetValueWithoutNotify(true);
}
}
}
void INotifyValueChanged<int>.SetValueWithoutNotify(int newValue)
{
SelectedIndex = newValue;
}
int INotifyValueChanged<int>.value
{
get => SelectedIndex;
set => SelectedIndex = value;
}
}
INotifyValueChange is the base interface for fields that contains a typed value and send ChangeEvent when value changes. This was a bit overkill for this example⌠so I edited the code so that the class inherits from BindableElement. Implementing IBindable and INotifyValueChange allows the element to be bound to SerializedObject/SerializedProperties.
In the original case, the since the UxmlTraits refered to the VisualElement one, it was redundant. However, now, it exposes the BindableElement attributes to UXML
UQuery is a way to search for elements in a hierarchy by creating uss selectors from code. The QueryState is a saved selector, applied on a root element. Special care was taken to avoid gc allocations while iterating and evaluating the queries. If the basic selectors + ForEach are now enough, you can still use ToList() then have fun with LINQ
Again, this is made to avoid gc allocations. Since events are often used but only live for a short time, we use an object pool. This is not mandatory, allocating them with new will work, it just puts more strain on the garbage collector.
The only documentation we have for now is the rather short development guide:
and the scripting api documentation:
Weâre currently working with the documentation team to improve this. In the meantime donât hesitate to ask questions here.
you are still creating GC because of the lambda closures (they capture locals, this requires at least 2 heap allocations â one for the closure and one for the delegate)
you could add IndexOf and similar APIs for these use cases, or implement IEnumerable<T> to allow foreach (var e in query) {...} (compiler can foreach without boxing if the IEnumerable pattern is fully visible to it)
example foreach without GC
struct a {
public b GetEnumerator() {return new b();}
}
struct b {
public int Current {get;set;}
public bool MoveNext() {return true;}
}
public static void Main()
{
foreach (var c in new a()) {
Console.Write("ok");
break;
}
}
How does the binding system work? What are the inputs/outputs? What connections are made?
I have no idea how to link what youâve written to the [not terribly helpful] documentation.
What? What does that sentence even mean?
What are BindableElement attributes? How are they different from VisualElement ones? If youâre extending the BindableElement class wouldnât it still be redundant to specify the UXMLTraits there due to the fact that one would assume that the BindableElement class would have already handled that internally? Or is this necessary because weâre totally shadowing the internal version? If itâs the shadowing thing, isnât this a bit of a code smell thing? Itâs not terribly discoverable - youâd kind of just have to âknowââŚ
Got it.
Some comments in that code snippet about what youâre actually doing and why would be very helpful, I think - particularly for anyone else who happens to say âhow do you do a radio button with UIElementsâ. Lots of learning opportunities in there and without the comments, youâre just kind of left scratching your head about what the code is and does and why any of it is necessaryâŚ
Using this in live code (for UIToolkit that was launched without this core class) I discovered that - of course! - it corrupts Unity prefabs: I think because it doesnât actively do anything with the binding part. Iâm currently experimenting with reverse-guessing WTF is needed to make binding work (from my previous investigations into the Binding docs, that were only published this year, more than two years after this thread (!)), hoping itâs straightforward.
TL;DR: do not use the original code snippet provided at top of page, it will corrupt your prefabs (they donât save properly, and they actively overwrite (randomly) ârealâ values with fake ones. I believe itâs something with serialized data getting pre-empted. It looks to me like it can be easily fixed by simply adding the missing calls to Bind() in the right places. If you can figure out exactly what those are (as far as I know: tehre is still literally no way to debug Binding problems?)
I appear to have got it working, by taking the example code in the new docs and extrapolating in obvious ways.
However this now triggers an annoying dumb (dumb because: it seems to be wrong: all the code is correct, everything is working (where it didnât before!), so UIToolkit is generating a fake error case here?) message from UIToolkit, a âWarningâ that spams the console - âField type is not compatible with Enum propertyâ - which appears to be this: EnumField is not compatible with Enum property
Iâm 99% sure that one of the most common cases for RadioButtonGroup is to represent an Enum (I mean ⌠thatâs pretty much literally what it is: a visual representation of a multi-value type where only one value can be active at once). But apparently some (undocumented?) magic is required to make UIToolkit stop complaining that youâre not using a âUIToolkit blessedâ EnumField. Presumably something can be implemented to make this message go away - any ideas?