Skip to content

Instantly share code, notes, and snippets.

@polerin
Created January 21, 2020 16:52
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save polerin/d93feca1f69534e1e64374324bbaf3cc to your computer and use it in GitHub Desktop.
Save polerin/d93feca1f69534e1e64374324bbaf3cc to your computer and use it in GitHub Desktop.
Example UI Elements form structure -- simple "start game" menu
using System;
using System.Collections.Generic;
using UnityEngine;
using Unity.UIElements.Runtime;
using UnityEngine.UIElements;
using Zenject;
using SMG.Common.Exceptions;
/// <summary>
/// Base utility class for user interface forms.
///
/// This class is not intended for use in an ongoing
/// interface such as a HUD, but should instead be used
/// for enabling menus, settings forms, and other submission
/// based user interactions.
/// </summary>
namespace SMG.Common.UserInterface
{
using Action = FormButtonEvent.ButtonActions;
public class Form : MonoBehaviour
{
// is the current form displaying?
public bool isActive {
get {
return _container.activeSelf;
}
}
[SerializeField, Tooltip("The container that shows/hides the from. Will use Gameobject if not set")]
protected GameObject container = null;
protected Settings settings;
[SerializeField, Tooltip("The UXML file to use a template, may be injected via settings if desired")]
protected VisualTreeAsset formTemplate;
protected PanelRenderer formPanel;
protected VisualElement formRoot;
protected List<FormButton> formButtons;
protected List<TextField> formTextItems;
protected FormButton SubmitButton;
protected FormButton CancelButton;
protected FormButton ApplyButton;
protected Dictionary<string, FormButton> OtherButtons;
private GameObject _container {
get {
if (container == null) {
return gameObject;
}
return container;
}
}
[Inject]
public void InitBase(
PanelRenderer formPanel,
Settings settings)
{
this.formPanel = formPanel;
this.settings = settings;
if (settings.FormTemplate != null) {
this.formTemplate = settings.FormTemplate;
}
if (this.formTemplate == null) {
throw new ConfigurationException("Unable to configure form (" + this.name + " ): No template configured");
}
}
public void Awake()
{
// Any time the root UXML is reloaded, we will need to rehoook.
// This will also enable us to hook on initial load.
formPanel.postUxmlReload += RehookForm;
}
public void ToggleFormActivation()
{
if (isActive) {
DeactivateForm();
return;
}
ActivateForm();
}
public virtual void DeactivateForm()
{
_container.SetActive(false);
}
public virtual void ActivateForm()
{
if (formRoot == null) {
SetFormRoot(settings.RootSelector);
}
formTemplate.CloneTree(formRoot);
_container.SetActive(true);
ActivateButtons();
}
public virtual void ActivateButtons()
{
UQueryBuilder<FormButton> buttons = formRoot.Query<FormButton>();
buttons.ForEach(ActivateButton);
}
public virtual void ActivateButton(FormButton button)
{
switch (button.action) {
case Action.Submit:
SubmitButton = button;
button.clickable.clicked += SubmitForm;
break;
case Action.Apply:
ApplyButton = button;
button.clickable.clicked += ApplyForm;
break;
case Action.Cancel:
CancelButton = button;
button.clickable.clicked += CancelForm;
break;
default:
OtherButtons.Add(button.name, button);
button.clickable.clicked += OtherFormAction;
break;
}
}
public void DeactivateButtons()
{
UQueryBuilder<FormButton> buttons = formRoot.Query<FormButton>();
buttons.ForEach(DeactivateButton);
}
public void DeactivateButton(FormButton button)
{
switch (button.action) {
case Action.Submit:
SubmitButton = button;
button.clickable.clicked -= SubmitForm;
break;
case Action.Apply:
ApplyButton = button;
button.clickable.clicked -= ApplyForm;
break;
case Action.Cancel:
CancelButton = button;
button.clickable.clicked -= CancelForm;
break;
default:
if (OtherButtons.ContainsKey(button.name)) {
OtherButtons.Remove(button.name);
}
button.clickable.clicked -= OtherFormAction;
break;
}
}
// @todo for follow-up, under what circumstances would we return non-null?
// Looks like it is for data binding? https://github.com/Unity-Technologies/UIElementsUniteCPH2019RuntimeDemo/blob/124f568c3eb325e19f83d005ddcbb86270ae2b8a/Assets/Tanks/Scripts/Managers/GameManager.cs#L180
protected IEnumerable<UnityEngine.Object> RehookForm()
{
SetFormRoot(settings.RootSelector);
if (!isActive) {
// If we aren't actually active, don't proceed.
return null;
}
// if we should be active, go ahead and fully activate
ActivateForm();
return null;
}
protected void SetFormRoot(string selector)
{
// @todo come back to this and create a custom element for form root
formRoot = formPanel.visualTree.Q<Box>(selector);
if (formRoot == null) {
throw new ConfigurationException("Unable to find the form root in supplied formPanel! Is there a build error?");
}
}
protected virtual void SubmitForm()
{
}
protected virtual void ApplyForm()
{
}
protected virtual void CancelForm()
{
}
protected virtual void OtherFormAction()
{
}
protected Dictionary<string, string> BuildFormData()
{
Dictionary<string, string> formData = new Dictionary<string, string>();
UQueryBuilder<TextField> fields = formRoot.Query<TextField>();
fields.ForEach((field) => formData.Add(field.name, field.value));
return formData;
}
[Serializable]
public class Settings : IFormSettings
{
// @todo ensure that the form's stylesheet is loaded in the panel? Need to figure out how
// StyleSheet formStylesheet;
[SerializeField, Tooltip("The individual form template")]
private VisualTreeAsset _formTemplate;
public VisualTreeAsset FormTemplate { get { return _formTemplate; } }
[SerializeField, Tooltip("The selector which references where to place the form in the panel.")]
private string _rootSelector = "form-root";
public string RootSelector { get { return _rootSelector; } }
}
}
}
using UnityEngine.UIElements;
using UnityEditor.UIElements; // Ugh, can't figure out how not to do
// [assembly: UxmlNamespacePrefix("SMG.Common.UserInterface", "smg")]
namespace SMG.Common.UserInterface
{
using Actions = FormButtonEvent.ButtonActions;
public class FormButton : Button
{
public class UmxlFactory : UxmlFactory<FormButton, UxmlTraits> { };
public new class UxmlTraits : Button.UxmlTraits
{
private UxmlEnumAttributeDescription<Actions> action = new UxmlEnumAttributeDescription<Actions> { name = "action" };
public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc)
{
base.Init(ve, bag, cc);
((FormButton)ve).action = action.GetValueFromBag(bag, cc);
}
}
public Actions action { get; set; }
public FormButton()
{
action = Actions.Other;
}
}
}
using UnityEngine;
using SMG.Common.UserInterface;
namespace SMG.Proximity.SceneObjects.UserInterface
{
public class MainMenu : Form
{
protected override void SubmitForm()
{
eventBus.Publish<ConnectToNetworkFormEvent>(new ConnectToNetworkFormEvent(BuildFormData(), GameModes.Race));
}
protected override void CancelForm()
{
eventBus.Publish<DisconnectFromNetworkFormEvent>(new DisconnectFromNetworkFormEvent(BuildFormData()));
}
}
}
<?xml version="1.0" encoding="utf-8"?>
<UXML
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="UnityEngine.UIElements"
xmlns:smg="SMG.Common.UserInterface"
xsi:noNamespaceSchemaLocation="../UIElementsSchema/UIElements.xsd"
xsi:schemaLocation="UnityEngine.UIElements ../UIElementsSchema/UnityEngine.UIElements.xsd"
>
<Box class="container container--user-info">
<Label text="User Name"/> <TextField name="UserName"/>
</Box>
<Box class="container container--buttons">
<smg:FormButton action="Submit" name="submit" text="Ready" />
<smg:FormButton action="Cancel" name="cancel" text="Quit Game"/>
</Box>
</UXML>
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements">
<!-- all kinds of decoration and pretty things here -->
<ui:Box name="form-root" class="form-root">
</ui:Box>
</ui:UXML>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment