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

Generate bindings for custom uxml components #25

Closed
claybrooks opened this issue Aug 12, 2023 · 6 comments
Closed

Generate bindings for custom uxml components #25

claybrooks opened this issue Aug 12, 2023 · 6 comments
Labels
planned question Further information is requested

Comments

@claybrooks
Copy link

claybrooks commented Aug 12, 2023

Unity supports re-use of uxml components in other uxml components. Rosalina doesn't generate bindings for these custom uxml components.

Pseudo-ish code for an idea about what to generate:

// CustomComponent.g.cs
public partial class CustomComponent
{
    private VisualElement _root;

    public CustomComponent(VisualElement root)
    {
        _root = root;
    }

    public Button MyButton { get; private set; }

    public void Initialize()
    {
        MyButton = (Button)_root?.Q("MyButton");
    }
}

...

// MainUIDocument.g.cs
public partial class MainUIDocument
{
    [SerializeField]
    private UIDocument _document;

    public CustomComponent MyCustomComponent { get; private set; }

    public VisualElement Root
    {
        get
        {
            return _document?.rootVisualElement;
        }
    }

    public void InitializeDocument()
    {
        MyCustomComponent = new CustomComponent((VisualElement)Root?.Q("MyCustomComponent"));
    }
}

In general, I have uxml components that will never be a top-level UI element. They will always be composed by something else, so their [SerializeField] UIDocument _document field is unnecessary.

@claybrooks claybrooks added the question Further information is requested label Aug 12, 2023
@Eastrall
Copy link
Owner

At the moment, Rosalina supports only native types:

private static readonly IReadOnlyDictionary<string, Type> _nativeUITypes = new Dictionary<string, Type>()
{
// Containers
{ "VisualElement", typeof(UnityEngine.UIElements.VisualElement) },
{ "ScrollView", typeof(UnityEngine.UIElements.ScrollView) },
{ "ListView", typeof(UnityEngine.UIElements.ListView) },
{ "IMGUIContainer", typeof(UnityEngine.UIElements.IMGUIContainer) },
{ "GroupBox", typeof(UnityEngine.UIElements.GroupBox) },
// Controls
{ "Label", typeof(UnityEngine.UIElements.Label) },
{ "Button", typeof(UnityEngine.UIElements.Button) },
{ "Toggle", typeof(UnityEngine.UIElements.Toggle) },
{ "Scroller", typeof(UnityEngine.UIElements.Scroller) },
{ "TextField", typeof(UnityEngine.UIElements.TextField) },
{ "Foldout", typeof(UnityEngine.UIElements.Foldout) },
{ "Slider", typeof(UnityEngine.UIElements.Slider) },
{ "SliderInt", typeof(UnityEngine.UIElements.SliderInt) },
{ "MinMaxSlider", typeof(UnityEngine.UIElements.MinMaxSlider) },
{ "ProgressBar", typeof(UnityEngine.UIElements.ProgressBar) },
{ "DropdownField", typeof(UnityEngine.UIElements.DropdownField) },
{ "RadioButton", typeof(UnityEngine.UIElements.RadioButton) },
{ "RadioButtonGroup", typeof(UnityEngine.UIElements.RadioButtonGroup) },
{ "Image", typeof(UnityEngine.UIElements.Image) },
};

I believe this is a good idea to add support of custom components, let's plan this for next releases. Thanks for the suggestion!

@Eastrall
Copy link
Owner

Eastrall commented Aug 15, 2023

I've been thinking how I could implement this, and I suggest the following code generation process:

Note: this is non-dependent wether the document is a "custom component" or any other component.
If I wanted to specify the component type within Rosalina, we'll need to wait for version 2023.X because Unity team is working on a way to extend the UI Builder inspector.
image
We could then imagine, a dropdown where you can select your "component" type (document, custom component, editor extension, ...)

// Menu.g.cs
public partial class Menu
{
    [SerializeField]
    private UIDocument _document;

    private VisualElement _rootElement;

    public Label Title { get; private set; }

    public MenuItems Items { get; private set; }

    public VisualElement Root => _rootElement ??= _document?.rootVisualElement;

    public InteractionMenu()
    {
    }

    public InteractionMenu(VisualElement rootElement)
    {
        _rootElement = rootElement;
    }

    public void InitializeDocument()
    {
        Title = (Label)Root?.Q("Title");
        MenuItemsContainer = new MenuItems(Root?.Q("Items"));
    }
}

// MenuItems.g.cs
public partial class InteractionMenuItem
{
    [SerializeField]
    private UIDocument _document; // Not used, but keeping it for now.

    private VisualElement _rootElement;

    public Label Title { get; private set; }

    public VisualElement MenuItemContainer { get; private set; }

    // Here _rootElement will be used since this instance will be initialized with the `VisualElement` constructor.
    public VisualElement Root => _rootElement ??= _document?.rootVisualElement;

    public InteractionMenuItem()
    {
    }

    public InteractionMenuItem(VisualElement root)
    {
        _rootElement = root;
    }

    public void InitializeDocument()
    {
        Title = (Label)Root?.Q("Title");
        MenuItemContainer = (VisualElement)Root?.Q("MenuItemContainer");
    }
}

Be careful though, the MenuItems script cannot be a MonoBehavior, otherwise, Unity will show you a warning that you should not create an instance of a MonoBehavior using the new keyword.

Please let me know your thoughts about this implementation.

@claybrooks
Copy link
Author

claybrooks commented Aug 15, 2023

The empty constructors pose a chance to break some code: Defining empty constructors means the other sides of partial implementations can't define them. Anyone currently defining the empty constructor in their app-level code will break.

I'm not entirely sure how people use Rosalina. If people bake a ton of logic in their partial implementations, then they'd probably want to write some code in the default constructor. If people use it as a pure binding layer, then they most likely wouldn't care to lose access to the default constructor.

The other thing I see, which I missed as well, is invoking InitializeDocument() on InteractionMenuItem's. You'd probably want to call that in the InitializeDocument() of InteractionMenu.cs in the case that it is a composed VisualElement.

@claybrooks
Copy link
Author

claybrooks commented Aug 15, 2023

In the implementation I posted, the code for the composed component was purpose built for it being composed, which lets the default constructors go undefined. I think if you try to make it generalized for both use cases (top-level vs. composed), it's going to be a game of compromise.

I'm not familiar with how the auto-generation code works, but maybe you can generate different versions of the file based on it's usage. In your example above, when the autogeneration code gets to Menu.uxml, it'll see that it contains a MenuItems template. Instead of generating the default MenuItems.g.cs, you could generate a MenuItems.Component.g.cs that only has a single constructor expecting a VisualElement. And when the code generation gets to MenuItems.uxml, it'll generate the default MenuItems.g.cs, which can be used as a normal top-level ui component.

This way, the empty constructors are left undefined and everyone can continue using Rosalina as is with the choice to opt in to the newly generated Component.g.cs files.

// Menu.g.cs (Top Level Component)
public partial class Menu
{
    [SerializeField]
    private UIDocument _document;

    public Label Title { get; private set; }

    public MenuItemsComponent Items { get; private set; }

    public VisualElement Root
    {
        get
        {
            return _document?.rootVisualElement;
        }
    }

    public void InitializeDocument()
    {
        Title = (Label)Root?.Q("Title");
        Items = new MenuItemsComponent(Root?.Q("Items"));
        Items.InitializeDocument();
    }
}

// MenuItems.Component.g.cs (Composed Component)
public partial class MenuItemsComponent
{
   private VisualElement _root;

    public MenuItemsComponent(VisualElement root)
    {
        _root = root;
    }

    public Label Title { get; private set; }

    public VisualElement MenuItemContainer { get; private set; }

    public void InitializeDocument()
    {
        Title = (Label)_root?.Q("Title");
        MenuItemContainer = (VisualElement)_root?.Q("MenuItemContainer");
    }
}

// MenuItems.g.cs (Top Level Component)
public partial class MenuItems
{
    [SerializeField]
    private UIDocument _document;

    public Label Title { get; private set; }

    public VisualElement MenuItemContainer { get; private set; }
    
    public VisualElement Root
    {
        get
        {
            return _document?.rootVisualElement;
        }
    }

    public void InitializeDocument()
    {
        Title = (Label)Root?.Q("Title");
        MenuItemContainer = (VisualElement)Root?.Q("MenuItemContainer");
    }
}

@Eastrall
Copy link
Owner

You are correct, the default constructor generation is a mistake. I thought that without a default constructor, Unity wouldn't be able to instanciate the Menu document as a component on a game object. Looks like I was wrong, since I removed it and it still worked.

I'm not familiar with how the auto-generation code works, but maybe you can generate different versions of the file based on it's usage. In your example above, when the autogeneration code gets to Menu.uxml, it'll see that it contains a MenuItems template. Instead of generating the default MenuItems.g.cs, you could generate a MenuItems.Component.g.cs that only has a single constructor expecting a VisualElement. And when the code generation gets to MenuItems.uxml, it'll generate the default MenuItems.g.cs, which can be used as a normal top-level ui component.

The code generation mechanism is triggered by uxml file. Which means that every uxml file is treated one by one. Thus looking for what uxml is a component wouldn't work in that case.
I tried to mix up both code generation processes because I currently don't have any other way to identify what is a component and is not.
But, once the Unity team finishes the UI Builder extension mechanism, I would be able to create a field that would allow the developer to specify what "kind" of document it is. For example, a document, a component, an extension, etc...

Another approach would be to have something more "manual" which could be integrated along with the suggestion made on issue #26 : Manual generation

By doing a right-click on the UXML file, you could :

  1. Activate / Deactivate the generation process
  2. Choose how to generate the UXML (document or component)

All this would be stored in the RosalinaSettings and the code generator would generate the correct code without having a UIDocument field for custom component.

@Eastrall
Copy link
Owner

Solved in #27, see release : https://github.com/Eastrall/Rosalina/releases/tag/v4.0.0

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
planned question Further information is requested
Projects
None yet
Development

No branches or pull requests

2 participants