Search Unity

Extensibility issues

Discussion in 'UI Toolkit' started by SudoCat, Jun 25, 2019.

  1. SudoCat

    SudoCat

    Joined:
    Feb 19, 2013
    Posts:
    64
    I've been playing around with UIElements for the past few weeks. First off, as someone who started their career as a web developer, I really love what y'all have made here. Making new editor tools with this has been really enjoyable.

    Unfortunately, I've been running into a few issues when trying to extend functionality in various places. Lots of the API is internal, which causes some friction depending on what you're trying to do.

    There's been two particular cases I've encountered.


    1. Trying to create new fields. To save on rewriting a lot of similar code, it seemed like a good idea to try extending from BaseField<T>, as it handles much of the labels, binding, valueChanged, focus order and such automatically. This plan was scuppered, as theBaseField<T>.visualInput is an internal member.


    2. Trying to extend binding. Boy, this one was not an easy thing to navigate. Unfortunately, the docs are still pretty sparse on extending bindings, to the point where I'm starting to question whether they were even meant to be extended, or if I'm doing things I shouldn't be.

    - All the binding event classes are internal. This makes it impossible to create a custom alternative to say, PropertyField or Inspector. Both of these classes rely on using the binding events to bind the more complicated property types, such as arrays. I was hoping to implement some custom array editors that would auto update, but this has been proving rather nightmarish when using bindings. The only way to use the binding events is via reflection, which is pretty nasty.

    - All of the IBinding implementations are private, making it impossible to interact with the binding (without reflection). Admittedly, I haven't found an explicit use case for this just yet, but it would be nice to be able to access the SerializedObject/SerializedProperty directly at times.


    Any hopes of getting these public? Or am I simply going about this the wrong way?

    EDIT: Having a further look, also appears most things to do with ViewData are also internal (e.g.
    VisualElement.OnViewDataReady()). Again, this makes it pretty hard to make custom uses of ViewData.
     
    Last edited: Jun 25, 2019
  2. antoine-unity

    antoine-unity

    Unity Technologies

    Joined:
    Sep 10, 2015
    Posts:
    780
    Hello,

    1. It's true that this is not easily feasible. Would you mind sharing an idea for a custom field that we could try to replicate and essentially go through the same pain points as yourself ?

    2. We are very aware of the situation with bindings and we are planning to do something about it for the 2020 release cycle. Mainly we will open things up in order to support both SerializedObject/SerializedProperty and a different solution in the runtime for games/apps. This should make more endpoints public with the goal to decouple elements from a specific data binding backend.

    3. We were not very happy with the state of ViewData when the API was experimental. It might make its way back at some point but we don't consider it as critical as other topics in terms of extensibility.

    I will forward this to team who might more insights about some the specific questions you have.

    Thanks for the detailed feedback.
     
    SudoCat likes this.
  3. SudoCat

    SudoCat

    Joined:
    Feb 19, 2013
    Posts:
    64
    Thanks for getting back so promptly!

    1. Sure thing. Right now I'm trying to create a field similar to enum picker, but with dynamic string values.
    In HTML terms, a <Select> element. I haven't dug too deep just yet (as said, I stumbled at the first hurdle with BaseField), but I'm presuming that implementing it the HTML way (with Option child elements) was going to be impractical (I don't think you can add type constraints to children?).
    So my plan was just to provide a list of string values to start, and allow adding comma delimited values in the XML via an "options" attribute.

    Of course, most of that functionality could be handled with an enum picker for most cases, but the lists I'll be populating it with will need to be dynamic (in this particular instance, it's selecting from a list of Playable Characters).

    2. Glad to hear it's on the horizon! Sounds like it'll be a delight once you've ironed out the details. Very stoked to make in-game UI using the tools I know and love so well.

    3. That makes sense! It looked a little rough around the edges, so I'd mostly steered clear of it for now, although it'll undoubtedly prove useful one day!
     
  4. SudoCat

    SudoCat

    Joined:
    Feb 19, 2013
    Posts:
    64
    I've just about wrapped up a simple implementation for the simple text select.

    I got everything working and running alongside other fields nicely, but there's a lot of boilerplate code (in some cases, literally copy+pasted from BaseField).

    There were also some settings configured in the BaseField constructor that I'm unable to replicate.

    Code (csharp):
    1. isCompositeRoot = true;
    2. ...
    3. ...
    4. excludeFromFocusRing = true;
    5. delegatesFocus = true;
    delegatesFocus is public, but it depends on isCompositeRoot being true, which is internal. excludeFromFocusRing is also internal.

    I haven't quite finished reading all the areas of the source code these effect, so I'm not certain what these do. From what I can see, it appears my custom field element does not have the same focus behaviour as the builtt-in ones (my label and control are receive independent focus, unlike those which extend BaseField).

    The focus behaviour on click is also not ideal.

    upload_2019-6-25_18-23-15.png

    vs

    upload_2019-6-25_18-26-59.png

    upload_2019-6-25_18-27-10.png

    Happy to share the code for this element if it will help!
     
  5. SudoCat

    SudoCat

    Joined:
    Feb 19, 2013
    Posts:
    64
    Later last night I did a bit more work on this. Most of the visual focus issues were resolved with some extra USS, but it still didn't quite match the behaviours of the built-in fields.

    Consequently I went back and tried to convert my existing SelectField class to inherit from BaseField<string>. After some faffing, this provided a much better end result, with pretty much identical behaviour to the existing one.

    The code to accomplish it wasn't quite ideal. As mentioned, with the visualInput property creates some issues, as you cannot reference it directly.

    To get around this, I used uQuery to find the element created by the BaseField class, and assigned it to a local variable in my SelectField class. This allowed me to work around the issue, but I'm not sure it's the tidiest solution.

    Here's my own implementation, not inheriting from BaseField<T>

    Code (csharp):
    1.  
    2. public class SelectField : VisualElement, IBindable, INotifyValueChanged<string>
    3. {
    4.     private readonly TextElement _textElement;
    5.  
    6.     private string[] _options;
    7.  
    8.     public IBinding binding { get; set; }
    9.     public string bindingPath { get; set; }
    10.  
    11.     private string _value;
    12.  
    13.     public string value
    14.     {
    15.         get => _value;
    16.         set
    17.         {
    18.             if (!EqualityComparer<string>.Default.Equals(_value, value))
    19.             {
    20.                 if (panel != null)
    21.                 {
    22.                     using (var evt = ChangeEvent<string>.GetPooled(_value, value))
    23.                     {
    24.                         evt.target = this;
    25.                         SetValueWithoutNotify(value);
    26.                         SendEvent(evt);
    27.                     }
    28.                 }
    29.                 else
    30.                 {
    31.                     SetValueWithoutNotify(value);
    32.                 }
    33.             }
    34.         }
    35.     }
    36.  
    37.     private VisualElement _visualInput;
    38.  
    39.     private VisualElement VisualInput
    40.     {
    41.         get => _visualInput;
    42.         set
    43.         {
    44.             if (_visualInput != null)
    45.             {
    46.                 if (_visualInput.parent == this)
    47.                     _visualInput.RemoveFromHierarchy();
    48.                 _visualInput = null;
    49.             }
    50.  
    51.             if (value != null)
    52.             {
    53.                 _visualInput = value;
    54.             }
    55.             else
    56.             {
    57.                 _visualInput = new VisualElement()
    58.                 {
    59.                     pickingMode = PickingMode.Ignore
    60.                 };
    61.             }
    62.             _visualInput.focusable = true;
    63.             _visualInput.AddToClassList(BaseField<string>.inputUssClassName);
    64.             Add(_visualInput);
    65.         }
    66.     }
    67.  
    68.     public Label LabelElement { get; }
    69.  
    70.     public string Label
    71.     {
    72.         get => LabelElement.text;
    73.         set
    74.         {
    75.             if (LabelElement.text == value)
    76.                 return;
    77.  
    78.             LabelElement.text = value;
    79.  
    80.             if (string.IsNullOrEmpty(LabelElement.text))
    81.             {
    82.                 AddToClassList(BaseField<string>.noLabelVariantUssClassName);
    83.                 LabelElement.RemoveFromHierarchy();
    84.             }
    85.             else if (!Contains(LabelElement))
    86.             {
    87.                 Insert(0, LabelElement);
    88.                 RemoveFromClassList(BaseField<string>.noLabelVariantUssClassName);
    89.             }
    90.         }
    91.     }
    92.  
    93.     public SelectField(string[] options, string label = null)
    94.     {
    95.         focusable = true;
    96.         tabIndex = 0;
    97.  
    98.         AddToClassList(BaseField<string>.ussClassName);
    99.         AddToClassList(EnumField.ussClassName);
    100.  
    101.         // Label
    102.         LabelElement = new Label
    103.         {
    104.             focusable = true,
    105.             tabIndex = -1
    106.         };
    107.         LabelElement.AddToClassList(BaseField<string>.labelUssClassName);
    108.         if (label != null)
    109.         {
    110.             Label = label;
    111.         }
    112.         else
    113.         {
    114.             AddToClassList(BaseField<string>.noLabelVariantUssClassName);
    115.         }
    116.  
    117.         // Control
    118.         VisualInput = new VisualElement
    119.         {
    120.             pickingMode = PickingMode.Ignore
    121.         };
    122.  
    123.         VisualInput.AddToClassList(EnumField.inputUssClassName);
    124.  
    125.         _textElement = new TextElement
    126.         {
    127.             pickingMode = PickingMode.Ignore
    128.         };
    129.  
    130.         VisualInput.Add(_textElement);
    131.  
    132.         _options = options;
    133.         SetValueWithoutNotify(_options[0]);
    134.     }
    135.  
    136.     public void SetValueWithoutNotify(string newValue)
    137.     {
    138.         _value = newValue;
    139.         _textElement.text = newValue;
    140.     }
    141.  
    142.     public void SetOptions(string[] options, int selected = 0)
    143.     {
    144.         _options = options;
    145.         value = _options[selected];
    146.     }
    147.  
    148.     protected override void ExecuteDefaultActionAtTarget(EventBase evt)
    149.     {
    150.         base.ExecuteDefaultActionAtTarget(evt);
    151.  
    152.         if (evt == null)
    153.             return;
    154.  
    155.         var showMenu = false;
    156.         if (evt is KeyDownEvent kde)
    157.         {
    158.             if (kde.keyCode == KeyCode.Space ||
    159.                 kde.keyCode == KeyCode.KeypadEnter ||
    160.                 kde.keyCode == KeyCode.Return)
    161.             {
    162.                 showMenu = true;
    163.             }
    164.         }
    165.         else if ((evt as MouseDownEvent)?.button == (int)MouseButton.LeftMouse)
    166.         {
    167.             var mde = (MouseDownEvent)evt;
    168.             if (VisualInput.ContainsPoint(VisualInput.WorldToLocal(mde.mousePosition)))
    169.             {
    170.                 showMenu = true;
    171.             }
    172.         }
    173.  
    174.         if (showMenu)
    175.         {
    176.             ShowMenu();
    177.             evt.StopPropagation();
    178.         }
    179.     }
    180.  
    181.     private void ShowMenu()
    182.     {
    183.         if (_options == null)
    184.             return;
    185.  
    186.         var menu = new GenericMenu();
    187.  
    188.         int selectedIndex = Array.IndexOf(_options, value);
    189.  
    190.         for (int i = 0; i < _options.Length; ++i)
    191.         {
    192.             bool isSelected = selectedIndex == i;
    193.             menu.AddItem(new GUIContent(_options[i]), isSelected, ChangeValueFromMenu, _options[i]);
    194.         }
    195.  
    196.         var menuPosition = new Vector2(VisualInput.layout.xMin, VisualInput.layout.height);
    197.         menuPosition = this.LocalToWorld(menuPosition);
    198.         var menuRect = new Rect(menuPosition, Vector2.zero);
    199.         menu.DropDown(menuRect);
    200.     }
    201.  
    202.     private void ChangeValueFromMenu(object menuItem)
    203.     {
    204.         value = menuItem as string;
    205.     }
    206. }
    207.  
    And here's the new and improved version, inheriting from BaseField<string>

    Code (csharp):
    1.  
    2.  
    3. public class SelectField : BaseField<string>
    4. {
    5.     private readonly TextElement _textElement;
    6.  
    7.     private string[] _options;
    8.  
    9.     private readonly VisualElement _visualInput;
    10.  
    11.     public SelectField(string[] options, string label = null) : base(label, null)
    12.     {
    13.         AddToClassList(EnumField.ussClassName);
    14.  
    15.         labelElement.AddToClassList(EnumField.labelUssClassName);
    16.         labelElement.focusable = false;
    17.  
    18.         _visualInput = this.Q<VisualElement>(string.Empty, inputUssClassName);
    19.  
    20.         _visualInput.AddToClassList(EnumField.inputUssClassName);
    21.  
    22.         _textElement = new TextElement
    23.         {
    24.             pickingMode = PickingMode.Ignore
    25.         };
    26.  
    27.         _visualInput.Add(_textElement);
    28.  
    29.         _options = options;
    30.         SetValueWithoutNotify(_options[0]);
    31.     }
    32.  
    33.     public sealed override void SetValueWithoutNotify(string newValue)
    34.     {
    35.         base.SetValueWithoutNotify(newValue);
    36.         _textElement.text = newValue;
    37.     }
    38.  
    39.     public void SetOptions(string[] options, int selected = 0)
    40.     {
    41.         _options = options;
    42.         value = _options[selected];
    43.     }
    44.  
    45.     protected override void ExecuteDefaultActionAtTarget(EventBase evt)
    46.     {
    47.         base.ExecuteDefaultActionAtTarget(evt);
    48.  
    49.         if (evt == null)
    50.             return;
    51.  
    52.         var showMenu = false;
    53.         if (evt is KeyDownEvent kde)
    54.         {
    55.             if (kde.keyCode == KeyCode.Space ||
    56.                 kde.keyCode == KeyCode.KeypadEnter ||
    57.                 kde.keyCode == KeyCode.Return)
    58.             {
    59.                 showMenu = true;
    60.             }
    61.         }
    62.         else if ((evt as MouseDownEvent)?.button == (int)MouseButton.LeftMouse)
    63.         {
    64.             var mde = (MouseDownEvent)evt;
    65.             if (_visualInput.ContainsPoint(_visualInput.WorldToLocal(mde.mousePosition)))
    66.             {
    67.                 showMenu = true;
    68.             }
    69.         }
    70.  
    71.         if (showMenu)
    72.         {
    73.             ShowMenu();
    74.             evt.StopPropagation();
    75.         }
    76.     }
    77.  
    78.     private void ShowMenu()
    79.     {
    80.         if (_options == null)
    81.             return;
    82.  
    83.         var menu = new GenericMenu();
    84.  
    85.         int selectedIndex = Array.IndexOf(_options, value);
    86.  
    87.         for (int i = 0; i < _options.Length; ++i)
    88.         {
    89.             bool isSelected = selectedIndex == i;
    90.             menu.AddItem(new GUIContent(_options[i]), isSelected, ChangeValueFromMenu, _options[i]);
    91.         }
    92.  
    93.         var menuPosition = new Vector2(_visualInput.layout.xMin, _visualInput.layout.height);
    94.         menuPosition = this.LocalToWorld(menuPosition);
    95.         var menuRect = new Rect(menuPosition, Vector2.zero);
    96.         menu.DropDown(menuRect);
    97.     }
    98.  
    99.     private void ChangeValueFromMenu(object menuItem)
    100.     {
    101.         value = menuItem as string;
    102.     }
    103. }
    104.  
    105.  
     
  6. antoine-unity

    antoine-unity

    Unity Technologies

    Joined:
    Sep 10, 2015
    Posts:
    780
    Hello,

    Thanks for the follow up. It seems like the PopupField<T> might implement some of the base functionality you need. It could require less work in terms of inheriting behaviour (although a non generic implementation will make it possible to use it in UXML).

    In 2019.2+ see Window > UI > UIElements Samples window and find the PopupField example in "Choice Fields".
     
    SudoCat likes this.
  7. SudoCat

    SudoCat

    Joined:
    Feb 19, 2013
    Posts:
    64
    Ah-ha! Thank you. I had been using the docs UXML elements reference page, so was unaware of that option.
     
  8. SudoCat

    SudoCat

    Joined:
    Feb 19, 2013
    Posts:
    64
    Running into some fun new extensibility limitations today!

    I'm attempting to create some new custom events, but unfortunately it seems that the propagation property and related enum have been marked internal.

    Would someone be able to explain the rationale behind this choice? Considering this property is accessed across ~20 of the built-in events, I can't see why we're not allowed to set this on our own events.

    While on the subject of events, could anyone explain the sporadically inconsistent API for adding events? For 90% of use cases, we can rely on RegisterCallback, which is lovely to use, and works great, especially with passing extra callback args. Then in seemingly arbitrary edge cases, we have to use awkward alternative solutions (Looking at you, Button.clickable.clicked!).

    This just seems... pretty bad, honestly. I think the countless questions I've read asking "how to add click action to button" really proves the point that this is an unintuitive API. I understand it's unlikely to change now, but it'd be nice to understand why it's so convoluted.
     
  9. patrickf

    patrickf

    Unity Technologies

    Joined:
    Oct 24, 2016
    Posts:
    57
    Hi!
    We are well aware of the awkwardness of the Button. I will not try to explain why it is like it is, but please know that we will try to improve it in upcoming releases.

    As for the propagation property, it is internal because we did not polish the use case where people would create their own event types. I added a task to our backlog for this.
     
    SudoCat likes this.
  10. SudoCat

    SudoCat

    Joined:
    Feb 19, 2013
    Posts:
    64
    Haha okay, I did manage to dig up a thread in here somewhere that dived into some of the implementation reasons, alas I've misplaced it now.

    Darn, that's a bugger. Well I've ended up being a filthy bastard and setting it through reflection for now.

    I do really love the new UI Elements system, but it is a darn shame that it still feels quite rough around the edges. I look forward to seeing it grow over the coming years!
     
  11. DGordon

    DGordon

    Joined:
    Dec 8, 2013
    Posts:
    649
    Just flagging that you guys should assume people will want to make custom editors for just about anything we can think of. The more control you give to us, the better. If you want to err, err on the side of giving us _too much_ control. I'd rather not have to resort to pouring through docs trying to find how to use reflection to do something that really ... should have just been made accessible in the first place.

    A good majority of what I do ends up being editor scripting to improve the workflow for whatever I can find. Its very frustrating when I'm locked out of something because Unity says so. Please give us the freedom to extend / manipulate the editor as much as we see fit, unless its required to make Unity actually work.

    Just my two cents. Thanks!
     
    SudoCat likes this.
  12. patrickf

    patrickf

    Unity Technologies

    Joined:
    Oct 24, 2016
    Posts:
    57
    I understand the frustration of finding out that something you need is an internal API. The reason we make things internal by default is that public functionality is somewhat set in stone: we cannot modify its behavior or its the function signature without breaking user code, whereas changing an internal function to public is really harmless.
     
  13. Leslie-Young

    Leslie-Young

    Joined:
    Dec 24, 2008
    Posts:
    1,148
    Having access to BaseField.visualInput seems important if we want to use BaseField<T> in custom controls since it is inserted during construction of the control.

    I just ran into an issue where I am trying to create a new control, derived from BaseField and found it is inserting this visual element which I can not access. Now I have to use the following code to get to it which means chance of it breaking is even greater if you do decide to make changes to it.

    Basically the request is, think of these controls as things your users will want to extend, both on player and editor side.

    Code (CSharp):
    1. public ColorField(string label)
    2.     : base(label, null)
    3. {
    4.     styleSheets.Add(Resources.Load<StyleSheet>(stylesResource));
    5.     AddToClassList(ussFieldName);
    6.  
    7.     labelElement.AddToClassList(ussFieldLabel);
    8.  
    9.     // set internal for some reason so I'll use this trick to get to BaseField.visualInput element
    10.     var visualInput = this.Q(className: inputUssClassName);
    11.     visualInput.AddToClassList(ussFieldInput);
    12. }
    [EDIT] Just in case someone else finds this thread. I looked at how TextField was implemented and there they pass the "visualInput" during construction. I've now changed my constructors to do the same thing and allow me to keep a reference to this element. It is a bit of a round about way of going about it since I had to create a new class and move some logic over to it but I feel much better about doing it this way than the previous one of this.Q(...) to get to the element.

    Code (CSharp):
    1. public ColorField(string label, string resetLabel = null)
    2.     : this(label, resetLabel, new ColorFieldInput())
    3. {  }      
    4.      
    5. private ColorField(string label, string resetLabel, ColorFieldInput colorFieldInput)
    6.     : base(label, colorFieldInput)
    7. {
    8.     this.colorFieldInput = colorFieldInput;
    9.     colorFieldInput.RegisterCallback<ClickEvent>(OnClickEvent);
    10.  
     
    Last edited: Jul 2, 2021
  14. Baawk

    Baawk

    Joined:
    Nov 15, 2017
    Posts:
    18
    I've hit the extensibility wall today in 2020.2.0b2 as well. For an overlay, I wanted to add an easier way to view and edit a rotation (Quaternion as euler angles, similar to how the transform inspector is doing it) and was surprised to find that there's no built-in element to do so.

    After messing around with BaseField, BindingElement and friends, I've given up and will likely revert to either just use an IMGUI container for the entire overlay, or ditch 2021.x altogether and go back to 2020.3 and custom inspectors/editors.

    My main issues were that:
    1. Due to scene view handles editing the rotation as well, I would have liked to bind them directly to the rotation of the ScriptableObject that I'm targetting. This works fine with Vector3Field and Vector3 properties, but is seemingly impossible to do with Quaternions (unless you wish to input them as xyzw). I think even if BaseField were public, because I'm trying to map a non-existing property (Quaternion.eulerAngles is not a property of the SerializedObject after all), I think it wouldn't have worked either way.
    2. SerializedObject/Property is unusable in this context, because there's no message loop with Visual Elements/overlays that I know of that could be hijacked to manually adjust the Vector3Field's values to mimick some kind of binding. As there aren't any events when the value changes (none that I know of, at least), I would have to fire some kind of event myself whenever a handle touches a rotation, for the off-chance that the overlay is currently displaying this rotation.
    Overall, this was one of my first experiences of using the new UI system, and it's been an incredibly frustrating one. It would be nice if there were a way to define a custom data binding to an object (i.e. get informed when a SerializedObject/Property changes its state and be able to adjust the VisualElement/BaseField accordingly, and vice-versa):
     
  15. uMathieu

    uMathieu

    Unity Technologies

    Joined:
    Jun 6, 2017
    Posts:
    398
    You're right that having visualInput as internal complicate things. You can workaround this limitation by creating it the visualInput in the constructor and query for it afterwards, it's clunky but it works.Otherwise, you can leave it null but assign it the BaseField.inputUssClassName uss class for styling.

    Here is the simplest QuaternionField I could think of and a demo of its usage with bindings in a custom inspector:
     
  16. uMathieu

    uMathieu

    Unity Technologies

    Joined:
    Jun 6, 2017
    Posts:
    398
    The files.
     

    Attached Files:

  17. manuelgoellnitz

    manuelgoellnitz

    Joined:
    Feb 15, 2017
    Posts:
    397
    In So many Unity packages you hit that "internal" wall when trying to modify it or just extend it with additional features. Please stop using it, use "protected virtual" instead. So we can at least extend it...
     
    a436t4ataf likes this.
  18. uMathieu

    uMathieu

    Unity Technologies

    Joined:
    Jun 6, 2017
    Posts:
    398
    As mentioned before, we do not keep things internal just for fun. Once an api is public, we have an obligation to not make any breaking change to it in the future. The proper deprecation process takes 3 LTS releases, which locks us and prevents us from being able to refactor or improve on it for a long time, or add pain to the upgrade process or every user project.

    Some internal things can be made public after a while when people ask for it, and once we're sure we don't plan any changes. The feedback you're giving us here is very valuable in that sense.
     
  19. a436t4ataf

    a436t4ataf

    Joined:
    May 19, 2013
    Posts:
    1,933
    This is hopelessly optimistic and borderline delusional. In practice, we're all using Unity's 'internal' members, on pretty much every project (via reflection, or copy/pasting the source code from the Unity class), because Unity teams aren't giving us a choice.

    None of us would keep our jobs if we turned up to work and said: "Oh, yeah, you can't have that feature in the app/game because I'd need to access an API that Unity's made 'internal'".

    Making things internal achieves nothing: it simply makes life harder for everyone, and it's even more difficult to handle when Unity inevitably changes the API since code-changes for reflection-invoked / copy-paste stuff are massively more complex than letting an IDE automatically follow an [Obsolete(...)] attribute.

    Please stop marking core, critical, public API's as 'internal'.
     
    npatch and manuelgoellnitz like this.
  20. Xan_9

    Xan_9

    Joined:
    Oct 7, 2020
    Posts:
    31
    I was able to change the X and Y labels (to Min, Max) of a Vector2Field and retain the rest of its functionality by inheriting from it directly.

    If anyone is interested:
    Code (CSharp):
    1. public class MinMaxField: Vector2Field
    2. {
    3.     public new class UxmlFactory : UxmlFactory<MinMaxField, UxmlTraits>{}
    4.  
    5.     public new class UxmlTraits : BaseField<Vector2>.UxmlTraits
    6.     {
    7.         private readonly UxmlFloatAttributeDescription m_XValue =
    8.              new(){ name = "Min value" };
    9.  
    10.         private readonly UxmlFloatAttributeDescription m_YValue =
    11.              new(){ name = "Max value" };
    12.  
    13.         public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc)
    14.         {
    15.             base.Init(ve, bag, cc);
    16.             var vector2Field = (Vector2Field)ve;
    17.             vector2Field.SetValueWithoutNotify(
    18.                  new Vector2(m_XValue.GetValueFromBag(bag, cc),
    19.                  m_YValue.GetValueFromBag(bag, cc)));
    20.         }
    21.     }
    22.     public MinMaxField() : base()
    23.     {
    24.         var minLabel = this.Q<FloatField>("unity-x-input").Q<Label>();
    25.         minLabel.text = "Min";
    26.         minLabel.style.minWidth = 25;
    27.  
    28.         var maxLabel = this.Q<FloatField>("unity-y-input").Q<Label>();
    29.         maxLabel.text = "Max";
    30.         maxLabel.style.minWidth = 29;
    31.     }
    32. }
     
    Last edited: Jan 22, 2022