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 ================================================

  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) ![last-commit](https://img.shields.io/github/last-commit/LudiKha/Graphene) ![open-issues](https://img.shields.io/github/issues/LudiKha/Graphene) [![](https://img.shields.io/twitter/follow/LudiKha.svg?label=Follow&style=social)](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 ![Git button](docs/images/installation/git.png) 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](images/graphene-logo-full.png) # 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(el, in context, in item, plate); // This shows/hides the bindable el.OnModelChange(item.Value); return; } } } private static void BindImage(Image el, ref object context, List> members, Plate plate) { // Then bind the items foreach (var item in members) { // Model items if (BindingPathOrTypeMatch("Image", item)) { el.image = item.Value as Texture; if (item.Attribute.hideIfEmpty) el.SetShowHide(el.image != null); return; } else if (BindingPathOrTypeMatch("Image", item)) { el.sprite = item.Value as Sprite; if (item.Attribute.hideIfEmpty) el.SetShowHide(el.sprite != null); return; } } } private static void BindFoldout(Foldout el, ref object context, List> members, Plate plate) { foreach (var member in members) { if (BindingPathMatch(el, in member) && member.Value is BindableBaseField baseField) { BindRecursive(el, baseField, null, plate, false); return; } } if (context is BindableBaseField baseFieldContext) { el.SetValueWithoutNotify(baseFieldContext.value); if (!string.IsNullOrWhiteSpace(baseFieldContext.Label)) el.text = baseFieldContext.Label; } foreach (var member in members) { if (member.Value is bool) plate.BindingsManager.TryCreate(el, in context, in member, plate); else if (member.Value is string) plate.BindingsManager.TryCreate(el, in context, in member, plate); } //var results = BindNotifyValueChange(el, ref context, members, plate); } private static void BindButtonGroup(ButtonGroup el, ref object context, List> members, Plate plate) { // Then bind the items foreach (var item in members) { // Model items if (BindingPathAndTypeMatch>(el, item)) { el.items = item.Value as List; break; } else if (BindingPathAndTypeMatch>(el, item)) { el.items.Clear(); var items = item.Value as List; if (items == null || items.Count == 0) continue; el.ClearItems(); foreach (var action in items) el.AddItem(action.Label, action.Tooltip); el.SourceData = items; //el.ClearCallback(); //el.clicked += (int i, string name) => items[i].OnClick?.Invoke(); break; } } } private static void BindText(TextElement el, ref object context, in ValueWithAttribute member, Plate plate) { // Add translation here el.text = ObjectToString(in member.Value, member.Type); plate.BindingsManager.TryCreate(el, ref context, in member, plate); } private static void BindClick(Button el, System.Action action, in object context, Plate plate) { el.clicked += action; BindCallbacks(el, context); } private static void BindClick(Button el, UnityEngine.Events.UnityEvent unityEvent, in object context, Plate plate) { el.clicked += delegate { unityEvent?.Invoke(); }; BindCallbacks(el, context); } [System.Obsolete("Use IBindableToVisualElement and apply in bindable object")] static void BindCallbacks(VisualElement el, in object context) { if (context is IHasTooltip tooltip) { el.tooltip = tooltip.Tooltip; if (context is IBindableToVisualElement bindable) { el.SetEnabled(bindable.isEnabled); el.SetActive(bindable.isActive2); el.SetShowHide(bindable.isShown); bindable.SetBinding(el); bindable.onSetEnabled += el.SetEnabled; bindable.onShowHide += el.SetShowHide; bindable.onSetActive += el.SetActive; //Debug.Log($"On bind element {el} to context {context} "); } } } public static string[] stringSplitOptions = new string[] { ".", "~", "::", "_" }; public const char nestedScopeChar = '.'; public const char relativeScopeChar = '_'; public const string oneTimeBindingChar = "::"; private static bool TryBranch(BindableElement el, object data, Plate owner) { var scopes = el.bindingPath.Split(nestedScopeChar); if (scopes.Length == 1) return false; // Create sub scope '~' bool createSubScope = false; string bindingPath = el.bindingPath; if (el.bindingPath.IndexOf(relativeScopeChar) == 0) { createSubScope = true; bindingPath = bindingPath.Remove(0, 1); } return DrillDownToChildScopeRecursive(el, data, owner, bindingPath, createSubScope); } private static bool DrillDownToChildScopeRecursive(BindableElement el, object data, Plate owner, string currentScope, bool createSubScope) { if (data == null) { Debug.LogError($"Data was null for scope {currentScope} {owner}", owner); return false; } //Debug.Log($"Drilling down to child scope {currentScope} {data} ({el})", data as UnityEngine.Object); // Get binding members info List> members = new List>(); TypeInfoCache.GetMemberValuesWithAttribute(data, members); // Context doesn't have any bindable members if (members.Count == 0) return false; // Split it & remove '~' and '::' var scopes = currentScope.Split(stringSplitOptions, StringSplitOptions.RemoveEmptyEntries); // We're at the leaf scope - bind if (scopes.Length == 1) { // Override the element's path now we found the scope el.bindingPath = currentScope; // Start a new binding branch here and terminate the one we came from if (createSubScope) { BindRecursive(el, data, members, owner, createSubScope); return true; } // Only bind the element values, and carry on with the child binding as usual else { BindElementValues(el, ref data, members, owner); return false; } } // Select the topmost scope string targetScope = scopes[0]; ValueWithAttribute[] matchingMembers = members.Where(x => x.Attribute.Path?.ToLower() == targetScope?.ToLower()).ToArray(); // Might need/want to throw an error here if (matchingMembers.Length == 0) return false; bool startedBranch = false; string newPath = currentScope.Substring(currentScope.IndexOf(nestedScopeChar) + 1); foreach (var member in matchingMembers) { if (DrillDownToChildScopeRecursive(el, member.Value, owner, newPath, createSubScope)) startedBranch = true; } return startedBranch; } #region Internals internal static bool BindingPathMatch(in string a, in string b) { return string.CompareOrdinal(a, b) == 0; } internal static bool BindingPathMatch(BindableElement el, in ValueWithAttribute member) { return string.CompareOrdinal(el.bindingPath, member.Attribute.Path) == 0; } internal static bool BindingPathOrTypeMatch(BindableElement el, in ValueWithAttribute member) { return string.CompareOrdinal(el.bindingPath, member.Attribute.Path) == 0 || (string.IsNullOrEmpty(member.Attribute.Path) && typeof(T).IsAssignableFrom(member.Type)); } internal static bool BindingPathOrTypeMatch(in string path, in ValueWithAttribute member) { return string.CompareOrdinal(path, member.Attribute.Path) == 0 || (string.IsNullOrEmpty(member.Attribute.Path) && typeof(T).IsAssignableFrom(member.Type)); } internal static bool BindingPathAndTypeMatch(in BindableElement el, in ValueWithAttribute member) { return string.CompareOrdinal(el.bindingPath, member.Attribute.Path) == 0 && typeof(T).IsAssignableFrom(member.Type); } internal static bool BindingPathAndTypeMatch(in string a, in ValueWithAttribute member) { return string.CompareOrdinal(a, member.Attribute.Path) == 0 && typeof(T).IsAssignableFrom(member.Type); } internal static string ObjectToString(in object obj, in Type t) { // Add translation here if (obj is string str) return str; else if (t.IsEnum) return Enum.GetName(t, obj); else if (obj != null) return obj.ToString(); return default; } #endregion } } ================================================ FILE: src/Core/Binding/Binder.cs.meta ================================================ fileFormatVersion: 2 guid: f25cd91944aa2e546b1708c66852751c MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Core/Binding/Binding.cs ================================================  using System; using System.ComponentModel; using UnityEngine; using UnityEngine.UIElements; namespace Graphene { using Elements; using Kinstrife.Core.ReflectionHelpers; /// /// Base class for runtime Bindings between a context object and a VisualElement /// public abstract class Binding : IDisposable, IBinding { public bool scheduleDispose; public virtual void Dispose() { } public abstract void PreUpdate(); public abstract void Release(); public abstract void Update(); } public abstract class Binding : Binding { protected object context; [SerializeField] protected T lastValue; [SerializeField] protected T newValue; protected BindableElement element; [SerializeField] BindAttribute attribute; // The target field protected string memberName; protected ExtendedTypeInfo extendedTypeInfo; public Binding(BindableElement el, in object context, in ValueWithAttribute member) { this.element = el; this.context = context; this.extendedTypeInfo = TypeInfoCache.GetExtendedTypeInfo(context.GetType()); // K: 28-10-2020 -> Could be optimized with member.MemberInfo.DeclaringType; this.attribute = member.Attribute; this.memberName = member.MemberInfo.Name; //el.binding = this; DetermineBindingMode(); RegisterEvents(); } void DetermineBindingMode() { if (context is INotifyPropertyChanged notifyPropertyChanged) { notifyPropertyChanged.PropertyChanged += Model_PropertyChanged; } // Specifically set to not have two-way binding if (attribute.bindingMode.HasValue) { if (attribute.bindingMode == BindingMode.TwoWay) RegisterTwoWayValueChangeCallback(); } // No value set - Determine based on control type else { // Can't two-way bind a label if (this.element is Label || element is If) return; else if (this.element is INotifyValueChanged) RegisterTwoWayValueChangeCallback(); } } void RegisterEvents() { if (context is IHasTooltip hasTooltip) { element.tooltip = hasTooltip.Tooltip; if (element is BaseField baseField) baseField.labelElement.tooltip = hasTooltip.Tooltip; if (context is IBindableToVisualElement bindable) { element.SetEnabled(bindable.isEnabled); element.SetActive(bindable.isActive2); element.SetShowHide(bindable.isShown); bindable.SetBinding(element); bindable.onSetEnabled += element.SetEnabled; bindable.onShowHide += element.SetShowHide; bindable.onSetActive += element.SetActive; element.RegisterCallback(OnDetach); } } } void OnDetach(DetachFromPanelEvent evt) => UnregisterEvents(); void UnregisterEvents() { if (context is IBindableToVisualElement bindable) { bindable.onSetEnabled -= element.SetEnabled; bindable.onShowHide -= element.SetShowHide; bindable.onSetActive -= element.SetActive; } } void SyncVisualElementToModel() { } protected virtual void Model_PropertyChanged(object sender, PropertyChangedEventArgs e) { } public override void Dispose() { base.Dispose(); UnregisterEvents(); } public override void PreUpdate() { throw new NotImplementedException(); } public override void Release() { throw new NotImplementedException(); } public override void Update() { // Needs to be disposed because the context ceased to exist if (context == null || !IsValidBinding()) { scheduleDispose = true; return; } newValue = GetValueFromMemberInfo(); UpdateFromModel(in newValue); } protected virtual void UpdateFromModel(in T newValue) { // Model changed -> Update view if (this.lastValue != null && !this.lastValue.Equals(newValue)) { if (newValue is T && element is INotifyValueChanged notifyValueChanged) notifyValueChanged.SetValueWithoutNotify(newValue); else if (newValue is string text && element is TextElement textEl) textEl.text = text; else if (newValue is string foldoutText && element is Foldout foldout) foldout.text = foldoutText; else if (element is IBindableElement bindableEl) bindableEl.OnModelChange(newValue); else Debug.LogError($"No binding found for {element?.bindingPath} {context}", context as UnityEngine.Object); } lastValue = newValue; } void RegisterTwoWayValueChangeCallback() { if (element is INotifyValueChanged notifyValChangedEl) { notifyValChangedEl.RegisterValueChangedCallback((evt) => { if (context is INotifyValueChanged ctx) ctx.value = notifyValChangedEl.value; else SetValueFromMemberInfo(evt.newValue); }); } } protected abstract bool IsValidBinding(); protected abstract T GetValueFromMemberInfo(); protected abstract void SetValueFromMemberInfo(T value); } } ================================================ FILE: src/Core/Binding/Binding.cs.meta ================================================ fileFormatVersion: 2 guid: 4c859ae80f4d89648b8c3b6dc1839149 MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Core/Binding/BindingAttribute.cs ================================================ using System.Collections; using System.Collections.Generic; using System.Runtime.CompilerServices; using UnityEngine; namespace Graphene { public class UIAttribute : System.Attribute { } public enum BindingMode { /// /// For immutable data. Use this for things like menu (buttons/labels) and static text. /// OneTime, /// /// For dynamic data. Use this for mutable data that isn't changable via UI. /// OneWay, /// /// For dynamic data which is changable through the UI. /// TwoWay } [System.AttributeUsage(System.AttributeTargets.Field | System.AttributeTargets.Property)] public class BindAttribute : UIAttribute { string path = ""; public string Path => path; /// /// The binding mode for this binding. When left null, the system will pick a binding mode based on the control type (recommended). /// public BindingMode? bindingMode = null; // By default don't override binding mode public bool hideIfEmpty; public BindAttribute() { } public BindAttribute(string path) { this.path = path; } public BindAttribute(BindingMode bindingMode) { this.bindingMode = bindingMode; } public BindAttribute(string path, BindingMode bindingMode) { this.path = path; this.bindingMode = bindingMode; } public BindAttribute(string path, BindingMode bindingMode, bool hideIfEmpty) { this.path = path; this.bindingMode = bindingMode; this.hideIfEmpty = hideIfEmpty; } } [System.AttributeUsage(System.AttributeTargets.Field | System.AttributeTargets.Property)] public class BindBaseFieldAttribute : BindAttribute { public string label; public bool showInputField; public BindBaseFieldAttribute(string path, string label = null, bool showInput = false) : base(path) { this.label = label; this.showInputField = showInput; } public BindBaseFieldAttribute(string path, BindingMode bindingMode, string label = null, bool showInput = false) : base(path, bindingMode) { this.label = label; this.showInputField = showInput; } } // For fields & properties [System.AttributeUsage(System.AttributeTargets.Field | System.AttributeTargets.Property)] public class BindFloatAttribute : BindBaseFieldAttribute { public float startingValue; public float lowValue; public float highValue; public BindFloatAttribute(string path, float startingValue, float min, float max, string label = null, bool showInput = false) : base(path, label, showInput) { this.startingValue = startingValue; this.lowValue = min; this.highValue = max; } public BindFloatAttribute(string path, BindingMode bindingMode, float startingValue, float min, float max, string label = null, bool showInput = false) : base(path, bindingMode, label, showInput) { this.startingValue = startingValue; this.lowValue = min; this.highValue = max; } } // For fields & properties [System.AttributeUsage(System.AttributeTargets.Field | System.AttributeTargets.Property)] public class BindIntAttribute : BindBaseFieldAttribute { public int startingValue; public int lowValue; public int highValue; public BindIntAttribute(string path, int startingValue, int min, int max, string label = null, bool showInput = false) : base(path, label, showInput) { this.startingValue = startingValue; this.lowValue = min; this.highValue = max; } public BindIntAttribute(string path, BindingMode bindingMode, int startingValue, int min, int max, string label = null, bool showInput = false) : base(path, bindingMode, label, showInput) { this.startingValue = startingValue; this.lowValue = min; this.highValue = max; } } // For fields & properties [System.AttributeUsage(System.AttributeTargets.Field | System.AttributeTargets.Property)] public class BindRangeAttribute : BindBaseFieldAttribute { public Vector2 startingValue; public float lowLimit; public float highLimit; public BindRangeAttribute(string path, Vector2 startingValue, float min, float max, string label = null) : base(path, label, showInput:false) { this.startingValue = startingValue; this.lowLimit = min; this.highLimit = max; } public BindRangeAttribute(string path, BindingMode bindingMode, Vector2 startingValue, float min, float max, string label = null) : base(path, bindingMode, label, showInput: false) { this.startingValue = startingValue; this.lowLimit = min; this.highLimit = max; } } // For fields & properties [System.AttributeUsage(System.AttributeTargets.Field | System.AttributeTargets.Property)] public class BindStringAttribute : BindBaseFieldAttribute { public readonly string startingValue; public readonly int maxLength; public readonly bool readOnly; public readonly bool multiLine; public readonly bool password; public BindStringAttribute(string path, string startingValue, int maxLength = -1, bool readOnly = false, bool multiLine = false, bool password = false, string label = null, bool showInput = false) : base(path, label, showInput) { this.startingValue = startingValue; this.maxLength = maxLength; this.readOnly = readOnly; this.multiLine = multiLine; } public BindStringAttribute(string path, BindingMode bindingMode, string startingValue, int maxLength = -1, bool readOnly = false, bool multiLine = false, bool password = false, string label = null, bool showInput = false) : base(path, bindingMode, label, showInput) { this.startingValue = startingValue; this.maxLength = maxLength; this.readOnly = readOnly; this.multiLine = multiLine; } } [System.AttributeUsage(System.AttributeTargets.Field | System.AttributeTargets.Property)] public class BindTooltip : BindAttribute { public BindTooltip(string path) : base(path) { } } [System.AttributeUsage(System.AttributeTargets.Field | System.AttributeTargets.Property)] public class BindValueChangeCallbackAttribute : BindAttribute { public BindValueChangeCallbackAttribute(string path) : base(path) { } } /// /// Marks a field or property to be drawn when an entire type is marked for rendering /// [System.AttributeUsage(System.AttributeTargets.Field | System.AttributeTargets.Property | System.AttributeTargets.Class)] public class DrawAttribute : System.Attribute { public ControlType controlType = ControlType.None; public int order; //public DrawAttribute(ControlType controlType) //{ // this.controlType = controlType; //} public DrawAttribute([CallerLineNumber]int order = 0) { this.order = order; } public DrawAttribute(ControlType controlType, [CallerLineNumber] int order = 0) { this.controlType = controlType; this.order = order; } } public enum Typography { None, h1, h2, h3, h4, h5, h6, subtitle1, subtitle2, body1, body2, caption, overline } /// /// Marks a field or property to be drawn when an entire type is marked for rendering /// [System.AttributeUsage(System.AttributeTargets.Field | System.AttributeTargets.Property | System.AttributeTargets.Class)] public class DrawTextAttribute : DrawAttribute { public Typography typography = Typography.None; public DrawTextAttribute([CallerLineNumber] int order = 0) : base(order) { this.order = order; } public DrawTextAttribute(Typography typography, [CallerLineNumber] int order = 0) : base(ControlType.Label, order) { this.typography = typography; } } [System.AttributeUsage(System.AttributeTargets.Field | System.AttributeTargets.Property)] public class RouteAttribute : UIAttribute { public RouteAttribute() { } } } ================================================ FILE: src/Core/Binding/BindingAttribute.cs.meta ================================================ fileFormatVersion: 2 guid: 1f614598db3d14645979fed59a3b5945 MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Core/Binding/BindingsManager.cs ================================================  using System; using System.Collections; using System.Collections.Generic; using System.Reflection; using UnityEngine.UIElements; namespace Graphene { using global::Graphene.Elements; using Kinstrife.Core.ReflectionHelpers; using UnityEngine; using UnityEngine.Profiling; /// /// Manages the creation, updating, and disposal of bindings between UI elements and data contexts. /// public class BindingsManager : GrapheneComponent { #region ShowInInspectorAttribute #if ODIN_INSPECTOR [Sirenix.OdinInspector.ShowInInspector] #elif NAUGHTY_ATTRIBUTES [NaughtyAttributes.ShowInInspector] #endif #endregion /// /// Mapping of all current bindings, keyed by panels /// Dictionary> bindings = new Dictionary>(); #region ShowInInspectorAttribute #if ODIN_INSPECTOR [Sirenix.OdinInspector.ShowInInspector] #elif NAUGHTY_ATTRIBUTES [NaughtyAttributes.ShowInInspector] #endif #endregion Dictionary> disposePostUpdate = new Dictionary>(); Dictionary> createPostUpdate = new Dictionary>(); internal uint bindingsCount; #if ODIN_INSPECTOR [Sirenix.OdinInspector.InfoBox("$bindingsInfo")] #elif NAUGHTY_ATTRIBUTES [NaughtyAttributes.InfoBox("bindingsInfo")] #endif [SerializeField] float bindingRefreshRate = 0.2f; #if UNITY_EDITOR public string bindingsInfo => $"{bindingsCount} bindings"; #endif #region ReadOnlyAttribute #if ODIN_INSPECTOR [Sirenix.OdinInspector.ReadOnly, Sirenix.OdinInspector.ShowInInspector] #elif NAUGHTY_ATTRIBUTES [NaughtyAttributes.ReadOnly, NaughtyAttributes.ShowInInspector] #endif #endregion float lastRefreshTime; //#if UNITY_EDITOR // [UnityEditor.InitializeOnEnterPlayMode] // public static void InitializeOnEnterPlayMode() // { // bindings = new Dictionary>(); // disposePostUpdate = new Dictionary>(); // createPostUpdate = new Dictionary>(); // } //#endif void LateUpdate() { #if UNITY_EDITOR if (!Application.isPlaying && !runInEditMode) return; #endif if (Time.unscaledTime - lastRefreshTime < bindingRefreshRate) return; if (!graphene || !graphene.IsActiveAndVisible) return; OnUpdate(); lastRefreshTime = Time.unscaledTime; } public void OnUpdate() { bindingsCount = 0; #if UNITY_ASSERTIONS Profiler.BeginSample("Update Bindings", this); #endif // Update the bindings for active/visible panels foreach (var kvp in bindings) { var plate = kvp.Key; // Was disposed if (!plate) continue; // The panel is invisible, or inactive if (!plate.IsActive || !plate.Graphene.IsActiveAndVisible) continue; if (plate.bindingRefreshMode == BindingRefreshMode.None || (plate.bindingRefreshMode == BindingRefreshMode.ModelChange && !plate.wasChangedThisFrame)) continue; plate.wasChangedThisFrame = false; #if UNITY_ASSERTIONS Profiler.BeginSample(plate.DebugName, plate); #endif foreach (var binding in kvp.Value) { // Needs to be disposed if (binding.scheduleDispose) { ScheduleDispose(kvp.Key, binding); } // Update the binding else { binding.Update(); bindingsCount++; } } #if UNITY_ASSERTIONS Profiler.EndSample(); #endif } #if UNITY_ASSERTIONS Profiler.EndSample(); #endif #if UNITY_ASSERTIONS Profiler.BeginSample("CreateDispose"); #endif // Create bindings foreach (var kvp in createPostUpdate) { var list = GetList(kvp.Key, bindings); foreach (var binding in kvp.Value) list.Add(binding); } // Dispose unused bindings foreach (var kvp in disposePostUpdate) foreach (var binding in kvp.Value) Destroy(kvp.Key, binding); createPostUpdate.Clear(); disposePostUpdate.Clear(); #if UNITY_ASSERTIONS Profiler.EndSample(); #endif } List GetList(Plate panel, Dictionary> bindings) { if (bindings.ContainsKey(panel)) return bindings[panel]; List list = new List(); bindings.Add(panel, list); return list; } /// /// Creates a continuous binding between a TextElement and a member variable on a context (scope) and panel /// /// /// /// /// public void TryCreate(TextElement el, ref object context, in ValueWithAttribute member, Plate panel) { // Specifically set to one-time -> cancel binding if (member.Attribute.bindingMode.HasValue && member.Attribute.bindingMode.Value == BindingMode.OneTime) return; CreateBinding(el, in context, in member, panel); } /// /// Creates a continuous binding between a BaseField and a member variable on a context (scope) and panel /// /// /// /// /// /// public void TryCreate(BaseField el, in object context, in ValueWithAttribute member, Plate panel) { // Specifically set to one-time if (member.Attribute.bindingMode.HasValue && member.Attribute.bindingMode.Value != BindingMode.OneTime) return; CreateBinding(el, in context, in member, panel); } /// /// Creates a continuous binding between a TextElement and a member variable on a context (scope) and panel /// /// /// /// /// public void TryCreate(BindableElement el, in object context, in ValueWithAttribute member, Plate panel) { // Specifically set to one-time -> cancel binding if (member.Attribute.bindingMode.HasValue && member.Attribute.bindingMode.Value == BindingMode.OneTime) return; CreateBinding(el, in context, in member, panel); } internal void CreateBinding(BindableElement el, in object context, in ValueWithAttribute member, Plate panel) { Binding binding = null; // Collection binding if (el is ListView && typeof(TValueType).IsAssignableFrom(typeof(ICollection))) binding = new CollectionBinding(el, in context, in member); // Single binding else binding = new MemberBinding(el, in context, in member); if (binding != null) GetList(panel, createPostUpdate).Add(binding); } void ScheduleDispose(Plate panel, Binding binding) { GetList(panel, disposePostUpdate).Add(binding); } void Destroy(Plate panel, Binding binding) { GetList(panel, bindings).Remove(binding); binding.Dispose(); binding = null; } internal void DisposePlate(Plate plate, bool isDestroyed) { if(bindings.TryGetValue(plate, out var list)) { if (isDestroyed) bindings.Remove(plate); else list.Clear(); } if (disposePostUpdate.ContainsKey(plate)) disposePostUpdate.Remove(plate); } } } ================================================ FILE: src/Core/Binding/BindingsManager.cs.meta ================================================ fileFormatVersion: 2 guid: 4e490e9f02423f245a808e66b6d9c602 MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Core/Binding/CollectionBinding.cs ================================================ using System.Collections; using UnityEngine.UIElements; namespace Graphene { using Kinstrife.Core.ReflectionHelpers; public class CollectionBinding : Binding { protected int? lastLength; public CollectionBinding(BindableElement el, in object context, in ValueWithAttribute member) : base(el, in context, in member) { if (member.Value is ICollection) { } else { scheduleDispose = true; return; } lastValue = GetValueFromMemberInfo(); lastLength = lastValue?.Count; } protected override bool IsValidBinding() { return memberName != null; } protected override ICollection GetValueFromMemberInfo() { return (ICollection)extendedTypeInfo.Accessor[context, memberName]; } protected override void SetValueFromMemberInfo(ICollection value) { extendedTypeInfo.Accessor[context, memberName] = value; } protected override void UpdateFromModel(in ICollection newValue) { // Collection reference/count changed -> Assign new list if (!this.lastValue.Equals(newValue) || newValue.Count != lastLength.Value) if (element is ListView listView && newValue is IList iList) { listView.itemsSource = iList; lastLength = iList?.Count; } } } } ================================================ FILE: src/Core/Binding/CollectionBinding.cs.meta ================================================ fileFormatVersion: 2 guid: 6df237851253fc241a8dd9e269f6276c MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Core/Binding/MemberBinding.cs ================================================ using UnityEngine.UIElements; namespace Graphene { using Kinstrife.Core.ReflectionHelpers; using UnityEngine; public class MemberBinding : Binding { public MemberBinding(BindableElement el, in object context, in ValueWithAttribute member) : base(el, in context, in member) { lastValue = GetValueFromMemberInfo(); } protected override bool IsValidBinding() { return context != null; } protected override T GetValueFromMemberInfo() { #if UNITY_ASSERTIONS var val = extendedTypeInfo.Accessor[context, memberName]; // if(val == null) // { //Debug.LogError($"Trying to cast a null member {memberName} {extendedTypeInfo.Accessor.Type}"); //return default(T); // } if (val != null && !typeof(T).IsAssignableFrom(val.GetType())) { Debug.LogError($"InvalidCastException for member {memberName} {extendedTypeInfo.Accessor.Type}: Trying to cast binding {val.GetType().Name} to {typeof(T).Name}. " + $"\n{element.GetType().Name}"); return default(T); } #endif return (T)extendedTypeInfo.Accessor[context, memberName]; } protected override void SetValueFromMemberInfo(T value) { extendedTypeInfo.Accessor[context, memberName] = value; } } } ================================================ FILE: src/Core/Binding/MemberBinding.cs.meta ================================================ fileFormatVersion: 2 guid: 51eed3340e1759745903e1cc5f44143d MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Core/Binding.meta ================================================ fileFormatVersion: 2 guid: 6c2a5a11ef9582442b314d518afb15ec folderAsset: yes DefaultImporter: externalObjects: {} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Core/Extensions/ButtonGroup.cs ================================================  using Graphene.ViewModel; using System; using System.Collections.Generic; using System.Linq; using UnityEngine; using UnityEngine.UIElements; namespace Graphene.Elements { public class ButtonGroup : GroupBox, IBindableElement, INotifyValueChanged { public const string itemsPath = "Items"; [SerializeField] private List m_Items = new List(); public List items { get => m_Items; set { SetItems(value); } } /// /// Instantiates a using the data read from a UXML file. /// public new class UxmlFactory : UxmlFactory { } /// /// Defines for the . /// public new class UxmlTraits : BindableElement.UxmlTraits { UxmlIntAttributeDescription m_ActiveIndex = new UxmlIntAttributeDescription { name = "activeIndex" }; UxmlStringAttributeDescription m_Items = new UxmlStringAttributeDescription { name = "items" }; /// /// Initialize properties using values from the attribute bag. /// /// The object to initialize. /// The attribute bag. /// The creation context; unused. public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc) { base.Init(ve, bag, cc); ButtonGroup buttonGroup = (ButtonGroup)ve; buttonGroup.value = m_ActiveIndex.GetValueFromBag(bag, cc); buttonGroup.items = SelectField.ParseChoiceList(m_Items.GetValueFromBag(bag, cc)); } } [SerializeField] private int m_ActiveIndex = 0; public virtual int value { get { return m_ActiveIndex; } set { value = Mathf.Clamp(value, 0, childCount - 1); if (m_ActiveIndex == value) { SetValueWithoutNotify(value); return; } // in order for the serialization binding to update it's expecting you // to dispatch the event using (ChangeEvent valueChangeEvent = ChangeEvent.GetPooled(m_ActiveIndex, value)) { valueChangeEvent.target = this; // very umportant SetValueWithoutNotify(value); // actually set the value and do any init with the value SendEvent(valueChangeEvent); } } } public event System.Action clicked; public void SetValueWithoutNotify(int value) { m_ActiveIndex = Mathf.Clamp(value, 0, childCount - 1); SetButtonActive(); } /// /// USS class name of elements of this type. /// /// /// Unity adds this USS class to every instance of the TabGroup element. Any styling applied to /// this class affects every button located beside, or below the stylesheet in the visual tree. /// public static new readonly string ussClassName = "gr-button-group "; public static readonly string ussActiveClassName = "active"; /// /// Constructs an TabGroup. /// public ButtonGroup() { AddToClassList(ussClassName); } /// /// Callback from two-way binding system that model changed /// /// public void OnModelChange(int newValue) { tabIndex = newValue; } internal void SetButtonActive() { int i = 0; foreach (var child in Children()) { if (i == value) { child.AddToClassList(ussActiveClassName); } else child.RemoveFromClassList(ussActiveClassName); i++; } } public void SetItems(List items) { m_Items = items ?? new List(); RefreshButtons(); } public IList SourceData = new List(); public void RefreshButtons() { Clear(); foreach (var item in items) { InternalAddItem(item); } } internal void ButtonClicked(int i) { value = i; clicked?.Invoke(i, items[i]); if (SourceData != null && i < SourceData.Count) SourceData[i].OnClick?.Invoke(); } public void ClearItems() { m_Items.Clear(); Clear(); } public void ClearCallback() { clicked = null; } public void AddItem(string text, string tooltip = null) { items.Add(text); InternalAddItem(text, tooltip); } void InternalAddItem(string text, string tooltip = null) { int buttonIndex = childCount; var btn = new Button(() => ButtonClicked(buttonIndex)); btn.text = text.ToUpper(); btn.tooltip = tooltip; btn.AddToClassList("gr-button"); Add(btn); } } } ================================================ FILE: src/Core/Extensions/ButtonGroup.cs.meta ================================================ fileFormatVersion: 2 guid: bc01c42213f651244a970ed2b40bca5a MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Core/Extensions/CycleField.cs ================================================ using System; using System.Collections; using System.Collections.Generic; using System.Linq; using UnityEngine; using UnityEngine.UIElements; namespace Graphene.Elements { public class CycleField : BaseField { public const string itemsPath = "Items"; [SerializeField] private List m_Items = new List(); public List items { get => m_Items; set { if (value != null) m_Items = value; else m_Items = new List(); } } /// /// Instantiates a using the data read from a UXML file. /// public new class UxmlFactory : UxmlFactory { } /// /// Defines for the . /// public new class UxmlTraits : BaseFieldTraits { UxmlStringAttributeDescription m_Text = new UxmlStringAttributeDescription { name = "text" }; UxmlStringAttributeDescription m_Items = new UxmlStringAttributeDescription { name = "items" }; UxmlStringAttributeDescription m_Plus = new UxmlStringAttributeDescription { name = "plusSymbol" }; UxmlStringAttributeDescription m_Minus = new UxmlStringAttributeDescription { name = "minusSymbol" }; /// /// Initialize properties using values from the attribute bag. /// /// The object to initialize. /// The attribute bag. /// The creation context; unused. public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc) { base.Init(ve, bag, cc); var cycleField = (CycleField)ve; cycleField.text = m_Text.GetValueFromBag(bag, cc); cycleField.items = m_Items.GetValueFromBag(bag, cc).Split(';').Where(x => !string.IsNullOrEmpty(x)).ToList(); cycleField.plusSymbol = m_Plus.GetValueFromBag(bag, cc); cycleField.minusSymbol = m_Minus.GetValueFromBag(bag, cc); } } /// /// USS class name of elements of this type. /// public new static readonly string ussClassName = "gr-cycle-field"; /// /// USS class name of labels in elements of this type. /// public new static readonly string labelUssClassName = ussClassName + "__label"; /// /// USS class name of input elements in elements of this type. /// public new static readonly string inputUssClassName = ussClassName + "__input"; /// /// USS class name of elements of this type, when there is no text. /// public static readonly string noTextVariantUssClassName = ussClassName + "--no-text"; public static readonly string previousButtonUssClassName = ussClassName + "__button-previous"; public static readonly string nextButtonUssClassName = ussClassName + "__button-next"; /// /// USS class name of text elements in elements of this type. /// public static readonly string textUssClassName = ussClassName + "__text"; public static readonly string hiddenClassName = "hidden"; private Label m_Label; private VisualElement visualInput; Button m_Next; Button m_Previous; public string plusSymbol = "›"; // › ▶〉→ public string minusSymbol = "‹"; // ‹ ◀〈 ← public CycleField() : this(null) { } public CycleField(string label) : base(label, null) { // Hax var children = hierarchy.Children().ToList(); visualInput = hierarchy.Children().ToList().Find(x => x.ClassListContains("unity-base-field__input")); AddToClassList(ussClassName); AddToClassList(noTextVariantUssClassName); visualInput.AddToClassList(inputUssClassName); labelElement.AddToClassList(labelUssClassName); // The picking mode needs to be Position in order to have the Pseudostate Hover applied... visualInput.pickingMode = PickingMode.Position; // Allocate and add the buttons to the hierarchy m_Previous = new Button(); m_Next = new Button(); m_Previous.text = minusSymbol; m_Next.text = plusSymbol; //m_Next.AddToClassList("bold"); //m_Previous.AddToClassList("bold"); m_Previous.focusable = true; m_Next.focusable = true; m_Previous.AddToClassList(previousButtonUssClassName); m_Next.AddToClassList(nextButtonUssClassName); m_Previous.AddToClassList("nomargin"); m_Next.AddToClassList("nomargin"); visualInput.Add(m_Previous); m_Label = new Label { pickingMode = PickingMode.Ignore }; m_Label.text = "Select an option"; visualInput.Add(m_Label); visualInput.Add(m_Next); m_Previous.clicked += M_Previous_clicked; m_Next.clicked += M_Next_clicked; // Set-up the label and text... text = null; } private void M_Previous_clicked() { int newValue = value - 1; if (newValue < 0) newValue = items.Count - 1; value = newValue; } private void M_Next_clicked() { int newValue = value + 1; if (newValue >= items.Count) newValue = 0; value = newValue; } public override void SetValueWithoutNotify(int newValue) { base.SetValueWithoutNotify(newValue); string newText = ""; if (newValue >= 0 && newValue < items.Count) newText = items[newValue]; text = newText; } /// /// Optional text after the toggle. /// public string text { get { return m_Label?.text; } set { if (!string.IsNullOrEmpty(value)) { // Lazy allocation of label if needed... if (m_Label == null) { m_Label = new Label { pickingMode = PickingMode.Ignore }; m_Label.AddToClassList(textUssClassName); RemoveFromClassList(noTextVariantUssClassName); visualInput.Add(m_Label); } m_Label.text = value; } } } } } ================================================ FILE: src/Core/Extensions/CycleField.cs.meta ================================================ fileFormatVersion: 2 guid: eeb1b79b04839bd4389dd0e1a6457758 MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Core/Extensions/Dialog.cs ================================================ using System; using System.Collections; using System.Collections.Generic; using System.Linq; using UnityEngine; using UnityEngine.UIElements; namespace Graphene.Elements { public class Dialog : VisualElement, IDisposable { /// /// Instantiates a using the data read from a UXML file. /// public new class UxmlFactory : UxmlFactory { } /// /// Defines for the . /// public new class UxmlTraits : BindableElement.UxmlTraits { /// /// Initialize properties using values from the attribute bag. /// /// The object to initialize. /// The attribute bag. /// The creation context; unused. public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc) { base.Init(ve, bag, cc); } } /// /// USS class name of elements of this type. /// public static readonly string ussClassName = "gr-dialog"; /// /// USS class name of elements of this type. /// public static readonly string dialogBackgroundUssClassName = ussClassName + "__background"; public static readonly string dialogPanelUssClassName = ussClassName + "__panel"; public static readonly string hiddenClassName = "hidden"; VisualElement m_Background; private View m_PanelView; public event System.Action onClose; public Dialog() : this(null, null) { } public Dialog(IPanel panel, VisualElement content) { AddToClassList(ussClassName); // Set-up the label and text... //this.AddManipulator(new Clickable(OnClickEvent)); if (panel == null) panel = this.panel; m_Background = new VisualElement(); m_Background.AddToClassList(dialogBackgroundUssClassName); m_Background.AddToClassList("unity-ui-document__child"); m_Background.AddManipulator(new Clickable(OnClickBackground)); // Add background to root panel?.TopRoot().Add(m_Background); //panel.visualTree.Add(m_Background); // Add dialog to background m_Background.Add(this); //hierarchy.Add(m_Background); //Set up view m_PanelView = new View("GR__Content"); this.Add(m_PanelView); m_PanelView.isDefault = true; m_PanelView.AddToClassList(dialogPanelUssClassName); m_PanelView.Focus(); if (content != null) { m_PanelView.Add(content); content.Focus(); } m_Background.BringToFront(); this.BringToFront(); } bool ProcessClick(EventBase evt) { if (evt.eventTypeId == MouseUpEvent.TypeId()) { var ce = (IMouseEvent)evt; if (ce.button == (int)MouseButton.LeftMouse) { return true; } } else if (evt.eventTypeId == PointerUpEvent.TypeId() || evt.eventTypeId == ClickEvent.TypeId()) { var ce = (IPointerEvent)evt; if (ce.button == (int)MouseButton.LeftMouse) { return true; } } return false; } void OnClickBackground(EventBase evt) { if (ProcessClick(evt)) { onClose?.Invoke(); Dispose(); } } public void Dispose() { if(this.m_Background != null && this.m_Background.parent != null) this.m_Background.parent.Remove(this.m_Background); } public Dialog WithStyles(VisualElementStyleSheetSet styleSheets) { for (int i = 0; i < styleSheets.count; i++) { m_Background.styleSheets.Add(styleSheets[i]); } return this; } } } ================================================ FILE: src/Core/Extensions/Dialog.cs.meta ================================================ fileFormatVersion: 2 guid: 6ba5abe3b1bb58c439073459c2b50355 MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Core/Extensions/GrapheneRoot.cs ================================================  using System; using UnityEngine; using UnityEngine.UIElements; namespace Graphene.Elements { /// /// Root Graphene class that contains injec /// public class GrapheneRoot : BindableElement { ///// ///// Instantiates a using the data read from a UXML file. ///// //public new class UxmlFactory : UxmlFactory { } [SerializeField] private Router m_Router; public virtual Router router { get { return router; } set { m_Router = value; } } /// /// USS class name of elements of this type. /// /// /// Unity adds this USS class to every instance of the GrapheneRoot element. Any styling applied to /// this class affects every button located beside, or below the stylesheet in the visual tree. /// public static readonly string ussClassName = "gr-root"; /// /// Constructs a GrapheneRoot. /// public GrapheneRoot() : this(null) { } /// /// Constructs a GrapheneRoot. /// public GrapheneRoot(Router router) { AddToClassList(ussClassName); this.AddMultipleToClassList($"absolute fill {VisualElementExtensions.documentChildUssClassName}"); this.router = router; } } } ================================================ FILE: src/Core/Extensions/GrapheneRoot.cs.meta ================================================ fileFormatVersion: 2 guid: 41eb40e62488c9842b7021276222eaf1 MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Core/Extensions/If.cs ================================================  using System; using UnityEngine; using UnityEngine.UIElements; namespace Graphene.Elements { public class If : BindableElement, IBindableElement { /// /// Instantiates a using the data read from a UXML file. /// public new class UxmlFactory : UxmlFactory { } /// /// Defines for the . /// public new class UxmlTraits : BindableElement.UxmlTraits { UxmlBoolAttributeDescription m_Value = new UxmlBoolAttributeDescription { name = "value" }; /// /// Initialize properties using values from the attribute bag. /// /// The object to initialize. /// The attribute bag. /// The creation context; unused. public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc) { base.Init(ve, bag, cc); bool startEnabled = false; #if UNITY_EDITOR startEnabled = Application.isPlaying ? false : m_Value.GetValueFromBag(bag, cc); #endif ((If)ve).value = startEnabled; } } private bool m_Value = false; public virtual bool value { get { return m_Value; } set { m_Value = value; if (m_Value) this.Show(); else this.Hide(); } } /// /// USS class name of elements of this type. /// /// /// Unity adds this USS class to every instance of the If element. Any styling applied to /// this class affects every button located beside, or below the stylesheet in the visual tree. /// public static readonly string ussClassName = "gr-if"; /// /// Constructs an If. /// public If() { AddToClassList(ussClassName); value = false; } public void OnModelChange(object newValue) { if (newValue is bool b) value = b; else if (ReferenceEquals(newValue, null)) value = false; else if (newValue is null || newValue.Equals(false)) value = false; else if (newValue is string s && (string.IsNullOrEmpty(s) || string.IsNullOrWhiteSpace(s))) value = false; else value = true; } } } ================================================ FILE: src/Core/Extensions/If.cs.meta ================================================ fileFormatVersion: 2 guid: 6c0886185b9292347a8eef22665cc9e5 MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Core/Extensions/Route.cs ================================================  using System; using UnityEngine; using UnityEngine.UIElements; namespace Graphene.Elements { public class Route : BindableElement { /// /// Instantiates a using the data read from a UXML file. /// public new class UxmlFactory : UxmlFactory { } /// /// Defines for the . /// public new class UxmlTraits : BindableElement.UxmlTraits { UxmlStringAttributeDescription m_Route = new UxmlStringAttributeDescription { name = "route" }; /// /// Initialize properties using values from the attribute bag. /// /// The object to initialize. /// The attribute bag. /// The creation context; unused. public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc) { base.Init(ve, bag, cc); ((Route)ve).route = m_Route.GetValueFromBag(bag, cc); } } public Router router; [SerializeField] private string m_Route = String.Empty; public virtual string route { get { return m_Route; } set { m_Route = value?.ToLower(); } } /// /// USS class name of elements of this type. /// /// /// Unity adds this USS class to every instance of the Route element. Any styling applied to /// this class affects every button located beside, or below the stylesheet in the visual tree. /// public static readonly string ussClassName = "unity-route"; /// /// Constructs a Route. /// public Route() : this(null) { } /// /// Constructs a route with an Action that is triggered when the button is clicked. /// /// The action triggered when the button is clicked. /// /// By default, a single left mouse click triggers the Action. To change the activator, modify . /// public Route(string route) { AddToClassList(ussClassName); this.route = route; // Add click to itself this.AddManipulator(new Clickable(OnClickEvent)); clicked += Clicked; } public Action clicked; public void Clicked() { router.TryChangeState(route); } bool ProcessClick(EventBase evt) { if (evt.eventTypeId == MouseUpEvent.TypeId()) { var ce = (IMouseEvent)evt; if (ce.button == (int)MouseButton.LeftMouse) { return true; } } else if (evt.eventTypeId == PointerUpEvent.TypeId() || evt.eventTypeId == ClickEvent.TypeId()) { var ce = (IPointerEvent)evt; if (ce.button == (int)MouseButton.LeftMouse) { return true; } } return false; } void OnClickEvent(EventBase evt) { if (ProcessClick(evt)) { clicked.Invoke(); } } internal void SetRouter(Router r) { this.router = r as Router; r.onRoutingBlocked += OnRoutingBlocked; r.onRoutingUnblocked += OnRoutingUnblocked; } private void OnRoutingUnblocked() { SetEnabled(true); } private void OnRoutingBlocked() { SetEnabled(false); } } } ================================================ FILE: src/Core/Extensions/Route.cs.meta ================================================ fileFormatVersion: 2 guid: 2cc316cb1922df641ac18819655d7e7b MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Core/Extensions/SelectField.cs ================================================ using System; using System.Collections; using System.Collections.Generic; using System.Linq; using UnityEngine; using UnityEngine.UIElements; namespace Graphene.Elements { public class SelectField : BaseField, IDisposable { public const string itemsPath = "Items"; public int defaultItemHeight = 24; [SerializeField] private List m_Items = new List(); public List items { get => m_Items; set { if (value != null) m_Items = value; else m_Items = new List(); } } /// /// Instantiates a using the data read from a UXML file. /// public new class UxmlFactory : UxmlFactory { } /// /// Defines for the . /// public new class UxmlTraits : BaseFieldTraits { UxmlIntAttributeDescription m_ItemHeight = new UxmlIntAttributeDescription { name = "itemHeight" }; UxmlStringAttributeDescription m_Text = new UxmlStringAttributeDescription { name = "text" }; UxmlStringAttributeDescription m_Items = new UxmlStringAttributeDescription { name = "items" }; /// /// Initialize properties using values from the attribute bag. /// /// The object to initialize. /// The attribute bag. /// The creation context; unused. public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc) { base.Init(ve, bag, cc); int itemHeight = m_ItemHeight.GetValueFromBag(bag, cc); if (itemHeight > 0) ((SelectField)ve).defaultItemHeight = itemHeight; ((SelectField)ve).text = m_Text.GetValueFromBag(bag, cc); ((SelectField)ve).items = ParseChoiceList(m_Items.GetValueFromBag(bag, cc)); } } /// /// USS class name of elements of this type. /// public new static readonly string ussClassName = "gr-select-field"; /// /// USS class name of labels in elements of this type. /// public new static readonly string labelUssClassName = ussClassName + "__label"; /// /// USS class name of input elements in elements of this type. /// public new static readonly string inputUssClassName = ussClassName + "__input"; /// /// USS class name of elements of this type, when there is no text. /// public static readonly string noTextVariantUssClassName = ussClassName + "--no-text"; /// /// USS class name of elements of this type. /// public static readonly string listContainerUssClassName = ussClassName + "__list-container"; public static readonly string listViewUssClassName = ussClassName + "__list-view"; /// /// USS class name of text elements in elements of this type. /// public static readonly string textUssClassName = ussClassName + "__text"; public static readonly string hiddenClassName = "hidden"; private Label m_Label; private VisualElement visualInput; private ListView m_ListView; private Dialog m_Dialog; Toggle m_Toggle; public SelectField() : this(null) { } public SelectField(string label) : base(label, null) { // Hax var children = hierarchy.Children().ToList(); visualInput = hierarchy.Children().ToList().Find(x => x.ClassListContains("unity-base-field__input")); AddToClassList(ussClassName); AddToClassList(noTextVariantUssClassName); visualInput.AddToClassList(inputUssClassName); labelElement.AddToClassList(labelUssClassName); // The picking mode needs to be Position in order to have the Pseudostate Hover applied... //visualInput.pickingMode = PickingMode.Position; // Set-up the label and text... text = null; this.AddManipulator(new Clickable(OnClickEvent)); m_Toggle = new Toggle(); m_Toggle.text = text; m_Toggle.RegisterValueChangedCallback((evt) => { SetToggleState(m_Toggle.value); evt.StopPropagation(); }); visualInput.Add(m_Toggle); m_Toggle.Hide(); // Manual dispose RegisterCallback((evt) => Dispose()); } private void M_ListView_onItemsChosen(IEnumerable obj) { value = m_ListView.selectedIndex; } private void M_ListView_onSelectionChange(IEnumerable obj) { value = m_ListView.selectedIndex; } public override void SetValueWithoutNotify(int newValue) { base.SetValueWithoutNotify(newValue); string newText = ""; if (newValue >= 0 && newValue < m_Items.Count) newText = m_Items[newValue]; text = newText; m_Toggle.text = newText; } /// /// Optional text after the toggle. /// public string text { get { return m_Label?.text; } set { if (!string.IsNullOrEmpty(value)) { // Lazy allocation of label if needed... if (m_Label == null) { m_Label = new Label { pickingMode = PickingMode.Ignore }; m_Label.AddToClassList(textUssClassName); RemoveFromClassList(noTextVariantUssClassName); visualInput.Add(m_Label); } m_Label.text = value; } else if (m_Label != null) { m_Label.RemoveFromHierarchy(); AddToClassList(noTextVariantUssClassName); m_Label = null; } } } bool ProcessClick(EventBase evt) { if (evt.eventTypeId == MouseUpEvent.TypeId()) { var ce = (IMouseEvent)evt; if (ce.button == (int)MouseButton.LeftMouse) { return true; } } else if (evt.eventTypeId == PointerUpEvent.TypeId() || evt.eventTypeId == ClickEvent.TypeId()) { var ce = (IPointerEvent)evt; if (ce.button == (int)MouseButton.LeftMouse) { return true; } } return false; } void OnClickEvent(EventBase evt) { if(ProcessClick(evt)) OnClick(); } protected override void ExecuteDefaultActionAtTarget(EventBase evt) { base.ExecuteDefaultActionAtTarget(evt); if (evt == null) { return; } if (IsActivationEvent(evt)) { OnClick(); evt.StopPropagation(); } bool IsActivationEvent(EventBase e) { if (e.eventTypeId == KeyDownEvent.TypeId()) { var keyDownEvent = (KeyDownEvent)e; return keyDownEvent.keyCode == KeyCode.KeypadEnter || keyDownEvent.keyCode == KeyCode.Return; } return false; } } void OnClick() { m_Toggle.value = !m_Toggle.value; } void SetToggleState(bool value) { if (value) { m_ListView = CreateListView(); m_Dialog = new Dialog(panel, m_ListView); m_Dialog.onClose += M_Dialog_onClose; } } private void M_Dialog_onClose() { // Focus back on the select field Focus(); } VisualElement MakeItem() { return new Button(); } void BindItem(VisualElement el, int index) { (el as TextElement).text = items[index]; Debug.Log($"Created item at element {index}"); } ListView CreateListView() { var listView = new ListView(items, defaultItemHeight, MakeItem, BindItem); listView.AddToClassList(listViewUssClassName); listView.AddToClassList("h6"); listView.bindingPath = "Items"; listView.focusable = true; listView.onSelectionChange += M_ListView_onSelectionChange; listView.onItemsChosen += M_ListView_onItemsChosen; Func makeItem = () => new Label("ListViewOption"); Action bindItem = (e, i) => (e as Label).text = (e as Label).text + " " + i; bindItem = (e, i) => (e as Label).text = items[i]; listView.makeItem = makeItem; listView.bindItem = bindItem; //m_ListView.reorderable = true; listView.itemsSource = items; return listView; } public void Dispose() { if (m_Dialog != null) m_Dialog.Dispose(); } #region Util internal static List ParseChoiceList(string choicesFromBag) { if (string.IsNullOrEmpty(choicesFromBag.Trim())) { return null; } string[] array = choicesFromBag.Split(new char[1] { ',' }); if (array.Length != 0) { List list = new List(); string[] array2 = array; foreach (string text in array2) { list.Add(text.Trim()); } return list; } return null; } #endregion } } ================================================ FILE: src/Core/Extensions/SelectField.cs.meta ================================================ fileFormatVersion: 2 guid: bac1afa6a79c1714680bfc884ada8948 MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Core/Extensions/TemplateTypes/Button.cs ================================================  using System; using UnityEngine; using UnityEngine.UIElements; namespace Graphene.Elements { public class GrButton : TemplateRef, IBindableElement, IGrapheneElement { /// /// Instantiates a using the data read from a UXML file. /// public new class UxmlFactory : UxmlFactory { } /// /// USS class name of elements of this type. /// /// /// Graphene adds this USS class to every instance of the Template element. Any styling applied to /// this class affects every button located beside, or below the stylesheet in the visual tree. /// public static readonly new string ussClassName = "gr-button-ref"; /// /// Constructs an Template. /// public GrButton() : this(null) { m_Type = ControlType.Button; } public GrButton(Renderer renderer) : base(renderer) { m_Type = ControlType.Button; AddToClassList(ussClassName); } } } ================================================ FILE: src/Core/Extensions/TemplateTypes/Button.cs.meta ================================================ fileFormatVersion: 2 guid: 4063ec610a60a9049a8dc4a3a2d75ab8 MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Core/Extensions/TemplateTypes/TemplateRef.cs ================================================ using System.Linq; using UnityEngine; using UnityEngine.UIElements; namespace Graphene.Elements { public class TemplateRef : BindableElement, IBindableElement, IGrapheneElement { /// /// Instantiates a using the data read from a UXML file. /// public new class UxmlFactory : UxmlFactory { } /// /// Defines for the . /// public new class UxmlTraits : BindableElement.UxmlTraits { UxmlEnumAttributeDescription m_Value = new UxmlEnumAttributeDescription { name = "type" }; /// /// Initialize properties using values from the attribute bag. /// /// The object to initialize. /// The attribute bag. /// The creation context; unused. public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc) { base.Init(ve, bag, cc); ControlType type = m_Value.GetValueFromBag(bag, cc); // Only override if defined if (type != ControlType.None) ((TemplateRef)ve).type = type; } } private GrapheneRoot root; protected VisualElement m_ChildTemplate; protected Renderer renderer; [SerializeField] protected ControlType m_Type = ControlType.None; public virtual ControlType type { get { return m_Type; } set { m_Type = value; Render(); } } /// /// USS class name of elements of this type. /// /// /// Unity adds this USS class to every instance of the Template element. Any styling applied to /// this class affects every button located beside, or below the stylesheet in the visual tree. /// public static readonly string ussClassName = "gr-template-ref"; /// /// Constructs an Template. /// public TemplateRef() : this(null) { } public TemplateRef(Renderer renderer) { AddToClassList(ussClassName); this.renderer = renderer; } public void Inject(GrapheneRoot root, Plate plate, Renderer renderer) { this.renderer = renderer; } public void OnModelChange(ControlType newValue) { type = newValue; } void InstantiateTemplate() { renderer.Templates.TryGetTemplateAsset(this.type, out VisualTreeAsset template); var clone = Binder.InternalInstantiate(template, renderer.Plate); string name = this.name.Replace("Template", "").Replace("TemplateRef", ""); // Transfer binding path to top-level children & custom classes if (!string.IsNullOrWhiteSpace(bindingPath)) { var classes = this.GetClasses().Where(c => c != ussClassName); clone.Query().Children().ForEach(x => { x.bindingPath = bindingPath; x.AddMultipleToClassList(classes); if (!string.IsNullOrWhiteSpace(name)) clone.name = name; } ); this.ClearClassList(); this.AddToClassList(ussClassName); } Add(clone); m_ChildTemplate = clone; } public void Render() { if (m_ChildTemplate != null) ClearTemplate(); if (!renderer) return; // Clear InstantiateTemplate(); } void ClearTemplate() { Remove(m_ChildTemplate); } } } ================================================ FILE: src/Core/Extensions/TemplateTypes/TemplateRef.cs.meta ================================================ fileFormatVersion: 2 guid: d20f4dbc23d31124ca95a7906001617f MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Core/Extensions/TemplateTypes.meta ================================================ fileFormatVersion: 2 guid: 4de6f3d3fa92a844a98caeaeabfba7d4 folderAsset: yes DefaultImporter: externalObjects: {} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Core/Extensions/View.cs ================================================  using System; using UnityEngine; using UnityEngine.UIElements; namespace Graphene.Elements { public class View : VisualElement { /// /// Instantiates a using the data read from a UXML file. /// public new class UxmlFactory : UxmlFactory { } /// /// Defines for the . /// public new class UxmlTraits : BindableElement.UxmlTraits { UxmlStringAttributeDescription m_Id = new UxmlStringAttributeDescription { name = "id" }; UxmlBoolAttributeDescription m_Default = new UxmlBoolAttributeDescription { name = "default" }; /// /// Initialize properties using values from the attribute bag. /// /// The object to initialize. /// The attribute bag. /// The creation context; unused. public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc) { base.Init(ve, bag, cc); ((View)ve).id = m_Id.GetValueFromBag(bag, cc); ((View)ve).isDefault = m_Default.GetValueFromBag(bag, cc); } } [SerializeField] private string m_Id = String.Empty; public virtual string id { get { return m_Id; } set { m_Id = value; } } [SerializeField] private bool m_Default = false; public virtual bool isDefault { get { return m_Default; } set { m_Default = value; } } /// /// USS class name of elements of this type. /// /// /// Unity adds this USS class to every instance of the View element. Any styling applied to /// this class affects every button located beside, or below the stylesheet in the visual tree. /// public static readonly string ussClassName = "unity-view"; /// /// Constructs a View. /// public View() : this(null) { } /// /// Constructs a View with an Action that is triggered when the button is clicked. /// /// The action triggered when the button is clicked. /// /// By default, a single left mouse click triggers the Action. To change the activator, modify . /// public View(string id) : base () { AddToClassList(ussClassName); this.id = id; } } } ================================================ FILE: src/Core/Extensions/View.cs.meta ================================================ fileFormatVersion: 2 guid: a191edcfe0e527d4d91c17ee1926751d MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Core/Extensions/VisualElementExtensions.cs ================================================ using System.Collections.Generic; using UnityEngine.UIElements; namespace Graphene { public static class VisualElementExtensions { internal const string documentRootUssClassName = "unity-ui-document__root"; internal const string documentChildUssClassName = "unity-ui-document__child"; internal const string hiddenUssClassName = "hidden"; internal const string invisibleUssClassName = "invisible"; internal const string fadeoutUssClassName = "fadeout"; internal const string transitioningUssClassName = "transitioning"; //internal const string fadeinUssClassName = "fadein"; internal const string activeUssClassName = "active"; internal const string selectedUssClassName = "selected"; public static bool IsHidden(this VisualElement el) { return el.ClassListContains(hiddenUssClassName); } public static void Show(this VisualElement el) { el.RemoveFromClassList(hiddenUssClassName); } public static void Hide(this VisualElement el) { el.AddToClassList(hiddenUssClassName); } public static void SetShowHide(this VisualElement el, bool value) { if (value) el.Show(); else el.Hide(); } public static void SetVisibility(this VisualElement el, bool value) { if (value) el.RemoveFromClassList(invisibleUssClassName); else el.AddToClassList(invisibleUssClassName); } public static void SetActive(this VisualElement el, bool value) { if(value) el.AddToClassList(activeUssClassName); else el.RemoveFromClassList(activeUssClassName); } public static void ToggleClass(this VisualElement el, string className, bool value) { if (value) el.AddToClassList(className); else el.RemoveFromClassList(className); } public static void FadeIn(this VisualElement el) { el.RemoveFromClassList(fadeoutUssClassName); } public static void FadeOut(this VisualElement el) { el.AddToClassList(fadeoutUssClassName); } public static void StartTransition(this VisualElement el) { el.AddToClassList(transitioningUssClassName); } public static void StopTransition(this VisualElement el) { el.RemoveFromClassList(transitioningUssClassName); } public static bool IsFadingOut(this VisualElement el) { return el.ClassListContains(fadeoutUssClassName); } public static bool IsTransitioning(this VisualElement el) { return el.ClassListContains(transitioningUssClassName); } public static VisualElement TopRoot(this VisualElement el) { return el.panel?.visualTree.Q(null, documentRootUssClassName); } public static VisualElement TopRoot(this IPanel panel) { return panel?.visualTree.Query(null, documentRootUssClassName).Last(); } public static void AddStyles(this VisualElement el, VisualElementStyleSheetSet styleSheets) { for (int i = 0; i < styleSheets.count; i++) { var sheet = styleSheets[i]; if (sheet != null) el.styleSheets.Add(sheet); #if UNITY_EDITOR else UnityEngine.Debug.LogError("Trying to add null stylesheet"); #endif } } public static void AddStyles(this VisualElement el, IEnumerable styleSheets) { foreach (var styleSheet in styleSheets) { if(styleSheet) el.styleSheets.Add(styleSheet); #if UNITY_EDITOR else UnityEngine.Debug.LogError("Trying to add null stylesheet"); #endif } } /// /// Adds multiple classes to VisualElement. ClassNames separated by space ' '. /// /// /// Separated by space public static void AddMultipleToClassList(this VisualElement el, string classes) { if (string.IsNullOrWhiteSpace(classes)) return; AddMultipleToClassList(el, Parse(classes)); } public static void AddMultipleToClassList(this VisualElement el, IEnumerable classes) { foreach (var className in classes) el.AddToClassList(className); } /// /// Removes multiple classes from VisualElement. ClassNames separated by space ' '. /// /// /// Separated by space public static void RemoveMultipleFromClassList(this VisualElement el, string classes) { if (string.IsNullOrWhiteSpace(classes)) return; RemoveMultipleFromClassList(el, Parse(classes)); } public static void RemoveMultipleFromClassList(this VisualElement el, IEnumerable classes) { foreach (var className in classes) el.RemoveFromClassList(className); } public static IEnumerable Parse(string classes) { return classes?.Split(new string[] { " " }, System.StringSplitOptions.RemoveEmptyEntries); } } } ================================================ FILE: src/Core/Extensions/VisualElementExtensions.cs.meta ================================================ fileFormatVersion: 2 guid: fd3bb9a5a9100d24cb94c9a528a30eb0 MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Core/Extensions.meta ================================================ fileFormatVersion: 2 guid: 4abeed0c73932b946972d66da4b7ae42 folderAsset: yes DefaultImporter: externalObjects: {} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Core/Graphene.cs ================================================ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using UnityEngine; using UnityEngine.Profiling; using UnityEngine.UIElements; namespace Graphene { using Elements; using System.Collections; [ExecuteInEditMode] [RequireComponent(typeof(UIDocument))] [RequireComponent(typeof(BindingsManager))] [DisallowMultipleComponent] public class Graphene : MonoBehaviour { [SerializeField, Tooltip("Disable this if you want to manually initialize Graphene")] bool initializeOnStart = true; [SerializeField, Tooltip("Disable this if you want to manually initialize Graphene")] new bool runInEditMode = false; [SerializeField] string addClasses; [SerializeField] public PickingMode defaultPickingMode; [SerializeField] List plates = new List(); public IReadOnlyList Plates => plates; List dependents = new List(); public event System.Action> onPreInitialize; public event System.Action> onPostInitialize; public event System.Action onBindElement; /// /// Root Graphene element controller /// GrapheneRoot grapheneRoot; public GrapheneRoot GrapheneRoot => grapheneRoot; /// /// The UI Document /// [SerializeField] UIDocument doc; public UIDocument Doc => doc; /// /// The router /// [SerializeField] Router router; public Router Router => router; [SerializeField] BindingsManager binder; public BindingsManager Binder => binder; public bool IsInitialized => grapheneRoot != null; public bool IsActiveAndInitialized => isActiveAndEnabled && IsInitialized; public bool IsActiveAndVisible => IsActiveAndInitialized && !grapheneRoot.IsHidden(); #region Events public event System.Action plateOnShow; public event System.Action plateOnHide; #endregion protected void Start() { GetLocalReferences(); if (!Application.isPlaying) { GetChildPlates(); return; } if (enabled && initializeOnStart) Initialize(); } #region Attributes #if ODIN_INSPECTOR [Sirenix.OdinInspector.ShowInInspector] #elif NAUGHTY_ATTRIBUTES [NaughtyAttributes.ShowInInspector] #endif #endregion public bool Initialized { get; private set; } public bool IsValid => doc && doc.panelSettings && doc.visualTreeAsset; #region ButtonAttribute #if ODIN_INSPECTOR [Sirenix.OdinInspector.ResponsiveButtonGroup] #elif NAUGHTY_ATTRIBUTES [NaughtyAttributes.Button] #endif #endregion public void Initialize() { if (Initialized) return; GetLocalReferences(); if (!IsValid) { UnityEngine.Debug.LogError($"Graphene missing requirements. Please make sure UIDocument is present, has PanelSettings and a VisualTreeAsset", this); return; } RunInstallation(); //doc.enabled = false; //doc.enabled = true; Initialized = true; FinalizeInitialzation(); } protected void GetLocalReferences() { doc ??= GetComponent(); router ??= GetComponent(); binder = GetComponent(); } #if ODIN_INSPECTOR [Sirenix.OdinInspector.ResponsiveButtonGroup] #endif public void GetChildPlates() { GetComponentsInChildren(includeInactive: true, plates); foreach (var p in plates) p.Inject(this); } protected void RunInstallation() { var sw = new Stopwatch(); sw.Start(); // Get all dependents GetComponentsInChildren(true, dependents); GetChildPlates(); // Inject Graphene into foreach (var c in dependents) { if (c is IGrapheneInjectable gc) gc.Inject(this); } Profiler.BeginSample("Graphene Initialize", this); onPreInitialize?.Invoke(dependents); // First initialize foreach (var item in dependents) { if (item is IGrapheneInitializable i) i.Initialize(); } Profiler.EndSample(); Profiler.BeginSample("Graphene Construct VisualTree", this); // Construct the visual tree hierarchy ConstructVisualTree(plates); Profiler.EndSample(); Profiler.BeginSample("Graphene Late Initialize", this); // Second initialize // First initialize foreach (var item in dependents) { if (item is IGrapheneLateInitializable i) i.LateInitialize(); } onPostInitialize?.Invoke(dependents); Profiler.EndSample(); sw.Stop(); //#if UNITY_EDITOR // UnityEngine.Debug.Log($"Graphene ({gameObject.scene.name}/{gameObject.name}) initialization: {sw.ElapsedMilliseconds}ms", this); //#endif } void ConstructVisualTree(List plates) { var sw = new Stopwatch(); sw.Start(); // Create the root controller CreateRootElement(); // Clone the visual tree for each plate foreach (Plate plate in plates) { if (!plate.VisualTreeAsset) { UnityEngine.Debug.LogError($"Missing Plate VisualTreeAsset {plate}", plate); continue; } try { plate.Dispose(); plate.ConstructVisualTree(); } catch (System.Exception e) { UnityEngine.Debug.LogError(e, plate); } } // Refresh hierarchy -> render & compose children foreach (Plate plate in plates) { if (!plate.VisualTreeAsset) continue; RegisterPlate(plate); } sw.Stop(); //UnityEngine.Debug.Log($"Graphene ConstructVisualTree: {sw.ElapsedMilliseconds}ms"); } public void AddPlate(Plate plate) { if (!plates.Contains(plate)) plates.Add(plate); } public void RegisterPlate(Plate plate) { if (plate.IsRootPlate) { grapheneRoot.Add(plate.Root); plate.Root.AddToClassList("unity-ui-document__child"); } plate.Root.name = $"{plate.gameObject.name}-container"; plate.RenderAndComposeChildren(); plate.HideImmediately(); // Hide immediately by default plate.onShow.AddListener(() => { plateOnShow?.Invoke(plate); }); plate.onHide.AddListener(() => { plateOnHide?.Invoke(plate); }); // Enable on start //if(plate.gameObject.activeSelf) plate.ReevaluateState(); } #region Build VisualElement void CreateRootElement() { // Create the root controller grapheneRoot = new GrapheneRoot(router); grapheneRoot.AddMultipleToClassList(addClasses); grapheneRoot.pickingMode = defaultPickingMode; #if UNITY_EDITOR grapheneRoot.RegisterCallback(DetachFromPanel); #endif doc.rootVisualElement.Add(grapheneRoot); } #if UNITY_EDITOR IEnumerator coroutine; void DetachFromPanel(DetachFromPanelEvent evt) { if (!Application.isPlaying || !gameObject.activeInHierarchy || !doc.enabled) return; coroutine = RebuildDelayed(0.1f); StartCoroutine(coroutine); } IEnumerator RebuildDelayed(float delay) { yield return new WaitForSeconds(delay); grapheneRoot.UnregisterCallback(DetachFromPanel); doc.rootVisualElement.Add(grapheneRoot); grapheneRoot.RegisterCallback(DetachFromPanel); yield break; //Rebuild(); } #endif void RebuildRootElement() { var oldRoot = grapheneRoot; CreateRootElement(); if (oldRoot != null) { // Add root plates if (oldRoot.childCount > 0) { var children = oldRoot.Children().ToList(); foreach (var child in children) grapheneRoot.Add(child); } doc.rootVisualElement.Remove(oldRoot); } } #endregion #region BuildHierarchy #endregion void FinalizeInitialzation() { /// Needs to go here because UIDocuments may initialize late grapheneRoot.BringToFront(); } public void RebuildBranch(Plate plate) { } // Needs to be in because UIDocument destroys the root private void OnEnable() { #if UNITY_EDITOR if (UnityEditor.EditorApplication.isCompiling || UnityEditor.BuildPipeline.isBuildingPlayer) return; #endif if (grapheneRoot != null && initializeOnStart) // Live reload Rebuild(); return; } private void OnDisable() { #if UNITY_EDITOR if (UnityEditor.EditorApplication.isCompiling || UnityEditor.BuildPipeline.isBuildingPlayer) return; #endif } bool canRebuild => Initialized && doc.enabled && isActiveAndEnabled; public event System.Action onRebuild; int rebuildCount; #region ButtonAttribute #if ODIN_INSPECTOR [Sirenix.OdinInspector.ResponsiveButtonGroup] #elif NAUGHTY_ATTRIBUTES [NaughtyAttributes.Button] #endif #endregion public void Rebuild() { if (!Application.isPlaying && !runInEditMode) return; if (!Initialized) { Initialize(); return; } if (!canRebuild) return; //#if UNITY_EDITOR // UnityEngine.Debug.Log($"Rebuilding Graphene {name} {gameObject.scene.name}", this); //#endif if (grapheneRoot != null) { grapheneRoot.parent?.Remove(grapheneRoot); grapheneRoot.Clear(); grapheneRoot = null; } ConstructVisualTree(plates); FinalizeInitialzation(); onRebuild?.Invoke(); } public void RefreshAllActiveContent() { foreach (var p in plates) { if (!p.IsActive) continue; p.Renderer?.Refresh(); } } //private void OnValidate() //{ // LiveLink // if (Application.isPlaying || doc.rootVisualElement == null) // return; // RebuildRootElement(); //} public void BroadcastBindCallback(BindableElement el, object context, Plate plate) => onBindElement?.Invoke(el, context); } } ================================================ FILE: src/Core/Graphene.cs.meta ================================================ fileFormatVersion: 2 guid: 3df2f2c9411e2af419b0ebc039577ce4 MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {fileID: 2800000, guid: a7567e94e4334dd4bb5bed2d09495114, type: 3} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Core/GrapheneComponent.cs ================================================ using UnityEngine; namespace Graphene { /// /// Base class for all components that participate in the Graphene dependency injection system. /// Provides access to the injected instance and its . /// public class GrapheneComponent : MonoBehaviour, IGrapheneInjectable { /// /// Cached debug name for this component, used for identification and logging. /// internal string debugNameCached; /// /// Gets the cached debug name for this component. /// public string DebugName => debugNameCached; #if ODIN_INSPECTOR [Sirenix.OdinInspector.HideIf(nameof(graphene))] #endif /// /// The injected Graphene instance that manages bindings and dependencies for this component. /// [SerializeField] protected Graphene graphene; /// /// Gets the injected instance. /// public Graphene Graphene => graphene; /// /// Gets the from the injected instance. /// public BindingsManager BindingsManager => graphene?.Binder; /// /// Injects the specified instance into this component. /// /// The Graphene instance to inject. public virtual void Inject(Graphene graphene) { this.graphene = graphene; if (string.IsNullOrEmpty(debugNameCached)) debugNameCached = name; } /// /// Unity Awake callback. Ensures the debug name is initialized. /// protected virtual void Awake() { if (string.IsNullOrEmpty(debugNameCached)) debugNameCached = name; } } } ================================================ FILE: src/Core/GrapheneComponent.cs.meta ================================================ fileFormatVersion: 2 guid: d32a65210f6ccde4ba4983f9164de0b3 MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Core/Injector.cs ================================================ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using UnityEngine; namespace Graphene { [DisallowMultipleComponent] public class Injector : MonoBehaviour { [SerializeField] Graphene graphene; private void Awake() { if (graphene || (graphene = GetComponent())) graphene.onPreInitialize += Graphene_onPreInitialize; } private void Graphene_onPreInitialize(ICollection dependents) { foreach (var dependent in dependents) { if (dependent is Router router) router.InjectIntoHierarchy(); } } } } ================================================ FILE: src/Core/Injector.cs.meta ================================================ fileFormatVersion: 2 guid: d8e759c0d6277b6448cce98e37e22305 MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {fileID: 2800000, guid: d249967811185734b8114d4b8977f433, type: 3} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Core/Interfaces.cs ================================================ namespace Graphene { using Elements; using UnityEngine.UIElements; public interface IGrapheneDependent { } public interface IBindableToVisualElement : IHasTooltip { bool isShown { get; } System.Action onShowHide { get; set; } bool isEnabled { get; } System.Action onSetEnabled { get; set; } bool isActive2 { get; } System.Action onSetActive { get; set; } //event System.Action onBindToElement;// { get; set; } VisualElement boundToElement { get; } void ResetCallbacks (); void SetBinding(VisualElement boundToElement); //System.Action syncVisualElement { get; set; } } public interface IBindableElement { /// /// Callback to VisualElement control that model changed, and view needs to be updated /// /// void OnModelChange(TValue newValue); } public enum InteractionMode { Button, Submit, Cancel } public interface IBindableInteractionType { public InteractionMode InteractionType { get; } public int Size { get; } } public interface IGrapheneElement { void Inject(GrapheneRoot root, Plate plate, Renderer renderer); } #if !DEPENDENCY_INJEcTION public interface IGrapheneInitializable : IGrapheneDependent { void Initialize(); } public interface IGrapheneLateInitializable : IGrapheneDependent { void LateInitialize(); } public interface IGrapheneInjectable : IGrapheneDependent { void Inject(Graphene graphene); } #endif #region IModel public interface IModel { void Initialize(VisualElement container, Plate plate); bool Render { get; } System.Action onModelChange { get; set; } void Refresh(VisualElement container); } /// /// Instructs the to use a custom context /// public interface ICustomBindContext { public object GetCustomBindContext { get; } } /// /// Instructs the to use a custom context /// public interface ICustomDrawContext { public object GetCustomDrawContext { get; } } public interface IForm { [Bind("Title")] string Title { get; } void OnSubmit(); void OnCancel(); } #endregion #region Misc public interface IHasTooltip { string Tooltip { get; } } public interface IHasCustomVisualTreeAsset { VisualTreeAsset VisualTreeAsset { get; } } #endregion } ================================================ FILE: src/Core/Interfaces.cs.meta ================================================ fileFormatVersion: 2 guid: 59b985a9f1ab787479e438f05a3b2567 MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Core/Model/BindableObjects/BindableBaseField.cs ================================================ using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Runtime.CompilerServices; using System.Runtime.Serialization; using System.Text; using UnityEngine; using UnityEngine.UIElements; #if ODIN_INSPECTOR using Sirenix.OdinInspector; #endif namespace Graphene.ViewModel { [System.Serializable] [DataContract] public abstract class BindableBaseField : BindableObjectBase { [field: SerializeField] [Bind(nameof(Label), BindingMode.OneWay)] [IgnoreDataMember] public virtual string Label { get; set; } } [System.Serializable] [DataContract] public abstract class BindableBaseField : BindableBaseField, INotifyValueChanged, INotifyPropertyChanged { [SerializeField] protected T m_Value; public BindableBaseField() { } public BindableBaseField([CallerMemberName] string label = "Label") : this() { this.Label = label; } [BindBaseField("Value")] [DataMember(Name = "Value")] public virtual T value { get => m_Value; set { SetValueWithoutNotify(value); ValueChangeCallback(m_Value); } } /*[BindValueChangeCallback(nameof(ValueChange))]*/ [IgnoreDataMember] public EventCallback> ValueChange => (changeEvent) => { ValueChangeCallback(changeEvent.newValue); }; public event System.EventHandler OnValueChange; public event PropertyChangedEventHandler PropertyChanged; public virtual void SetValueWithoutNotify(T newValue) { m_Value = newValue; } #if UNITY_EDITOR PropertyChangedEventArgs propertyChangedArgs; #endif protected virtual void ValueChangeCallback(T value) { OnValueChange?.Invoke(this, value); #if UNITY_EDITOR if (propertyChangedArgs == null) propertyChangedArgs = new PropertyChangedEventArgs(Label); PropertyChanged?.Invoke(this, propertyChangedArgs); #else PropertyChanged?.Invoke(this, null); #endif } public override void ResetCallbacks() { base.ResetCallbacks(); OnValueChange = null; PropertyChanged = null; } } [System.Serializable, Draw(ControlType.Toggle), DataContract] public class BindableBool : BindableBaseField { public BindableBool(string name = null) : base(name) { } public BindableBool() : base(null) {} } [System.Serializable, Draw(ControlType.Slider), DataContract] public abstract class RangeBaseField : BindableBaseField { public abstract float normalizedValue { get; } // These come first, so the ranges are deserialized _before_ the value #if ODIN_INSPECTOR [PropertyOrder(1)] #endif [Bind("Min"), DataMember(Name = "Min")] public TValueType min; #if ODIN_INSPECTOR [PropertyOrder(1)] #endif [Bind("Max"), DataMember(Name = "Max")] public TValueType max; [BindBaseField("Value")] [DataMember(Name = "Value")] #if ODIN_INSPECTOR [Sirenix.OdinInspector.ShowInInspector, Sirenix.OdinInspector.PropertyRange(0, 1, MinGetter = nameof(min), MaxGetter = nameof(max))] #endif public override TValueType value { get => base.value; set => base.value = value; } public TValueType Min { get => min; set => min = value; } public TValueType Max { get => max; set => max = value; } public RangeBaseField() : base() { } protected float Normalize(float value, float min, float max) { return Mathf.InverseLerp(min, max, value); } } public interface INumericBindable { float normalizedValue { get; } float value { get; set; } float Min { get; set; } float Max { get; set; } void SetValueWithoutNotify(float value); } [System.Serializable, Draw(ControlType.Slider), DataContract] public class BindableFloat : RangeBaseField, INumericBindable { #if ODIN_INSPECTOR [Sirenix.OdinInspector.ShowInInspector, Sirenix.OdinInspector.PropertyRange(0, 1)] #endif public override float normalizedValue => Normalize(m_Value, min, max); public BindableFloat() : base() { min = 0; max = 1; m_Value = 0.5f; } public override void SetValueWithoutNotify(float newValue) { m_Value = Mathf.Clamp(newValue, min, max); } } [System.Serializable, Draw(ControlType.SliderInt), DataContract] public class BindableInt : RangeBaseField, INumericBindable { #if ODIN_INSPECTOR [Sirenix.OdinInspector.ShowInInspector, Sirenix.OdinInspector.PropertyRange(0, 1)] #endif public override float normalizedValue => Normalize(m_Value, min, max); float INumericBindable.value { get => value; set => base.value = (int)value; } float INumericBindable.Min { get => min; set => min = (int)value; } float INumericBindable.Max { get => max; set => max = (int)value; } public BindableInt() : base() { min = 0; max = 100; m_Value = 50; //m_Value = 5; } public override void SetValueWithoutNotify(int newValue) { m_Value = Mathf.Clamp(newValue, min, max); } void INumericBindable.SetValueWithoutNotify(float value) => SetValueWithoutNotify((int)value); } [System.Serializable, Draw(ControlType.CycleField), DataContract] public class BindableNamedInt : BindableBaseField { [field: SerializeField] [Bind("Items"), IgnoreDataMember] public List items { get; set; } = new List(); public float normalizedValue => (float)m_Value / items.Count; public void InitFromEnum(bool splitUppercase = true) { this.items.Clear(); foreach (string s in System.Enum.GetNames(typeof(T)).ToList()) { string item = s; if (splitUppercase) item = StringUtility.InsertSpaceBeforeUpperCase(s); item = item.Replace("_", " ").Trim(); this.items.Add(item); } } public void InitFromList(IEnumerable list) { this.items.Clear(); this.items = list.ToList(); } public BindableNamedInt() : base() { } } [System.Serializable, Draw(ControlType.DropdownField), DataContract] public class BindableStringSelect : BindableBaseField { [field: SerializeField] [Bind("Items"), IgnoreDataMember] public List items { get; set; } = new List(); public float normalizedValue => (float)Index / items.Count; public int Index => items.IndexOf(value); public void InitFromEnum(bool splitUppercase = true) { this.items.Clear(); foreach (string s in System.Enum.GetNames(typeof(T)).ToList()) { string item = s; if (splitUppercase) item = StringUtility.InsertSpaceBeforeUpperCase(s); this.items.Add(item); } } public void InitFromList(IEnumerable list) { this.items.Clear(); this.items = list.ToList(); } public BindableStringSelect() : base() { } } [System.Serializable, Draw(ControlType.TextField), DataContract] public class BindableInput : BindableBaseField { public BindableInput() : base() { } } [System.Serializable, Draw(ControlType.MinMaxSlider), DataContract] public class BindableRange : BindableBaseField { [BindBaseField("Value")] [DataMember(Name = "Value")] #if ODIN_INSPECTOR [Sirenix.OdinInspector.ShowInInspector, Sirenix.OdinInspector.MinMaxSlider(0, 1, MinValueGetter = nameof(min), MaxValueGetter = nameof(max))] #endif public override Vector2 value { get => base.value; set => base.value = value; } [Bind("Min"), DataMember(Name = "Min")] public float min; [Bind("Max"), DataMember(Name = "Max")] public float max; public float Min { get => min; set => min = value; } public float Max { get => max; set => max = value; } public BindableRange() : base() { min = 0; max = 1; m_Value = new Vector2(0, 1); } protected float Normalize(float value, float min, float max) { return Mathf.InverseLerp(min, max, value); } } public static class StringUtility { public static string InsertSpaceBeforeUpperCase(this string str) { var sb = new StringBuilder(); char previousChar = char.MinValue; // Unicode '\0' foreach (char c in str) { if (char.IsUpper(c)) { // If not the first character and previous character is not a space, insert a space before uppercase if (sb.Length != 0 && previousChar != ' ') { sb.Append(' '); } } sb.Append(c); previousChar = c; } return sb.ToString(); } } } ================================================ FILE: src/Core/Model/BindableObjects/BindableBaseField.cs.meta ================================================ fileFormatVersion: 2 guid: b7a28165c3f71c34e9b0eb23b27cb50d MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Core/Model/BindableObjects/BindableObject.cs ================================================ using System; using System.Collections.Generic; using System.Runtime.Serialization; using UnityEngine; using UnityEngine.Events; using UnityEngine.UIElements; #if ODIN_INSPECTOR using Sirenix.OdinInspector; #endif namespace Graphene.ViewModel { /// /// BindableObjects are the atomic data units that can be bound to VisualElements. They are used to author data in the inspector, or can be generated at runtime to decouple data from view logic. /// /// Common use cases include buttons, labels, toggles, sliders etc. [System.Serializable] #if ODIN_INSPECTOR [Toggle("k__BackingField")] #endif public abstract class BindableObjectBase : IBindableToVisualElement { #if ODIN_INSPECTOR [field: LabelText(SdfIconType.TypeStrikethrough)] #endif [field: SerializeField, IgnoreDataMember, OnValueChanged(nameof(SetEnabled))] public bool isEnabled { get; set; } = true; public Action onSetEnabled { get; set; } #if ODIN_INSPECTOR [field: LabelText(SdfIconType.Eye), OnValueChanged(nameof(SetShow))] #endif [field: SerializeField, IgnoreDataMember] public bool isShown { get; set; } = true; public Action onShowHide { get; set; } #if ODIN_INSPECTOR [field: LabelText(SdfIconType.Check), OnValueChanged(nameof(SetActive))] #endif [field: SerializeField, IgnoreDataMember] public bool isActive2 { get; set; } = false; public Action onSetActive { get; set; } public VisualElement boundToElement { get; set; } public System.Action onBindToElement;// { get; set; } public void SetBinding(VisualElement el) { boundToElement = el; onBindToElement?.Invoke(el); } [field: SerializeField] public string Tooltip { get; set; } void ToggleEnable() => SetEnabled(!isEnabled); void ToggleShow() => SetShow(!isShown); void ToggleActive() => SetActive(!isActive2); public void SetEnabled(bool enabled) { isEnabled = enabled; onSetEnabled?.Invoke(enabled); } public void SetShow(bool show) { isShown = show; onShowHide?.Invoke(show); } public void SetActive(bool active) { isActive2 = active; onSetActive?.Invoke(active); } public virtual void ResetCallbacks() { onSetEnabled = null; onShowHide = null; } } /// /// BindableObjects are the atomic data units that can be bound to VisualElements. They are used to author data in the inspector, or can be generated at runtime to decouple data from view logic. /// /// Common use cases include buttons, labels, toggles, sliders etc. // Atomic "Über" object for the view [System.Serializable, Draw(ControlType.Button)] public class BindableObject : BindableObjectBase, IRoute, ICustomControlType, ICustomAddClasses, ICustomName, IHasCustomVisualTreeAsset { [field: SerializeField] public ControlType ControlType { get; set; } [field: SerializeField] public VisualTreeAsset VisualTreeAsset { get; set; } [field: SerializeField] [Bind("Label", BindingMode.OneWay)] public string Name { get; set; } [field: SerializeField] [Bind("Value")] public string Value { get; set; } [field: SerializeField] [Route] public string route; [field: SerializeField] [BindTooltip("Description")] public string Description { get; set; } [field: SerializeField] [Bind("Image")] public Texture Image { get; set; } #region FoldoutAttribute #if ODIN_INSPECTOR [Sirenix.OdinInspector.FoldoutGroup("Additionals")] #elif NAUGHTY_ATTRIBUTES [NaughtyAttributes.Foldout("Additionals")] #endif #endregion public string addClass; public string ClassesToAdd => addClass; #region FoldoutAttribute #if ODIN_INSPECTOR [Sirenix.OdinInspector.FoldoutGroup("Additionals")] #elif NAUGHTY_ATTRIBUTES [NaughtyAttributes.Foldout("Additionals")] #endif #endregion public string customName; public string CustomName => customName; #region FoldoutAttribute #if ODIN_INSPECTOR [Sirenix.OdinInspector.FoldoutGroup("Additionals")] #elif NAUGHTY_ATTRIBUTES [NaughtyAttributes.Foldout("Additionals")] #endif #endregion [Bind("")] public UnityEvent OnClick = new UnityEvent(); #region Util public override string ToString() { return $"{this.GetType().Name} - [{this.Name}]"; } #endregion public BindableObject() { } public BindableObject(UnityAction callback) : this() { OnClick.AddListener(callback); } } // Atomic "Über" object for the view [System.Serializable, Draw(ControlType.Button)] public class ContextBindableObject : BindableObject, IBindableInteractionType { [Bind("Content")] [field: SerializeField] public List Content { get; private set; } [Bind("HasContent")][field: SerializeField] public bool HasContent { get; private set; } [Bind("Actions")] [field: SerializeField] public List Actions { get; set; } [Bind("HasActions")] public bool HasActions => Actions != null && Actions.Count > 0; [field: SerializeField] public InteractionMode InteractionType { get; set; } [field: SerializeField] public int Size { get; set; } public void AddAction(string name, Action callback, string tooltip = null) { if (Actions == null) Actions = new List(); var action = new ActionButton { Label = name, Tooltip = tooltip, OnClick = callback }; Actions.Add(action); } } [Draw(ControlType.Button)] [System.Serializable] public class ActionButton : IHasTooltip { [field: SerializeField] [Bind("Label")] public string Label { get; set; } [field: SerializeField] [Bind] public System.Action OnClick { get; set; } [field: SerializeField] public string Tooltip { get; set; } [Bind("Enabled")] public bool Enabled => OnClick != null; } } ================================================ FILE: src/Core/Model/BindableObjects/BindableObject.cs.meta ================================================ fileFormatVersion: 2 guid: 2a481b0795ebfad4c9771a06eb5677c0 MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Core/Model/BindableObjects/ListBindable.cs ================================================ using Sirenix.OdinInspector; using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UIElements; namespace Graphene.ViewModel { public interface IListViewBindable { IList ItemsSource { get; } ControlType ItemControlType { get; } CollectionVirtualizationMethod CollectionVirtualizationMethod { get; } SelectionType SelectionType { get; } bool ShowBorder { get; } string HeaderTitle { get; } bool ShowFoldoutHeader { get; } bool ShowAddRemoveFooter { get; } AlternatingRowBackground AlternatingRowBackground { get; } bool Reorderable { get; } ListViewReorderMode ReorderMode { get; } bool ShowCollectionSize { get; } void Apply(ListView el); bool ElementsPickable { get; } } public abstract class ListBindable : BindableObjectBase { [SerializeField] private ControlType controlType = ControlType.ListItem; public ControlType ItemControlType => controlType; public CollectionVirtualizationMethod collectionVirtualizationMethod = CollectionVirtualizationMethod.DynamicHeight; public CollectionVirtualizationMethod CollectionVirtualizationMethod => collectionVirtualizationMethod; [Range(0, 100)] public int height = 30; public SelectionType selectionType = SelectionType.Single; public SelectionType SelectionType => selectionType; public bool showBorder; public bool ShowBorder => showBorder; public string headerTitle; public string HeaderTitle => headerTitle; public bool showFoldoutHeader; public bool ShowFoldoutHeader => showFoldoutHeader; public bool showAddRemoveFooter; public bool ShowAddRemoveFooter => showAddRemoveFooter; public AlternatingRowBackground alternatingRowBackground; public AlternatingRowBackground AlternatingRowBackground => alternatingRowBackground; public bool reorderable; public bool Reorderable => reorderable; public ListViewReorderMode reorderMode; public ListViewReorderMode ReorderMode => reorderMode; public bool showCollectionSize; public bool ShowCollectionSize => showCollectionSize; public bool elementsPickable = true; public bool ElementsPickable => elementsPickable; // Bound element public ListView listView; [ResponsiveButtonGroup] public void Rebuild() { if (listView == null) return; Apply(listView); listView.Rebuild(); } public void Apply(ListView el) { this.listView = el; el.virtualizationMethod = collectionVirtualizationMethod; el.fixedItemHeight = this.height; el.selectionType = selectionType; el.showBorder = showBorder; el.headerTitle = headerTitle; el.showFoldoutHeader = showFoldoutHeader; el.showAddRemoveFooter = showAddRemoveFooter; el.showAlternatingRowBackgrounds = alternatingRowBackground; el.reorderable = reorderable; el.reorderMode = reorderMode; el.showBoundCollectionSize = showCollectionSize; } } [System.Serializable] [Draw(controlType = ControlType.ListView)] public class ListBindable : ListBindable, IListViewBindable//, IList { [Bind("Items")] public List SourceItems = new List(); public IList ItemsSource => SourceItems; #region IList public TObjectType this[int index] { get => SourceItems[index]; set => SourceItems[index] = value; } public int Count => SourceItems.Count; public bool IsReadOnly => false; public void Add(TObjectType item) => SourceItems.Add(item); public void Clear() => SourceItems.Clear(); public bool Contains(TObjectType item) => SourceItems.Contains(item); public void CopyTo(TObjectType[] array, int arrayIndex) => SourceItems.CopyTo(array, arrayIndex); public IEnumerator GetEnumerator() => SourceItems.GetEnumerator(); public int IndexOf(TObjectType item) => SourceItems.IndexOf(item); public void Insert(int index, TObjectType item) => SourceItems.Insert(index, item); public bool Remove(TObjectType item) => SourceItems.Remove(item); public void RemoveAt(int index) => SourceItems.RemoveAt(index); //IEnumerator IEnumerable.GetEnumerator() => SourceItems.GetEnumerator(); #endregion } } ================================================ FILE: src/Core/Model/BindableObjects/ListBindable.cs.meta ================================================ fileFormatVersion: 2 guid: 90b16a7dd3d8c654693abccb70045774 MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Core/Model/BindableObjects.meta ================================================ fileFormatVersion: 2 guid: 21f4f86d9ed39a04dae5f8a10dc50541 folderAsset: yes DefaultImporter: externalObjects: {} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Core/Model/GenericModelAsset.cs ================================================ using System.Collections.Generic; using UnityEngine; using UnityEngine.UIElements; namespace Graphene.ViewModel { public abstract class ScriptableObjectModel : ScriptableObjectModel { [Draw(ControlType.Button)] public List model = new List(); } /// /// A generic model asset that can be used to author a list of BindableObject. /// [CreateAssetMenu(menuName = "Graphene/Model/GenericModelAsset")] public class GenericModelAsset : ScriptableObjectModel { public override void Initialize(VisualElement container, Plate plate) { } } } ================================================ FILE: src/Core/Model/GenericModelAsset.cs.meta ================================================ fileFormatVersion: 2 guid: 3c77b204580fbe947b11e389b5321ed7 MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {fileID: 2800000, guid: 806f5574c9706cd4e812686596744160, type: 3} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Core/Model/GenericModelBehaviour.cs ================================================ using System.Collections.Generic; using UnityEngine; using UnityEngine.UIElements; namespace Graphene.ViewModel { public abstract class GenericModelBehaviour : ViewModelComponent { public override bool HasContent => items?.Count > 0; [Draw(ControlType.Button)] [SerializeField] protected List items = new List(); public abstract List Items { get; } } public class GenericModelBehaviour : GenericModelBehaviour { [SerializeField] bool validateRouteAdresses = true; [SerializeField] bool hideInvalidAdress; protected StringRouter router; public override List Items => items; public override void Inject(Graphene graphene) { base.Inject(graphene); if (graphene.Router is StringRouter stringRouter) router = stringRouter; } protected override void OnShow() { base.OnShow(); ValidateAddresses(); } void ValidateAddresses() { if (router && validateRouteAdresses) { foreach (var item in items) { if (!string.IsNullOrEmpty(item.route)) { bool exists = router.ValidateAddress(item.route); item.SetEnabled(exists); //item.isEnabled = exists; if (hideInvalidAdress) item.SetShow(exists); } } } } //public override void Refresh(VisualElement container) //{ // if (router && validateRouteAdresses) // { // foreach (var item in items) // { // if (!string.IsNullOrEmpty(item.route)) // { // bool exists = router.ValidateAddress(item.route); // item.isEnabled = exists; // if (hideInvalidAdress) // item.isShown = exists; // } // } // } // base.Refresh(container); //} public override void Initialize(VisualElement container, Plate plate) { ValidateAddresses(); } } } ================================================ FILE: src/Core/Model/GenericModelBehaviour.cs.meta ================================================ fileFormatVersion: 2 guid: 35cb04044c5eb594495aaeae5ccb11a2 MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {fileID: 2800000, guid: 806f5574c9706cd4e812686596744160, type: 3} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Core/Model/ScriptableObjectModel.cs ================================================  using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.UIElements; namespace Graphene { /// /// An IModel that is also a ScriptableObject, allowing it to be saved as an asset in Unity. /// public abstract class ScriptableObjectModel : ScriptableObject, IModel { [field: SerializeField][Bind("Title")] public string Title { get; set; } = "Title"; [field: SerializeField][Bind("Render")] public bool Render { get; set; } = true; public Action onModelChange { get; set; } public event System.Action Redraw; //public abstract List GetDrawableObjects() { } public abstract void Initialize(VisualElement container, Plate plate); public void Refresh(VisualElement container) { } #region ButtonAttribute #if ODIN_INSPECTOR [Sirenix.OdinInspector.Button] #elif NAUGHTY_ATTRIBUTES [NaughtyAttributes.Button] #endif #endregion internal void ForceRedraw() { Redraw?.Invoke(); } //public abstract void Render(UnityEngine.UIElements.VisualElement container, UIControlsTemplates templates); //[Button] //public abstract void OnSubmit(); //[Button] //public abstract void OnCancel(); } } ================================================ FILE: src/Core/Model/ScriptableObjectModel.cs.meta ================================================ fileFormatVersion: 2 guid: 9c7aa560e9cd18a45baf58f91137a8cb MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {fileID: 2800000, guid: 806f5574c9706cd4e812686596744160, type: 3} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Core/Model/ViewModel/FormViewModel.cs ================================================ using Sirenix.OdinInspector; using UnityEngine; using UnityEngine.UIElements; namespace Graphene.ViewModel { public interface IFormViewModel : IModel { bool IsModelDirty { get; } bool BlockRoutingOnDirty { get; set; } bool HideButtons { get; set; } bool PlateIsActive { get; } void Submit(); void Cancel(); void Reset(); void PromptReset(); public event System.Action onSubmit; public event System.Action onCancel; void UpdateFormButtonsState(bool enabled, bool active); } public abstract class FormViewModel : ViewModelComponent, IFormViewModel, IStateInterpreter, IGrapheneInitializable { #region Bindables //[Bind("Title")] public override string Title => originalCached?.Title; public event System.Action onSubmit; public event System.Action onCancel; public event System.Action onReset; [Bind("Submit")] public BindableObject submitBinding = new BindableObject(); [Bind("Cancel")] public BindableObject cancelBinding = new BindableObject(); [Bind("Reset")] public BindableObject resetBinding = new BindableObject(); bool initialized; #if ODIN_INSPECTOR [field: ShowInInspector] #endif public bool IsModelDirty { get; set; } [field: SerializeField] public bool BlockRoutingOnDirty { get; set; } = true; [field: SerializeField] public bool HideButtons { get; set; } = false; #endregion #region VisualElement Button submitButton; Button cancelButton; Button resetButton; #endregion Router router; #region LifeCycle protected override void Awake() { base.Awake(); //submitBinding = Submit; //cancelBinding = Cancel; submitBinding.OnClick.AddListener(Submit); cancelBinding.OnClick.AddListener(Cancel); resetBinding.OnClick.AddListener(Reset); MarkDirty(false); } public virtual void Initialize() { router = graphene.Router as Router; router.RegisterInterpreter(this); initialized = true; } void OnEnable() { if (!initialized) return; router.RegisterInterpreter(this); Subscribe(); } void OnDisable() { UnSubscribe(); graphene?.Router.UnregisterInterpreter(this); } bool subscribed; void Subscribe() { if (subscribed) return; subscribed = true; } void UnSubscribe() { subscribed = false; } #endregion public bool TrySubmit() { if (!enabled) return false; Submit(); return true; } public bool TryCancel() { Cancel(); return true; } public bool TryReset() { Reset(); return true; } bool IStateInterpreter.CanCatch(object state) => TryCatch((string)state); public bool CanCatch(string state) { if (!enabled || !gameObject.activeInHierarchy) return false; if (state == "submit") return true; else if (state == "cancel") return true; else if (state == "reset") return true; return false; } bool IStateInterpreter.TryCatch(object state) => TryCatch((string)state); public bool TryCatch(string state) { if (!enabled || !gameObject.activeInHierarchy) return false; if (state == "submit") { TrySubmit(); return true; } else if (state == "cancel") { TryCancel(); return true; } else if (state == "reset") { TryReset(); return true; } // Show confirmation dialog here if (IsModelDirty) return true; return false; } [ResponsiveButtonGroup] public abstract void Cancel(); [ResponsiveButtonGroup] public abstract void Submit(); public abstract void Reset(); public abstract void PromptReset(); protected void MarkDirty(bool dirty) { IsModelDirty = dirty; SetButtonsDirty(dirty); // Block router if (BlockRoutingOnDirty && graphene?.Router) { if (dirty) graphene.Router.TryBlock(this); else graphene.Router.TryUnblock(this); } ModelChange(); } [Button] protected virtual void SetButtonsDirty(bool dirty) { submitButton?.SetEnabled(dirty && CanSubmit()); cancelButton?.SetEnabled(dirty && CanCancel()); submitBinding?.SetEnabled(dirty && CanSubmit()); cancelBinding?.SetEnabled(dirty && CanCancel()); resetButton?.SetEnabled(CanReset()); resetBinding?.SetEnabled(CanReset()); } public void UpdateFormButtonsState(bool enabled, bool active) { SetButtonsDirty(enabled); submitButton?.SetActive(active); cancelButton?.SetActive(active); submitBinding?.SetActive(active); cancelBinding?.SetActive(active); resetButton?.SetActive(active); resetBinding?.SetActive(active); } public virtual bool CanSubmit() => true; public virtual bool CanCancel() => true; public virtual bool CanReset() => true; } public abstract class FormViewModel : FormViewModel, ICustomDrawContext { [ShowInInspector] T originalCached; [System.NonSerialized, ShowInInspector, InlineEditor] public T viewModelCopy; object ICustomDrawContext.GetCustomDrawContext { get => viewModelCopy; } public abstract void UpdateSourceData(); } } ================================================ FILE: src/Core/Model/ViewModel/FormViewModel.cs.meta ================================================ fileFormatVersion: 2 guid: 4966129684538d749affd14e9fd70661 MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Core/Model/ViewModel/MultiFormViewModel.cs ================================================ using Sirenix.OdinInspector; using System.Collections.Generic; using System.Linq; using UnityEngine; using UnityEngine.UIElements; namespace Graphene.ViewModel { public class MultiFormViewModel : FormViewModel, IStateInterpreter, IFormViewModel { [SerializeField] GenericModelBehaviour buttonsViewModel; #if ODIN_INSPECTOR [ShowInInspector] #endif List childForms; bool formsInitialized; #region LifeCycle public override void Initialize(VisualElement container, Plate plate) { if (!formsInitialized) { var forms = transform.GetComponentsInChildren().ToList(); forms.Remove(this); childForms = forms; foreach (var item in childForms) { item.BlockRoutingOnDirty = false; item.HideButtons = true; item.UpdateFormButtonsState(false, false); } formsInitialized = true; } if (buttonsViewModel) { buttonsViewModel.Items.Clear(); buttonsViewModel.Items.Add(new BindableObject { Name = "SUBMIT", customName = "SubmitButton", addClass = "submit", route = "index" }); buttonsViewModel.Items.Add(new BindableObject { Name = "CANCEL", customName = "CancelButton", addClass = "return", route = "index" }); buttonsViewModel.Items.Add(new BindableObject { Name = "RESET", customName = "ResetButton", addClass = "cancel", //route = "index" }); buttonsViewModel.Items[0].OnClick.AddListener(Submit); buttonsViewModel.Items[1].OnClick.AddListener(Cancel); buttonsViewModel.Items[2].OnClick.AddListener(Reset); } } #endregion public override void Cancel() { foreach (var form in childForms) { if (form.IsModelDirty) form.Cancel(); } } public override void Submit() { foreach (var form in childForms) { if (form.IsModelDirty) form.Submit(); } } public override void Reset() { foreach (var form in childForms) { if (form.PlateIsActive) form.PromptReset(); } } public override void PromptReset() { throw new System.NotImplementedException(); } protected override void SetButtonsDirty(bool dirty) { if (buttonsViewModel && buttonsViewModel.Items.Count > 1) { buttonsViewModel.Items[0].SetEnabled(dirty); buttonsViewModel.Items[1].SetEnabled(dirty); } } } } ================================================ FILE: src/Core/Model/ViewModel/MultiFormViewModel.cs.meta ================================================ fileFormatVersion: 2 guid: bf9529738e742e84fbf6e0d45ae2e2d8 MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Core/Model/ViewModel/NavViewModel.cs ================================================ using System.Collections.Generic; using UnityEngine; using UnityEngine.UIElements; namespace Graphene.ViewModel { public class NavViewModel : GenericModelBehaviour { [Bind("HasContent")] public override bool HasContent => Routes != null && Routes.Count > 0 || base.HasContent; [field: SerializeField] public bool TitleFromRoutes { get; private set; } public enum RenderMode { Manual, Siblings, SiblingsWithState } [field: SerializeField] public RenderMode renderMode { get; private set; } = NavViewModel.RenderMode.SiblingsWithState; [field: SerializeField] public Plate OverridePlate { get; private set; } [Bind("Routes")] public List Routes = new List(); public override void Initialize(VisualElement container, Plate plate) { switch (renderMode) { case RenderMode.SiblingsWithState: CreateBindableObjectsFromSiblingsWithState(OverridePlate ?? plate); break; } } public override void Inject(Graphene graphene) { base.Inject(graphene); if(router) router.onStateChange += Router_onStateChange; } private void Router_onStateChange(string newState) { if (TitleFromRoutes) { var index = Routes.IndexOf(newState); if (index >= 0) Title = Routes[index].ToUpper(); ModelChange(); } } void OnDestroy() { if (router) router.onStateChange -= Router_onStateChange; } void CreateBindableObjectsFromSiblingsWithState(Plate plate) { this.Routes.Clear(); IReadOnlyList children = plate.Parent ? plate.Parent.Children : null; if (children == null || children.Count == 0) return; foreach (var sibling in children) { if (!sibling || !(sibling.StateHandle is StringStateHandle stringStateHandle)) continue; if (stringStateHandle) { Routes.Add(stringStateHandle.StateID); } } } } } ================================================ FILE: src/Core/Model/ViewModel/NavViewModel.cs.meta ================================================ fileFormatVersion: 2 guid: ddce112e23d338443b4d10a813ce4c25 MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Core/Model/ViewModel/ViewModelComponent.cs ================================================ using System; using UnityEngine; using UnityEngine.UIElements; namespace Graphene.ViewModel { /// /// A ViewModel is a component that implements IModel and is responsible for providing data to be rendered by a Renderer on a Plate. /// [RequireComponent(typeof(Renderer))] public abstract class ViewModelComponent : GrapheneComponent, IModel { [SerializeField, HideInInspector] protected new Renderer renderer; public Renderer Renderer => renderer; [SerializeField, HideInInspector] protected Plate plate; public Plate Plate => plate; [Bind("Title", BindingMode.OneWay)] [field: SerializeField] public virtual string Title { get; set; } [Bind("HasContent")] public virtual bool HasContent => Render; [field: SerializeField] public bool Render { get; set; } = true; public Action onModelChange { get; set; } public bool PlateIsActive => plate && plate.IsActive; protected override void Awake() { base.Awake(); if (!this.plate) this.plate = GetComponent(); if (!this.renderer) renderer = GetComponent(); } public override void Inject(Graphene graphene) { base.Inject(graphene); if(!this.plate) this.plate = GetComponent(); if(!this.renderer) renderer = GetComponent(); plate.onShow.AddListener(OnShow); plate.onHide.AddListener(OnHide); } public abstract void Initialize(VisualElement container, Plate plate); // For enabled void Start() { } protected virtual void OnShow() { } protected virtual void OnHide() { } #if ODIN_INSPECTOR [Sirenix.OdinInspector.ResponsiveButtonGroup] #endif public virtual void ModelChange() { onModelChange?.Invoke(); } #if ODIN_INSPECTOR [Sirenix.OdinInspector.ResponsiveButtonGroup] #endif public virtual void Refresh(VisualElement container) { } } } ================================================ FILE: src/Core/Model/ViewModel/ViewModelComponent.cs.meta ================================================ fileFormatVersion: 2 guid: 94c3d559523a63c4298689c839b2109d MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Core/Model/ViewModel.meta ================================================ fileFormatVersion: 2 guid: c868cc3a0db70c940af6b51404f58456 folderAsset: yes DefaultImporter: externalObjects: {} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Core/Model.meta ================================================ fileFormatVersion: 2 guid: d78566ea2cede254091b6cbfd364100d folderAsset: yes DefaultImporter: externalObjects: {} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Core/Plate.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using UnityEngine; using UnityEngine.Events; using UnityEngine.UIElements; #if ODIN_INSPECTOR using Sirenix.OdinInspector; #endif namespace Graphene { using Elements; using UnityEngine.Profiling; /// /// Determines the mode for a plate when the hierarchy is built /// public enum PositionMode { /// /// Don't override the mode /// None, /// /// Sets the plate to /// Relative, /// /// Sets the plate to /// Absolute } /// /// Determines how the binding system refreshes bindings on this plate /// public enum BindingRefreshMode { None, /// /// Bindings are refreshed every update of the BindingsManager. /// Continuous, /// /// Bindings only update when the model triggers its ModelChanged callback. /// ModelChange } /// /// Determines whether the plate shows/hides immediately or with a transition. /// public enum ShowHideMode { Immediate, Transition } /// /// A `Plate` represents a view controller in the VisualTree, and is used when by Graphene to the hierarchy, its states and views. /// Read more in the online documentation /// [DisallowMultipleComponent] public class Plate : GrapheneComponent, IGrapheneInitializable, IDisposable { #region Constants /// /// The default class name for a plate root element. /// public const string plateClassName = "plate"; /// /// Selector for the content view. /// public const string contentViewSelector = "Content"; /// /// Selector for the children view. /// public const string childViewSelector = "Children"; #endregion #region Inspector/Authoring /// /// The VisualTreeAsset used to instantiate the plate's UI. /// [SerializeField, OnValueChanged(nameof(OnChangeDocument))] VisualTreeAsset visualAsset; /// /// Gets or sets the VisualTreeAsset for this plate. /// public VisualTreeAsset VisualTreeAsset { get => visualAsset; set => SetVisualTreeAsset(value); } [SerializeField, HideInInspector] VisualTreeAsset cachedAsset; /// /// Determines how picking is handled for the root element. /// [SerializeField] PickingMode pickingMode = PickingMode.Position; /// /// Controls how bindings are refreshed. /// [SerializeField] public BindingRefreshMode bindingRefreshMode = BindingRefreshMode.ModelChange; /// /// Inline style overrides for the plate. /// [SerializeField, FoldoutGroup("Styles Overrides")] InlineStyleOverrides styleOverrides = new InlineStyleOverrides() { positionMode = PositionMode.Relative }; /// /// List of style overrides for individual views. /// [SerializeField, FoldoutGroup("Styles Overrides"), ListDrawerSettings(HideAddButton = true, HideRemoveButton = true, ListElementLabelName = "Id")] List viewStyleOverrides = new List(); #endregion #region State /// /// Indicates if the plate was changed this frame. /// internal bool wasChangedThisFrame; bool isActive = true; /// /// Gets whether the plate is currently active and enabled in the hierarchy. /// public bool IsActive => isActive && enabled && gameObject.activeInHierarchy; #endregion #region Properties /// /// Returns true if this plate is the root plate (has no parent). /// public bool IsRootPlate => !parent; /// /// Returns true if this plate requires views to be rendered. /// public bool RequiresViews => children.Count > 0 || renderer != null; /// /// Returns true if the plate requires a rebuild of its views. /// public bool RequiresRebuild => cachedAsset != visualAsset || viewIds.Count != viewStyleOverrides.Count; #endregion #region Component Reference [ShowInInspector] Plate parent; /// /// Gets the parent plate, if any. /// public Plate Parent => parent; [ShowInInspector] List children = new List(); /// /// Gets the child plates of this plate. /// public IReadOnlyList Children => children; /// /// List of view IDs in the template. /// [SerializeField, ReadOnly] public List viewIds = new List(); /// /// Reference to the default view. /// [SerializeField] public ViewRef defaultViewRef = new ViewRef(childViewSelector); /// /// Reference to the content view. /// [SerializeField] public ViewRef contentViewRef = new ViewRef(contentViewSelector); /// /// Reference to the view to attach to in the parent plate. /// [SerializeField] public ViewRef attachToParentView = new ViewRef(""); /// /// The router associated with this plate. /// [SerializeField] protected Router router; public Router Router => router; /// /// The state handle associated with this plate. /// [SerializeField] protected StateHandle stateHandle; public StateHandle StateHandle => stateHandle; /// /// The renderer associated with this plate. /// [SerializeField] new protected Renderer renderer; public Renderer Renderer => renderer; #endregion #region VisualElements Reference private TemplateContainer clone; /// /// The root visual element of this plate. /// public VisualElement Root { get; private set; } /// /// Main container for attached renderer's output of (repeat) elements. /// public VisualElement ContentContainer => contentViewRef.view; /// /// The default view. This controller's children will be added to this by default. /// View defaultView => defaultViewRef.view; /// /// Dictionary of views in the template, keyed by ID. /// Dictionary views = new Dictionary(); /// /// Tracks which child plates are attached to which views. /// Dictionary> childAttachments = new Dictionary>(); #endregion #region (Unity) Events /// /// Invoked when the plate's state is evaluated. /// public event System.Action onEvaluateState; /// /// Invoked when static content is refreshed. /// public event System.Action onRefreshStatic; /// /// Invoked when dynamic content is refreshed. /// public event System.Action onRefreshDynamic; /// /// Invoked when the visual tree is refreshed. /// public event System.Action onRefreshVisualTree; /// /// Unity event invoked when the plate is shown. /// public UnityEvent onShow = new UnityEvent(); /// /// Unity event invoked when the plate is hidden. /// public UnityEvent onHide = new UnityEvent(); #endregion /// /// Gets or sets whether the plate has been initialized. /// public bool Initialized { get; set; } bool registeredToParent; /// /// Initializes the plate, setting up references and parent-child relationships. /// public virtual void Initialize() { GetLocalReferences(); if (parent && !registeredToParent) { parent.onShow.AddListener(Parent_OnShow); parent.onHide.AddListener(Parent_OnHide); registeredToParent = true; } } /// /// Called when the component awakens. Clears events and child list. /// protected override void Awake() { base.Awake(); // Clear events onRefreshDynamic = null; onRefreshStatic = null; onRefreshVisualTree = null; onShow.RemoveAllListeners(); onHide.RemoveAllListeners(); children.Clear(); } /// /// Gets local component references and registers with parent plate if applicable. /// protected virtual void GetLocalReferences() { if (graphene) { router ??= graphene.Router; } //customView ??= GetComponent(); stateHandle ??= GetComponent(); renderer ??= GetComponent(); // Get nearest parent if ((Application.isPlaying && parent) || (parent = transform.parent?.GetComponentInParent(true))) parent.RegisterChild(this); } /// /// Constructs the visual tree for this plate, instantiating views and applying styles. /// internal void ConstructVisualTree() { Profiler.BeginSample("Graphene Plate Construct VisualTree", this); Root?.Clear(); clone = visualAsset.CloneTree(); Root = clone.Children().First(); #if UNITY_ASSERTIONS Debug.Assert(clone != null, this); Debug.Assert(Root != null, this); Debug.Assert(clone.childCount == 1, $"{nameof(Plate)} {nameof(TemplateContainer)} must have exactly 1 child {nameof(VisualElement)}", this); //Assert.IsNotNull(RootPlateEl); #endif Root.pickingMode = pickingMode; if(pickingMode == PickingMode.Ignore) Root.Query().ForEach(t => t.pickingMode = PickingMode.Ignore); if (RequiresRebuild) EditModeCacheViewIds(); // Get views InitViewsRuntime(); RefreshClassesAndStyles(); Root.AddToClassList(plateClassName); Initialized = true; onRefreshVisualTree?.Invoke(); Root.RegisterCallback((evt) => ChangeEvent()); Root.RegisterCallback((evt) => ChangeEvent()); Root.RegisterCallback((evt) => ChangeEvent()); Root.RegisterCallback((evt) => ChangeEvent()); Root.RegisterCallback((evt) => ChangeEvent()); if (IsRootPlate) { graphene.GrapheneRoot.Add(Root); Root.AddToClassList("unity-ui-document__child"); } // Hide on start if (styleOverrides.showHideMode == ShowHideMode.Transition) Root.FadeOut(); // Fadeout events //Root.RegisterCallback(Root_StartTransition); Profiler.EndSample(); } void Root_StartTransition(TransitionStartEvent evt) { //Debug.Log($"Start transition {evt.target}"); } void Root_EndTransition(TransitionEndEvent evt) { //Debug.Log($"End transition {evt.target}"); if (evt.target != Root) return; ApplyActiveState(); Root.UnregisterCallback(Root_EndTransition); //if (!isActive || Root.ClassListContains(VisualElementExtensions.fadeoutUssClassName)) //{ //} //else // Show(); } void ChangeEvent() { wasChangedThisFrame = true; } protected void RegisterChild(Plate child) { if (!children.Contains(child)) children.Add(child); } #region ButtonAttribute #if ODIN_INSPECTOR [Sirenix.OdinInspector.ResponsiveButtonGroup("ShowHide/Actions"), FoldoutGroup("ShowHide")] #elif NAUGHTY_ATTRIBUTES [NaughtyAttributes.Button] #endif #endregion protected virtual void Clear() { // Clear the dynamic content ContentContainer?.Clear(); } public void RefreshContentContainer() { DetachChildPlates(contentViewRef.view); Clear(); ReattachChildPlates(contentViewRef.view); } #region ButtonAttribute #if ODIN_INSPECTOR [Sirenix.OdinInspector.ResponsiveButtonGroup("ShowHide/Actions"), FoldoutGroup("ShowHide")] #elif NAUGHTY_ATTRIBUTES [NaughtyAttributes.Button] #endif #endregion internal virtual void RenderAndComposeChildren() { Profiler.BeginSample("RenderAndComposeChildren", this); // Detach the children so they don't get bound to the scope DetachChildPlates(); Clear(); onRefreshStatic?.Invoke(); // (Re)attach & compose the tree AttachChildPlates(); onRefreshDynamic?.Invoke(); Profiler.EndSample(); } // UIDocument removes the root OnDisable, so we only need OnEnable private void OnEnable() { if (!Initialized) return; Show(); //ConstructVisualTree(); //RenderAndComposeChildren(); //ReevaluateState(); } private void OnDisable() { if (!Initialized) return; Hide(); } void InitViewsRuntime() { if (Root == null) return; var views = Root.Query().ToList(); this.views.Clear(); View resolvedDefaultView = null; foreach (var view in views) { this.views.Add(view.id, view); if (view.isDefault || (resolvedDefaultView == null)) resolvedDefaultView = view; } defaultViewRef.ResolveView(this); if (resolvedDefaultView == null) resolvedDefaultView = views.FirstOrDefault(); if (!defaultViewRef.initialized) defaultViewRef.view = resolvedDefaultView; if (!defaultViewRef.initialized) { #if UNITY_ASSERTIONS if(children.Count > 0 || renderer != null) Debug.LogWarning($"No default view {defaultViewRef.Id}", this); #endif } contentViewRef.ResolveView(this); if (parent != null) attachToParentView.ResolveView(parent); } void EditModeCacheViewIds() { // Playing & already initialized if (Root != null) return; var el = Root != null ? Root : this.visualAsset.CloneTree(); viewIds.Clear(); el.Query().ForEach(v => viewIds.Add(v.id)); // Sync Ids if (this.viewStyleOverrides.Count != viewIds.Count) { this.viewStyleOverrides = new List(); foreach (var view in viewIds) this.viewStyleOverrides.Add(new SerializedView(view)); } } void SetVisualTreeAsset(VisualTreeAsset asset) { if(this.visualAsset == asset) return; this.visualAsset = asset; OnChangeDocument(); } void OnChangeDocument() { UpdateViewPlates(); RefreshClassesAndStyles(); } void UpdateViewPlates() { EditModeCacheViewIds(); defaultViewRef.SetPlate(this); contentViewRef.SetPlate(this); if (parent != null) attachToParentView.SetPlate(parent); else attachToParentView.NoParent(); } #region ButtonAttribute #if ODIN_INSPECTOR [Sirenix.OdinInspector.ResponsiveButtonGroup("ShowHide/Actions"), FoldoutGroup("ShowHide")] #elif NAUGHTY_ATTRIBUTES [NaughtyAttributes.Button] #endif #endregion public void Show() { if (!Initialized) return; if (isActive) return; if (canShow != null && !canShow.Invoke()) return; // Cannot show if (transform.parent?.gameObject.activeInHierarchy == false) return; SetActive(true); ApplyActiveState(); // Immediately activate GO // Enable if (styleOverrides.showHideMode == ShowHideMode.Transition) Root.FadeIn(); } #region ButtonAttribute #if ODIN_INSPECTOR [Sirenix.OdinInspector.ResponsiveButtonGroup("ShowHide/Actions"), FoldoutGroup("ShowHide")] #elif NAUGHTY_ATTRIBUTES [NaughtyAttributes.Button] #endif #endregion public void Hide() { if (!Initialized) return; if (!isActive) return; SetActive(false); if (styleOverrides.showHideMode == ShowHideMode.Immediate) ApplyActiveState(); else { Root.RegisterCallback(Root_EndTransition); Root.FadeOut(); } } internal void HideImmediately() { if (!Initialized) return; SetActive(false); ApplyActiveState(); } void SetActive(bool active) { // Not changed if (this.isActive == active) return; this.isActive = active; } void ApplyActiveState() { gameObject.SetActive(isActive); RefreshClassesAndStyles(); if (Root != null) { if (isActive) { Root.Show(); //ContentContainer?.Focus(); onShow?.Invoke(); ChangeEvent(); } else { Root.Hide(); onHide?.Invoke(); } } #if UNITY_EDITOR if (Application.isPlaying) { //gameObject.SetActive(IsActive); gameObject.name = gameObject.name.Trim('*'); if (isActive) gameObject.name = gameObject.name + "*"; } #endif } internal void ReevaluateState() { if (!Initialized) return; if (!stateHandle) { if(!parent || parent.isActive) Show(); } onEvaluateState?.Invoke(); } public Func canShow; void Parent_OnShow() { if (!stateHandle) Show(); } void Parent_OnHide() { if (!stateHandle) Hide(); } VisualElement temp; internal void Detach() { if(temp == null) temp = new VisualElement(); temp.Add(Root); } protected virtual void DetachChildPlates() { foreach (var child in children) { if (child) child.Detach(); } } protected virtual void DetachChildPlates(View view) { if(view == null) return; if (childAttachments.TryGetValue(view, out var children)) { foreach (var child in children) child.Detach(); } } protected virtual void ReattachChildPlates(View view) { if (view == null) return; if (childAttachments.TryGetValue(view, out var children)) { foreach (var child in children) InternalAttach(view, child); } } /// /// Attaches child plates into designated view(s) /// void AttachChildPlates() { // Prolly unnecessary and will prevent dynamic child attackments if (childAttachments.Count > 0) ReattachChildren(); // Rebuild from afresh childAttachments.Clear(); foreach (var child in children) { // Child can have optional view override if (child.attachToParentView) { var customView = GetViewById(child.attachToParentView.Id); AttachChild(customView, child); //customView.Add(child.Root); } else { // By default we attach children to default view AttachChild(defaultView, child); //defaultView.Add(child.Root); } } } void ReattachChildren() { foreach (var kvp in childAttachments) { var view = kvp.Key; var children = kvp.Value; foreach (var child in children) { InternalAttach(view, child); } } } void AttachChild(View view, Plate child) { if(childAttachments.TryGetValue(view, out var children)) { children.Add(child); } else { var list = new List(); childAttachments.Add(view, list); list.Add(child); } InternalAttach(view, child); } void InternalAttach(View view, Plate child) { view.Add(child.Root); } #region Helper Methods /// /// Gets a visual element for a collection of selectors by name /// /// /// public VisualElement GetVisualElement(ICollection names) { VisualElement target = Root; foreach (var name in names) { // Drill down var newTarget = target.Q(name); if (newTarget == null) { Debug.LogError($"VisualElement with name {name} not found as child of {target}", this); break; } target = newTarget; } return target; } public View GetViewById(string id) { if (views.TryGetValue(id, out View view)) return view; else return defaultView; } private void OnDestroy() { Dispose(); } public void Dispose() { if(BindingsManager) BindingsManager.DisposePlate(this, false); } void OnValidate() { if (isPrefab && !RequiresRebuild) return; GetLocalReferences(); UpdateViewPlates(); RefreshClassesAndStyles(); this.cachedAsset = visualAsset; } bool isPrefab => !gameObject.scene.isLoaded; const string positionModeRelativeClassNames = "flex-grow"; const string positionModeAbsoluteClassNames = "absolute fill"; const string showHideModeTransitionClassNames = "fade"; internal void RefreshClassesAndStyles() { if (Root == null) return; this.styleOverrides.Apply(Root); foreach (var viewStyleOverride in viewStyleOverrides) { if (!viewStyleOverride.Enabled) continue; if (views.TryGetValue(viewStyleOverride.Id, out View view)) { viewStyleOverride.Apply(view); } } } #endregion } } ================================================ FILE: src/Core/Plate.cs.meta ================================================ fileFormatVersion: 2 guid: 53b76209daa58fc4787cd6e9815c5a8c MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: - graphene: {instanceID: 0} - visualAsset: {fileID: 9197481963319205126, guid: 79af3ccbc09e9cf409acfe9306ae2a11, type: 3} - cachedAsset: {instanceID: 0} - router: {instanceID: 0} - stateHandle: {instanceID: 0} - renderer: {instanceID: 0} executionOrder: 0 icon: {fileID: 2800000, guid: d5720deaef5568642aae7621cd85c1d4, type: 3} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Core/Rendering/RenderUtils.cs ================================================ using Kinstrife.Core.ReflectionHelpers; using System; using System.Collections; using System.Collections.Generic; using System.Linq; using UnityEngine; using UnityEngine.UIElements; namespace Graphene { internal static class RenderUtils { internal static TemplatePreset templatesDefault; // Hax internal readonly static System.Type stringType = typeof(string); /// /// /// /// /// internal static bool IsPrimitiveContext(this System.Type type) => type.IsPrimitive || type.IsEnum || type == stringType; static int recursiveCheck = 0; /// /// Draws controls for all members of a context object /// /// /// /// /// internal static void DrawDataContainer(Plate plate, VisualElement container, in object context, TemplatePreset templates) { if (recursiveCheck > 5) { Debug.LogError($"Recursive error {plate}", plate); return; } if (context is ICustomDrawContext customDrawContext) { recursiveCheck++; DrawDataContainer(plate, container, customDrawContext.GetCustomDrawContext, templates); recursiveCheck--; return; } if (!templates) { UnityEngine.Debug.LogError($"Assign templates to Renderer for plate for {plate}", plate); return; } else if (container == null) { #if UNITY_ASSERTIONS UnityEngine.Debug.LogWarning($"Trying to draw to null VisualElement container {plate.name}", plate); #endif return; } else if (context == null) { #if UNITY_ASSERTIONS && false UnityEngine.Debug.LogError("Trying to draw null context", plate); #endif return; } templatesDefault = templates; // Get members List> drawableMembers = new List>(); TypeInfoCache.GetMemberValuesWithAttribute(context, drawableMembers); // Sort draw order drawableMembers = drawableMembers.OrderBy(x => x.Attribute.order).ToList();// as List>; List> bindableMembers = new List>(); TypeInfoCache.GetMemberValuesWithAttribute(context, bindableMembers); // Check how each member should be drawn foreach (var member in drawableMembers) { if (member.Value is null) continue; else if (member.Type.IsPrimitiveContext()) DrawFromPrimitiveContext(plate, container, in context, templates, member, bindableMembers); else if (member.Value is IEnumerable enumerable) DrawFromEnumerableContext(plate, container, in context, templates, member, bindableMembers); else DrawFromObjectContext(plate, container, in context, templates, member); } } static void DrawFromObjectContext(Plate panel, VisualElement container, in object context, TemplatePreset templates, ValueWithAttribute drawMember) { VisualTreeAsset template; if (drawMember.Value is IHasCustomVisualTreeAsset customVisualTreeAsset && customVisualTreeAsset.VisualTreeAsset != null) template = customVisualTreeAsset.VisualTreeAsset; else { ControlType controlType = ControlType.None; // Override control for class if (drawMember.Value is ICustomControlType customControl && customControl.ControlType != ControlType.None) controlType = customControl.ControlType; else controlType = TemplatePreset.ResolveControlType(drawMember.Value, isPrimitiveContext: false, drawMember.Attribute); // Drill down to subcontext if (controlType == ControlType.None || controlType == ControlType.SubContext) { //Debug.Log($"{controlType} {panel.name} {drawMember} {context}", panel); recursiveCheck++; DrawDataContainer(panel, container, drawMember.Value, templates); recursiveCheck--; return; } if (!templates.TryGetTemplateAsset(controlType, out template)) { Debug.LogError($"Failed to instantiate template {controlType} for field {drawMember.MemberInfo.Name}", panel); return; } } // Clone & bind the control VisualElement clone = Binder.Instantiate(in drawMember.Value, template, panel); // Add the control to the container container.Add(clone); } static void DrawFromPrimitiveContext(Plate panel, VisualElement container, in object context, TemplatePreset templates, ValueWithAttribute drawMember, List> bindableMembers) { var bind = bindableMembers.Find(x => x.MemberInfo.Equals(drawMember.MemberInfo)); if (bind.MemberInfo == null) bind = new ValueWithAttribute(drawMember.Value, new BindAttribute("Label", BindingMode.OneTime), drawMember.MemberInfo); // Get template, clone & bind the control ControlType controlType = TemplatePreset.ResolveControlType(drawMember.Value, isPrimitiveContext: true, drawMember.Attribute); templates.TryGetTemplateAsset(controlType, out VisualTreeAsset template); VisualElement clone = Binder.InstantiatePrimitive(in context, ref bind, template, panel); // Add any custom typography if (drawMember.Attribute is DrawTextAttribute text && text.typography != Typography.None) clone.AddToClassList(Enum.GetName(typeof(Typography), text.typography)); // Add the control to the container container.Add(clone); } static void DrawFromEnumerableContext(Plate plate, VisualElement container, in object context, TemplatePreset templates, ValueWithAttribute drawMember, List> bindableMembers) { // Don't support primitives or string //if (typeof(T).IsPrimitive) // return; // If element is listview -> use native functionality if (container is ListView listView && drawMember.Value is IList listContext) { var bind = bindableMembers.Find(x => x.MemberInfo.Equals(drawMember.MemberInfo)); DrawListView(plate, listView, listContext, templates, in drawMember, bind); return; } foreach (var item in drawMember.Value as IEnumerable) { if (IsPrimitiveContext(item.GetType())) { // Fugly, but works (for now) var bind = new ValueWithAttribute(item, new BindAttribute("Label", BindingMode.OneTime), drawMember.MemberInfo); DrawFromPrimitiveContext(plate, container, in item, templates, new ValueWithAttribute(item, drawMember.Attribute, drawMember.MemberInfo), new List> { bind }); } else { var draw = new ValueWithAttribute(item, drawMember.Attribute, drawMember.MemberInfo); DrawFromObjectContext(plate, container, in item, templates, draw); //var template = templates.TryGetTemplateAsset(item, drawMember.Attribute); //// Clone & bind the control //VisualElement clone = Binder.Instantiate(in item, template, panel); //// Add the control to the container //container.Add(clone); } } } static void DrawListView(Plate plate, ListView listView, in object context, TemplatePreset templates, in ValueWithAttribute drawMember, in ValueWithAttribute bindMember) { ControlType controlType = TemplatePreset.ResolveControlType(drawMember.Value, isPrimitiveContext: false, drawMember.Attribute); templates.TryGetTemplateAsset(controlType, out VisualTreeAsset template); Binder.BindListView(listView, in context, plate, template, in bindMember); } } } ================================================ FILE: src/Core/Rendering/RenderUtils.cs.meta ================================================ fileFormatVersion: 2 guid: 8d558fdcdf4792b49aaa6d7a87ce230c MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Core/Rendering/Renderer.cs ================================================ using System.Collections; using UnityEngine; using UnityEngine.UIElements; namespace Graphene { using Elements; /// /// A Renderer is responsible for binding a Model to a Plate, and rendering the Model's data to the Plate's content container. /// It works with the static part of the Plate (the TemplateRef components) and a dynamic part (the data drawn from the Model). /// [RequireComponent(typeof(Plate))] public class Renderer : MonoBehaviour, IGrapheneInitializable { [SerializeField] Plate plate; public Plate Plate => plate; [field: SerializeField]/*[Bind("Model")]*/ public Object Model { get; set; } [SerializeField] protected TemplatePreset templates; public TemplatePreset Templates => templates; /// /// Overriding this will target a non-default content container (as defined in Plate) /// [SerializeField] protected string[] contentSelector = new string[] { }; /// /// The ViewModel attached to this Renderer /// IModel viewModel; public void Initialize() { if (plate || (plate = GetComponent())) { plate.onRefreshStatic += RebindStatic; plate.onRefreshDynamic += HardRefresh; if ((Model && Model is IModel || (Model = GetComponent() as Object))) { if (Model is IModel iModel) SetModel(iModel); } } } void SetModel(IModel newViewModel) { // Unsubscribe to old if(viewModel != null) viewModel.onModelChange -= Model_onModelChange; // Subscribe to new viewModel = newViewModel; viewModel.onModelChange = Model_onModelChange; } public void RebindStatic() { // Render the template components plate.Root.Query().ForEach(t => { t.Inject(null, plate, this); t.Render(); } ); // Initialize the ViewModel if (viewModel != null) { try { viewModel.Initialize(GetDrawContainer(), plate); } catch(System.Exception e) { Debug.LogException(e, this); } if (!viewModel.Render) return; } // Bind the static template to the viewmodel BindStatic(viewModel); } internal void BindStatic(IModel viewModelContext) { if (viewModelContext is ICustomBindContext customBindContext && customBindContext != null) Binder.BindRecursive(plate.Root, customBindContext.GetCustomBindContext, null, plate, true); if (viewModelContext != null) Binder.BindRecursive(plate.Root, viewModelContext, null, plate, true); // Bind empty model -> routes (NOTE: Not that great of an option) else { Binder.BindRecursive(plate.Root, this, null, plate, true); } } // Callbacks internal void Plate_onRefreshDynamic() { HardRefresh(); } internal void Model_onModelChange() { plate.wasChangedThisFrame = true; } internal void RenderToContainer(VisualElement container) { // Initialize & render the form if (viewModel == null) return; if (!templates) templates = GetTemplatesFromParentsRecursive(this); //else RenderUtils.DrawDataContainer(plate, container, viewModel, templates); // if (viewModel is ICustomDrawContext customDrawContext) //RenderUtils.DrawDataContainer(plate, container, customDrawContext.GetCustomDrawContext, templates); viewModel.Refresh(container); viewModel.onModelChange?.Invoke(); } TemplatePreset GetTemplatesFromParentsRecursive(Renderer current) { if(current.templates) return current.templates; else if(current.plate?.Parent?.Renderer) return GetTemplatesFromParentsRecursive(current.plate.Parent.Renderer); else return null; } #region Public API #if ODIN_INSPECTOR [Sirenix.OdinInspector.ResponsiveButtonGroup("Actions")] #elif NAUGHTY_ATTRIBUTES [NaughtyAttributes.Button] #endif public void Refresh() { viewModel?.Refresh(GetDrawContainer()); } bool isRefreshing; #if ODIN_INSPECTOR [Sirenix.OdinInspector.ResponsiveButtonGroup("Actions")] #elif NAUGHTY_ATTRIBUTES [NaughtyAttributes.Button] #endif public void HardRefresh() { if (isRefreshing) { Debug.LogError($"Recursing refresh {name}", this); return; } isRefreshing = true; ClearContent(); RenderToContainer(GetDrawContainer()); isRefreshing = false; } #if ODIN_INSPECTOR [Sirenix.OdinInspector.ResponsiveButtonGroup("Actions")] #elif NAUGHTY_ATTRIBUTES [NaughtyAttributes.Button] #endif public void ClearContent() { plate.RefreshContentContainer(); } #endregion #region Helper Methods internal VisualElement GetDrawContainer() { // Default use the plate's default container VisualElement container = plate.ContentContainer; if (contentSelector.Length > 0) container = plate.GetVisualElement(contentSelector); return container; } #endregion } } ================================================ FILE: src/Core/Rendering/Renderer.cs.meta ================================================ fileFormatVersion: 2 guid: e3452e75856c69d42a3a5f4e1dbea800 MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: - plate: {instanceID: 0} - blueprint: {instanceID: 0} - templates: {fileID: 11400000, guid: 577f37ff3c038f0418ead2d83c53c59c, type: 2} executionOrder: 0 icon: {fileID: 2800000, guid: 9d85d8a741327e143b28987b5a69ef93, type: 3} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Core/Rendering.meta ================================================ fileFormatVersion: 2 guid: 0dd9ac2526742164ea65cbcc31b12095 folderAsset: yes DefaultImporter: externalObjects: {} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Core/Routing/EnableOnState.cs ================================================ using System.Collections.Generic; using UnityEngine; namespace Graphene { /// /// Component that activates or deactivates a Plate based on the current state of a Router. /// /// [RequireComponent(typeof(Plate))] [DisallowMultipleComponent] public abstract class EnableOnState : GrapheneComponent, IGrapheneInjectable, IGrapheneInitializable { public enum ActivationMode { EnableOnStates, DisableOnStates } [SerializeField] ActivationMode mode; [SerializeField] protected Plate plate; [SerializeField] List states = new List(); protected Router router; public bool Initialized { get; private set; } public virtual void Initialize() { if (Initialized) return; Initialized = true; if (plate || (plate = GetComponent())) plate.onEvaluateState += Plate_onEvaluateState; // Get the router in case we didn't inject router ??= graphene.Router as Router; // Subscribe to router state changes router.onStateChange += Router_onStateChange; } /// /// Dependency injection handle /// /// public void Inject(Router router) { this.router = router; } private void Router_onStateChange(T address) { bool match = states.IndexOf(router.LeafStateFromAddress(address)) >= 0; bool show = false; switch (mode) { case ActivationMode.EnableOnStates: show = match; break; case ActivationMode.DisableOnStates: show = !match; break; default: break; } if (show) plate.Show(); else plate.Hide(); } protected void Plate_onEvaluateState() { Router_onStateChange(router.CurrentState); } } } ================================================ FILE: src/Core/Routing/EnableOnState.cs.meta ================================================ fileFormatVersion: 2 guid: 5cb892b6070f21c4aa006bc9cde8ecf0 MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {fileID: 2800000, guid: 724b44860269a9f4abed118244135936, type: 3} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Core/Routing/Interpreters/ApplicationStateInterpreter.cs ================================================ using Graphene; using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UIElements; #if ODIN_INSPECTOR using Sirenix.OdinInspector; #endif namespace Graphene { using UnityEngine.Events; public enum RouterCommand { None, Back, Previous, Exit, Root, Menu, ToggleUI } [System.Flags] public enum NavigationInput { None = 0, NavigationMove = 1 << 0, NavigationSubmit = 1 << 1, NavigationCancel = 1 << 2 } #if ODIN_INSPECTOR [Toggle("enabled", CollapseOthersOnExpand = false)] #endif [System.Serializable] public class InputOverride { public string name => input.ToString(); public bool enabled; public NavigationInput input; public TrickleDown trickleDown = TrickleDown.TrickleDown; [BoxGroup("Output")] public RouterCommand routerCommand; [BoxGroup("Output")] public UnityEvent OnInput; [BoxGroup("Output")] public bool preventDefault = true; } //[RequireComponent(typeof(Plate))] /// /// Listens to state change commands at a Router, and triggers actions when a matching command is detected. /// It can consume the state change request, reroute it to another state, and/or trigger Unity Events in response. /// public class ApplicationStateInterpreter : StateInterpreter, IGrapheneInjectable, IGrapheneInitializable { #if ODIN_INSPECTOR [Toggle("enabled", CollapseOthersOnExpand = false)] #endif [System.Serializable] public struct StateCommandHandle { public string name => stateCommand; public bool enabled; [SerializeField] public string stateCommand; #if ODIN_INSPECTOR [ValidateInput(nameof(ValidateCustomState), "Custom state reroute should be different from input state command")] #endif [BoxGroup("Output")] public string customState; [BoxGroup("Output"), DisableIf(nameof(hasCustomState))] public RouterCommand routerCommand; [BoxGroup("Output")] public UnityEvent OnStateEnter; internal bool hasCustomState => !System.String.IsNullOrWhiteSpace(customState) && customState != stateCommand; #if ODIN_INSPECTOR bool ValidateCustomState(string customState) { return customState != stateCommand; } #endif } #if ODIN_INSPECTOR [ListDrawerSettings(ListElementLabelName = nameof(StateCommandHandle.name))] #endif public StateCommandHandle[] commands = new StateCommandHandle[0]; public InputOverride[] inputs = new InputOverride[0]; Router router; Plate plate; public void Inject(Router router) { this.router = router; } public bool Initialized { get; private set; } public void Initialize() { if (Initialized) return; Initialized = true; router = graphene.Router as Router; router.RegisterInterpreter(this); if (plate || TryGetComponent(out plate)) { plate.onShow.AddListener(Plate_OnShow); plate.onHide.AddListener(Plate_OnHide); RegisterInput(); plate.onRefreshVisualTree += RegisterInput; } // foreach (var command in commands) // { //if (command.enabled && !string.IsNullOrEmpty(command.stateCommand)) // router.RegisterState(command.stateCommand, null); // } } public override bool CanCatch(object state) { return CanCatch((string)state); } public override bool CanCatch(string state) { if (!enabled) return false; foreach (var command in commands) { if (!command.enabled) continue; if(state.Equals(command.stateCommand, System.StringComparison.OrdinalIgnoreCase)) return true; } return false; } public override bool TryCatch(object state) { return TryCatch((string)state); } public override bool TryCatch(string state) { if (!enabled || !gameObject.activeInHierarchy) return false; foreach (var command in commands) { if (!command.enabled || command.stateCommand != state) continue; if (command.OnStateEnter != null || command.routerCommand != RouterCommand.None || command.hasCustomState) { if (command.hasCustomState) { if (command.customState.IndexOf("http") == 0) Application.OpenURL(command.customState); else router.TryChangeState(command.customState); } else HandleRouterCommand(command.routerCommand); command.OnStateEnter?.Invoke(); return true; } } return false; } void HandleRouterCommand(RouterCommand routerCommand) { switch (routerCommand) { case RouterCommand.None: break; case RouterCommand.Back: router.TryGoUpOneState(); break; case RouterCommand.Previous: router.TryGoToPreviousState(); break; case RouterCommand.Exit: TryExit(); break; case RouterCommand.Root: router.ResetState(); break; default: break; } } public virtual void TryExit() { #if UNITY_EDITOR UnityEditor.EditorApplication.isPlaying = false; #elif UNITY_WEBPLAYER Application.OpenURL("https://github.com/LudiKha/Graphene"); #else Application.Quit(); #endif } protected virtual void Plate_OnShow() { enabled = true; } protected virtual void Plate_OnHide() { enabled = false; } private void OnEnable() { if (!router) return; router.RegisterInterpreter(this); // foreach (var command in commands) // { //if (command.enabled && !string.IsNullOrEmpty(command.stateCommand)) // router.RegisterState(command.stateCommand, null); // } } private void OnDisable() { if (!router) return; router.UnregisterInterpreter(this); // foreach (var command in commands) // { //if (!string.IsNullOrEmpty(command.stateCommand)) // router.UnregisterState(command.stateCommand); // } } private void OnDestroy() { router?.InterpreterDestroyed(this); } bool inputRegistered; void RegisterInput() { if (inputRegistered || !plate || plate.Root == null) return; inputRegistered = true; foreach (var item in inputs) { Debug.Log($"Registering Input {item.routerCommand}"); if ((item.input & NavigationInput.NavigationCancel) != 0) { plate.Root.RegisterCallback(ctx => OnNavigationCancel(item, ctx), item.trickleDown); Debug.Log($"Registering Input Event {typeof(NavigationCancelEvent).Name} {item.routerCommand}"); } if ((item.input & NavigationInput.NavigationSubmit) != 0) plate.Root.RegisterCallback(ctx => OnNavigationSubmit(item, ctx), item.trickleDown); if ((item.input & NavigationInput.NavigationMove) != 0) plate.Root.RegisterCallback(ctx => OnNavigationMove(item, ctx), item.trickleDown); } } void OnNavigationSubmit(InputOverride item, NavigationSubmitEvent evt) { Debug.Log($"isActiveAndEnabled {item.routerCommand}"); if (!isActiveAndEnabled) return; Handle(item, evt); } void OnNavigationCancel(InputOverride item, NavigationCancelEvent evt) { Debug.Log($"isActiveAndEnabled {item.routerCommand}"); if (!isActiveAndEnabled) return; Handle(item, evt); } void OnNavigationMove(InputOverride item, NavigationMoveEvent evt) { Debug.Log($"isActiveAndEnabled {item.routerCommand}"); if (!isActiveAndEnabled) return; Handle(item, evt); } void Handle(InputOverride item, EventBase evt) { Debug.Log($"Handling {item.routerCommand}"); if (item.routerCommand != RouterCommand.None || item.OnInput != null) { Debug.Log($"Valid Handling {item.routerCommand}"); item.OnInput?.Invoke(); HandleRouterCommand(item.routerCommand); evt.PreventDefault(); } } } } ================================================ FILE: src/Core/Routing/Interpreters/ApplicationStateInterpreter.cs.meta ================================================ fileFormatVersion: 2 guid: 11e217e82870451488cdb1a2928425f1 MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Core/Routing/Interpreters/NavigationStateHandler.cs ================================================ using UnityEngine; using UnityEngine.UIElements; namespace Graphene { using Elements; /// /// NavigationStateHandler is a StateInterpreter that listens for "previous" and "next" commands, typically on a Navbar ButtonGroup, and navigates through the states registered in a Router. /// // Comment K: This should be split up into (ButtonGroup->StateButtonGroup) & (NavigationStateInterpreter) [RequireComponent(typeof(Plate))] public class NavigationStateHandler : StateInterpreter, IGrapheneInjectable, IGrapheneInitializable { [SerializeField] string previousCommand = "previous"; [SerializeField] string nextCommand = "next"; Router router; Plate plate; ButtonGroup navigationButtonGroup; public void Inject(Router router) { this.router = router; } public bool Initialized { get; private set; } public void Initialize() { if (Initialized) return; Initialized = true; router ??= graphene.Router as Router; router.RegisterInterpreter(this); router.onStateChange += Router_onStateChange; plate ??= GetComponent(); plate.onShow.AddListener(Plate_OnShow); plate.onHide.AddListener(Plate_OnHide); Plate_OnHide(); } bool HasElements() { if (navigationButtonGroup != null) return true; navigationButtonGroup = plate?.Root?.Q(); if (navigationButtonGroup == null) { Debug.LogError($"{GetType().Name} requires a ButtonGroup VisualElement in its static template. Select a template that contains a ButtonGroup element.", this); return false; } //navigationButtonGroup.RegisterValueChangedCallback(evt => //{ // router.TryChangeState(navigationButtonGroup.items[navigationButtonGroup.value]); //}); return true; } private void Router_onStateChange(string newState) { if (!HasElements()) return; int i = 0; foreach (var state in navigationButtonGroup.items) { if (router.StateIsActive(state)) { navigationButtonGroup.SetValueWithoutNotify(i); return; } i++; } } public override bool TryCatch(object state) { return TryCatch((string)state); } public override bool TryCatch(string state) { if (!enabled || !gameObject.activeInHierarchy) return false; else if (state == previousCommand) { navigationButtonGroup.value -= 1; router.TryChangeState(navigationButtonGroup.items[navigationButtonGroup.value]); } else if (state == nextCommand) { navigationButtonGroup.value += 1; router.TryChangeState(navigationButtonGroup.items[navigationButtonGroup.value]); } else return false; return true; } public override bool CanCatch(object state) { return CanCatch((string)state); } public override bool CanCatch(string state) { if (!enabled || !gameObject.activeInHierarchy) return false; else if (state == previousCommand) return true; else if (state == nextCommand) return true; else return false; } internal void Plate_OnShow() { if (!HasElements()) return; navigationButtonGroup.SetValueWithoutNotify(0); enabled = true; } internal void Plate_OnHide() { enabled = false; } private void OnEnable() { if (!Initialized) return; router.RegisterInterpreter(this); } private void OnDisable() { if (!Initialized) return; router.UnregisterInterpreter(this); } } } ================================================ FILE: src/Core/Routing/Interpreters/NavigationStateHandler.cs.meta ================================================ fileFormatVersion: 2 guid: db9704c18ef205445b684710a72a91dc MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Core/Routing/Interpreters/StateInterpreter.cs ================================================ using System.Collections; using System.Collections.Generic; using UnityEngine; namespace Graphene { public interface IStateInterpreter { bool CanCatch(object state); bool TryCatch(object state); } public interface IStateInterpreter : IStateInterpreter { bool CanCatch(TStateType state); bool TryCatch(TStateType state); } /// /// A StateInterpreter is a component that can intercept a state change commands at a Router, consume it, prevent it from propagating further, and optionally trigger other logic. /// public abstract class StateInterpreter : GrapheneComponent, IStateInterpreter { public abstract bool CanCatch(object state); public abstract bool TryCatch(object state); } public abstract class StateInterpreter : StateInterpreter, IStateInterpreter { public abstract bool CanCatch(TStateType state); public abstract bool TryCatch(TStateType state); } } ================================================ FILE: src/Core/Routing/Interpreters/StateInterpreter.cs.meta ================================================ fileFormatVersion: 2 guid: 8142e1a2c5cc51e4495dd52a607b82ae MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Core/Routing/Interpreters.meta ================================================ fileFormatVersion: 2 guid: 7b0d94d5c697e88419f85d034e9257a5 folderAsset: yes DefaultImporter: externalObjects: {} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Core/Routing/Router.cs ================================================  using System.Collections.Generic; using System.Linq; using UnityEngine; using UnityEngine.UIElements; namespace Graphene { using Elements; using Kinstrife.Core.ReflectionHelpers; /// /// A router is responsible for managing application state and navigation between different states or views. It uses a hierarchical state model, allowing for nested states and complex navigation paths. /// [RequireComponent(typeof(Graphene))] [DefaultExecutionOrder(-100)] [DisallowMultipleComponent] public abstract class Router : GrapheneComponent, IGrapheneDependent, IGrapheneInitializable { /// /// List of interpreters in the hierarchy that can intercept a state change request /// #if ODIN_INSPECTOR [Sirenix.OdinInspector.ShowInInspector] #endif protected List activeInterpreters = new(); #if ODIN_INSPECTOR [Sirenix.OdinInspector.ShowInInspector] #endif protected List allInterpreters = new(); public abstract void InjectIntoHierarchy(); public abstract void Initialize(); public abstract void BindRouteToContext(BindableElement el, object data); public abstract void BindRoute(Route el, object data); public abstract void TryGoToPreviousState(); public abstract void TryGoToNextState(); public abstract void TryGoUpOneState(); public abstract void ResetState(); public void RegisterInterpreter(IStateInterpreter stateInterpreter) { if (allInterpreters.Contains(stateInterpreter)) allInterpreters.Add(stateInterpreter); if (!activeInterpreters.Contains(stateInterpreter)) activeInterpreters.Add(stateInterpreter); } public void UnregisterInterpreter(IStateInterpreter stateInterpreter) { if (activeInterpreters.Contains(stateInterpreter)) activeInterpreters.Remove(stateInterpreter); } private Object blocker; public bool IsBlocked => blocker; public event System.Action onRoutingBlocked; public event System.Action onRoutingUnblocked; public void TryBlock(Object caller) { if (blocker) return; blocker = caller; onRoutingBlocked?.Invoke(); } public void TryUnblock(Object caller) { if (!blocker) return; blocker = null; onRoutingUnblocked?.Invoke(); } bool isPrefab => !gameObject.scene.isLoaded; protected void OnValidate() { if (isPrefab) return; if (!graphene) graphene = GetComponent(); } } public abstract class Router : Router, IGrapheneInitializable, IGrapheneLateInitializable { // state & parent #if ODIN_INSPECTOR [Sirenix.OdinInspector.ShowInInspector] protected SortedDictionary states = new SortedDictionary(); #endif public SortedDictionary States => states; #if ODIN_INSPECTOR [Sirenix.OdinInspector.ValueDropdown(nameof(GetStateKeys))] #endif [SerializeField] public T startingState; public T StartingState => startingState; public T CurrentState => activeStates.LastOrDefault(); #if ODIN_INSPECTOR [Sirenix.OdinInspector.ShowInInspector] #endif List activeStates = new List(); public IReadOnlyList ActiveStates => activeStates; #if ODIN_INSPECTOR [Sirenix.OdinInspector.ShowInInspector] #endif List traversedStates = new List(); public IReadOnlyList TraversedStates => traversedStates; // Events public event System.Action onStateChange; #if UNITY_EDITOR && ODIN_INSPECTOR [Sirenix.OdinInspector.ShowInInspector, Sirenix.OdinInspector.ValueDropdown("StateKeys"), Sirenix.OdinInspector.OnValueChanged("TryChangeState")] T changeState; #endif internal IEnumerable StateKeys => states.Keys; IEnumerable GetStateKeys() { if (!graphene) graphene = GetComponent(); foreach (var plate in graphene.Plates) { if (!plate) continue; if (plate.StateHandle is StateHandle stateHandle) yield return stateHandle.StateID; } } #region Initialization // Injects the router into public override void InjectIntoHierarchy() { foreach (var stateHandle in GetComponentsInChildren>()) { stateHandle.Inject(this); } } public bool Initialized { get; private set; } public override void Initialize() { if (Application.isPlaying && Initialized) return; Initialized = true; traversedStates.Clear(); states.Clear(); onStateChange = null; RegisterState(startingState, default); } public bool LateInitialized { get; private set; } public void LateInitialize() { if (Application.isPlaying && LateInitialized) return; var targetState = StartingState; if (!ValidState(targetState) || !states.ContainsKey(targetState)) { foreach (var kvp in states) { targetState = kvp.Key; break; } } if (targetState != null) TryChangeState(targetState); } #endregion #region Binding public override void BindRouteToContext(BindableElement el, object context) { if (el is Button btn) { // Get members List> members = new List>(); TypeInfoCache.GetMemberValuesWithAttribute(context, members); foreach (var item in members) { T targetState = (T)item.Value; if (!ValidState(targetState)) continue; btn.clicked += delegate { TryChangeState(targetState); }; } } else if (el is Route routeEl) { } } public override void BindRoute(Route el, object context) { T targetState = default; if (el.route is T stateFromEl) targetState = stateFromEl; // Does the element have a route defined if (ValidState(targetState)) Bind(targetState); // Route is not defined in element -> look in members with Route attributes else { // Get members List> members = new List>(); TypeInfoCache.GetMemberValuesWithAttribute(context, members); foreach (var item in members) { if (ValidState((T)item.Value)) { Bind((T)item.Value); break; } } } void Bind(T target) { if (target is string str) el.route = str; else { el.clicked += delegate { TryChangeState(target); }; } } } #endregion #region Public API public void RegisterState(T state, T parentState) { if (!ValidState(state)) { Debug.LogError($"Trying to register invalid route: {state}", this); return; } //state = ProcessRouteRequest(state); if (states.ContainsKey(state)) return; else states.Add(state, parentState); } public void UnregisterState(T state) { if (!ValidState(state)) { Debug.LogError($"Trying to register invalid route: {state}", this); return; } if (states.ContainsKey(state)) states.Remove(state); } public void RegisterInterpreter(IStateInterpreter stateInterpreter) { if (!allInterpreters.Contains(stateInterpreter)) allInterpreters.Add(stateInterpreter); if (!activeInterpreters.Contains(stateInterpreter)) activeInterpreters.Add(stateInterpreter); } public void UnregisterInterpreter(IStateInterpreter stateInterpreter) { if (activeInterpreters.Contains(stateInterpreter)) activeInterpreters.Remove(stateInterpreter); } public void InterpreterDestroyed(IStateInterpreter stateInterpreter) { if (allInterpreters.Contains(stateInterpreter)) allInterpreters.Remove(stateInterpreter); if (activeInterpreters.Contains(stateInterpreter)) activeInterpreters.Remove(stateInterpreter); } public bool ValidateAddress(T state) { // Iterate over interpreters, last added ones (deeper in hierarchy) first, more generic (higher up last) for (int i = allInterpreters.Count - 1; i >= 0; i--) { var interpreter = allInterpreters[i]; if (interpreter != null && interpreter.CanCatch(state)) return true; } return AddressExists(state); } public virtual bool TryChangeState(T state) { if (IsBlocked) return false; // See if the router for (int i = activeInterpreters.Count - 1; i >= 0; i--) { var interpreter = activeInterpreters[i]; if (interpreter != null && interpreter.TryCatch(state)) return true; } //foreach (var interpreter in interpreters) //{ // if (interpreter != null && interpreter.TryCatch(state)) // return true; //} // Only contained keys allowed if (!AddressExists(state)) { Debug.LogError($"Trying to change state {state}, which isn't registered", this); return false; } this.activeStates = GetActiveStateHierarchy(state).ToList(); UpdateTraversedStates(this.activeStates.Last()); onStateChange?.Invoke(state); return true; } public override void TryGoToPreviousState() { if (traversedStates.Count <= 1) return; T previousState = traversedStates[traversedStates.IndexOf(CurrentState) - 1]; TryChangeState(previousState); } public override void TryGoUpOneState() { // Already at root if (activeStates.Count <= 1) { ResetState(); return; } T previousState = activeStates[activeStates.IndexOf(CurrentState) - 1]; TryChangeState(previousState); } public override void TryGoToNextState() { } public override void ResetState() { TryChangeState(startingState); } public bool StateIsActive(T state) { return activeStates.IndexOf(state) != -1; } public bool IsSiblingState(T state, T otherState) { return EqualityComparer.Default.Equals(states[state], states[otherState]); } public bool IsSiblingToCurrentState(T state) { if (!states.TryGetValue(state, out var parent)) return false; //Debug.Log($"{state}@{parent} {CurrentState}@{states[CurrentState]}"); return EqualityComparer.Default.Equals(parent, states[CurrentState]); } #endregion #region Helper Methods public abstract bool ValidState(T state); public abstract bool AddressExists(T address); public abstract T[] GetStatesFromAddress(T address); public abstract T LeafStateFromAddress(T address); protected abstract T[] GetActiveStateHierarchy(T state); protected void UpdateTraversedStates(T newState) { int curIndex = traversedStates.IndexOf(newState); if (curIndex >= 0) // Already visited this state -> trim the tree { int excessStates = traversedStates.Count - (curIndex + 1); traversedStates.RemoveRange(curIndex + 1, excessStates); } else traversedStates.Add(newState); } #endregion } } ================================================ FILE: src/Core/Routing/Router.cs.meta ================================================ fileFormatVersion: 2 guid: b0572ba23ed7bfb45bccb3d798fa328e MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {fileID: 2800000, guid: 3fa61ff64b7c4e64e8b944ba5d308365, type: 3} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Core/Routing/StateHandle.cs ================================================ using UnityEngine; namespace Graphene { public enum ChildActivationMode { Manual, ShowWithParent, DefaultState } [RequireComponent(typeof(Plate))] [DisallowMultipleComponent] public abstract class StateHandle : GrapheneComponent, IGrapheneInjectable, IGrapheneInitializable { public abstract Router Router { get; } [SerializeField] protected Plate plate; /// /// This will Show the plate when the parent is activated. /// [SerializeField] protected ChildActivationMode activationMode = ChildActivationMode.Manual; public bool Initialized { get; private set; } public virtual void Initialize() { if (Initialized) return; Initialized = true; if (plate || (plate = GetComponent())) { plate.onEvaluateState += Plate_onEvaluateState; } } protected abstract void Plate_onEvaluateState(); public virtual bool TryActivate() => throw new System.NotImplementedException(); } public class StateHandle : StateHandle { [SerializeField] protected T stateID; public virtual T StateID => stateID; [SerializeField] T parentStateID; protected Router router; public override Router Router => router as Router; /// /// Dependency injection handle /// /// public void Inject(Router router) { this.router = router; } public override void Initialize() { base.Initialize(); // Get the router in case we didn't inject router ??= graphene.Router as Router; // Get parent state StateHandle parentStateHandle = transform.parent.GetComponentInParent>(true); parentStateID = parentStateHandle ? parentStateHandle.StateID : default; // Register the state at the router router.RegisterState(stateID, parentStateID); // Subscribe to router state changes router.onStateChange += Router_onStateChange; } private void OnDestroy() { if (router) { router.onStateChange -= Router_onStateChange; router.UnregisterState(stateID); } } private void Router_onStateChange(T address) { if (!router.ValidState(stateID)) return; bool parentWasTarget = router.StateIsActive(parentStateID) && router.LeafStateFromAddress(address).Equals(parentStateID); // Try Change state to this if (activationMode == ChildActivationMode.DefaultState && parentWasTarget) { if (router.TryChangeState(stateID)) return; } // Check if our state is active if (router.StateIsActive(stateID)) plate.Show(); else if (activationMode == ChildActivationMode.ShowWithParent && parentWasTarget) plate.Show(); else plate.Hide(); } protected override void Plate_onEvaluateState() { Router_onStateChange(router.CurrentState); } #if ODIN_INSPECTOR [Sirenix.OdinInspector.ResponsiveButtonGroup] #endif public override bool TryActivate() { return router.TryChangeState(stateID); } } } ================================================ FILE: src/Core/Routing/StateHandle.cs.meta ================================================ fileFormatVersion: 2 guid: ed9a3898638b389418cb68abe6b00ef9 MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {fileID: 2800000, guid: 724b44860269a9f4abed118244135936, type: 3} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Core/Routing/StringEnableOnState.cs ================================================ using System.Collections; using System.Collections.Generic; using UnityEngine; namespace Graphene { /// /// Component that activates or deactivates a Plate based on the current state of a Router. /// public class StringEnableOnState : EnableOnState { } } ================================================ FILE: src/Core/Routing/StringEnableOnState.cs.meta ================================================ fileFormatVersion: 2 guid: 67e79be0a9fe43f41bd545143176b6fa MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {fileID: 2800000, guid: 724b44860269a9f4abed118244135936, type: 3} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Core/Routing/StringRouter.cs ================================================  using System.Collections.Generic; using System.Diagnostics; using System.Linq; namespace Graphene { /// /// A string router uses string addresses to represent states and routes. It manages navigation between different states based on string paths. /// /// Common examples of string-based routing include URL routing in web applications, file path navigation in operating system, such as "index/settings/video" /// public class StringRouter : Router { const char separator = '/'; static char[] separatorArray = new char[] { separator }; public StringRouter() { startingState = "app"; } public override string[] GetStatesFromAddress(string address) { if (string.IsNullOrWhiteSpace(address)) return new string[0]; var states = address.Split(separatorArray, System.StringSplitOptions.RemoveEmptyEntries); return states; } protected override string[] GetActiveStateHierarchy(string routeRequest) { // Trim starting & trailing whitespace routeRequest = routeRequest.Trim(); // Check if the address is relative, absolute or a leaf int index = routeRequest.IndexOf(separator); // Starting with '/', check for relative address if (index == 0) return AddressFromRelativeState(routeRequest); return AddressFromRelativeState(routeRequest); } public string[] AddressFromRelativeState(string relativeAddress) { string[] states = GetStatesFromAddress(relativeAddress); // Starting at the leaf, finding n parent states //string current = default; //for (int i = states.Length - 1; i >= 0; i++) //{ // current = states[i]; // if (this.states.ContainsKey(current)) // continue; // else // return false; //} // Get parent states var parentStates = GetParentStatesRecursive(states.First(), states.ToList()); // Add relative states parentStates.AddRange(states); return parentStates.Distinct().ToArray(); } List GetParentStatesRecursive(string state, List list) { // Travel upwards if(states.TryGetValue(state, out string parent)) { if (!ValidState(parent)) return list; list.Insert(0, parent); return GetParentStatesRecursive(parent, list); } // Out of parents return list; } #if ODIN_INSPECTOR [Sirenix.OdinInspector.Button] #endif public bool ChangeState(string path) => base.TryChangeState(path); #region Helper Methods public override bool ValidState(string state) { return !string.IsNullOrWhiteSpace(state); } // Should improve this check public override bool AddressExists(string address) { foreach (var state in GetStatesFromAddress(address)) { if (!states.ContainsKey(state)) return false; } return true; } public override string LeafStateFromAddress(string address) { return GetStatesFromAddress(address).LastOrDefault(); } #endregion } } ================================================ FILE: src/Core/Routing/StringRouter.cs.meta ================================================ fileFormatVersion: 2 guid: 905d8383600c12b4dba5c1916e276442 MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {fileID: 2800000, guid: 3fa61ff64b7c4e64e8b944ba5d308365, type: 3} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Core/Routing/StringStateHandle.cs ================================================ using System.Collections; using System.Collections.Generic; using UnityEngine; namespace Graphene { /// /// Component that marks a Plate as having a string-based state ID. Adding this to a plate indicates that this Gameobject is capable of being routed to using a string address. /// public class StringStateHandle : StateHandle { [field: SerializeField] public bool StateFromGameObjectName { get; private set; } = true; public override string StateID => Application.isPlaying ? stateID : StateFromGameObjectName ? GameObjectNameAsStateId() : base.StateID; protected override void Awake() { base.Awake(); if (StateFromGameObjectName) this.stateID = GameObjectNameAsStateId(); } string GameObjectNameAsStateId () => gameObject.name.ToLower(); } } ================================================ FILE: src/Core/Routing/StringStateHandle.cs.meta ================================================ fileFormatVersion: 2 guid: 7203da84032242f48a61f59888ca04d7 MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {fileID: 2800000, guid: 724b44860269a9f4abed118244135936, type: 3} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Core/Routing.meta ================================================ fileFormatVersion: 2 guid: e687d2c6d99c2d4459b39b680325660b folderAsset: yes DefaultImporter: externalObjects: {} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Core/Styling/AlignItemsOverride.cs ================================================ using UnityEngine.UIElements; #if ODIN_INSPECTOR #endif namespace Graphene { [System.Serializable] public sealed class AlignItemsOverride : StyleOverride { public override void TryApply(VisualElement visualElement) { if (enabled) visualElement.style.alignItems = value; else visualElement.style.alignItems = base.Null; } } } ================================================ FILE: src/Core/Styling/AlignItemsOverride.cs.meta ================================================ fileFormatVersion: 2 guid: c9b2129d52baf0043a23668a36c090a4 MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Core/Styling/FlexDirectionOverride.cs ================================================ using UnityEngine.UIElements; #if ODIN_INSPECTOR #endif namespace Graphene { [System.Serializable] public sealed class FlexDirectionOverride : StyleOverride { public override void TryApply(VisualElement visualElement) { if (enabled) visualElement.style.flexDirection = value; else visualElement.style.flexDirection = base.Null; } } [System.Serializable] public sealed class FlexGrowOverride : StyleOverride { public override void TryApply(VisualElement visualElement) { if (enabled) visualElement.style.flexGrow = value; else visualElement.style.flexGrow = new StyleFloat(StyleKeyword.Null); } } } ================================================ FILE: src/Core/Styling/FlexDirectionOverride.cs.meta ================================================ fileFormatVersion: 2 guid: efa16af0abd783f4aab05bc4bec5f92b MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Core/Styling/InlineStyleOverrides.cs ================================================ using UnityEngine; using UnityEngine.UIElements; using Sirenix.OdinInspector; namespace Graphene { /// /// Used to author inline style overrides for an element, applied at runtime. /// [System.Serializable] public class InlineStyleOverrides { const string positionModeRelativeClassNames = "flex-grow"; const string positionModeAbsoluteClassNames = "absolute fill"; const string showHideModeTransitionClassNames = "fade"; [Tooltip("Adds any number of classes to the root element. Separated by space")] [SerializeField] protected string addClasses; [SerializeField, EnumToggleButtons, HideLabel] internal PickingMode pickingMode = PickingMode.Position; [SerializeField, EnumToggleButtons, HideLabel] internal PositionMode positionMode = PositionMode.None; [SerializeField, EnumToggleButtons, HideLabel] internal ShowHideMode showHideMode = ShowHideMode.Immediate; [SerializeField, FoldoutGroup("Detail")] FlexGrowOverride flexGrowOverride = new FlexGrowOverride(); [SerializeField, FoldoutGroup("Detail")] JustifyOverride justifyContent = new JustifyOverride(); [SerializeField, FoldoutGroup("Detail")] AlignItemsOverride alignItemsOverride = new AlignItemsOverride(); [SerializeField, FoldoutGroup("Detail")] FlexDirectionOverride flexDirectionOverride = new FlexDirectionOverride(); [SerializeField, FoldoutGroup("Detail")] WrapOverride wrapOverride = new WrapOverride(); [SerializeField, FoldoutGroup("Detail")] WidthOverride widthOverride = new WidthOverride(); [SerializeField, FoldoutGroup("Detail")] HeightOverride heightOverride = new HeightOverride(); internal void Apply(VisualElement el) { if (el == null) return; el.AddMultipleToClassList(addClasses); if (positionMode == PositionMode.Relative) { el.RemoveMultipleFromClassList(positionModeAbsoluteClassNames); el.AddToClassList(positionModeRelativeClassNames); } else if (positionMode == PositionMode.Absolute) { el.RemoveFromClassList(positionModeRelativeClassNames); el.AddMultipleToClassList(positionModeAbsoluteClassNames); } if (showHideMode == ShowHideMode.Immediate) { el.RemoveFromClassList(showHideModeTransitionClassNames); } else if (showHideMode == ShowHideMode.Transition) { el.AddToClassList(showHideModeTransitionClassNames); // When transitioning, we can only position absolutely, as the fadeout process will interfere with routing el.AddMultipleToClassList(positionModeAbsoluteClassNames); } flexGrowOverride.TryApply(el); justifyContent.TryApply(el); //alignContent.TryApply(Root); alignItemsOverride.TryApply(el); flexDirectionOverride.TryApply(el); wrapOverride.TryApply(el); widthOverride.TryApply(el); heightOverride.TryApply(el); } } } ================================================ FILE: src/Core/Styling/InlineStyleOverrides.cs.meta ================================================ fileFormatVersion: 2 guid: 1997ab07644fc094695caf1539f96170 MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Core/Styling/JustifyOverride.cs ================================================ using UnityEngine.UIElements; using static UnityEngine.UI.CanvasScaler; #if ODIN_INSPECTOR #endif namespace Graphene { [System.Serializable] public sealed class JustifyOverride : StyleOverride { public override void TryApply(VisualElement visualElement) { if (enabled) visualElement.style.justifyContent = value; else visualElement.style.justifyContent = base.Null; } } public abstract class StyleLengthOverride : StyleOverride { public LengthUnit unit = LengthUnit.Percent; } [System.Serializable] public sealed class WidthOverride : StyleLengthOverride { public override void TryApply(VisualElement visualElement) { if (enabled) visualElement.style.width = new Length(value, unit); else visualElement.style.width = new StyleLength(StyleKeyword.Null);// base.Null; } } [System.Serializable] public sealed class HeightOverride : StyleLengthOverride { public override void TryApply(VisualElement visualElement) { if (enabled) visualElement.style.height = new Length(value, unit); else visualElement.style.height = new StyleLength(StyleKeyword.Null);// base.Null; } } } ================================================ FILE: src/Core/Styling/JustifyOverride.cs.meta ================================================ fileFormatVersion: 2 guid: cd169699503c46e49b265c0504c2e1e2 MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Core/Styling/StyleOverride.cs ================================================ using System; using UnityEngine.UIElements; #if ODIN_INSPECTOR #endif namespace Graphene { #if ODIN_INSPECTOR [Sirenix.OdinInspector.Toggle("enabled")] #endif [System.Serializable] public abstract class StyleOverride where T : struct, IConvertible { public bool enabled; public T value; public abstract void TryApply(VisualElement visualElement); public static implicit operator bool (StyleOverride styleOverride) => styleOverride != null && styleOverride.enabled; protected StyleEnum Null => new StyleEnum(StyleKeyword.Null); public StyleOverride() { } public StyleOverride(T value) { this.value = value; } } } ================================================ FILE: src/Core/Styling/StyleOverride.cs.meta ================================================ fileFormatVersion: 2 guid: 80d10b93078bca343931a18a83a5027d MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Core/Styling/WrapOverride.cs ================================================ using UnityEngine.UIElements; #if ODIN_INSPECTOR #endif namespace Graphene { [System.Serializable] public sealed class WrapOverride : StyleOverride { public override void TryApply(VisualElement visualElement) { if (enabled) visualElement.style.flexWrap = value; else visualElement.style.flexWrap = base.Null; } } } ================================================ FILE: src/Core/Styling/WrapOverride.cs.meta ================================================ fileFormatVersion: 2 guid: abf88d1f7dd2cef43ae6a6fc899bdb04 MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Core/Styling.meta ================================================ fileFormatVersion: 2 guid: 7243c0fb90369024fadd0cb72120c84f folderAsset: yes DefaultImporter: externalObjects: {} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Core/Templating/TemplatePreset.cs ================================================  using System; using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Events; using UnityEngine.UIElements; using Kinstrife.Core.ReflectionHelpers; namespace Graphene { /// /// A list of supported control types that can be rendered by Graphene /// public enum ControlType { None, Label, Button, Slider, SliderInt, Toggle, Foldout, ListView, ListItem, SelectField, CycleField, TextField, Title, SubTitle, Body, Border, DropdownField, Card, ButtonGroup, SubContext, MinMaxSlider, ProgressBar } public interface ICustomControlType { ControlType ControlType { get; } } public interface ICustomAddClasses { string ClassesToAdd { get; } } public interface ICustomName { string CustomName { get; } } public interface ISubContext { } [Serializable] public class ControlVisualTreeAssetMapping : SerializableDictionary { } /// /// A TemplatePreset allows you to define a set of VisualTreeAssets to be used as templates for different . /// [CreateAssetMenu(menuName = "Graphene/Templating/ComponentTemplates")] public class TemplatePreset : ScriptableObject { [SerializeField] TemplatePreset parent; public TemplatePreset Parent => parent; [SerializeField] ControlVisualTreeAssetMapping data = new ControlVisualTreeAssetMapping(); public static ControlType ResolveControlType(object data, bool isPrimitiveContext, DrawAttribute drawAttribute = null) { ControlType controlType = ControlType.None; // Try get from attributes // No member draw attribute -> try get ControlType from class attribute if (!isPrimitiveContext && (drawAttribute == null || drawAttribute.controlType == ControlType.None)) { var info = TypeInfoCache.GetExtendedTypeInfo(data.GetType()); if (info.HasTypeAttribute()) drawAttribute = info.GetTypeAttribute(); } // Set ControlType from attribute (if present) if (drawAttribute != null && drawAttribute.controlType != ControlType.None) controlType = drawAttribute.controlType; // Didn't find in attribute else controlType = GetControlTypeFromData(data, isPrimitiveContext); return controlType; } public static ControlType GetControlTypeFromData(object data, bool isPrimitiveContext) { if (data is bool) return ControlType.Toggle; else if (data is float) return ControlType.Slider; else if (data is int) return ControlType.SliderInt; else if (data is string) return ControlType.Label; else if (data is System.Action || data is UnityEvent) return ControlType.Button; else if (data is Vector2) return ControlType.MinMaxSlider; else if (data is IList) return ControlType.ListView; else if (data is Enum) return ControlType.DropdownField; else if (!isPrimitiveContext) // Use nested scope return ControlType.SubContext; return ControlType.None; } public bool TryGetTemplateAsset(ControlType controlType, out VisualTreeAsset visualTreeAsset) { if (data.TryGetValue(controlType, out visualTreeAsset)) return true; else if (parent) return parent.TryGetTemplateAsset(controlType, out visualTreeAsset); else Debug.LogError($"Didn't find template for control {controlType}", this); return false; } } } ================================================ FILE: src/Core/Templating/TemplatePreset.cs.meta ================================================ fileFormatVersion: 2 guid: fecf578bfcf34544fb992fb36cc63ce3 MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {fileID: 2800000, guid: d17647a721ae9b6418d60aabba5be55f, type: 3} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Core/Templating.meta ================================================ fileFormatVersion: 2 guid: 18f3fea75a8c9b24da7bb9ab083e531d folderAsset: yes DefaultImporter: externalObjects: {} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Core/View/SerializedView.cs ================================================ using UnityEngine; using UnityEngine.UIElements; #if ODIN_INSPECTOR using Sirenix.OdinInspector; #endif namespace Graphene { /// /// Used to serialize a reference of a in a , including style overrides to apply at runtime. /// [System.Serializable, Toggle(nameof(Enabled))] public struct SerializedView { [SerializeField] public bool Enabled; [ReadOnly, SerializeField] public string Id; [SerializeField] InlineStyleOverrides StyleOverrides; public SerializedView(string id) { Enabled = false; this.Id = id; StyleOverrides = new InlineStyleOverrides(); } public void Apply(VisualElement el) { StyleOverrides.Apply(el); } } } ================================================ FILE: src/Core/View/SerializedView.cs.meta ================================================ fileFormatVersion: 2 guid: 7b1deb9f87a8f914cac2440ff9be962a MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Core/View/ViewHandle.cs ================================================ using Graphene.Elements; using System; using System.Collections.Generic; using System.Linq; using UnityEngine; using UnityEngine.UIElements; namespace Graphene { /// /// Represents a reference to a within a . /// Used to resolve and access views by ID, and to manage their association with plates. /// [Serializable] public class ViewRef { /// /// The default selector used if no ID is specified. /// public readonly string defaultSelector; [SerializeField, HideInInspector] Plate plate; #region ValueDropdownAttribute #if ODIN_INSPECTOR [Sirenix.OdinInspector.ValueDropdown("GetViewsFromVisualTreeAsset")] #endif #endregion /// /// The ID of the view to reference. /// [SerializeField] protected string id; /// /// Gets the ID of the referenced view. /// public string Id => id; /// /// The resolved instance. /// public View view; #region ShowInInspectorAttribute #if ODIN_INSPECTOR [Sirenix.OdinInspector.ShowInInspector] #endif #endregion /// /// Indicates whether the view has been successfully resolved. /// public bool initialized => view != null; /// /// Returns a list of available view IDs from the associated plate's visual tree asset. /// public IEnumerable GetViewsFromVisualTreeAsset() { if (!plate) return Enumerable.Empty(); else if (plate.viewIds != null && plate.viewIds.Count > 0) return plate.viewIds; else return new List { "" }; } /// /// Constructs a new with the specified default ID. /// /// The default view selector. public ViewRef(string defaultId) { this.defaultSelector = defaultId; } /// /// Implicitly converts a to a boolean indicating if it is valid and has a non-empty ID. /// public static implicit operator bool(ViewRef viewRef) => viewRef != null && !string.IsNullOrWhiteSpace(viewRef.id); /// /// Resolves the view reference using the specified plate, updating the field. /// /// The plate to resolve the view from. public void ResolveView(Plate plate) { this.plate = plate; // Use default selector if ID is not set if (string.IsNullOrWhiteSpace(id)) id = defaultSelector; view = plate.GetViewById(id); } /// /// Sets the plate associated with this view reference. /// /// The plate to associate. public void SetPlate(Plate plate) { this.plate = plate; } /// /// Clears the parent plate and view reference. /// public void NoParent() { this.view = null; this.plate = null; } } [System.Obsolete("Use ViewRef instead")] /// /// Component for referencing a view by ID within a plate. /// Used to select and interact with views in the UI hierarchy. /// [DisallowMultipleComponent] public class ViewHandle : MonoBehaviour { #region ButtonAttribute #if ODIN_INSPECTOR [Sirenix.OdinInspector.ValueDropdown("GetViewsFromVisualTreeAsset")] #endif #endregion /// /// The ID of the view to reference. /// [SerializeField] protected string id; /// /// Gets the ID of the referenced view. /// public string Id => id; /// /// Returns a list of available view IDs from the parent plate's visual tree asset. /// public IEnumerable GetViewsFromVisualTreeAsset() { Plate plate = GetComponent(); var root = plate.transform.parent.GetComponent().VisualTreeAsset.CloneTree(); // Get views return root?.Query().ToList().Select(v => v.id); } } } ================================================ FILE: src/Core/View/ViewHandle.cs.meta ================================================ fileFormatVersion: 2 guid: 6ebff670864e5cd4d87ede0a925c3a68 MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {fileID: 2800000, guid: 779432a5e218db5438ca646cb9970341, type: 3} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Core/View.meta ================================================ fileFormatVersion: 2 guid: 6f3253f7286180045afee8f02c25b53b folderAsset: yes DefaultImporter: externalObjects: {} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Core.meta ================================================ fileFormatVersion: 2 guid: 3e44d045c365d3345a3368115362a35f folderAsset: yes DefaultImporter: externalObjects: {} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Editor/CustomDictionaryPropertyDrawers.cs ================================================ using System.Collections; using System.Collections.Generic; using UnityEditor; using UnityEngine; using UnityEngine.UIElements; namespace Graphene.Editor { [CustomPropertyDrawer(typeof(SerializableDictionary))] [CustomPropertyDrawer(typeof(ControlVisualTreeAssetMapping))] public class AnySerializableDictionaryStoragePropertyDrawer : SerializableDictionaryPropertyDrawer { } } ================================================ FILE: src/Editor/CustomDictionaryPropertyDrawers.cs.meta ================================================ fileFormatVersion: 2 guid: 46100f4664e80f14dab215b9e06a4763 MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Editor/Graphene.Editor.asmdef ================================================ { "name": "Graphene.Editor", "references": [ "GUID:5182bff32a626fa409ad510e8c679a69", "GUID:a9d2a6efafebd5e47b01684a70d95cc5", "GUID:dd4a8158362adf24fa2558eb7adb4b12", "GUID:478a2357cc57436488a56e564b08d223" ], "includePlatforms": [ "Editor" ], "excludePlatforms": [], "allowUnsafeCode": false, "overrideReferences": false, "precompiledReferences": [], "autoReferenced": true, "defineConstraints": [], "versionDefines": [], "noEngineReferences": false } ================================================ FILE: src/Editor/Graphene.Editor.asmdef.meta ================================================ fileFormatVersion: 2 guid: 979c01504af7b11408532aa7eaf930d2 AssemblyDefinitionImporter: externalObjects: {} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Editor/GrapheneEditorUtilities.cs ================================================ using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEditor; using Unity.EditorCoroutines.Editor; namespace Graphene { internal static class GrapheneEditorUtilities { public const string uuid = "com.graphene.core"; public const string gitUrlCore = "https://github.com/LudiKha/Graphene.git?path=/src"; public const string gitUrlComponents = "https://github.com/LudiKha/Graphene-Components.git?path=/src"; public class PackageRequest { } [MenuItem("Window/Graphene/Check for updates/Graphene Core")] static void CheckForUpdates() { var owner = new PackageRequest(); EditorCoroutineUtility.StartCoroutine(MonitorPackageUpdate(owner, gitUrlCore, "Graphene Core"), owner); } [MenuItem("Window/Graphene/Check for updates/Graphene Components")] static void CheckForUpdatesComponents() { var owner = new PackageRequest(); EditorCoroutineUtility.StartCoroutine(MonitorPackageUpdate(owner, gitUrlComponents, "Graphene Components"), owner); } static IEnumerator MonitorPackageUpdate(PackageRequest owner, string gitUrl, string packageName) { Debug.Log($"Checking for updates for {packageName}..."); var request = UnityEditor.PackageManager.Client.Add(gitUrl); while (!request.IsCompleted) { yield return null; } if(request.Error!= null) Debug.LogError($"Error code {request.Error.message}: {request.Error.message}"); Debug.Log($"Latest version: {request.Result.version}"); yield break; } } } ================================================ FILE: src/Editor/GrapheneEditorUtilities.cs.meta ================================================ fileFormatVersion: 2 guid: 49bdd157a434fa944a0149e48ef81046 MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Editor/ViewSelectorStringDrawer.cs ================================================ //#if ODIN_INSPECTOR //using System.Collections; //using System.Collections.Generic; //using UnityEngine; //using UnityEditor; //using Sirenix.OdinInspector.Editor; //using Sirenix.Utilities.Editor; //namespace Graphene.Editor //{ // public class ViewRefDrawer : OdinValueDrawer // { // protected override void DrawPropertyLayout(GUIContent label) // { // base.DrawPropertyLayout(label); // return; // var value = this.ValueEntry.SmartValue; // SirenixEditorGUI.BeginBox(); // SirenixEditorGUI.BeginBoxHeader(); // SirenixEditorGUI.EndBoxHeader(); // SirenixEditorGUI.BeginVerticalMenuList("Views"); // SirenixEditorGUI.BeginListItem(); // //int selected = SirenixEditorFields.Dropdown(0, value.pl) // SirenixEditorGUI.EndListItem(); // SirenixEditorGUI.EndVerticalMenuList(); // SirenixEditorGUI.EndBox(); // } // } // } //#endif ================================================ FILE: src/Editor/ViewSelectorStringDrawer.cs.meta ================================================ fileFormatVersion: 2 guid: 5ae8cf884934ad44b9796dc46b098012 MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Editor.meta ================================================ fileFormatVersion: 2 guid: 199242626b096f64a80e9a0629ed98dc folderAsset: yes DefaultImporter: externalObjects: {} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Graphene.Core.asmdef ================================================ { "name": "Graphene.Core", "rootNamespace": "", "references": [ "GUID:a9d2a6efafebd5e47b01684a70d95cc5", "GUID:0b2cc0b34570a2247ae360ab103f281d", "GUID:67b49b9f9092f8a46950e54a0344fbb8" ], "includePlatforms": [], "excludePlatforms": [], "allowUnsafeCode": false, "overrideReferences": false, "precompiledReferences": [], "autoReferenced": true, "defineConstraints": [], "versionDefines": [], "noEngineReferences": false } ================================================ FILE: src/Graphene.Core.asmdef.meta ================================================ fileFormatVersion: 2 guid: 5182bff32a626fa409ad510e8c679a69 AssemblyDefinitionImporter: externalObjects: {} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Lib/DragManipulator/DragManipulator.cs ================================================ /* Original code[1] Copyright (c) 2022 Shane Celis[2] Licensed under the MIT License[3] [1]: https://gist.github.com/shanecelis/b6fb3fe8ed5356be1a3aeeb9e7d2c145 [2]: https://twitter.com/shanecelis [3]: https://opensource.org/licenses/MIT */ using UnityEngine; using UnityEngine.UIElements; /** This manipulator makes a visual element draggable at runtime. Unity's UIToolkit also has a [drag-and-drop system][1] but it is only appropriate for use within its editor. ## Usage ``` element.AddManipulator(new DragManipulator()); element.RegisterCallback(evt => Debug.Log($"{evt.target} dropped on {evt.droppable}"); ``` OR ``` foreach (var element in root.Query(className: "draggable").Build()) { element.AddManipulator(new DragManipulator()); } root.RegisterCallback(evt => Debug.Log($"{evt.target} dropped on {evt.droppable}"); ``` ### Styling When dragging, one should be able to style the participating elements. Coupled with Unity Style Sheet (USS) transitions, one can provide automatic tweens. | USS Selectors | Description | |----------------------+-----------------------------------------------| | .draggable | Present on any element with a DragManipulator | | .draggable--dragging | Present while dragging | | .draggable--can-drop | Present while dragging over a droppable | | .droppable | Identifies a droppable element (editable) | | .droppable--can-drop | Present while a draggable is hovering | A custom property also allows one to disable dragging via the style sheet. | USS Properties | Description | |---------------------+------------------------------------------------| | --draggable-enabled | When set to false, dragging is disabled | ## Requirements - Unity 2020.3 or later ## Dragging Clicking and dragging on the draggable element will cause it to move. The USS class "draggable--dragging" will be present during the duration. ### Remove USS Class on Drag One can remove a USS class while dragging by setting the following parameter at initialization: ``` var dragger = new DragManipulator { removeClassOnDrag = "transitions" }; ``` Usage: If one has translation USS transitions set, dragging may look wrong and may not be smooth. Placing transitions into a special class and removing that class during the drag fixed that problem. ## Dropping Elements that have a "droppable" USS class will be considered droppable. When dragging and hovering over a droppable element, the USS class "droppable--can-drop" will be added; the draggable element will have "draggable--can-drop" added to it. If the draggable element is dropped on a non-droppable element, the draggable element's position is reset. It is suggested that one turn on USS transitions if one wants the draggable to tween back into its original place. ### Distinct Droppables If one has distinct droppable objects, one set the `droppableId` on the `DragManipulator` to something other than "droppable". ``` var dragger = new DragManipulator { droppableId = "discard-pile" }; ``` ## Handling Events When a draggable element is released on a droppable element or its child, a `DropEvent` is emitted. The position of the element is not reset automatically in that case. If the dropped object is supposed to return to its original position, one ought to do that in the callback code. ``` void OnDrag(DropEvent evt) { evt.target.transform.position = Vector3.zero; // OR // evt.dragger.ResetPosition(); } ``` ## Limitations This manipulator changes the `transform.position` of the target element while dragging. If one's styling is making use of that, the behavior is undefined. ## Notes The drop event bubbles up, so the callback can be placed on the parent or root element. Acknowledgments to Crayz[2] and Stacey[3] for their inspiring code. [1]: https://forum.unity.com/threads/visualelement-drag-and-drop-during-runtime.930000/#post-6373881 [2]: https://forum.unity.com/threads/creating-draggable-visualelement-and-clamping-it-to-screen.1017715/ [3]: https://gamedev-resources.com/create-an-in-game-inventory-ui-with-ui-toolkit/ */ public class DragManipulator : IManipulator { private VisualElement _target; public VisualElement target { get => _target; set { if (_target != null) { if (_target == value) return; _target.UnregisterCallback(DragBegin); _target.UnregisterCallback(DragEnd); _target.UnregisterCallback(PointerMove); _target.UnregisterCallback(OnCustomStyleResolved); _target.RemoveFromClassList("draggable"); lastDroppable?.RemoveFromClassList("droppable--can-drop"); lastDroppable = null; } _target = value; _target.RegisterCallback(DragBegin); _target.RegisterCallback(DragEnd); _target.RegisterCallback(PointerMove); _target.RegisterCallback(OnCustomStyleResolved); _target.AddToClassList("draggable"); } } protected static readonly CustomStyleProperty draggableEnabledProperty = new CustomStyleProperty("--draggable-enabled"); protected Vector3 offset; private bool isDragging = false; private VisualElement lastDroppable = null; private string _droppableId = "droppable"; /** This is the USS class that is determines whether the target can be dropped on it. It is "droppable" by default. */ public string droppableId { get => _droppableId; init => _droppableId = value; } /** This manipulator can be disabled. */ public bool enabled { get; set; } = true; private PickingMode lastPickingMode; private string _removeClassOnDrag; /** Optional. Remove the given class from the target element during the drag. If removed, replace when drag ends. */ public string removeClassOnDrag { get => _removeClassOnDrag; init => _removeClassOnDrag = value; } private bool removedClass = false; private void OnCustomStyleResolved(CustomStyleResolvedEvent e) { if (e.customStyle.TryGetValue(draggableEnabledProperty, out bool got)) enabled = got; } private void DragBegin(PointerDownEvent ev) { if (! enabled) return; target.AddToClassList("draggable--dragging"); if (removeClassOnDrag != null) { removedClass = target.ClassListContains(removeClassOnDrag); if (removedClass) target.RemoveFromClassList(removeClassOnDrag); } lastPickingMode = target.pickingMode; target.pickingMode = PickingMode.Ignore; isDragging = true; offset = ev.localPosition; target.CapturePointer(ev.pointerId); } private void DragEnd(IPointerEvent ev) { if (! isDragging) return; VisualElement droppable; bool canDrop = CanDrop(ev.position, out droppable); //Debug.Log($"droppable {droppable}"); if (canDrop) droppable.RemoveFromClassList("droppable--can-drop"); target.RemoveFromClassList("draggable--dragging"); target.RemoveFromClassList("draggable--can-drop"); lastDroppable?.RemoveFromClassList("droppable--can-drop"); lastDroppable = null; target.ReleasePointer(ev.pointerId); target.pickingMode = lastPickingMode; isDragging = false; if (canDrop) Drop(droppable); else ResetPosition(); if (removeClassOnDrag != null && removedClass) target.AddToClassList(removeClassOnDrag); } protected virtual void Drop(VisualElement droppable) { var e = DropEvent.GetPooled(this, droppable); e.target = this.target; // We send the event one tick later so that our changes to the class list // will take effect. this.target.schedule.Execute(() => e.target.SendEvent(e)); } /** Change parent while preserving position via `transform.position`. Usage: While dragging-and-dropping an element, if the dropped element were to change its parent in the hierarchy, but preserve its position on screen, which can be done with `transform.position`. Then one can lerp that position to zero for a nice clean transition. Notes: The algorithm isn't difficult. It's find position wrt new parent, zero out the `transform.position`, add it to the parent, find position wrt new parent, set `transform.position` such that its screen position will be the same as before. The tricky part is when you add this element to a newParent, you can't query for its position (at least not in a way I could find). You have to wait a beat. Then whatever was necessary to update will update. */ public static IVisualElementScheduledItem ChangeParent(VisualElement target, VisualElement newParent) { var position_parent = target.ChangeCoordinatesTo(newParent, Vector2.zero); target.RemoveFromHierarchy(); target.transform.position = Vector3.zero; newParent.Add(target); // ChangeCoordinatesTo will not be correct unless you wait a tick. #hardwon // target.transform.position = position_parent - target.ChangeCoordinatesTo(newParent, // Vector2.zero); return target.schedule.Execute(() => { var newPosition = position_parent - target.ChangeCoordinatesTo(newParent, Vector2.zero); target.RemoveFromHierarchy(); target.transform.position = newPosition; newParent.Add(target); }); } /** Reset the target's position to zero. Note: Schedules the change so that the USS classes will be restored when run. (Helps when a "transitions" USS class is used.) */ public virtual void ResetPosition() { target.transform.position = Vector3.zero; } protected virtual bool CanDrop(Vector3 position, out VisualElement droppable) { droppable = target.panel.Pick(position); var element = droppable; // Walk up parent elements to see if any are droppable. while (element != null && ! element.ClassListContains(droppableId)) element = element.parent; if (element != null) { droppable = element; return true; } return false; } private void PointerMove(PointerMoveEvent ev) { if (! isDragging) return; if (! enabled) { DragEnd(ev); return; } Vector3 delta = ev.localPosition - (Vector3) offset; target.transform.position += delta; if (CanDrop(ev.position, out var droppable)) { target.AddToClassList("draggable--can-drop"); droppable.AddToClassList("droppable--can-drop"); if (lastDroppable != droppable) lastDroppable?.RemoveFromClassList("droppable--can-drop"); lastDroppable = droppable; } else { target.RemoveFromClassList("draggable--can-drop"); lastDroppable?.RemoveFromClassList("droppable--can-drop"); lastDroppable = null; } } } /** This event represents a runtime drag and drop event. */ public class DropEvent : EventBase { public DragManipulator dragger { get; protected set; } public VisualElement droppable { get; protected set; } protected override void Init() { base.Init(); this.LocalInit(); } private void LocalInit() { this.bubbles = true; this.tricklesDown = false; } public static DropEvent GetPooled(DragManipulator dragger, VisualElement droppable) { DropEvent pooled = EventBase.GetPooled(); pooled.dragger = dragger; pooled.droppable = droppable; return pooled; } public DropEvent() => this.LocalInit(); } // This hack allows us to use init properties in earlier versions of Unity. #if UNITY_5_3_OR_NEWER && ! UNITY_2021_OR_NEWER // https://stackoverflow.com/a/62656145 namespace System.Runtime.CompilerServices { using System.ComponentModel; [EditorBrowsable(EditorBrowsableState.Never)] internal class IsExternalInit{} } #endif ================================================ FILE: src/Lib/DragManipulator/DragManipulator.cs.meta ================================================ fileFormatVersion: 2 guid: 4ed0bf7b15ca4544e9c8ca59068db3c1 MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Lib/DragManipulator.meta ================================================ fileFormatVersion: 2 guid: 04fced0da9d8a4c4bb8d1a6e2af0ecff folderAsset: yes DefaultImporter: externalObjects: {} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Lib/SerializableDictionary/Editor/SerializableDictionary.Editor.asmdef ================================================ { "name": "SerializableDictionary.Editor", "references": [ "GUID:a9d2a6efafebd5e47b01684a70d95cc5" ], "includePlatforms": [ "Editor" ], "excludePlatforms": [], "allowUnsafeCode": false, "overrideReferences": false, "precompiledReferences": [], "autoReferenced": true, "defineConstraints": [], "versionDefines": [], "noEngineReferences": false } ================================================ FILE: src/Lib/SerializableDictionary/Editor/SerializableDictionary.Editor.asmdef.meta ================================================ fileFormatVersion: 2 guid: dd4a8158362adf24fa2558eb7adb4b12 AssemblyDefinitionImporter: externalObjects: {} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Lib/SerializableDictionary/Editor/SerializableDictionaryPropertyDrawer.cs ================================================ using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEditor; using System.Reflection; using System; public class SerializableDictionaryPropertyDrawer : PropertyDrawer { const string KeysFieldName = "m_keys"; const string ValuesFieldName = "m_values"; protected const float IndentWidth = 15f; static GUIContent s_iconPlus = IconContent ("Toolbar Plus", "Add entry"); static GUIContent s_iconMinus = IconContent ("Toolbar Minus", "Remove entry"); static GUIContent s_warningIconConflict = IconContent ("console.warnicon.sml", "Conflicting key, this entry will be lost"); static GUIContent s_warningIconOther = IconContent ("console.infoicon.sml", "Conflicting key"); static GUIContent s_warningIconNull = IconContent ("console.warnicon.sml", "Null key, this entry will be lost"); static GUIStyle s_buttonStyle = GUIStyle.none; static GUIContent s_tempContent = new GUIContent(); class ConflictState { public object conflictKey = null; public object conflictValue = null; public int conflictIndex = -1 ; public int conflictOtherIndex = -1 ; public bool conflictKeyPropertyExpanded = false; public bool conflictValuePropertyExpanded = false; public float conflictLineHeight = 0f; } struct PropertyIdentity { public PropertyIdentity(SerializedProperty property) { this.instance = property.serializedObject.targetObject; this.propertyPath = property.propertyPath; } public UnityEngine.Object instance; public string propertyPath; } static Dictionary s_conflictStateDict = new Dictionary(); enum Action { None, Add, Remove } public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { label = EditorGUI.BeginProperty(position, label, property); Action buttonAction = Action.None; int buttonActionIndex = 0; var keyArrayProperty = property.FindPropertyRelative(KeysFieldName); var valueArrayProperty = property.FindPropertyRelative(ValuesFieldName); ConflictState conflictState = GetConflictState(property); if(conflictState.conflictIndex != -1) { keyArrayProperty.InsertArrayElementAtIndex(conflictState.conflictIndex); var keyProperty = keyArrayProperty.GetArrayElementAtIndex(conflictState.conflictIndex); SetPropertyValue(keyProperty, conflictState.conflictKey); keyProperty.isExpanded = conflictState.conflictKeyPropertyExpanded; valueArrayProperty.InsertArrayElementAtIndex(conflictState.conflictIndex); var valueProperty = valueArrayProperty.GetArrayElementAtIndex(conflictState.conflictIndex); SetPropertyValue(valueProperty, conflictState.conflictValue); valueProperty.isExpanded = conflictState.conflictValuePropertyExpanded; } var buttonWidth = s_buttonStyle.CalcSize(s_iconPlus).x; var labelPosition = position; labelPosition.height = EditorGUIUtility.singleLineHeight; if (property.isExpanded) labelPosition.xMax -= s_buttonStyle.CalcSize(s_iconPlus).x; EditorGUI.PropertyField(labelPosition, property, label, false); // property.isExpanded = EditorGUI.Foldout(labelPosition, property.isExpanded, label); if (property.isExpanded) { var buttonPosition = position; buttonPosition.xMin = buttonPosition.xMax - buttonWidth; buttonPosition.height = EditorGUIUtility.singleLineHeight; EditorGUI.BeginDisabledGroup(conflictState.conflictIndex != -1); if(GUI.Button(buttonPosition, s_iconPlus, s_buttonStyle)) { buttonAction = Action.Add; buttonActionIndex = keyArrayProperty.arraySize; } EditorGUI.EndDisabledGroup(); EditorGUI.indentLevel++; var linePosition = position; linePosition.y += EditorGUIUtility.singleLineHeight; linePosition.xMax -= buttonWidth; foreach(var entry in EnumerateEntries(keyArrayProperty, valueArrayProperty)) { var keyProperty = entry.keyProperty; var valueProperty = entry.valueProperty; int i = entry.index; float lineHeight = DrawKeyValueLine(keyProperty, valueProperty, linePosition, i); buttonPosition = linePosition; buttonPosition.x = linePosition.xMax; buttonPosition.height = EditorGUIUtility.singleLineHeight; if(GUI.Button(buttonPosition, s_iconMinus, s_buttonStyle)) { buttonAction = Action.Remove; buttonActionIndex = i; } if(i == conflictState.conflictIndex && conflictState.conflictOtherIndex == -1) { var iconPosition = linePosition; iconPosition.size = s_buttonStyle.CalcSize(s_warningIconNull); GUI.Label(iconPosition, s_warningIconNull); } else if(i == conflictState.conflictIndex) { var iconPosition = linePosition; iconPosition.size = s_buttonStyle.CalcSize(s_warningIconConflict); GUI.Label(iconPosition, s_warningIconConflict); } else if(i == conflictState.conflictOtherIndex) { var iconPosition = linePosition; iconPosition.size = s_buttonStyle.CalcSize(s_warningIconOther); GUI.Label(iconPosition, s_warningIconOther); } linePosition.y += lineHeight; } EditorGUI.indentLevel--; } if(buttonAction == Action.Add) { keyArrayProperty.InsertArrayElementAtIndex(buttonActionIndex); valueArrayProperty.InsertArrayElementAtIndex(buttonActionIndex); } else if(buttonAction == Action.Remove) { DeleteArrayElementAtIndex(keyArrayProperty, buttonActionIndex); DeleteArrayElementAtIndex(valueArrayProperty, buttonActionIndex); } conflictState.conflictKey = null; conflictState.conflictValue = null; conflictState.conflictIndex = -1; conflictState.conflictOtherIndex = -1; conflictState.conflictLineHeight = 0f; conflictState.conflictKeyPropertyExpanded = false; conflictState.conflictValuePropertyExpanded = false; foreach(var entry1 in EnumerateEntries(keyArrayProperty, valueArrayProperty)) { var keyProperty1 = entry1.keyProperty; int i = entry1.index; object keyProperty1Value = GetPropertyValue(keyProperty1); if(keyProperty1Value == null) { var valueProperty1 = entry1.valueProperty; SaveProperty(keyProperty1, valueProperty1, i, -1, conflictState); DeleteArrayElementAtIndex(valueArrayProperty, i); DeleteArrayElementAtIndex(keyArrayProperty, i); break; } foreach(var entry2 in EnumerateEntries(keyArrayProperty, valueArrayProperty, i + 1)) { var keyProperty2 = entry2.keyProperty; int j = entry2.index; object keyProperty2Value = GetPropertyValue(keyProperty2); if(ComparePropertyValues(keyProperty1Value, keyProperty2Value)) { var valueProperty2 = entry2.valueProperty; SaveProperty(keyProperty2, valueProperty2, j, i, conflictState); DeleteArrayElementAtIndex(keyArrayProperty, j); DeleteArrayElementAtIndex(valueArrayProperty, j); goto breakLoops; } } } breakLoops: EditorGUI.EndProperty(); } static float DrawKeyValueLine(SerializedProperty keyProperty, SerializedProperty valueProperty, Rect linePosition, int index) { bool keyCanBeExpanded = CanPropertyBeExpanded(keyProperty); bool valueCanBeExpanded = CanPropertyBeExpanded(valueProperty); if(!keyCanBeExpanded && valueCanBeExpanded) { return DrawKeyValueLineExpand(keyProperty, valueProperty, linePosition); } else { var keyLabel = keyCanBeExpanded ? ("Key " + index.ToString()) : ""; var valueLabel = valueCanBeExpanded ? ("Value " + index.ToString()) : ""; return DrawKeyValueLineSimple(keyProperty, valueProperty, keyLabel, valueLabel, linePosition); } } static float DrawKeyValueLineSimple(SerializedProperty keyProperty, SerializedProperty valueProperty, string keyLabel, string valueLabel, Rect linePosition) { float labelWidth = EditorGUIUtility.labelWidth; float labelWidthRelative = labelWidth / linePosition.width; float keyPropertyHeight = EditorGUI.GetPropertyHeight(keyProperty); var keyPosition = linePosition; keyPosition.height = keyPropertyHeight; keyPosition.width = labelWidth - IndentWidth; EditorGUIUtility.labelWidth = keyPosition.width * labelWidthRelative; EditorGUI.PropertyField(keyPosition, keyProperty, TempContent(keyLabel), true); float valuePropertyHeight = EditorGUI.GetPropertyHeight(valueProperty); var valuePosition = linePosition; valuePosition.height = valuePropertyHeight; valuePosition.xMin += labelWidth; EditorGUIUtility.labelWidth = valuePosition.width * labelWidthRelative; EditorGUI.indentLevel--; EditorGUI.PropertyField(valuePosition, valueProperty, TempContent(valueLabel), true); EditorGUI.indentLevel++; EditorGUIUtility.labelWidth = labelWidth; return Mathf.Max(keyPropertyHeight, valuePropertyHeight); } static float DrawKeyValueLineExpand(SerializedProperty keyProperty, SerializedProperty valueProperty, Rect linePosition) { float labelWidth = EditorGUIUtility.labelWidth; float keyPropertyHeight = EditorGUI.GetPropertyHeight(keyProperty); var keyPosition = linePosition; keyPosition.height = keyPropertyHeight; keyPosition.width = labelWidth - IndentWidth; EditorGUI.PropertyField(keyPosition, keyProperty, GUIContent.none, true); float valuePropertyHeight = EditorGUI.GetPropertyHeight(valueProperty); var valuePosition = linePosition; valuePosition.height = valuePropertyHeight; EditorGUI.PropertyField(valuePosition, valueProperty, GUIContent.none, true); EditorGUIUtility.labelWidth = labelWidth; return Mathf.Max(keyPropertyHeight, valuePropertyHeight); } static bool CanPropertyBeExpanded(SerializedProperty property) { switch(property.propertyType) { case SerializedPropertyType.Generic: case SerializedPropertyType.Vector4: case SerializedPropertyType.Quaternion: return true; default: return false; } } static void SaveProperty(SerializedProperty keyProperty, SerializedProperty valueProperty, int index, int otherIndex, ConflictState conflictState) { conflictState.conflictKey = GetPropertyValue(keyProperty); conflictState.conflictValue = GetPropertyValue(valueProperty); float keyPropertyHeight = EditorGUI.GetPropertyHeight(keyProperty); float valuePropertyHeight = EditorGUI.GetPropertyHeight(valueProperty); float lineHeight = Mathf.Max(keyPropertyHeight, valuePropertyHeight); conflictState.conflictLineHeight = lineHeight; conflictState.conflictIndex = index; conflictState.conflictOtherIndex = otherIndex; conflictState.conflictKeyPropertyExpanded = keyProperty.isExpanded; conflictState.conflictValuePropertyExpanded = valueProperty.isExpanded; } public override float GetPropertyHeight(SerializedProperty property, GUIContent label) { float propertyHeight = EditorGUIUtility.singleLineHeight; if (property.isExpanded) { var keysProperty = property.FindPropertyRelative(KeysFieldName); var valuesProperty = property.FindPropertyRelative(ValuesFieldName); foreach(var entry in EnumerateEntries(keysProperty, valuesProperty)) { var keyProperty = entry.keyProperty; var valueProperty = entry.valueProperty; float keyPropertyHeight = EditorGUI.GetPropertyHeight(keyProperty); float valuePropertyHeight = EditorGUI.GetPropertyHeight(valueProperty); float lineHeight = Mathf.Max(keyPropertyHeight, valuePropertyHeight); propertyHeight += lineHeight; } ConflictState conflictState = GetConflictState(property); if(conflictState.conflictIndex != -1) { propertyHeight += conflictState.conflictLineHeight; } } return propertyHeight; } static ConflictState GetConflictState(SerializedProperty property) { ConflictState conflictState; PropertyIdentity propId = new PropertyIdentity(property); if(!s_conflictStateDict.TryGetValue(propId, out conflictState)) { conflictState = new ConflictState(); s_conflictStateDict.Add(propId, conflictState); } return conflictState; } static Dictionary s_serializedPropertyValueAccessorsDict; static SerializableDictionaryPropertyDrawer() { Dictionary serializedPropertyValueAccessorsNameDict = new Dictionary() { { SerializedPropertyType.Integer, "intValue" }, { SerializedPropertyType.Boolean, "boolValue" }, { SerializedPropertyType.Float, "floatValue" }, { SerializedPropertyType.String, "stringValue" }, { SerializedPropertyType.Color, "colorValue" }, { SerializedPropertyType.ObjectReference, "objectReferenceValue" }, { SerializedPropertyType.LayerMask, "intValue" }, { SerializedPropertyType.Enum, "intValue" }, { SerializedPropertyType.Vector2, "vector2Value" }, { SerializedPropertyType.Vector3, "vector3Value" }, { SerializedPropertyType.Vector4, "vector4Value" }, { SerializedPropertyType.Rect, "rectValue" }, { SerializedPropertyType.ArraySize, "intValue" }, { SerializedPropertyType.Character, "intValue" }, { SerializedPropertyType.AnimationCurve, "animationCurveValue" }, { SerializedPropertyType.Bounds, "boundsValue" }, { SerializedPropertyType.Quaternion, "quaternionValue" }, }; Type serializedPropertyType = typeof(SerializedProperty); s_serializedPropertyValueAccessorsDict = new Dictionary(); BindingFlags flags = BindingFlags.Instance | BindingFlags.Public; foreach(var kvp in serializedPropertyValueAccessorsNameDict) { PropertyInfo propertyInfo = serializedPropertyType.GetProperty(kvp.Value, flags); s_serializedPropertyValueAccessorsDict.Add(kvp.Key, propertyInfo); } } static GUIContent IconContent(string name, string tooltip) { var builtinIcon = EditorGUIUtility.IconContent (name); return new GUIContent(builtinIcon.image, tooltip); } static GUIContent TempContent(string text) { s_tempContent.text = text; return s_tempContent; } static void DeleteArrayElementAtIndex(SerializedProperty arrayProperty, int index) { var property = arrayProperty.GetArrayElementAtIndex(index); // if(arrayProperty.arrayElementType.StartsWith("PPtr<$")) if(property.propertyType == SerializedPropertyType.ObjectReference) { property.objectReferenceValue = null; } arrayProperty.DeleteArrayElementAtIndex(index); } public static object GetPropertyValue(SerializedProperty p) { PropertyInfo propertyInfo; if(s_serializedPropertyValueAccessorsDict.TryGetValue(p.propertyType, out propertyInfo)) { return propertyInfo.GetValue(p, null); } else { if(p.isArray) return GetPropertyValueArray(p); else return GetPropertyValueGeneric(p); } } static void SetPropertyValue(SerializedProperty p, object v) { PropertyInfo propertyInfo; if(s_serializedPropertyValueAccessorsDict.TryGetValue(p.propertyType, out propertyInfo)) { propertyInfo.SetValue(p, v, null); } else { if(p.isArray) SetPropertyValueArray(p, v); else SetPropertyValueGeneric(p, v); } } static object GetPropertyValueArray(SerializedProperty property) { object[] array = new object[property.arraySize]; for(int i = 0; i < property.arraySize; i++) { SerializedProperty item = property.GetArrayElementAtIndex(i); array[i] = GetPropertyValue(item); } return array; } static object GetPropertyValueGeneric(SerializedProperty property) { Dictionary dict = new Dictionary(); var iterator = property.Copy(); if(iterator.Next(true)) { var end = property.GetEndProperty(); do { string name = iterator.name; object value = GetPropertyValue(iterator); dict.Add(name, value); } while(iterator.Next(false) && iterator.propertyPath != end.propertyPath); } return dict; } static void SetPropertyValueArray(SerializedProperty property, object v) { object[] array = (object[]) v; property.arraySize = array.Length; for(int i = 0; i < property.arraySize; i++) { SerializedProperty item = property.GetArrayElementAtIndex(i); SetPropertyValue(item, array[i]); } } static void SetPropertyValueGeneric(SerializedProperty property, object v) { Dictionary dict = (Dictionary) v; var iterator = property.Copy(); if(iterator.Next(true)) { var end = property.GetEndProperty(); do { string name = iterator.name; SetPropertyValue(iterator, dict[name]); } while(iterator.Next(false) && iterator.propertyPath != end.propertyPath); } } static bool ComparePropertyValues(object value1, object value2) { if(value1 is Dictionary && value2 is Dictionary) { var dict1 = (Dictionary)value1; var dict2 = (Dictionary)value2; return CompareDictionaries(dict1, dict2); } else { return object.Equals(value1, value2); } } static bool CompareDictionaries(Dictionary dict1, Dictionary dict2) { if(dict1.Count != dict2.Count) return false; foreach(var kvp1 in dict1) { var key1 = kvp1.Key; object value1 = kvp1.Value; object value2; if(!dict2.TryGetValue(key1, out value2)) return false; if(!ComparePropertyValues(value1, value2)) return false; } return true; } struct EnumerationEntry { public SerializedProperty keyProperty; public SerializedProperty valueProperty; public int index; public EnumerationEntry(SerializedProperty keyProperty, SerializedProperty valueProperty, int index) { this.keyProperty = keyProperty; this.valueProperty = valueProperty; this.index = index; } } static IEnumerable EnumerateEntries(SerializedProperty keyArrayProperty, SerializedProperty valueArrayProperty, int startIndex = 0) { if(keyArrayProperty.arraySize > startIndex) { int index = startIndex; var keyProperty = keyArrayProperty.GetArrayElementAtIndex(startIndex); var valueProperty = valueArrayProperty.GetArrayElementAtIndex(startIndex); var endProperty = keyArrayProperty.GetEndProperty(); do { yield return new EnumerationEntry(keyProperty, valueProperty, index); index++; } while(keyProperty.Next(false) && valueProperty.Next(false) && !SerializedProperty.EqualContents(keyProperty, endProperty)); } } } public class SerializableDictionaryStoragePropertyDrawer : PropertyDrawer { public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { property.Next(true); EditorGUI.PropertyField(position, property, label, true); } public override float GetPropertyHeight(SerializedProperty property, GUIContent label) { property.Next(true); return EditorGUI.GetPropertyHeight(property); } } ================================================ FILE: src/Lib/SerializableDictionary/Editor/SerializableDictionaryPropertyDrawer.cs.meta ================================================ fileFormatVersion: 2 guid: 91da51d02ab9ebc459d80d5965d40d19 timeCreated: 1492869349 licenseType: Store MonoImporter: serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Lib/SerializableDictionary/Editor.meta ================================================ fileFormatVersion: 2 guid: 38f5d7794faaa21468f705ca2a23149f folderAsset: yes DefaultImporter: externalObjects: {} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Lib/SerializableDictionary/SerializableDictionary.Runtime.asmdef ================================================ { "name": "SerializableDictionary.Runtime" } ================================================ FILE: src/Lib/SerializableDictionary/SerializableDictionary.Runtime.asmdef.meta ================================================ fileFormatVersion: 2 guid: a9d2a6efafebd5e47b01684a70d95cc5 AssemblyDefinitionImporter: externalObjects: {} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Lib/SerializableDictionary/SerializableDictionary.cs ================================================ using System; using System.Collections; using System.Collections.Generic; using System.Runtime.Serialization; using UnityEngine; public abstract class SerializableDictionaryBase : Dictionary, ISerializationCallbackReceiver { [SerializeField] TKey[] m_keys; [SerializeField] TValueStorage[] m_values; public SerializableDictionaryBase() { } public SerializableDictionaryBase(IDictionary dict) : base(dict.Count) { foreach (var kvp in dict) { this[kvp.Key] = kvp.Value; } } protected SerializableDictionaryBase(SerializationInfo info, StreamingContext context) : base(info,context){} protected abstract void SetValue(TValueStorage[] storage, int i, TValue value); protected abstract TValue GetValue(TValueStorage[] storage, int i); public void CopyFrom(IDictionary dict) { this.Clear(); foreach (var kvp in dict) { this[kvp.Key] = kvp.Value; } } public void OnAfterDeserialize() { if(m_keys != null && m_values != null && m_keys.Length == m_values.Length) { this.Clear(); int n = m_keys.Length; for(int i = 0; i < n; ++i) { this[m_keys[i]] = GetValue(m_values, i); } m_keys = null; m_values = null; } } public void OnBeforeSerialize() { int n = this.Count; m_keys = new TKey[n]; m_values = new TValueStorage[n]; int i = 0; foreach(var kvp in this) { m_keys[i] = kvp.Key; SetValue(m_values, i, kvp.Value); ++i; } } } public class SerializableDictionary : SerializableDictionaryBase { public SerializableDictionary() { } public SerializableDictionary(IDictionary dict) : base(dict) { } protected SerializableDictionary(SerializationInfo info, StreamingContext context) : base(info,context){} protected override TValue GetValue(TValue[] storage, int i) { return storage[i]; } protected override void SetValue(TValue[] storage, int i, TValue value) { storage[i] = value; } } public static class SerializableDictionary { public class Storage { public T data; } } public class SerializableDictionary : SerializableDictionaryBase where TValueStorage : SerializableDictionary.Storage, new() { public SerializableDictionary() { } public SerializableDictionary(IDictionary dict) : base(dict) { } protected SerializableDictionary(SerializationInfo info, StreamingContext context) : base(info,context){} protected override TValue GetValue(TValueStorage[] storage, int i) { return storage[i].data; } protected override void SetValue(TValueStorage[] storage, int i, TValue value) { storage[i] = new TValueStorage(); storage[i].data = value; } } ================================================ FILE: src/Lib/SerializableDictionary/SerializableDictionary.cs.meta ================================================ fileFormatVersion: 2 guid: e7be1c9624387604fba4005ccf7dbd5a timeCreated: 1492868176 licenseType: Store MonoImporter: serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Lib/SerializableDictionary.meta ================================================ fileFormatVersion: 2 guid: ec22cd43b8631bf4dbb28130429205be folderAsset: yes DefaultImporter: externalObjects: {} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Lib.meta ================================================ fileFormatVersion: 2 guid: 896708c62f5edba45b8a132d08ee914d folderAsset: yes DefaultImporter: externalObjects: {} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Resources/Icons/atom.png.meta ================================================ fileFormatVersion: 2 guid: d17647a721ae9b6418d60aabba5be55f TextureImporter: internalIDToNameTable: [] externalObjects: {} serializedVersion: 12 mipmaps: mipMapMode: 0 enableMipMap: 0 sRGBTexture: 1 linearTexture: 0 fadeOut: 0 borderMipMap: 0 mipMapsPreserveCoverage: 0 alphaTestReferenceValue: 0.5 mipMapFadeDistanceStart: 1 mipMapFadeDistanceEnd: 3 bumpmap: convertToNormalMap: 0 externalNormalMap: 0 heightScale: 0.25 normalMapFilter: 0 isReadable: 0 streamingMipmaps: 0 streamingMipmapsPriority: 0 vTOnly: 0 ignoreMasterTextureLimit: 0 grayScaleToAlpha: 0 generateCubemap: 6 cubemapConvolution: 0 seamlessCubemap: 0 textureFormat: 1 maxTextureSize: 2048 textureSettings: serializedVersion: 2 filterMode: 1 aniso: 0 mipBias: 0 wrapU: 1 wrapV: 1 wrapW: 0 nPOTScale: 0 lightmap: 0 compressionQuality: 50 spriteMode: 1 spriteExtrude: 1 spriteMeshType: 1 alignment: 0 spritePivot: {x: 0.5, y: 0.5} spritePixelsToUnits: 100 spriteBorder: {x: 0, y: 0, z: 0, w: 0} spriteGenerateFallbackPhysicsShape: 0 alphaUsage: 1 alphaIsTransparency: 1 spriteTessellationDetail: -1 textureType: 8 textureShape: 1 singleChannelComponent: 0 flipbookRows: 1 flipbookColumns: 1 maxTextureSizeSet: 0 compressionQualitySet: 0 textureFormatSet: 0 ignorePngGamma: 0 applyGammaDecoding: 0 cookieLightType: 1 platformSettings: - serializedVersion: 3 buildTarget: DefaultTexturePlatform maxTextureSize: 512 resizeAlgorithm: 0 textureFormat: -1 textureCompression: 1 compressionQuality: 50 crunchedCompression: 0 allowsAlphaSplitting: 0 overridden: 0 androidETC2FallbackOverride: 0 forceMaximumCompressionQuality_BC6H_BC7: 0 - serializedVersion: 3 buildTarget: Standalone maxTextureSize: 512 resizeAlgorithm: 0 textureFormat: -1 textureCompression: 1 compressionQuality: 50 crunchedCompression: 0 allowsAlphaSplitting: 0 overridden: 0 androidETC2FallbackOverride: 0 forceMaximumCompressionQuality_BC6H_BC7: 0 spriteSheet: serializedVersion: 2 sprites: [] outline: [] physicsShape: [] bones: [] spriteID: 5e97eb03825dee720800000000000000 internalID: 0 vertices: [] indices: edges: [] weights: [] secondaryTextures: [] nameFileIdTable: {} spritePackingTag: pSDRemoveMatte: 0 pSDShowRemoveMatteOption: 0 userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Resources/Icons/form.png.meta ================================================ fileFormatVersion: 2 guid: 806f5574c9706cd4e812686596744160 TextureImporter: internalIDToNameTable: [] externalObjects: {} serializedVersion: 12 mipmaps: mipMapMode: 0 enableMipMap: 0 sRGBTexture: 1 linearTexture: 0 fadeOut: 0 borderMipMap: 0 mipMapsPreserveCoverage: 0 alphaTestReferenceValue: 0.5 mipMapFadeDistanceStart: 1 mipMapFadeDistanceEnd: 3 bumpmap: convertToNormalMap: 0 externalNormalMap: 0 heightScale: 0.25 normalMapFilter: 0 isReadable: 0 streamingMipmaps: 0 streamingMipmapsPriority: 0 vTOnly: 0 ignoreMasterTextureLimit: 0 grayScaleToAlpha: 0 generateCubemap: 6 cubemapConvolution: 0 seamlessCubemap: 0 textureFormat: 1 maxTextureSize: 2048 textureSettings: serializedVersion: 2 filterMode: 1 aniso: 0 mipBias: 0 wrapU: 1 wrapV: 1 wrapW: 0 nPOTScale: 0 lightmap: 0 compressionQuality: 50 spriteMode: 1 spriteExtrude: 1 spriteMeshType: 1 alignment: 0 spritePivot: {x: 0.5, y: 0.5} spritePixelsToUnits: 100 spriteBorder: {x: 0, y: 0, z: 0, w: 0} spriteGenerateFallbackPhysicsShape: 0 alphaUsage: 1 alphaIsTransparency: 1 spriteTessellationDetail: -1 textureType: 8 textureShape: 1 singleChannelComponent: 0 flipbookRows: 1 flipbookColumns: 1 maxTextureSizeSet: 0 compressionQualitySet: 0 textureFormatSet: 0 ignorePngGamma: 0 applyGammaDecoding: 0 cookieLightType: 1 platformSettings: - serializedVersion: 3 buildTarget: DefaultTexturePlatform maxTextureSize: 512 resizeAlgorithm: 0 textureFormat: -1 textureCompression: 1 compressionQuality: 50 crunchedCompression: 0 allowsAlphaSplitting: 0 overridden: 0 androidETC2FallbackOverride: 0 forceMaximumCompressionQuality_BC6H_BC7: 0 - serializedVersion: 3 buildTarget: Standalone maxTextureSize: 512 resizeAlgorithm: 0 textureFormat: -1 textureCompression: 1 compressionQuality: 50 crunchedCompression: 0 allowsAlphaSplitting: 0 overridden: 0 androidETC2FallbackOverride: 0 forceMaximumCompressionQuality_BC6H_BC7: 0 spriteSheet: serializedVersion: 2 sprites: [] outline: [] physicsShape: [] bones: [] spriteID: 5e97eb03825dee720800000000000000 internalID: 0 vertices: [] indices: edges: [] weights: [] secondaryTextures: [] nameFileIdTable: {} spritePackingTag: pSDRemoveMatte: 0 pSDShowRemoveMatteOption: 0 userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Resources/Icons/graphene.png.meta ================================================ fileFormatVersion: 2 guid: db72b2a2b6f47b544a0458d9aa7b0e69 TextureImporter: internalIDToNameTable: [] externalObjects: {} serializedVersion: 12 mipmaps: mipMapMode: 0 enableMipMap: 0 sRGBTexture: 1 linearTexture: 0 fadeOut: 0 borderMipMap: 0 mipMapsPreserveCoverage: 0 alphaTestReferenceValue: 0.5 mipMapFadeDistanceStart: 1 mipMapFadeDistanceEnd: 3 bumpmap: convertToNormalMap: 0 externalNormalMap: 0 heightScale: 0.25 normalMapFilter: 0 isReadable: 0 streamingMipmaps: 0 streamingMipmapsPriority: 0 vTOnly: 0 ignoreMasterTextureLimit: 0 grayScaleToAlpha: 0 generateCubemap: 6 cubemapConvolution: 0 seamlessCubemap: 0 textureFormat: 1 maxTextureSize: 2048 textureSettings: serializedVersion: 2 filterMode: 1 aniso: 0 mipBias: 0 wrapU: 1 wrapV: 1 wrapW: 0 nPOTScale: 0 lightmap: 0 compressionQuality: 50 spriteMode: 1 spriteExtrude: 1 spriteMeshType: 1 alignment: 0 spritePivot: {x: 0.5, y: 0.5} spritePixelsToUnits: 100 spriteBorder: {x: 0, y: 0, z: 0, w: 0} spriteGenerateFallbackPhysicsShape: 0 alphaUsage: 1 alphaIsTransparency: 1 spriteTessellationDetail: -1 textureType: 8 textureShape: 1 singleChannelComponent: 0 flipbookRows: 1 flipbookColumns: 1 maxTextureSizeSet: 0 compressionQualitySet: 0 textureFormatSet: 0 ignorePngGamma: 0 applyGammaDecoding: 0 cookieLightType: 1 platformSettings: - serializedVersion: 3 buildTarget: DefaultTexturePlatform maxTextureSize: 512 resizeAlgorithm: 0 textureFormat: -1 textureCompression: 1 compressionQuality: 50 crunchedCompression: 0 allowsAlphaSplitting: 0 overridden: 0 androidETC2FallbackOverride: 0 forceMaximumCompressionQuality_BC6H_BC7: 0 - serializedVersion: 3 buildTarget: Standalone maxTextureSize: 512 resizeAlgorithm: 0 textureFormat: -1 textureCompression: 1 compressionQuality: 50 crunchedCompression: 0 allowsAlphaSplitting: 0 overridden: 0 androidETC2FallbackOverride: 0 forceMaximumCompressionQuality_BC6H_BC7: 0 spriteSheet: serializedVersion: 2 sprites: [] outline: [] physicsShape: [] bones: [] spriteID: 5e97eb03825dee720800000000000000 internalID: 0 vertices: [] indices: edges: [] weights: [] secondaryTextures: [] nameFileIdTable: {} spritePackingTag: pSDRemoveMatte: 0 pSDShowRemoveMatteOption: 0 userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Resources/Icons/injector.png.meta ================================================ fileFormatVersion: 2 guid: d249967811185734b8114d4b8977f433 TextureImporter: internalIDToNameTable: [] externalObjects: {} serializedVersion: 12 mipmaps: mipMapMode: 0 enableMipMap: 0 sRGBTexture: 1 linearTexture: 0 fadeOut: 0 borderMipMap: 0 mipMapsPreserveCoverage: 0 alphaTestReferenceValue: 0.5 mipMapFadeDistanceStart: 1 mipMapFadeDistanceEnd: 3 bumpmap: convertToNormalMap: 0 externalNormalMap: 0 heightScale: 0.25 normalMapFilter: 0 isReadable: 0 streamingMipmaps: 0 streamingMipmapsPriority: 0 vTOnly: 0 ignoreMasterTextureLimit: 0 grayScaleToAlpha: 0 generateCubemap: 6 cubemapConvolution: 0 seamlessCubemap: 0 textureFormat: 1 maxTextureSize: 2048 textureSettings: serializedVersion: 2 filterMode: 1 aniso: 0 mipBias: 0 wrapU: 1 wrapV: 1 wrapW: 0 nPOTScale: 0 lightmap: 0 compressionQuality: 50 spriteMode: 1 spriteExtrude: 1 spriteMeshType: 1 alignment: 0 spritePivot: {x: 0.5, y: 0.5} spritePixelsToUnits: 100 spriteBorder: {x: 0, y: 0, z: 0, w: 0} spriteGenerateFallbackPhysicsShape: 0 alphaUsage: 1 alphaIsTransparency: 1 spriteTessellationDetail: -1 textureType: 8 textureShape: 1 singleChannelComponent: 0 flipbookRows: 1 flipbookColumns: 1 maxTextureSizeSet: 0 compressionQualitySet: 0 textureFormatSet: 0 ignorePngGamma: 0 applyGammaDecoding: 0 cookieLightType: 1 platformSettings: - serializedVersion: 3 buildTarget: DefaultTexturePlatform maxTextureSize: 512 resizeAlgorithm: 0 textureFormat: -1 textureCompression: 1 compressionQuality: 50 crunchedCompression: 0 allowsAlphaSplitting: 0 overridden: 0 androidETC2FallbackOverride: 0 forceMaximumCompressionQuality_BC6H_BC7: 0 - serializedVersion: 3 buildTarget: Standalone maxTextureSize: 512 resizeAlgorithm: 0 textureFormat: -1 textureCompression: 1 compressionQuality: 50 crunchedCompression: 0 allowsAlphaSplitting: 0 overridden: 0 androidETC2FallbackOverride: 0 forceMaximumCompressionQuality_BC6H_BC7: 0 spriteSheet: serializedVersion: 2 sprites: [] outline: [] physicsShape: [] bones: [] spriteID: 5e97eb03825dee720800000000000000 internalID: 0 vertices: [] indices: edges: [] weights: [] secondaryTextures: [] nameFileIdTable: {} spritePackingTag: pSDRemoveMatte: 0 pSDShowRemoveMatteOption: 0 userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Resources/Icons/layout.png.meta ================================================ fileFormatVersion: 2 guid: 779432a5e218db5438ca646cb9970341 TextureImporter: internalIDToNameTable: [] externalObjects: {} serializedVersion: 11 mipmaps: mipMapMode: 0 enableMipMap: 1 sRGBTexture: 1 linearTexture: 0 fadeOut: 0 borderMipMap: 0 mipMapsPreserveCoverage: 0 alphaTestReferenceValue: 0.5 mipMapFadeDistanceStart: 1 mipMapFadeDistanceEnd: 3 bumpmap: convertToNormalMap: 0 externalNormalMap: 0 heightScale: 0.25 normalMapFilter: 0 isReadable: 0 streamingMipmaps: 0 streamingMipmapsPriority: 0 vTOnly: 0 grayScaleToAlpha: 0 generateCubemap: 6 cubemapConvolution: 0 seamlessCubemap: 0 textureFormat: 1 maxTextureSize: 2048 textureSettings: serializedVersion: 2 filterMode: -1 aniso: -1 mipBias: -100 wrapU: -1 wrapV: -1 wrapW: -1 nPOTScale: 1 lightmap: 0 compressionQuality: 50 spriteMode: 0 spriteExtrude: 1 spriteMeshType: 1 alignment: 0 spritePivot: {x: 0.5, y: 0.5} spritePixelsToUnits: 100 spriteBorder: {x: 0, y: 0, z: 0, w: 0} spriteGenerateFallbackPhysicsShape: 1 alphaUsage: 1 alphaIsTransparency: 0 spriteTessellationDetail: -1 textureType: 0 textureShape: 1 singleChannelComponent: 0 maxTextureSizeSet: 0 compressionQualitySet: 0 textureFormatSet: 0 ignorePngGamma: 0 applyGammaDecoding: 0 platformSettings: - serializedVersion: 3 buildTarget: DefaultTexturePlatform maxTextureSize: 2048 resizeAlgorithm: 0 textureFormat: -1 textureCompression: 1 compressionQuality: 50 crunchedCompression: 0 allowsAlphaSplitting: 0 overridden: 0 androidETC2FallbackOverride: 0 forceMaximumCompressionQuality_BC6H_BC7: 0 spriteSheet: serializedVersion: 2 sprites: [] outline: [] physicsShape: [] bones: [] spriteID: internalID: 0 vertices: [] indices: edges: [] weights: [] secondaryTextures: [] spritePackingTag: pSDRemoveMatte: 0 pSDShowRemoveMatteOption: 0 userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Resources/Icons/molecule.png.meta ================================================ fileFormatVersion: 2 guid: 2a950971ac44a1f4bbec1a1338d2f483 TextureImporter: internalIDToNameTable: [] externalObjects: {} serializedVersion: 12 mipmaps: mipMapMode: 0 enableMipMap: 0 sRGBTexture: 1 linearTexture: 0 fadeOut: 0 borderMipMap: 0 mipMapsPreserveCoverage: 0 alphaTestReferenceValue: 0.5 mipMapFadeDistanceStart: 1 mipMapFadeDistanceEnd: 3 bumpmap: convertToNormalMap: 0 externalNormalMap: 0 heightScale: 0.25 normalMapFilter: 0 isReadable: 0 streamingMipmaps: 0 streamingMipmapsPriority: 0 vTOnly: 0 ignoreMasterTextureLimit: 0 grayScaleToAlpha: 0 generateCubemap: 6 cubemapConvolution: 0 seamlessCubemap: 0 textureFormat: 1 maxTextureSize: 2048 textureSettings: serializedVersion: 2 filterMode: 1 aniso: 0 mipBias: 0 wrapU: 1 wrapV: 1 wrapW: 0 nPOTScale: 0 lightmap: 0 compressionQuality: 50 spriteMode: 1 spriteExtrude: 1 spriteMeshType: 1 alignment: 0 spritePivot: {x: 0.5, y: 0.5} spritePixelsToUnits: 100 spriteBorder: {x: 0, y: 0, z: 0, w: 0} spriteGenerateFallbackPhysicsShape: 0 alphaUsage: 1 alphaIsTransparency: 1 spriteTessellationDetail: -1 textureType: 8 textureShape: 1 singleChannelComponent: 0 flipbookRows: 1 flipbookColumns: 1 maxTextureSizeSet: 0 compressionQualitySet: 0 textureFormatSet: 0 ignorePngGamma: 0 applyGammaDecoding: 0 cookieLightType: 1 platformSettings: - serializedVersion: 3 buildTarget: DefaultTexturePlatform maxTextureSize: 512 resizeAlgorithm: 0 textureFormat: -1 textureCompression: 1 compressionQuality: 50 crunchedCompression: 0 allowsAlphaSplitting: 0 overridden: 0 androidETC2FallbackOverride: 0 forceMaximumCompressionQuality_BC6H_BC7: 0 - serializedVersion: 3 buildTarget: Standalone maxTextureSize: 512 resizeAlgorithm: 0 textureFormat: -1 textureCompression: 1 compressionQuality: 50 crunchedCompression: 0 allowsAlphaSplitting: 0 overridden: 0 androidETC2FallbackOverride: 0 forceMaximumCompressionQuality_BC6H_BC7: 0 spriteSheet: serializedVersion: 2 sprites: [] outline: [] physicsShape: [] bones: [] spriteID: 5e97eb03825dee720800000000000000 internalID: 0 vertices: [] indices: edges: [] weights: [] secondaryTextures: [] nameFileIdTable: {} spritePackingTag: pSDRemoveMatte: 0 pSDShowRemoveMatteOption: 0 userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Resources/Icons/plate.png.meta ================================================ fileFormatVersion: 2 guid: d5720deaef5568642aae7621cd85c1d4 TextureImporter: internalIDToNameTable: [] externalObjects: {} serializedVersion: 12 mipmaps: mipMapMode: 0 enableMipMap: 0 sRGBTexture: 1 linearTexture: 0 fadeOut: 0 borderMipMap: 0 mipMapsPreserveCoverage: 0 alphaTestReferenceValue: 0.5 mipMapFadeDistanceStart: 1 mipMapFadeDistanceEnd: 3 bumpmap: convertToNormalMap: 0 externalNormalMap: 0 heightScale: 0.25 normalMapFilter: 0 isReadable: 0 streamingMipmaps: 0 streamingMipmapsPriority: 0 vTOnly: 0 ignoreMasterTextureLimit: 0 grayScaleToAlpha: 0 generateCubemap: 6 cubemapConvolution: 0 seamlessCubemap: 0 textureFormat: 1 maxTextureSize: 2048 textureSettings: serializedVersion: 2 filterMode: 1 aniso: 0 mipBias: 0 wrapU: 1 wrapV: 1 wrapW: 0 nPOTScale: 0 lightmap: 0 compressionQuality: 50 spriteMode: 1 spriteExtrude: 1 spriteMeshType: 1 alignment: 0 spritePivot: {x: 0.5, y: 0.5} spritePixelsToUnits: 100 spriteBorder: {x: 0, y: 0, z: 0, w: 0} spriteGenerateFallbackPhysicsShape: 0 alphaUsage: 1 alphaIsTransparency: 1 spriteTessellationDetail: -1 textureType: 8 textureShape: 1 singleChannelComponent: 0 flipbookRows: 1 flipbookColumns: 1 maxTextureSizeSet: 0 compressionQualitySet: 0 textureFormatSet: 0 ignorePngGamma: 0 applyGammaDecoding: 0 cookieLightType: 1 platformSettings: - serializedVersion: 3 buildTarget: DefaultTexturePlatform maxTextureSize: 512 resizeAlgorithm: 0 textureFormat: -1 textureCompression: 1 compressionQuality: 50 crunchedCompression: 0 allowsAlphaSplitting: 0 overridden: 0 androidETC2FallbackOverride: 0 forceMaximumCompressionQuality_BC6H_BC7: 0 - serializedVersion: 3 buildTarget: Standalone maxTextureSize: 512 resizeAlgorithm: 0 textureFormat: -1 textureCompression: 1 compressionQuality: 50 crunchedCompression: 0 allowsAlphaSplitting: 0 overridden: 0 androidETC2FallbackOverride: 0 forceMaximumCompressionQuality_BC6H_BC7: 0 spriteSheet: serializedVersion: 2 sprites: [] outline: [] physicsShape: [] bones: [] spriteID: 5e97eb03825dee720800000000000000 internalID: 0 vertices: [] indices: edges: [] weights: [] secondaryTextures: [] nameFileIdTable: {} spritePackingTag: pSDRemoveMatte: 0 pSDShowRemoveMatteOption: 0 userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Resources/Icons/quantum.png.meta ================================================ fileFormatVersion: 2 guid: ab20f1c106c55d84b82bc7d5a640819b TextureImporter: internalIDToNameTable: [] externalObjects: {} serializedVersion: 12 mipmaps: mipMapMode: 0 enableMipMap: 0 sRGBTexture: 1 linearTexture: 0 fadeOut: 0 borderMipMap: 0 mipMapsPreserveCoverage: 0 alphaTestReferenceValue: 0.5 mipMapFadeDistanceStart: 1 mipMapFadeDistanceEnd: 3 bumpmap: convertToNormalMap: 0 externalNormalMap: 0 heightScale: 0.25 normalMapFilter: 0 isReadable: 0 streamingMipmaps: 0 streamingMipmapsPriority: 0 vTOnly: 0 ignoreMasterTextureLimit: 0 grayScaleToAlpha: 0 generateCubemap: 6 cubemapConvolution: 0 seamlessCubemap: 0 textureFormat: 1 maxTextureSize: 2048 textureSettings: serializedVersion: 2 filterMode: 1 aniso: 0 mipBias: 0 wrapU: 1 wrapV: 1 wrapW: 0 nPOTScale: 0 lightmap: 0 compressionQuality: 50 spriteMode: 1 spriteExtrude: 1 spriteMeshType: 1 alignment: 0 spritePivot: {x: 0.5, y: 0.5} spritePixelsToUnits: 100 spriteBorder: {x: 0, y: 0, z: 0, w: 0} spriteGenerateFallbackPhysicsShape: 0 alphaUsage: 1 alphaIsTransparency: 1 spriteTessellationDetail: -1 textureType: 8 textureShape: 1 singleChannelComponent: 0 flipbookRows: 1 flipbookColumns: 1 maxTextureSizeSet: 0 compressionQualitySet: 0 textureFormatSet: 0 ignorePngGamma: 0 applyGammaDecoding: 0 cookieLightType: 1 platformSettings: - serializedVersion: 3 buildTarget: DefaultTexturePlatform maxTextureSize: 512 resizeAlgorithm: 0 textureFormat: -1 textureCompression: 1 compressionQuality: 50 crunchedCompression: 0 allowsAlphaSplitting: 0 overridden: 0 androidETC2FallbackOverride: 0 forceMaximumCompressionQuality_BC6H_BC7: 0 - serializedVersion: 3 buildTarget: Standalone maxTextureSize: 512 resizeAlgorithm: 0 textureFormat: -1 textureCompression: 1 compressionQuality: 50 crunchedCompression: 0 allowsAlphaSplitting: 0 overridden: 0 androidETC2FallbackOverride: 0 forceMaximumCompressionQuality_BC6H_BC7: 0 spriteSheet: serializedVersion: 2 sprites: [] outline: [] physicsShape: [] bones: [] spriteID: 5e97eb03825dee720800000000000000 internalID: 0 vertices: [] indices: edges: [] weights: [] secondaryTextures: [] nameFileIdTable: {} spritePackingTag: pSDRemoveMatte: 0 pSDShowRemoveMatteOption: 0 userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Resources/Icons/renderer.png.meta ================================================ fileFormatVersion: 2 guid: 9d85d8a741327e143b28987b5a69ef93 TextureImporter: internalIDToNameTable: [] externalObjects: {} serializedVersion: 12 mipmaps: mipMapMode: 0 enableMipMap: 0 sRGBTexture: 1 linearTexture: 0 fadeOut: 0 borderMipMap: 0 mipMapsPreserveCoverage: 0 alphaTestReferenceValue: 0.5 mipMapFadeDistanceStart: 1 mipMapFadeDistanceEnd: 3 bumpmap: convertToNormalMap: 0 externalNormalMap: 0 heightScale: 0.25 normalMapFilter: 0 isReadable: 0 streamingMipmaps: 0 streamingMipmapsPriority: 0 vTOnly: 0 ignoreMasterTextureLimit: 0 grayScaleToAlpha: 0 generateCubemap: 6 cubemapConvolution: 0 seamlessCubemap: 0 textureFormat: 1 maxTextureSize: 2048 textureSettings: serializedVersion: 2 filterMode: 1 aniso: 0 mipBias: 0 wrapU: 1 wrapV: 1 wrapW: 0 nPOTScale: 0 lightmap: 0 compressionQuality: 50 spriteMode: 1 spriteExtrude: 1 spriteMeshType: 1 alignment: 0 spritePivot: {x: 0.5, y: 0.5} spritePixelsToUnits: 100 spriteBorder: {x: 0, y: 0, z: 0, w: 0} spriteGenerateFallbackPhysicsShape: 0 alphaUsage: 1 alphaIsTransparency: 1 spriteTessellationDetail: -1 textureType: 8 textureShape: 1 singleChannelComponent: 0 flipbookRows: 1 flipbookColumns: 1 maxTextureSizeSet: 0 compressionQualitySet: 0 textureFormatSet: 0 ignorePngGamma: 0 applyGammaDecoding: 0 cookieLightType: 1 platformSettings: - serializedVersion: 3 buildTarget: DefaultTexturePlatform maxTextureSize: 512 resizeAlgorithm: 0 textureFormat: -1 textureCompression: 1 compressionQuality: 50 crunchedCompression: 0 allowsAlphaSplitting: 0 overridden: 0 androidETC2FallbackOverride: 0 forceMaximumCompressionQuality_BC6H_BC7: 0 - serializedVersion: 3 buildTarget: Standalone maxTextureSize: 512 resizeAlgorithm: 0 textureFormat: -1 textureCompression: 1 compressionQuality: 50 crunchedCompression: 0 allowsAlphaSplitting: 0 overridden: 0 androidETC2FallbackOverride: 0 forceMaximumCompressionQuality_BC6H_BC7: 0 spriteSheet: serializedVersion: 2 sprites: [] outline: [] physicsShape: [] bones: [] spriteID: 5e97eb03825dee720800000000000000 internalID: 0 vertices: [] indices: edges: [] weights: [] secondaryTextures: [] nameFileIdTable: {} spritePackingTag: pSDRemoveMatte: 0 pSDShowRemoveMatteOption: 0 userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Resources/Icons/router.png.meta ================================================ fileFormatVersion: 2 guid: 3fa61ff64b7c4e64e8b944ba5d308365 TextureImporter: internalIDToNameTable: [] externalObjects: {} serializedVersion: 12 mipmaps: mipMapMode: 0 enableMipMap: 0 sRGBTexture: 1 linearTexture: 0 fadeOut: 0 borderMipMap: 0 mipMapsPreserveCoverage: 0 alphaTestReferenceValue: 0.5 mipMapFadeDistanceStart: 1 mipMapFadeDistanceEnd: 3 bumpmap: convertToNormalMap: 0 externalNormalMap: 0 heightScale: 0.25 normalMapFilter: 0 isReadable: 0 streamingMipmaps: 0 streamingMipmapsPriority: 0 vTOnly: 0 ignoreMasterTextureLimit: 0 grayScaleToAlpha: 0 generateCubemap: 6 cubemapConvolution: 0 seamlessCubemap: 0 textureFormat: 1 maxTextureSize: 2048 textureSettings: serializedVersion: 2 filterMode: 1 aniso: 0 mipBias: 0 wrapU: 1 wrapV: 1 wrapW: 0 nPOTScale: 0 lightmap: 0 compressionQuality: 50 spriteMode: 1 spriteExtrude: 1 spriteMeshType: 1 alignment: 0 spritePivot: {x: 0.5, y: 0.5} spritePixelsToUnits: 100 spriteBorder: {x: 0, y: 0, z: 0, w: 0} spriteGenerateFallbackPhysicsShape: 0 alphaUsage: 1 alphaIsTransparency: 1 spriteTessellationDetail: -1 textureType: 8 textureShape: 1 singleChannelComponent: 0 flipbookRows: 1 flipbookColumns: 1 maxTextureSizeSet: 0 compressionQualitySet: 0 textureFormatSet: 0 ignorePngGamma: 0 applyGammaDecoding: 0 cookieLightType: 1 platformSettings: - serializedVersion: 3 buildTarget: DefaultTexturePlatform maxTextureSize: 512 resizeAlgorithm: 0 textureFormat: -1 textureCompression: 1 compressionQuality: 50 crunchedCompression: 0 allowsAlphaSplitting: 0 overridden: 0 androidETC2FallbackOverride: 0 forceMaximumCompressionQuality_BC6H_BC7: 0 - serializedVersion: 3 buildTarget: Standalone maxTextureSize: 512 resizeAlgorithm: 0 textureFormat: -1 textureCompression: 1 compressionQuality: 50 crunchedCompression: 0 allowsAlphaSplitting: 0 overridden: 0 androidETC2FallbackOverride: 0 forceMaximumCompressionQuality_BC6H_BC7: 0 spriteSheet: serializedVersion: 2 sprites: [] outline: [] physicsShape: [] bones: [] spriteID: 5e97eb03825dee720800000000000000 internalID: 0 vertices: [] indices: edges: [] weights: [] secondaryTextures: [] nameFileIdTable: {} spritePackingTag: pSDRemoveMatte: 0 pSDShowRemoveMatteOption: 0 userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Resources/Icons/state.png.meta ================================================ fileFormatVersion: 2 guid: 724b44860269a9f4abed118244135936 TextureImporter: internalIDToNameTable: [] externalObjects: {} serializedVersion: 12 mipmaps: mipMapMode: 0 enableMipMap: 0 sRGBTexture: 1 linearTexture: 0 fadeOut: 0 borderMipMap: 0 mipMapsPreserveCoverage: 0 alphaTestReferenceValue: 0.5 mipMapFadeDistanceStart: 1 mipMapFadeDistanceEnd: 3 bumpmap: convertToNormalMap: 0 externalNormalMap: 0 heightScale: 0.25 normalMapFilter: 0 isReadable: 0 streamingMipmaps: 0 streamingMipmapsPriority: 0 vTOnly: 0 ignoreMasterTextureLimit: 0 grayScaleToAlpha: 0 generateCubemap: 6 cubemapConvolution: 0 seamlessCubemap: 0 textureFormat: 1 maxTextureSize: 2048 textureSettings: serializedVersion: 2 filterMode: 1 aniso: 0 mipBias: 0 wrapU: 1 wrapV: 1 wrapW: 0 nPOTScale: 0 lightmap: 0 compressionQuality: 50 spriteMode: 1 spriteExtrude: 1 spriteMeshType: 1 alignment: 0 spritePivot: {x: 0.5, y: 0.5} spritePixelsToUnits: 100 spriteBorder: {x: 0, y: 0, z: 0, w: 0} spriteGenerateFallbackPhysicsShape: 0 alphaUsage: 1 alphaIsTransparency: 1 spriteTessellationDetail: -1 textureType: 8 textureShape: 1 singleChannelComponent: 0 flipbookRows: 1 flipbookColumns: 1 maxTextureSizeSet: 0 compressionQualitySet: 0 textureFormatSet: 0 ignorePngGamma: 0 applyGammaDecoding: 0 cookieLightType: 1 platformSettings: - serializedVersion: 3 buildTarget: DefaultTexturePlatform maxTextureSize: 512 resizeAlgorithm: 0 textureFormat: -1 textureCompression: 1 compressionQuality: 50 crunchedCompression: 0 allowsAlphaSplitting: 0 overridden: 0 androidETC2FallbackOverride: 0 forceMaximumCompressionQuality_BC6H_BC7: 0 - serializedVersion: 3 buildTarget: Standalone maxTextureSize: 512 resizeAlgorithm: 0 textureFormat: -1 textureCompression: 1 compressionQuality: 50 crunchedCompression: 0 allowsAlphaSplitting: 0 overridden: 0 androidETC2FallbackOverride: 0 forceMaximumCompressionQuality_BC6H_BC7: 0 spriteSheet: serializedVersion: 2 sprites: [] outline: [] physicsShape: [] bones: [] spriteID: 5e97eb03825dee720800000000000000 internalID: 0 vertices: [] indices: edges: [] weights: [] secondaryTextures: [] nameFileIdTable: {} spritePackingTag: pSDRemoveMatte: 0 pSDShowRemoveMatteOption: 0 userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Resources/Icons/template.png.meta ================================================ fileFormatVersion: 2 guid: 9c6112e092c235c4388c5cc406349194 TextureImporter: internalIDToNameTable: [] externalObjects: {} serializedVersion: 12 mipmaps: mipMapMode: 0 enableMipMap: 0 sRGBTexture: 1 linearTexture: 0 fadeOut: 0 borderMipMap: 0 mipMapsPreserveCoverage: 0 alphaTestReferenceValue: 0.5 mipMapFadeDistanceStart: 1 mipMapFadeDistanceEnd: 3 bumpmap: convertToNormalMap: 0 externalNormalMap: 0 heightScale: 0.25 normalMapFilter: 0 isReadable: 0 streamingMipmaps: 0 streamingMipmapsPriority: 0 vTOnly: 0 ignoreMasterTextureLimit: 0 grayScaleToAlpha: 0 generateCubemap: 6 cubemapConvolution: 0 seamlessCubemap: 0 textureFormat: 1 maxTextureSize: 2048 textureSettings: serializedVersion: 2 filterMode: 1 aniso: 0 mipBias: 0 wrapU: 1 wrapV: 1 wrapW: 0 nPOTScale: 0 lightmap: 0 compressionQuality: 50 spriteMode: 1 spriteExtrude: 1 spriteMeshType: 1 alignment: 0 spritePivot: {x: 0.5, y: 0.5} spritePixelsToUnits: 100 spriteBorder: {x: 0, y: 0, z: 0, w: 0} spriteGenerateFallbackPhysicsShape: 0 alphaUsage: 1 alphaIsTransparency: 1 spriteTessellationDetail: -1 textureType: 8 textureShape: 1 singleChannelComponent: 0 flipbookRows: 1 flipbookColumns: 1 maxTextureSizeSet: 0 compressionQualitySet: 0 textureFormatSet: 0 ignorePngGamma: 0 applyGammaDecoding: 0 cookieLightType: 1 platformSettings: - serializedVersion: 3 buildTarget: DefaultTexturePlatform maxTextureSize: 512 resizeAlgorithm: 0 textureFormat: -1 textureCompression: 1 compressionQuality: 50 crunchedCompression: 0 allowsAlphaSplitting: 0 overridden: 0 androidETC2FallbackOverride: 0 forceMaximumCompressionQuality_BC6H_BC7: 0 - serializedVersion: 3 buildTarget: Standalone maxTextureSize: 512 resizeAlgorithm: 0 textureFormat: -1 textureCompression: 1 compressionQuality: 50 crunchedCompression: 0 allowsAlphaSplitting: 0 overridden: 0 androidETC2FallbackOverride: 0 forceMaximumCompressionQuality_BC6H_BC7: 0 spriteSheet: serializedVersion: 2 sprites: [] outline: [] physicsShape: [] bones: [] spriteID: 5e97eb03825dee720800000000000000 internalID: 0 vertices: [] indices: edges: [] weights: [] secondaryTextures: [] nameFileIdTable: {} spritePackingTag: pSDRemoveMatte: 0 pSDShowRemoveMatteOption: 0 userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Resources/Icons/theme.png.meta ================================================ fileFormatVersion: 2 guid: 73d27894540ca4a47ba97356fee56092 TextureImporter: internalIDToNameTable: [] externalObjects: {} serializedVersion: 12 mipmaps: mipMapMode: 0 enableMipMap: 0 sRGBTexture: 1 linearTexture: 0 fadeOut: 0 borderMipMap: 0 mipMapsPreserveCoverage: 0 alphaTestReferenceValue: 0.5 mipMapFadeDistanceStart: 1 mipMapFadeDistanceEnd: 3 bumpmap: convertToNormalMap: 0 externalNormalMap: 0 heightScale: 0.25 normalMapFilter: 0 isReadable: 0 streamingMipmaps: 0 streamingMipmapsPriority: 0 vTOnly: 0 ignoreMasterTextureLimit: 0 grayScaleToAlpha: 0 generateCubemap: 6 cubemapConvolution: 0 seamlessCubemap: 0 textureFormat: 1 maxTextureSize: 2048 textureSettings: serializedVersion: 2 filterMode: 1 aniso: 1 mipBias: 0 wrapU: 1 wrapV: 1 wrapW: 0 nPOTScale: 0 lightmap: 0 compressionQuality: 50 spriteMode: 1 spriteExtrude: 1 spriteMeshType: 1 alignment: 0 spritePivot: {x: 0.5, y: 0.5} spritePixelsToUnits: 100 spriteBorder: {x: 0, y: 0, z: 0, w: 0} spriteGenerateFallbackPhysicsShape: 0 alphaUsage: 1 alphaIsTransparency: 1 spriteTessellationDetail: -1 textureType: 8 textureShape: 1 singleChannelComponent: 0 flipbookRows: 1 flipbookColumns: 1 maxTextureSizeSet: 0 compressionQualitySet: 0 textureFormatSet: 0 ignorePngGamma: 0 applyGammaDecoding: 0 cookieLightType: 1 platformSettings: - serializedVersion: 3 buildTarget: DefaultTexturePlatform maxTextureSize: 2048 resizeAlgorithm: 0 textureFormat: -1 textureCompression: 1 compressionQuality: 50 crunchedCompression: 0 allowsAlphaSplitting: 0 overridden: 0 androidETC2FallbackOverride: 0 forceMaximumCompressionQuality_BC6H_BC7: 0 - serializedVersion: 3 buildTarget: Standalone maxTextureSize: 2048 resizeAlgorithm: 0 textureFormat: -1 textureCompression: 1 compressionQuality: 50 crunchedCompression: 0 allowsAlphaSplitting: 0 overridden: 0 androidETC2FallbackOverride: 0 forceMaximumCompressionQuality_BC6H_BC7: 0 spriteSheet: serializedVersion: 2 sprites: [] outline: [] physicsShape: [] bones: [] spriteID: 5e97eb03825dee720800000000000000 internalID: 0 vertices: [] indices: edges: [] weights: [] secondaryTextures: [] nameFileIdTable: {} spritePackingTag: pSDRemoveMatte: 0 pSDShowRemoveMatteOption: 0 userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Resources/Icons.meta ================================================ fileFormatVersion: 2 guid: 0df9eec597c9c3f4db1681e862dd42b9 folderAsset: yes DefaultImporter: externalObjects: {} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Resources/Logo/graphene-logo-full.png.meta ================================================ fileFormatVersion: 2 guid: 4ba3b61aa9eee63418d9a0c7be4901f8 TextureImporter: internalIDToNameTable: [] externalObjects: {} serializedVersion: 12 mipmaps: mipMapMode: 0 enableMipMap: 0 sRGBTexture: 1 linearTexture: 0 fadeOut: 0 borderMipMap: 0 mipMapsPreserveCoverage: 0 alphaTestReferenceValue: 0.5 mipMapFadeDistanceStart: 1 mipMapFadeDistanceEnd: 3 bumpmap: convertToNormalMap: 0 externalNormalMap: 0 heightScale: 0.25 normalMapFilter: 0 isReadable: 0 streamingMipmaps: 0 streamingMipmapsPriority: 0 vTOnly: 0 ignoreMasterTextureLimit: 0 grayScaleToAlpha: 0 generateCubemap: 6 cubemapConvolution: 0 seamlessCubemap: 0 textureFormat: 1 maxTextureSize: 2048 textureSettings: serializedVersion: 2 filterMode: 1 aniso: 1 mipBias: 0 wrapU: 1 wrapV: 1 wrapW: 0 nPOTScale: 0 lightmap: 0 compressionQuality: 50 spriteMode: 1 spriteExtrude: 1 spriteMeshType: 1 alignment: 0 spritePivot: {x: 0.5, y: 0.5} spritePixelsToUnits: 100 spriteBorder: {x: 0, y: 0, z: 0, w: 0} spriteGenerateFallbackPhysicsShape: 0 alphaUsage: 1 alphaIsTransparency: 1 spriteTessellationDetail: -1 textureType: 8 textureShape: 1 singleChannelComponent: 0 flipbookRows: 1 flipbookColumns: 1 maxTextureSizeSet: 0 compressionQualitySet: 0 textureFormatSet: 0 ignorePngGamma: 0 applyGammaDecoding: 0 cookieLightType: 1 platformSettings: - serializedVersion: 3 buildTarget: DefaultTexturePlatform maxTextureSize: 2048 resizeAlgorithm: 0 textureFormat: -1 textureCompression: 1 compressionQuality: 50 crunchedCompression: 0 allowsAlphaSplitting: 0 overridden: 0 androidETC2FallbackOverride: 0 forceMaximumCompressionQuality_BC6H_BC7: 0 - serializedVersion: 3 buildTarget: Standalone maxTextureSize: 2048 resizeAlgorithm: 0 textureFormat: -1 textureCompression: 1 compressionQuality: 50 crunchedCompression: 0 allowsAlphaSplitting: 0 overridden: 0 androidETC2FallbackOverride: 0 forceMaximumCompressionQuality_BC6H_BC7: 0 spriteSheet: serializedVersion: 2 sprites: [] outline: [] physicsShape: [] bones: [] spriteID: 5e97eb03825dee720800000000000000 internalID: 0 vertices: [] indices: edges: [] weights: [] secondaryTextures: [] nameFileIdTable: {} spritePackingTag: pSDRemoveMatte: 0 pSDShowRemoveMatteOption: 0 userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Resources/Logo/graphene-logo-white.png.meta ================================================ fileFormatVersion: 2 guid: 79495ad5c6a23a54e9a080b692eca7b1 TextureImporter: internalIDToNameTable: [] externalObjects: {} serializedVersion: 12 mipmaps: mipMapMode: 0 enableMipMap: 0 sRGBTexture: 1 linearTexture: 0 fadeOut: 0 borderMipMap: 0 mipMapsPreserveCoverage: 0 alphaTestReferenceValue: 0.5 mipMapFadeDistanceStart: 1 mipMapFadeDistanceEnd: 3 bumpmap: convertToNormalMap: 0 externalNormalMap: 0 heightScale: 0.25 normalMapFilter: 0 isReadable: 0 streamingMipmaps: 0 streamingMipmapsPriority: 0 vTOnly: 0 ignoreMasterTextureLimit: 0 grayScaleToAlpha: 0 generateCubemap: 6 cubemapConvolution: 0 seamlessCubemap: 0 textureFormat: 1 maxTextureSize: 2048 textureSettings: serializedVersion: 2 filterMode: 1 aniso: 1 mipBias: 0 wrapU: 1 wrapV: 1 wrapW: 0 nPOTScale: 0 lightmap: 0 compressionQuality: 50 spriteMode: 1 spriteExtrude: 1 spriteMeshType: 1 alignment: 0 spritePivot: {x: 0.5, y: 0.5} spritePixelsToUnits: 100 spriteBorder: {x: 0, y: 0, z: 0, w: 0} spriteGenerateFallbackPhysicsShape: 0 alphaUsage: 1 alphaIsTransparency: 1 spriteTessellationDetail: -1 textureType: 8 textureShape: 1 singleChannelComponent: 0 flipbookRows: 1 flipbookColumns: 1 maxTextureSizeSet: 0 compressionQualitySet: 0 textureFormatSet: 0 ignorePngGamma: 0 applyGammaDecoding: 0 cookieLightType: 1 platformSettings: - serializedVersion: 3 buildTarget: DefaultTexturePlatform maxTextureSize: 2048 resizeAlgorithm: 0 textureFormat: -1 textureCompression: 1 compressionQuality: 50 crunchedCompression: 0 allowsAlphaSplitting: 0 overridden: 0 androidETC2FallbackOverride: 0 forceMaximumCompressionQuality_BC6H_BC7: 0 - serializedVersion: 3 buildTarget: Standalone maxTextureSize: 2048 resizeAlgorithm: 0 textureFormat: -1 textureCompression: 1 compressionQuality: 50 crunchedCompression: 0 allowsAlphaSplitting: 0 overridden: 0 androidETC2FallbackOverride: 0 forceMaximumCompressionQuality_BC6H_BC7: 0 - serializedVersion: 3 buildTarget: WebGL maxTextureSize: 2048 resizeAlgorithm: 0 textureFormat: -1 textureCompression: 1 compressionQuality: 50 crunchedCompression: 0 allowsAlphaSplitting: 0 overridden: 0 androidETC2FallbackOverride: 0 forceMaximumCompressionQuality_BC6H_BC7: 0 spriteSheet: serializedVersion: 2 sprites: [] outline: [] physicsShape: [] bones: [] spriteID: 5e97eb03825dee720800000000000000 internalID: 0 vertices: [] indices: edges: [] weights: [] secondaryTextures: [] nameFileIdTable: {} spritePackingTag: pSDRemoveMatte: 0 pSDShowRemoveMatteOption: 0 userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Resources/Logo/graphene-logo.png.meta ================================================ fileFormatVersion: 2 guid: a7567e94e4334dd4bb5bed2d09495114 TextureImporter: internalIDToNameTable: [] externalObjects: {} serializedVersion: 12 mipmaps: mipMapMode: 0 enableMipMap: 0 sRGBTexture: 1 linearTexture: 0 fadeOut: 0 borderMipMap: 0 mipMapsPreserveCoverage: 0 alphaTestReferenceValue: 0.5 mipMapFadeDistanceStart: 1 mipMapFadeDistanceEnd: 3 bumpmap: convertToNormalMap: 0 externalNormalMap: 0 heightScale: 0.25 normalMapFilter: 0 isReadable: 0 streamingMipmaps: 0 streamingMipmapsPriority: 0 vTOnly: 0 ignoreMasterTextureLimit: 0 grayScaleToAlpha: 0 generateCubemap: 6 cubemapConvolution: 0 seamlessCubemap: 0 textureFormat: 1 maxTextureSize: 2048 textureSettings: serializedVersion: 2 filterMode: 1 aniso: 1 mipBias: 0 wrapU: 1 wrapV: 1 wrapW: 0 nPOTScale: 0 lightmap: 0 compressionQuality: 50 spriteMode: 1 spriteExtrude: 1 spriteMeshType: 1 alignment: 0 spritePivot: {x: 0.5, y: 0.5} spritePixelsToUnits: 100 spriteBorder: {x: 0, y: 0, z: 0, w: 0} spriteGenerateFallbackPhysicsShape: 0 alphaUsage: 1 alphaIsTransparency: 1 spriteTessellationDetail: -1 textureType: 8 textureShape: 1 singleChannelComponent: 0 flipbookRows: 1 flipbookColumns: 1 maxTextureSizeSet: 0 compressionQualitySet: 0 textureFormatSet: 0 ignorePngGamma: 0 applyGammaDecoding: 0 cookieLightType: 1 platformSettings: - serializedVersion: 3 buildTarget: DefaultTexturePlatform maxTextureSize: 512 resizeAlgorithm: 0 textureFormat: -1 textureCompression: 1 compressionQuality: 50 crunchedCompression: 0 allowsAlphaSplitting: 0 overridden: 0 androidETC2FallbackOverride: 0 forceMaximumCompressionQuality_BC6H_BC7: 0 - serializedVersion: 3 buildTarget: Standalone maxTextureSize: 512 resizeAlgorithm: 0 textureFormat: -1 textureCompression: 1 compressionQuality: 50 crunchedCompression: 0 allowsAlphaSplitting: 0 overridden: 0 androidETC2FallbackOverride: 0 forceMaximumCompressionQuality_BC6H_BC7: 0 spriteSheet: serializedVersion: 2 sprites: [] outline: [] physicsShape: [] bones: [] spriteID: 5e97eb03825dee720800000000000000 internalID: 0 vertices: [] indices: edges: [] weights: [] secondaryTextures: [] nameFileIdTable: {} spritePackingTag: pSDRemoveMatte: 0 pSDShowRemoveMatteOption: 0 userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Resources/Logo.meta ================================================ fileFormatVersion: 2 guid: 2a9a343a7c0ac0d4781c5c2075c9129c folderAsset: yes DefaultImporter: externalObjects: {} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/Resources.meta ================================================ fileFormatVersion: 2 guid: c57b36d4b8516cb49b133e196e2765ed folderAsset: yes DefaultImporter: externalObjects: {} userData: assetBundleName: assetBundleVariant: ================================================ FILE: src/package.json ================================================ { "name": "com.graphene.core", "version": "0.1.4", "displayName": "Graphene", "description": "A lightweight & flexible open-source UI framework for Unity's UI Toolkit.", "unity": "2018.3", "dependencies": { "com.kinstrife.core.reflection-helper": "file:/GameDev/Kinstrife/Packages/Core/ReflectionHelper", "com.externals.fast-member": "file:/GameDev/Externals/FastMember", "com.externals.odin": "file:/GameDev/Externals/Sirenix", "com.unity.editorcoroutines": "1.0.0" }, "keywords": [], "author": { "email": "info@kinstrife.com", "name": "CupBearer", "url": "https://www.cup-bearer.com" } } ================================================ FILE: src/package.json.meta ================================================ fileFormatVersion: 2 guid: a7886040c573d5c4183352bdef617071 PackageManifestImporter: externalObjects: {} userData: assetBundleName: assetBundleVariant: