Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

UIElements needs a High Level API - So I built one!

Discussion in 'UI Toolkit' started by Rocktavious, Jan 21, 2019.

?

Would you use this library?

  1. Yes

  2. No

Results are only viewable after voting.
  1. Rocktavious

    Rocktavious

    Joined:
    May 10, 2017
    Posts:
    44
    Hey All,

    I'm loving the new UIElements system, but after diving in and applying it to some real world cases i found myself writing a HUGE amount of boiler plate from editor to editor. So in my "engineer" ways i set out to write some library code to make it easier and faster to write or convert existing code to UIElements. Its still early days for the library, but i'm opensourcing it to allow others to leverage it, commit back, add features etc. Lets all build new editor tools in UIElements!!!! (and then eventually in-game UI)

    https://github.com/rocktavious/UIEX

    I've tried to document the library in the main readme pretty thoroughly so you can get up and running quickly.

    Just to give you a short example of the power - here are two examples of UIElements c# code - one without the framework and one with the framework.

    Without:
    Code (CSharp):
    1. using System.Collections.Generic;
    2. using UnityEngine;
    3. using UnityEditor;
    4. using UnityEngine.Experimental.UIElements;
    5.  
    6. namespace RedOwl.Demo
    7. {
    8.     public class PanManipulator : MouseManipulator
    9.     {
    10.         private Action<Vector2> callback
    11.         private Vector2 _mouseStart;
    12.         private bool _active;
    13.  
    14.         public PanManipulator(Action<Vector2> callback, params ManipulatorActivationFilter[] filters) : base()
    15.         {
    16.             base()
    17.             foreach (var filter in filters)
    18.             {
    19.                 activators.Add(filter)
    20.             }
    21.             this.callback = callback;
    22.             _active = false;
    23.         }
    24.      
    25.         protected override void RegisterCallbacksOnTarget()
    26.         {
    27.             target.RegisterCallback<MouseDownEvent>(OnMouseDown);
    28.             target.RegisterCallback<MouseMoveEvent>(OnMouseMove);
    29.             target.RegisterCallback<MouseUpEvent>(OnMouseUp);
    30.         }
    31.  
    32.         protected override void UnregisterCallbacksFromTarget()
    33.         {
    34.             target.UnregisterCallback<MouseDownEvent>(OnMouseDown);
    35.             target.UnregisterCallback<MouseMoveEvent>(OnMouseMove);
    36.             target.UnregisterCallback<MouseUpEvent>(OnMouseUp);
    37.         }
    38.      
    39.         protected void OnMouseDown(MouseDownEvent evt)
    40.         {
    41.             if (_active)
    42.             {
    43.                 evt.StopImmediatePropagation();
    44.                 return;
    45.             }
    46.  
    47.             if (CanStartManipulation(evt))
    48.             {
    49.                 _mouseStart = evt.localMousePosition;
    50.                 _active = true;
    51.                 target.CaptureMouse();
    52.                 evt.StopPropagation();
    53.             }
    54.         }
    55.  
    56.         protected void OnMouseMove(MouseMoveEvent evt)
    57.         {
    58.             if (!_active || !target.HasMouseCapture()) return;
    59.             callback(evt.localMousePosition - _mouseStart);
    60.             _mouseStart = evt.localMousePosition;
    61.             evt.StopPropagation();
    62.         }
    63.  
    64.         protected void OnMouseUp(MouseUpEvent evt)
    65.         {
    66.             if (!_active || !target.HasMouseCapture() || !CanStopManipulation(evt)) return;
    67.             _active = false;
    68.             target.ReleaseMouse();
    69.             evt.StopPropagation();
    70.         }
    71.     }
    72.  
    73.     public class Demo : EditorWindow
    74.     {
    75.         const string uxmlPath = "RedOwl/Demo/DemoLayout";
    76.         const string ussPath = "RedOwl/Demo/DemoStyle";
    77.  
    78.         VisualElement frame;
    79.  
    80.         [MenuItem("Tools/Unity")]
    81.         public static void Open()
    82.         {
    83.             var wnd = GetWindow<Demo>();
    84.         }
    85.  
    86.         public void OnEnable()
    87.         {
    88.             var root = this.GetRootVisualContainer();
    89.             var visualTree = Resources.Load<VisualTreeAsset>(uxmlPAth);
    90.             visualTree.CloneTree(root, null);
    91.             root.AddStyleSheetPath(ussPath);
    92.  
    93.             frame = root.Q("frame");
    94.  
    95.             root.AddManipulator(OnPath, new ManipulatorActivationFilter { button = MouseButton.RightMouse})
    96.         }
    97.      
    98.         public void OnPan(Vector2 delta)
    99.         {
    100.             Vector3 current = frame.transform.position;
    101.             frame.transform.position = new Vector3(current.x + delta.x, current.y + delta.y, -100f);
    102.         }
    103.     }
    104. }

    With:
    Code (csharp):
    1. using System.Collections.Generic;
    2. using UnityEngine;
    3. using UnityEngine.Experimental.UIElements;
    4. using RedOwl.Editor;
    5.  
    6. namespace RedOwl.Demo
    7. {
    8.     [UXML, USS]
    9.     public class Demo : RedOwlEditorWindow<Demo>, IOnMouse
    10.     {
    11.         [UXMLReference]
    12.         VisualElement frame;
    13.  
    14.         [MenuItem("Tools/RedOwl")]
    15.         public static void Open()
    16.         {
    17.             EnsureWindow();
    18.         }
    19.  
    20.         public IEnumerable<MouseFilter> MouseFilters {
    21.             get {
    22.                 yield return new MouseFilter {
    23.                     button = MouseButton.RightMouse,
    24.                     OnMove = OnPan
    25.                 };
    26.             }
    27.         }
    28.  
    29.         public void OnPan(MouseMoveEvent evt, Vector2 delta)
    30.         {
    31.             Vector3 current = frame.transform.position;
    32.             frame.transform.position = new Vector3(current.x + delta.x, current.y + delta.y, -100f);
    33.         }
    34.     }
    35. }

    Without is 104 lines of code and with is only 34. This library truly cuts down on boilerplate, makes your UIElements code easier and faster to write and helps you keep clear separation of concerns by not cluttering your business logic code with UI and Input handling code.

    - Cheers, Rocktavious

    (really interested in hear what others think - feel free to give feedback or post issues on the github too!)
     
    Last edited: Jan 22, 2019
  2. aybeone

    aybeone

    Joined:
    May 24, 2015
    Posts:
    107
    This is my opinion after spending 10 mins on your GH:

    • it is convention-based (personally I don't like that)
    • the examples are confusing, I guess it needs more comments and/or a proper examples folder
    • attributes everywhere :eek: isn't the whole point of UXML to do less C# in the end ?
    • haven't seen any schemas, one has to figure out your controls for use in UXML files ?
    • and no documentation at all on classes, a major stopper for me
    (this is my opinion, do not take it personally :))

    One thing I'm waiting is the next big update from Unity with that so said designer, it's likely to be a game changer.
     
  3. Rocktavious

    Rocktavious

    Joined:
    May 10, 2017
    Posts:
    44
    Hey @aybe - Thanks for taking a look (don't worry feedback is always welcome - thats how you make things better!)

    I totally get not liking the convention - IMO - the point of using a "framework" is that you lean on the convention and it makes it so you have to write less code /shrug

    I've just started working on the documentation but an examples folder would be well worth the effort - so thanks!

    The reason i went with attributes was to reduce the boilerplate c# you have to write to load UXML and USS - with those attributes (and UXMLReference) you can eliminate many many lines of code from every single custom editor/visual element
    I think where your getting tripped up is in the USSClass attribute which allows you to add USS class names to the element - maybe i should downplay this in the docs as its really only a "last resort" kind of thing

    IMO the major benefit I see to the UXML and USS attributes on your editor classes is not having to write Resources.Load and VisualElement.CloneTree in every single editor. Personally i like the Attributes approach as it separates the wheat from the chafe in that now your VisualElement code is just business logic not filled with code for loading UXML and Querying for its elements.

    I'm not really sure what you mean by the UXML schemas comment? are you referring to the custom controls?
    I personally don't use an IDE that reads schema files for when your writing UXML so maybe submitting a PR if you know how to create those easily would be great. Not having them doesn't break any functionality it just doesn't mean your IDE will beable to tell the possible attributes on the custom controls. (if i'm missing the mark here let me know)

    I havn't documented the classes on purpose because they are still in a state of flux - but the RedOwlVisualElement - theres really nothing to document, everything it does is documented by everything else - it really is just a shim class that hooks up the rest of the stuff if its configured/implemented. (i could stand to comment the code more though - hehe)

    Thanks for your feedback it was appreciated and i'll keep plugging along on the framework!

    - Cheers
     
  4. aybeone

    aybeone

    Joined:
    May 24, 2015
    Posts:
    107
    You are probably covering cases I haven't yet, currently using it against a single editor and was looking for a reactive UI where like WPF, a 'command' not available is grayed out.

    Currently I'm spending most of my time tweaking my window to my needs than requiring boilerplate code for UXML. I have a huge OnEnable to define my controls properties like a designer.cs file for WinForms and if there's one thing UXML is lacking, it'd be automatic class generation so one wouldn't have to query controls manually. But OTOH my way is cool because when there's a control I just need to set and forget I just do tree.Q<Button>(...).... and not even keep a reference to it.

    For the schemas you see the folder Unity adds with schemas at ..\Assets, well you can edit that one to add your controls so you get IntelliSense in UXML, don't forget to add [assembly: UxmlNamespacePrefix("XXX", "xxx")] to each of your control class and then it's easier to use. For the control XSD template I just cloned VisualElement's one and changed it accordingly. You edit/adjust Unity*.UIElements.xsd to your own XSD file, you reference it in UIElements.xsd then in project context menu invoke 'Update UIElements schemas'.
     
  5. Rocktavious

    Rocktavious

    Joined:
    May 10, 2017
    Posts:
    44
    @aybe

    Ya - if you use my framework you don't end up with huge functions like that so when you get to writing multiple editors over and over you don't have to duplicate code

    For example given this UXML
    Code (csharp):
    1. <?xml version="1.0" encoding="utf-8"?>
    2. <UXML xmlns="UnityEngine.Experimental.UIElements">
    3.    <VisualElement name="Content">
    4.        <VisualElement name="SideBar">
    5.           <Button name="Submit" />
    6.        </VisualElement>
    7.    </VisualElement>
    8. </UXML>
    You could write the below c# and the framework would automatically populate those fields for you during initialization so you don't have to write any queries or run them anytime you want access to an element - cuts down on that boilerplate loading and querying code (also better performance to query and cache them once)
    Code (csharp):
    1. [UXML]
    2. public class DemoElement : RedOwlVisualElement
    3. {
    4.     [UXMLReference]
    5.     VisualElement Content;
    6.  
    7.     [UXMLReference("SideBar")]
    8.     VisualElement Navigation;
    9.  
    10.     [UXMLReference]
    11.     Button Submit
    12. }
    https://github.com/rocktavious/UIEX#uxmlreference

    Thanks for the tips on the schema stuff - i'll have to try and dig in once i get a change - BTW what IDE do you use? VisualStudio?

    - Cheers
     
  6. aybeone

    aybeone

    Joined:
    May 24, 2015
    Posts:
    107
    I use VS, note that Rider has also very good support for Unity, better than VS in many aspects but debugging compatibility in some cases.

    Interesting your example, it'd be a good thing to also have an opt out approach where you add an attribute if you don't want it to be populated, in most scenarios it'd be enough since you're not likely to have such types as fields unless you need to refer to them, just like how Serializefield is implicit for public fields. Maybe a single class level attribute could specify desired approach.
     
  7. Rocktavious

    Rocktavious

    Joined:
    May 10, 2017
    Posts:
    44
    @aybe - so Attributes by nature are opt-in - which means for the opt-out you just don't add them - for example you could end up building your VisualElement like this
    Code (csharp):
    1. public class DemoElement : VisualElement, IOnMouse
    2. {
    3.     VisualElement frame;
    4.  
    5.     public DemoElement()
    6.     {
    7.         RedOwlUtils.Setup(this, this);
    8.     }
    9.  
    10.     [UICallback(1, true)]
    11.     private void InitUI()
    12.     {
    13.         frame = new VisualElement();
    14.     }
    15.  
    16.     public IEnumerable<MouseFilter> MouseFilters {
    17.         get {
    18.             yield return new MouseFilter {
    19.                 button = MouseButton.RightMouse,
    20.                 OnMove = OnPan
    21.             };
    22.         }
    23.     }
    24.  
    25.     private void OnPan(MouseMoveEvent evt, Vector2 delta)
    26.     {
    27.         Vector3 current = frame.transform.position;
    28.         frame.transform.position = new Vector3(current.x + delta.x, current.y + delta.y, -100f);
    29.     }
    30. }
    So I don't even inherit from RedOwlVisualElement and don't have to have any UXML or USS attributes so no auto loading of files and I can build the UI directly in c#, but i can still leverage the library for auto scheduling of callbacks with the `UICallback` attribute and i still leverage the way the library improves Input manipulator configuration by implementing the `IOnMouse` interface so i don't have to write my own input manipulator. Once again reducing the amount of boiler plate code in each of my visual element classes and encapsulating it all in the `RedOwlUtils.Setup` function. If you go read the code for RedOwlVisualElement - its literally just a shim of a class that calls `RedOwlUtils.Setup` for you in the constructor saving you a few more lines of code from the example above.

    - Cheers[/code]
     
  8. aybeone

    aybeone

    Joined:
    May 24, 2015
    Posts:
    107
    This is my attempt to do the same as you but in less intrusive way: an extension method!

    https://gist.github.com/aybe/07c1330d49d08b497a85f320c9e02e34

    Code (CSharp):
    1. using JetBrains.Annotations;
    2. using UnityEditor;
    3. using UnityEngine;
    4. using UnityEngine.UIElements;
    5.  
    6. public class TestWindow : EditorWindow
    7. {
    8.     [NotNull]
    9.     private Foldout foldout1;
    10.  
    11.     [MenuItem("Window/UIElements/TestWindow")]
    12.     public static void ShowExample()
    13.     {
    14.         GetWindow<TestWindow>().titleContent = new GUIContent("TestWindow");
    15.     }
    16.  
    17.     public void OnEnable()
    18.     {
    19.         this.LoadVisualTree();
    20.  
    21.         foldout1.text = "abcd";
    22.     }
    23. }
    With this single method, I've done without your UXML, UXMLReference, USS attributes. I am somewhat skeptical in regards to USSClass and UICallback, with the former someone would be in deep poop should they want to switch classes dynamically as it's done with pseudo states; for the latter it could be useful though.

    IMO make it first work for simple out of the box cases, then try provide more customization but don't try to handle it all.
     
  9. Rocktavious

    Rocktavious

    Joined:
    May 10, 2017
    Posts:
    44
    @aybe - So this is almost exactly how i started - your extension method is basically what RedOwlUtils.Setup does it just doesn't optionally look for attributes it enforces that you have a UXML and USS. This is why i added the attributes because then you can opt-in to loading UXML or opt-in to loading a USS. With your way it all or nothing, you cannot pick and choose (the same concept applies to the other attributes in the library). You also have no way to override the filename of the UXML or USS file loaded - with my attributes it gives you that ability (which i've used). Sure i could probably make extension functions to callout to RedOwlUtils.Setup - but thats just a nice to have thing so you don't have to import RedOwl.Editor namespace. Something i can add to the roadmap to do because it is a quality of life feature. But at its heart what you've done is exactly what my setup function is.

    I can see the skepticism for the USSClass attribute and i've only used it sparingly so far in my usecases, but i think it utility comes from when you don't want to load a UXML file it gives you a really easy way to add classes to elements without having to think about it.

    As for UICallback - this just eliminates long 1 liners that happen when you use the schedule function. Under the hood all that the setup function is doing is looking for method marked with that attribute and setting those functions up with the VisualElement schedule method for you so you don't have to, all you have to worry about is writing the business logic of that function and marking it for being a callback. The attribute is also written in a way that makes it so you can choose to be called back only once or repeatedly.
     
  10. wang37921

    wang37921

    Joined:
    Aug 1, 2014
    Posts:
    102
    look great!
     
  11. Rocktavious

    Rocktavious

    Joined:
    May 10, 2017
    Posts:
    44
  12. Dennin-Dalke

    Dennin-Dalke

    Joined:
    Aug 10, 2014
    Posts:
    25
    I've noticed you use Resource.Load to handle uxml and uss loading, wouldn't it be better if you use
    AssetDatabase.LoadAssetAtPath?
     
  13. Rocktavious

    Rocktavious

    Joined:
    May 10, 2017
    Posts:
    44
    @Dennin-Dalke I did this because UIElements will be Runtime Supported in 2020 and as far as i'm aware AssetDatabase is not available in the Runtime. If this changes i'd change it. If i'm incorrect let me know and i'd be happy to change it.
     
  14. Amitloaf

    Amitloaf

    Joined:
    Jan 30, 2012
    Posts:
    97
    I like it :)
     
  15. taylank

    taylank

    Joined:
    Nov 3, 2012
    Posts:
    182
    One thing that looked a bit odd to me is how you generate an element from the UXML attribute. For a given VisualElement derived class with the UXML attribute, it appears one passes an instance of that class to RedOwlUtilts.Setup, and then one gets a clone of the tree generated from that UXML under the instance that was passed.

    Like your example above, the UXML for DemoElement is only the inner structure, which made me worry about whether this would be compatible with using UXML templates to compose more complex objects. There appears to be no factory definition within DemoElement, so I don't see any way to just include a DemoElement in the UXML structure of another class elsewhere.

    So I modified your code a bit so that the instance is generated from the UXML with a dummy template container as the root. And after that elsewhere I unparent the generated instance from that dummy root and re-parent it to wherever it's supposed to go.

    Code (CSharp):
    1. public static T GenerateFromUXMLAttribute<T>() where T : VisualElement, new()
    2.         {
    3.             var container = new TemplateContainer();
    4.             var templateFound = false;
    5.             var path = "";
    6.             typeof(T).WithAttr<UXMLAttribute>((attr) =>
    7.                 {
    8.                  
    9.                     path = GetUXMLPath<T>(attr.path);
    10.                     var layout = Resources.Load<VisualTreeAsset>(path);
    11.                     if (layout != null)
    12.                     {
    13.                         templateFound = true;
    14.                         //Debug.LogFormat("Loading '{0}.uxml' for '{1}'", path, instance.GetType().Name);
    15.                         layout.CloneTree(container);
    16.                     }
    17.                 },
    18.                 false);
    19.  
    20.             if (!templateFound)
    21.             {
    22.                 return new T();
    23.             }
    24.             // the returned T is to be parented somewhere else
    25.             return container.Q<T>();
    26.         }
     
    Last edited: Jul 30, 2019