Repository: LudiKha/Graphene
Branch: master
Commit: d1527f324fbb
Files: 169
Total size: 333.3 KB
Directory structure:
gitextract_6hvvmno2/
├── .github/
│ └── FUNDING.yml
├── LICENSE
├── README.md
├── docs/
│ ├── About.md
│ ├── _config.yml
│ └── howto.md
└── src/
├── Core/
│ ├── Binding/
│ │ ├── Binder.cs
│ │ ├── Binder.cs.meta
│ │ ├── Binding.cs
│ │ ├── Binding.cs.meta
│ │ ├── BindingAttribute.cs
│ │ ├── BindingAttribute.cs.meta
│ │ ├── BindingsManager.cs
│ │ ├── BindingsManager.cs.meta
│ │ ├── CollectionBinding.cs
│ │ ├── CollectionBinding.cs.meta
│ │ ├── MemberBinding.cs
│ │ └── MemberBinding.cs.meta
│ ├── Binding.meta
│ ├── Extensions/
│ │ ├── ButtonGroup.cs
│ │ ├── ButtonGroup.cs.meta
│ │ ├── CycleField.cs
│ │ ├── CycleField.cs.meta
│ │ ├── Dialog.cs
│ │ ├── Dialog.cs.meta
│ │ ├── GrapheneRoot.cs
│ │ ├── GrapheneRoot.cs.meta
│ │ ├── If.cs
│ │ ├── If.cs.meta
│ │ ├── Route.cs
│ │ ├── Route.cs.meta
│ │ ├── SelectField.cs
│ │ ├── SelectField.cs.meta
│ │ ├── TemplateTypes/
│ │ │ ├── Button.cs
│ │ │ ├── Button.cs.meta
│ │ │ ├── TemplateRef.cs
│ │ │ └── TemplateRef.cs.meta
│ │ ├── TemplateTypes.meta
│ │ ├── View.cs
│ │ ├── View.cs.meta
│ │ ├── VisualElementExtensions.cs
│ │ └── VisualElementExtensions.cs.meta
│ ├── Extensions.meta
│ ├── Graphene.cs
│ ├── Graphene.cs.meta
│ ├── GrapheneComponent.cs
│ ├── GrapheneComponent.cs.meta
│ ├── Injector.cs
│ ├── Injector.cs.meta
│ ├── Interfaces.cs
│ ├── Interfaces.cs.meta
│ ├── Model/
│ │ ├── BindableObjects/
│ │ │ ├── BindableBaseField.cs
│ │ │ ├── BindableBaseField.cs.meta
│ │ │ ├── BindableObject.cs
│ │ │ ├── BindableObject.cs.meta
│ │ │ ├── ListBindable.cs
│ │ │ └── ListBindable.cs.meta
│ │ ├── BindableObjects.meta
│ │ ├── GenericModelAsset.cs
│ │ ├── GenericModelAsset.cs.meta
│ │ ├── GenericModelBehaviour.cs
│ │ ├── GenericModelBehaviour.cs.meta
│ │ ├── ScriptableObjectModel.cs
│ │ ├── ScriptableObjectModel.cs.meta
│ │ ├── ViewModel/
│ │ │ ├── FormViewModel.cs
│ │ │ ├── FormViewModel.cs.meta
│ │ │ ├── MultiFormViewModel.cs
│ │ │ ├── MultiFormViewModel.cs.meta
│ │ │ ├── NavViewModel.cs
│ │ │ ├── NavViewModel.cs.meta
│ │ │ ├── ViewModelComponent.cs
│ │ │ └── ViewModelComponent.cs.meta
│ │ └── ViewModel.meta
│ ├── Model.meta
│ ├── Plate.cs
│ ├── Plate.cs.meta
│ ├── Rendering/
│ │ ├── RenderUtils.cs
│ │ ├── RenderUtils.cs.meta
│ │ ├── Renderer.cs
│ │ └── Renderer.cs.meta
│ ├── Rendering.meta
│ ├── Routing/
│ │ ├── EnableOnState.cs
│ │ ├── EnableOnState.cs.meta
│ │ ├── Interpreters/
│ │ │ ├── ApplicationStateInterpreter.cs
│ │ │ ├── ApplicationStateInterpreter.cs.meta
│ │ │ ├── NavigationStateHandler.cs
│ │ │ ├── NavigationStateHandler.cs.meta
│ │ │ ├── StateInterpreter.cs
│ │ │ └── StateInterpreter.cs.meta
│ │ ├── Interpreters.meta
│ │ ├── Router.cs
│ │ ├── Router.cs.meta
│ │ ├── StateHandle.cs
│ │ ├── StateHandle.cs.meta
│ │ ├── StringEnableOnState.cs
│ │ ├── StringEnableOnState.cs.meta
│ │ ├── StringRouter.cs
│ │ ├── StringRouter.cs.meta
│ │ ├── StringStateHandle.cs
│ │ └── StringStateHandle.cs.meta
│ ├── Routing.meta
│ ├── Styling/
│ │ ├── AlignItemsOverride.cs
│ │ ├── AlignItemsOverride.cs.meta
│ │ ├── FlexDirectionOverride.cs
│ │ ├── FlexDirectionOverride.cs.meta
│ │ ├── InlineStyleOverrides.cs
│ │ ├── InlineStyleOverrides.cs.meta
│ │ ├── JustifyOverride.cs
│ │ ├── JustifyOverride.cs.meta
│ │ ├── StyleOverride.cs
│ │ ├── StyleOverride.cs.meta
│ │ ├── WrapOverride.cs
│ │ └── WrapOverride.cs.meta
│ ├── Styling.meta
│ ├── Templating/
│ │ ├── TemplatePreset.cs
│ │ └── TemplatePreset.cs.meta
│ ├── Templating.meta
│ ├── View/
│ │ ├── SerializedView.cs
│ │ ├── SerializedView.cs.meta
│ │ ├── ViewHandle.cs
│ │ └── ViewHandle.cs.meta
│ └── View.meta
├── Core.meta
├── Editor/
│ ├── CustomDictionaryPropertyDrawers.cs
│ ├── CustomDictionaryPropertyDrawers.cs.meta
│ ├── Graphene.Editor.asmdef
│ ├── Graphene.Editor.asmdef.meta
│ ├── GrapheneEditorUtilities.cs
│ ├── GrapheneEditorUtilities.cs.meta
│ ├── ViewSelectorStringDrawer.cs
│ └── ViewSelectorStringDrawer.cs.meta
├── Editor.meta
├── Graphene.Core.asmdef
├── Graphene.Core.asmdef.meta
├── Lib/
│ ├── DragManipulator/
│ │ ├── DragManipulator.cs
│ │ └── DragManipulator.cs.meta
│ ├── DragManipulator.meta
│ ├── SerializableDictionary/
│ │ ├── Editor/
│ │ │ ├── SerializableDictionary.Editor.asmdef
│ │ │ ├── SerializableDictionary.Editor.asmdef.meta
│ │ │ ├── SerializableDictionaryPropertyDrawer.cs
│ │ │ └── SerializableDictionaryPropertyDrawer.cs.meta
│ │ ├── Editor.meta
│ │ ├── SerializableDictionary.Runtime.asmdef
│ │ ├── SerializableDictionary.Runtime.asmdef.meta
│ │ ├── SerializableDictionary.cs
│ │ └── SerializableDictionary.cs.meta
│ └── SerializableDictionary.meta
├── Lib.meta
├── Resources/
│ ├── Icons/
│ │ ├── atom.png.meta
│ │ ├── form.png.meta
│ │ ├── graphene.png.meta
│ │ ├── injector.png.meta
│ │ ├── layout.png.meta
│ │ ├── molecule.png.meta
│ │ ├── plate.png.meta
│ │ ├── quantum.png.meta
│ │ ├── renderer.png.meta
│ │ ├── router.png.meta
│ │ ├── state.png.meta
│ │ ├── template.png.meta
│ │ └── theme.png.meta
│ ├── Icons.meta
│ ├── Logo/
│ │ ├── graphene-logo-full.png.meta
│ │ ├── graphene-logo-white.png.meta
│ │ └── graphene-logo.png.meta
│ └── Logo.meta
├── Resources.meta
├── package.json
└── package.json.meta
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms
github: [LudiKha] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2020 Khaya Ludidi - CupBearer Interactive
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
[](https://opensource.org/licenses/MIT)   [](https://twitter.com/intent/follow?screen_name=LudiKha)
Graphene is a lightweight and modular framework for building runtime user interfaces with Unity's [UI Toolkit][0f273cb2].
[0f273cb2]: https://docs.unity3d.com/2020.1/Documentation/Manual/UIElements.html "UI Toolkit"
# Intro
Graphene **superconducts** your creativity for efficiently building modern interactive UI for games. It takes care of the heavy lifting by providing a framework inspired by Web standards, right into Unity.
It's **lightweight** and modular - you get to pick and choose which parts you need for your projects.
- **Declarative Hierarchy**: Graphene makes it painless to design interactive UI. Use the familiar GameObjects hierarchy to design simple `Views` as sections of the screen or nested states.
- **Reduce Boilerplate**: Focus on your custom logic and design of building UI instead of repeating low-level tasks for each unique screen you are building. Graphene comes with a number of `Controls` that greatly enhance the speed of creating interactive UI, whilst reducing the need of custom view controllers in C# by exposing vital functionality in Uxml.
- **Attribute-Based**: Instruct your UI to both draw _and_ bind templates using any data-container with a `[Bind]` attribute. Primitives, objects, collections, one-way, two-way binding, specific control selection: the parts you'll be most frequently developing with in C# are exposed via attributes
- **State-Based Routing**: Use the GameObject hierarchy dynamically construct your router's states. Its functionality mimics url-based addresses: `index/settings/video`.
- **Template Composition**: Reuse your static assets by writing atomic templates, and dynamically compose them in runtime.
It comes with a **[component-kit][e593c071]** library, **[sample project][05f1d8f6]** several VisualElement extensions and an **[online demo][84ed822d]** to get you started.
[e593c071]: https://github.com/LudiKha/Graphene-Components "Graphene Components"
[05f1d8f6]: https://github.com/LudiKha/Graphene-Sample-Project "Graphene-Sample-Project"
[84ed822d]: https://ludikha.github.io/Graphene-WebGL-Demo/ "Graphene-WebGL-Demo"
## Online Demo
### [Check out the WebGL demo ][f45eaa31]
[f45eaa31]: https://ludikha.github.io/Graphene-WebGL-Demo/ "Graphene WebGL demo"
## Installation
### Using Unity Package Manager (For Unity 2018.3 or later)
You can install the package via UPM by adding the line below to your project's `Packages/manifest.json` file.
>You can find this file by opening your project's *Packages* folder in a file browser, it is not displayed in the editor.
```
{
"dependencies": {
"com.graphene.core": "https://github.com/LudiKha/Graphene.git?path=/src",
"com.graphene.components": "https://github.com/LudiKha/Graphene-Components.git?path=/src",
...
},
}
```
>Do note that although both components and demo are optional packages, it is recommended you use them to kickstart your own Graphene-based development environment.
#### Staying updated
Updating the package can be done via `Window/Graphene/Check for updates`. Unity currently does not support updating Git packages via the Package Manager automatically.
### Using UPM Git Extension
The best way to install Graphene and stay up to date with the latest versions, is to use [UPM Git Extension][49fed258].
1. Follow the [installation instructions][2ddc031d]
2. In the Package Manager, click the  button, and add `https://github.com/LudiKha/Graphene.git` under subdirectory `src`, with the latest version.
3. Voilá! As an added bonus you are now able to update the package via the package manager.
[49fed258]: https://github.com/mob-sakai/UpmGitExtension "Upm Git Extension"
[2ddc031d]: https://github.com/mob-sakai/UpmGitExtension#installation "UP Git Extension Installation Instructions"
---
# Quickstart
For a quick start, Graphene comes with a component library and [sample project][52a86b14] - it is **highly** recommended to start your new project using the demo scene and resources provided within this project.
[52a86b14]: https://github.com/LudiKha/Graphene-Sample-Project "Graphene Sample Project"
#### 1: Constructing the hierarchy
- Construct the high-level UI hierarchy, where each unique state is represented by a GameObject
- Add a [`Plate`][0fb2479e] component to each GameObject in the tree, with a `Graphene` component at the root.
- For each Plate in the tree, assign a static asset to its UIDocument. Root states will typically need a Layout-style [`template`](https://github.com/LudiKha/Graphene#template) for their children to be fitted in.
Press play - Graphene will now dynamically construct the VisualTree based on your GameObject hierarchy. You've completed the required part of Graphene - however, we are still getting started.
Let's draw and bind some data onto our UI.
#### 2: Rendering & binding a model
- Add a [`Theme`][a617f693] to the root Graphene component.
- Add one or more [`Renderer`][b39c255d] components to each `Plate` that has dynamic (instantiated) content
- Assign a [`Model`][19f2ae47] to the Renderer - this is a data container that serves as the model for the data-binding.
- In the type(s) assigned as model, select the members you wish to expose for binding by adding [`BindAttribute`][b3387189]s. Add an additional `DrawAttribute` to dynamically instantiate controls in runtime using [`Templates`][fe269940].
[a617f693]: https://github.com/LudiKha/Graphene#theming "Theming"
Press play - Graphene will draw templates, and bind them to the model. If a static asset contained a control with a binding-path (e.g. a label with [`Model.Title`][04efb446]), this will be bound to the model too.
[04efb446]: https://github.com/LudiKha/Graphene#scopes "Scopes"
The hierarchy is created and detail fields are rendered dynamically - now all that remains is to switch states.
#### 3: Routing
- Add a [`StringRouter`][1015cb88] to the root GameObject.
- Add a `StringStateHandle` to each `Plate` GameObject that needs to be activated or deactivated based on states. Children are automatically deactivated with their parents. Give the StateHandle `StateId` unique names (e.g. "start", "load", "exit").
- For each `Plate` that has one or more children using states, select which child state is enabled by default by ticking `enableWithParent`
- In order to navigate, we can instantiate controls with a `RouteAttribute` or statically type them in UXML. Make sure to set the route member to a value that corresponds the available states.
> Note: It is also possible to encapsulate a button within a Route element.
> ```html
>
>
>
>
>
>```
Press play - The router constructs its state tree from the `Plate` hierarchy. When clicking a route element (or child button), the router will attempt to change states and the view will display this state change accordingly.
[0fb2479e]: https://github.com/LudiKha/Graphene#plates "Plates"
[b39c255d]: https://github.com/LudiKha/Graphene#rendering "Renderer"
[19f2ae47]: https://github.com/LudiKha/Graphene#model "Model"
[1015cb88]: https://github.com/LudiKha/Graphene#routing "Router"
[b3387189]: https://github.com/LudiKha/Graphene#binding "Binding"
[fe269940]: https://github.com/LudiKha/Graphene#templating "Templating"
Congrats! You're now done with the Quickstart and ready to tackle your first project using Graphene.
---
# Core Concepts
Graphene decouples fine-grained authoring from high-level logic, and in doing so aims to leverage UI Toolkit's innovations to the fullest.
## Plates
A `Plate` represents a view controller in the VisualTree, and is used by Graphene to display the hierarchy, its states and views.
A Graphene hierarchy consists of nested components called `Plates`, with a `Graphene` component at the root. `Plate`s are the core of Graphene, are analogous for a general-purpose UI controller that can be switched on or off. Other, optional MonoBehaviour components may hook into a plate, and have their functionality based on whether a plate is active or not.
The following components and logic depends on plates:
- View
These can be authored in the familiar GameObject hierarchy. Graphene then constructs the VisualElement tree at runtime into a nested view.
## Views
A `View` represents a section of the screen, and is defined as a VisualElement within the `Plate`'s UXML asset. It contains an 'id', which serves as a unique identifier within the plate. Examples of views include a sidebar, a content area, a header or footer.
Views can be used to compose static content (i.e. child Plates composing into parent Views), or to render dynamic content into (i.e. a renderer drawing a list of items into a View).
By default, Graphene components assume these two types of views will be present on a `Plate`:
- **Content View**: A view that is used to render dynamic content into, using a `Renderer` component.
- **Children View**: A view that is used to compose child plates into, based on the GameObject hierarchy.
## Rendering & Binding
Rendering refers to the process of drawing and binding a model to the view. This is done using a `Renderer` component. It requires a `Model` to be assigned, which serves as the data container for the binding.
It consists of two passes:
1. A static binding pass, where elements that are part of the static UXML asset assigned to the `Plate`'s `VisualTreeAsset` are bound to the model.
2. A dynamic rendering and binding pass, where templates are rendered and bound from the model.
A manager class called `Binder` takes care of the binding process and lifecycle management, and supports one-way and two-way binding modes.
## Model
A `Model` is a data container that serves as the model for the data-binding. Any class or struct can be used as a model, as long as it has been decorated with the `[Bind]` attribute on the members you wish to expose for binding.
## Templating
Templating refers to the process of dynamically instantiating static UXML assets at runtime, based on a predetermined type of `Control` (i.e. a button, a toggle, a slider etc). UXML templates are mapped to a `ControlType` enum in a `TemplateAsset`, and DrawAttributes are used to instruct the `Renderer` which templates to instantiate.
### Template
A `Template` is a semantic name for static asset that represents a chunk of UXML of varying granularity and complexity, which are used as building blocks to build and render the application. Moreover, templates can be declared directly in UXML based, and will be rendered at runtime based on the `Renderer` template configuration. Templates are wrapped in a `TemplateAsset` ScriptableObject, where additional variants can be created without needing to create and maintain copies of the base template.
#### Why use templates
When creating a simple element, such as a button, it may quickly end up consisting of several carefully configured elements and bindings:
```html
```
Maintaining multiple versions and instances of the same chunk of UXML throughout multiple files can be both error prone and time intensive. Graphene allows you to reuse the same template, and instantiate them at runtime when required.
#### Creating a template
Create a `TemplateAsset` via the following menu command:
> `Assets/Create/Graphene/Templating/TemplateAsset`
Assign a static UXML template, and give it an appropriate name.
#### Instantiating a template via CSharp
Templates can be instantiated directly in C# via a reference of the `TemplateAsset`.
```csharp
var clone = myTemplateAsset.Instantiate();
...
```
#### Instantiating a template via UXML
Graphene allows you to statically type control types using the following syntax:
```html
```
At runtime, the button will be rendered to the full syntax of the first snippet, using the `Template` configuration of the `Renderer` component that initiates the binding.
## Binding
### Binding Modes
Graphene supports 3 modes of binding a model to the view. These can either be specified in the BindAttribute on the model, or using the Binder API directly.
- **OneTime**: Instructs the `Binder` to only "print" the model once onto the view. No continuous binding will be attempted. Useful for immutable data, such as titles, labels or button callbacks.
>Note: Prefixing the binding-path with the `::` syntax instructs the binder to use a one-time binding.
>```html
>
>```
- **OneWay**: Creates a continuous binding from the model to the view. Updates are polled continuously but only for bindings that are currently visible (based on `Plate` state). Polling rate can be configured in the `Graphene` component.
- **TwoWay**: Creates a two-directional binding (from model to view, and view to model) for controls that support two-way binding. View to model binding is based on `INotifyPropertyChange` callbacks.
### Scopes
- Scope drilldown `.` `Renderer.Model.ChildObject.Title`
- Scope transferral `~` `~Renderer.Model` > `ChildObject.Title`
### Binding Passes
1. Static
2. Dynamic
### Static Binding
Static binding refers to binding elements that are part of the static UXML asset assigned to the `Plate`'s `VisualTreeAsset`. It allows you to bind elements that are always present in the view, such as titles, labels, buttons or other controls that may have a binding-path assigned.
Common use-cases include binding a title label to `Model.Title`, or binding a button's `Route` to a member in the model.
### Dynamic Binding
Dynamic binding refers to binding elements that have been instantiated dynamically via the `Renderer`'s `Template` configuration, or viewmodel's `Instantiate` method.
Common use-cases include rendering a list of items from a collection, or rendering detail fields from a complex object, such as a Settings ScriptableObject.
## Routing
Graphene supports url-like state routing via the `Router` component. This allows you to construct a state hierarchy using the GameObject hierarchy, and navigate between states using `Route` elements.
The routing system is inspired by web standards, and mimics url-based addresses: `index/settings/video`.
## Localization
TBD
## Step-by-step process
1. Static view composition
2. Static binding pass
3. Render templates from dynamic model
4. Dynamic binding pass
5. Runtime one-way/two-way binding
---
# Inspector Extensions
Graphene supports two third party inspector extensions out of the box: [Sirenix Odin Inspector][5855fee6] (paid) and [NaughtyAttributes][06602d8c] (free). Graphene relies on these to expose optional enhanced functionality to the inspector windows.
[5855fee6]: https://odininspector.com/ "Sirenix Odin Inspector"
[06602d8c]: https://github.com/dbrizov/NaughtyAttributes "Naughty Attributes"
## Installation
### Odin Inspector
This asset is automatically setup when it is included in the project.
> Define symbol: `ODIN_INSPECTOR`
### Naughty Attributes
This package requires you to follow the following steps:
1. Add the package to the project `Packages/manifest.json` file:
>`"com.dbrizov.naughtyattributes": "https://github.com/dbrizov/NaughtyAttributes.git#upm"`
2. In Project Settings/Player/Scripting define symbols, add the following entry: `NAUGHTY_ATTRIBUTES;`
After recompilation, your project will now have enhanced inspector functionality for Graphene components.
================================================
FILE: docs/About.md
================================================
layout: page
title: "About Graphene"
permalink: /about/

# Graphene
## UI Framework for unity
## WebGL Demo
================================================
FILE: docs/_config.yml
================================================
theme: jekyll-theme-cayman
================================================
FILE: docs/howto.md
================================================
================================================
FILE: src/Core/Binding/Binder.cs
================================================
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UIElements;
namespace Graphene
{
using Elements;
using global::Graphene.ViewModel;
using Kinstrife.Core.ReflectionHelpers;
using System.Collections;
using System.Linq;
public interface IRoute
{
}
///
/// Utility class to bind bindable data objects to VisualElements, using the to match members to s.
///
/// Common use cases include buttons, labels, toggles, sliders etc. Supports recursive binding and drilling down into sub-scopes defined in UXML.
public static class Binder
{
internal static VisualElement InternalInstantiate(VisualTreeAsset template, Plate plate)
{
var clone = template.Instantiate();
return clone.Children().First();
}
///
/// Binds the tree recursively
///
///
///
///
public static VisualElement Instantiate(in object context, VisualTreeAsset template, Plate plate)
{
var clone = InternalInstantiate(template, plate);
var t = context.GetType();
if (RenderUtils.IsPrimitiveContext(t))
{
}
// Bind class with its own context
else
{
if (context is ICustomAddClasses customAddClasses)
clone.AddMultipleToClassList(customAddClasses.ClassesToAdd);
if (context is ICustomName customName && !string.IsNullOrWhiteSpace(customName.CustomName))
clone.name = customName.CustomName;
// Get members
List> members = new List>();
TypeInfoCache.GetMemberValuesWithAttribute(context, members);
Binder.BindRecursive(clone, context, members, plate, false);
}
return clone;
}
///
/// Binds the tree recursively
///
///
///
///
public static VisualElement InstantiatePrimitive(in object context, ref ValueWithAttribute bindableMember, VisualTreeAsset template, Plate plate)
{
var clone = InternalInstantiate(template, plate);
if (bindableMember.Attribute == null)
{
Debug.LogError($"Drawing {template.name} for primitive on {context} without Bind Attribute", template);
return clone;
}
// Get members
List> members = new List>();
members.Add(bindableMember);
// Bind without scope drilldown
Binder.BindRecursive(clone, context, members, plate, false);
return clone;
}
///
/// Binds the tree recursively
///
///
///
///
public static void BindRecursive(VisualElement element, object context, List> members, Plate plate, bool notFullyDrilledDown)
{
if (members == null)
{
// Get members
members = new List>();
TypeInfoCache.GetMemberValuesWithAttribute(context, members);
}
// Is bindable with binding-path in uxml
if (element is BindableElement el && !string.IsNullOrWhiteSpace(el.bindingPath))
{
// Should drill down to a child's scope (based on binding-path '.', and scope ovveride '~')
bool branched = notFullyDrilledDown && TryBranch(el, context, plate);
if (branched) // Started branch via drilled down scope branch
return;
BindElementValues(el, ref context, members, plate);
// Context potentially has routing binding (TODO remove interface check)
if (context is IRoute && plate.Router)
plate.Router.BindRouteToContext(el, context);
}
// Image (not bindable)
else if (element is Image image)
{
BindImage(image, ref context, members, plate);
}
// Rout el special case
else if (element is Route route)
{
BindRoute(route, ref context, plate);
}
BindChildren(element, context, members, plate, notFullyDrilledDown);
}
static void BindChildren(VisualElement element, object context, List> members, Plate plate, bool scopeDrillDown)
{
//element.BindValues(data);
if (element.childCount == 0)
{
return;
}
// Loop through children and bind data to them
foreach (var child in element.Children())
{
BindRecursive(child, context, members, plate, scopeDrillDown);
}
}
///
/// Binds values of a particular VisualElement to an IBindable
///
///
///
///
private static void BindElementValues(V el, ref object context, List> members, Plate plate) where V : BindableElement
{
if (el.binding != null/* || el.userData != null*/)
{
Debug.LogError($"Binding twice! {el}");
return;
}
else
{
el.userData = context;
plate.Graphene.BroadcastBindCallback(el, context, plate);
}
// Pass in list of properties for possible custom logic spanning multiple properties
if (el is Label)
BindLabel(el as Label, ref context, members, plate);
else if (el is Button)
BindButton(el as Button, ref context, members, plate);
else if (el is If)
BindIf(el as If, ref context, members, plate);
else if (el is Image image)
BindImage(image, ref context, members, plate);
else if (el is CycleField)
BindCycleField(el as CycleField, ref context, members, plate);
else if (el is DropdownField)
BindDropdownField(el as DropdownField, ref context, members, plate);
else if (el is ListView)
BindListView(el as ListView, ref context, members, plate);
else if (el is SelectField)
BindSelectField(el as SelectField, ref context, members, plate);
else if (el is Toggle)
BindBaseField(el as Toggle, ref context, members, plate);
else if (el is Slider)
BindSlider(el as Slider, ref context, members, plate);
else if (el is SliderInt sliderInt)
BindSlider(sliderInt, ref context, members, plate);
else if (el is MinMaxSlider minMax)
BindMinMaxSlider(minMax, ref context, members, plate);
else if (el is AbstractProgressBar progress)
BindProgress(progress, ref context, members, plate);
else if (el is Foldout foldout)
BindFoldout(foldout, ref context, members, plate);
else if (el is ButtonGroup buttonGroup)
BindButtonGroup(buttonGroup, ref context, members, plate);
else if (el is TextField)
BindTextField(el as TextField, ref context, members, plate);
else if (el is TextElement)
BindTextElement(el as TextElement, ref context, members, plate);
}
private static void BindTextElement(TextElement el, ref object context, List> members, Plate plate)
{
foreach (var item in members)
{
if (BindingPathOrTypeMatch(el, in item))
{
BindText(el, ref context, in item, plate);
break;
}
}
}
private static void BindLabel(Label el, ref object context, List> members, Plate plate)
{
foreach (var item in members)
{
if (BindingPathOrTypeMatch(el, in item))
{
BindText(el, ref context, in item, plate);
break;
}
}
}
private static void BindButton(Button el, ref object context, List> members, Plate plate)
{
foreach (var item in members)
{
if (BindingPathAndTypeMatch(el, in item))
{
//Debug.Log($"Binding BindableObject click {el.bindingPath} {context}", context as UnityEngine.Object);
//var data = item.Value as BindableObject;
BindRecursive(el, item.Value, null, plate, false);
var bindable = (BindableObject)item.Value;
el.tooltip = bindable.Tooltip;
BindCallbacks(el, context);
//Debug.Log(bindable);
break;
}
else if (BindingPathAndTypeMatch(el, in item))
{
//Debug.Log($"Binding ActionButton click {el.bindingPath} {context}", context as UnityEngine.Object);
BindRecursive(el, item.Value, null, plate, false);
el.tooltip = ((ActionButton)item.Value).Tooltip;
break;
}
else if (BindingPathAndTypeMatch(el, in item))
{
//Debug.Log($"Binding string click {el.bindingPath} {item.Value} {context}", context as UnityEngine.Object);
BindText(el, ref context, in item, plate);
//break;
}
else if (BindingPathOrTypeMatch(el, in item))
{
//Debug.Log($"Binding click {el.GetType().Name} {el.bindingPath} ({item.MemberInfo.Name} {item.Type.Name} in {context})", context as UnityEngine.Object);
BindClick(el, (Action)item.Value, context, plate);
break;
}
else if (BindingPathOrTypeMatch(el, in item))
{
//Debug.Log($"Binding UnityEvent click {el.GetType().Name} {el.bindingPath} ({item.MemberInfo.Name} {item.Type.Name} in {context})", context as UnityEngine.Object);
BindClick(el, (UnityEngine.Events.UnityEvent)item.Value, context, plate);
break;
}
}
}
internal static void BindRoute(Route el, ref object context, Plate plate)
{
// Check if parent is a button -> propagate click
if (el.parent is Button button)
{
BindClick(button, el.clicked, context, plate);
}
else
{
foreach (var item in el.Children())
{
if (item is Button btn)
{
BindClick(btn, el.clicked, context, plate);
}
else if (item is ButtonGroup btnGroup)
{
btnGroup.clicked += (int index, string route) => { el.route = route; el.clicked?.Invoke(); }; // A bit hacky perhaps
}
}
}
el.SetRouter(plate.Router);
// Let the (generic) router handle the way it binds routes
plate.Router.BindRoute(el, context);
}
private static void BindSlider(Slider el, ref object context, List> members, Plate plate)
{
el.Q("unity-dragger").pickingMode = PickingMode.Ignore;
// Slider specifics
foreach (var item in members)
{
// Primary
if (BindingPathOrTypeMatch(el, in item))
{
if (item.Attribute is BindFloatAttribute floatAttribute)
{
el.value = floatAttribute.startingValue;
el.lowValue = floatAttribute.lowValue;
el.highValue = floatAttribute.highValue;
el.showInputField = floatAttribute.showInputField;
break;
}
}
else if (BindingPathAndTypeMatch("Min", item))
el.lowValue = (float)item.Value;
else if (BindingPathAndTypeMatch("Max", item))
el.highValue = (float)item.Value;
}
// Bind base field value & callback
BindBaseField(el, ref context, members, plate);
}
private static void BindSlider(SliderInt el, ref object context, List> members, Plate plate)
{
el.Q("unity-dragger").pickingMode = PickingMode.Ignore;
// Slider specifics
foreach (var item in members)
{
// Primary
if (BindingPathOrTypeMatch(el, item))
{
if (item.Attribute is BindIntAttribute att)
{
el.value = att.startingValue;
el.lowValue = att.lowValue;
el.highValue = att.highValue;
el.showInputField = att.showInputField;
break;
}
}
else if (BindingPathAndTypeMatch("Min", item))
el.lowValue = (int)item.Value;
else if (BindingPathAndTypeMatch("Max", item))
el.highValue = (int)item.Value;
}
//el./*showMixedValue*/ = true;
//el.showInputField = true;
// Bind base field value & callback
BindBaseField(el, ref context, members, plate);
}
private static void BindMinMaxSlider(MinMaxSlider el, ref object context, List> members, Plate plate)
{
el.Q("unity-dragger").pickingMode = PickingMode.Ignore;
// Slider specifics
foreach (var item in members)
{
// Primary
if (BindingPathOrTypeMatch(el, in item))
{
if (item.Attribute is BindRangeAttribute floatAttribute)
{
el.value = floatAttribute.startingValue;
el.lowLimit = floatAttribute.lowLimit;
el.highLimit = floatAttribute.highLimit;
break;
}
}
else if (BindingPathAndTypeMatch("Min", item))
el.lowLimit = (float)item.Value;
else if (BindingPathAndTypeMatch("Max", item))
el.highLimit = (float)item.Value;
}
// Bind base field value & callback
BindBaseField(el, ref context, members, plate);
}
private static void BindProgress(AbstractProgressBar el, ref object context, List> members, Plate plate)
{
// Progress Bar specifics
foreach (var item in members)
{
// Primary
if (BindingPathOrTypeMatch(el, item))
{
if (item.Attribute is BindFloatAttribute att)
{
el.value = att.startingValue;
el.lowValue = att.lowValue;
el.highValue = att.highValue;
break;
}
}
else if (BindingPathAndTypeMatch("Min", item))
el.lowValue = (float)item.Value;
else if (BindingPathAndTypeMatch("Max", item))
el.highValue = (float)item.Value;
}
var results = BindNotifyValueChange(el, ref context, members, plate);
el.value = results.value;
el.title = results.label;
}
private static void BindTextField(TextField el, ref object context, List> members, Plate plate)
{
foreach (var item in members)
{
// Primary
if (BindingPathOrTypeMatch(el, in item))
{
if (item.Attribute is BindStringAttribute stringAttribute)
{
el.value = stringAttribute.startingValue;
el.isPasswordField = stringAttribute.password;
el.isReadOnly = stringAttribute.readOnly;
el.multiline = stringAttribute.multiLine;
if (stringAttribute.maxLength >= 0)
el.maxLength = stringAttribute.maxLength;
break;
}
}
}
BindBaseField(el, ref context, members, plate);
}
private static (TValueType value, string label) BindNotifyValueChange(TElementType el, ref object context, List> members, Plate plate) where TElementType : BindableElement, INotifyValueChanged
{
bool labelFromAttribute = false;
string label = null;
foreach (var item in members)
{
// Primary (value)
if (BindingPathOrTypeMatch(el, in item))
{
if (item.Value is TValueType value)
{
el.SetValueWithoutNotify(value);
plate.BindingsManager.TryCreate(el, in context, in item, plate);
}
else if (item.Value is BindableBaseField baseField)
{
el.SetValueWithoutNotify(baseField.value);
if (!string.IsNullOrWhiteSpace(baseField.Label))
label = baseField.Label;
plate.BindingsManager.TryCreate(el, in item.Value, in item, plate);
}
// Set label from attribute
if (item.Attribute is BindBaseFieldAttribute att)
{
if (!string.IsNullOrWhiteSpace(att.label))
{
//el.text = att.label;
labelFromAttribute = true;
label = att.label;
}
}
}
// Set register callback event
else if (item.Attribute is BindValueChangeCallbackAttribute callbackAttribute)
{
var target = item.Value as EventCallback>;
#if UNITY_ASSERTIONS
UnityEngine.Assertions.Assert.IsNotNull(target, "Bindable item Invalid callback - ValueType mismatch.");
#endif
el.RegisterValueChangedCallback(target);
}
// Set label from field, if not from attribute
else if (!labelFromAttribute && item.Attribute.Path == "Label" && item.Value is string labelText)
label = labelText;
//BindText(el.labelElement, ref context, labelText, in item, plate);
else if (item.Attribute is BindTooltip)
el.tooltip = ObjectToString(item.Value, item.Type);
}
// Returning value & label tuple because each element implements text differently. (E.g. Foldout vs. Basefield)
return (el.value, label);
}
private static void BindBaseField(BaseField el, ref object context, List> members, Plate plate)
{
var results = BindNotifyValueChange, TValueType>(el, ref context, members, plate);
el.label = results.label;
}
private static void BindDropdownField(DropdownField el, ref object context, List> members, Plate plate)
{
// Then bind the items
foreach (var item in members)
{
// Model items
if (BindingPathMatch(item.Attribute.Path, SelectField.itemsPath))
{
el.choices = item.Value as List;
break;
}
else if (item.Type.IsEnum) // Primitive -> Can only do two way
{
el.choices = Enum.GetNames(item.Type).ToList();
el.SetValueWithoutNotify(ObjectToString(item.Value, item.Type) ?? el.value);
var t = item.Type;
var memberName = item.MemberInfo.Name;
var accessor = TypeInfoCache.GetExtendedTypeInfo(context.GetType()).Accessor;
var ctx = context;
// Filthy hax
el.RegisterValueChangedCallback((evt) =>
{
var val = Enum.Parse(t, evt.newValue);
accessor[ctx, memberName] = val;
});
}
}
// First bind base field (string)
BindBaseField(el, ref context, members, plate);
}
private static void BindSelectField(SelectField el, ref object context, List> members, Plate plate)
{
// Then bind the items
foreach (var item in members)
{
// Model items
if (BindingPathMatch(item.Attribute.Path, SelectField.itemsPath))
{
el.items = item.Value as List;
break;
}
}
// First bind base field (int)
BindBaseField(el, ref context, members, plate);
}
private static void BindListView(ListView el, ref object context, List> members, Plate plate)
{
foreach (var bindMember in members)
{
// Primary
if (BindingPathMatch(bindMember.Attribute.Path, el.bindingPath))
{
ControlType controlType = ControlType.ListItem;
if (context is IListViewBindable listViewBindable)
{
controlType = listViewBindable.ItemControlType;
}
IList list = bindMember.Value as IList;
RenderUtils.templatesDefault.TryGetTemplateAsset(controlType, out VisualTreeAsset template);
InternalBindListView(el, in context, list, template, plate);
plate.BindingsManager.TryCreate(el, in context, in bindMember, plate);
break;
}
}
// Fallback
if (context is IListViewBindable listViewBindable2)
{
RenderUtils.templatesDefault.TryGetTemplateAsset(listViewBindable2.ItemControlType, out VisualTreeAsset template);
InternalBindListView(el, in context, listViewBindable2.ItemsSource, template, plate);
}
}
internal static void BindListView(ListView el, in object context, Plate plate, VisualTreeAsset templateAsset, in ValueWithAttribute member)
{
IList list = member.Value as IList;
InternalBindListView(el, in context, list, templateAsset, plate);
plate.BindingsManager.TryCreate(el, in context, in member, plate);
}
internal static void InternalBindListView(ListView el, in object context, IList itemsSource, VisualTreeAsset templateAsset, Plate plate)
{
bool elementsPickable = true;
if (context is IListViewBindable bindable)
{
bindable.Apply(el);
BindCallbacks(el, context);
elementsPickable = bindable.ElementsPickable;
}
Func makeItem = () =>
{
var e = InternalInstantiate(templateAsset, plate);
e.pickingMode = elementsPickable ? PickingMode.Position : PickingMode.Ignore;
return e;
};
Action bindItem = (e, i) =>
{
var src = itemsSource[i];
e.userData = src;
Binder.BindRecursive(e, src, null, plate, false);
if (!(e is BindableElement))
{
BindCallbacks(e, src);
}
};
el.makeItem = makeItem;
el.bindItem = bindItem;
el.itemsSource = itemsSource;
}
private static void BindCycleField(CycleField el, ref object context, List> members, Plate plate)
{
// Then bind the items
foreach (var item in members)
{
// Model items
if (BindingPathMatch(item.Attribute.Path, CycleField.itemsPath))
{
el.items = item.Value as List;
break;
}
}
// First bind base field (int)
BindBaseField(el, ref context, members, plate);
}
private static void BindIf(If el, ref object context, List> members, Plate plate)
{
// Then bind the items
foreach (var item in members)
{
// Model items
if (BindingPathMatch(el, in item))
{
plate.BindingsManager.TryCreate