Skip to main content

Inspector controls

Guide

Hide fields, register UI Toolkit controls and validation rules, and reuse GCS fields and dropdowns

Use Editor registries to hide an inherited field, replace a field's control, or report a project validation issue. Keep the fields and content types in the runtime assembly described by Inspector extensions.

Download the complete sample

Samples~/InspectorExtensions/ in GCS-Samples.zip contains Runtime and Editor examples. Extract the archive outside Unity and copy the directories you need into your project.

Reference the Editor assembliesโ€‹

Create an Editor-only Assembly Definition that references the project content assembly, GCS Editor, and Shared Editor:

{
"name": "YourGame.GCS.Content.Editor",
"references": [
"YourGame.GCS.Content",
"TinyGiants.GCS.Runtime",
"TinyGiants.GCS.Editor",
"TinyGiants.Shared.Editor"
],
"includePlatforms": [
"Editor"
]
}

Hide inherited fieldsโ€‹

Register field names with GCSInspectorVisibilityRegistry after each domain reload. This changes which fields Workbench displays. The serialized data remains available to project code.

[InitializeOnLoad]
public static class SampleInspectorRegistration
{
static SampleInspectorRegistration()
{
GCSInspectorVisibilityRegistry.Hide<SamplePlayer>(
nameof(SamplePlayer.MaxHp),
nameof(SamplePlayer.BaseEnergy),
nameof(SamplePlayer.EnergyDisplay));
}
}

Open a SamplePlayer asset in Workbench. Max Hp, Base Energy, and Energy Display are hidden, and a foldout disappears if all of its fields are hidden.

Base-type registrations also apply to derived types, remove them when disabling or replacing the Editor integration

GCSInspectorVisibilityRegistry.Unhide<SamplePlayer>(
nameof(SamplePlayer.MaxHp),
nameof(SamplePlayer.BaseEnergy),
nameof(SamplePlayer.EnergyDisplay));

Register one field controlโ€‹

GCSInspectorControlRegistry matches a content type and serialized field name. Register during every Domain Reload with [InitializeOnLoad] or [InitializeOnLoadMethod].

using System.Collections.Generic;
using TinyGiants.GCS.Editor;
using UnityEditor;
using UnityEngine.UIElements;

[InitializeOnLoad]
public static class SampleInspectorRegistration
{
static SampleInspectorRegistration()
{
GCSInspectorVisibilityRegistry.Hide<SamplePlayer>(
nameof(SamplePlayer.MaxHp),
nameof(SamplePlayer.BaseEnergy),
nameof(SamplePlayer.EnergyDisplay));

GCSInspectorControlRegistry.Register<SampleCard>(
nameof(SampleCard.SpellLoadout),
CreateSpellLoadoutControl);

GCSInspectorValidationRegistry.Register<SampleCard>(
"sample.card.progression",
ValidateProgression);
}

private static VisualElement CreateSpellLoadoutControl(
GCSInspectorFieldContext context)
{
context.UseFullWidth = true;
return new SampleSpellLoadoutField(context);
}

private static IEnumerable<GCSInspectorValidationResult> ValidateProgression(
SampleCard card)
{
if (card.RequiredLevel < 1)
{
yield return new GCSInspectorValidationResult(
GCSInspectorValidationSeverity.Error,
"Required Level must be at least 1.",
fieldLabel: "Required Level",
category: "Sample Progression",
propertyPath: nameof(SampleCard.RequiredLevel));
}
}
}

SampleSpellLoadoutField draws SampleCard.SpellLoadout, and the validator reports an error when RequiredLevel is below 1. Both registrations also apply to classes derived from SampleCard. Registering the same type and field again replaces its factory. If both base and derived types register that field, Workbench uses the factory for the closest matching type.

Use the serialized field contextโ€‹

Read and edit fields through GCSInspectorFieldContext to use Workbench serialization, Undo, and refresh handling in custom controls

MemberUse
TargetCurrent concrete ScriptableObject
SerializedObjectSerialized wrapper for the current target
PropertyPathExact path of the field being replaced
FieldReflected field metadata
LabelFinal displayed label after GCSLabel, InspectorName, or nicification
GetProperty()Updates the serialized object and resolves the current property
Modify(undoLabel, mutation)Applies a serialized change, records Undo, marks the target dirty, and refreshes Workbench
RegisterRefresh(refresh)Refreshes the control after Undo/Redo or an external serialized change
UseFullWidthMakes the returned element occupy the complete section width
tip

Call GetProperty() each time you read the field, because array edits, Undo, and Inspector rebuilds can invalidate a cached SerializedProperty. In RegisterRefresh, use SetValueWithoutNotify to refresh the displayed value without calling the edit callback again. Set UseFullWidth if a composite control needs the full row.

private static VisualElement BuildTimelineControl(
GCSInspectorFieldContext context)
{
context.UseFullWidth = true;
var timeline = new VisualElement();
timeline.AddToClassList("sample-timeline");
return timeline;
}

Return a UI Toolkit VisualElement from the factory. If you use IMGUI, wrap the control in IMGUIContainer and handle focus and serialized changes. If the factory returns null or throws, Workbench uses the control specified by the field attribute or the standard serialized control. Check the Console for exceptions.

Reuse GCS field controls directlyโ€‹

Control factories can also create these built-in GCS controls

ControlPurpose
GCSSelectFieldSelection-only string field
GCSEditableSelectFieldEditable string plus option dropdown
GCSTagFieldComma-separated values plus multiple-selection dropdown
GCSChipMultiSelectFieldOrdered removable chips plus multiple-selection dropdown
GCSEnumDropdown<T>Enum picker using GCS dropdown presentation
GCSBuffDebuffPickerTwo-segment Boolean control with custom labels
GCSDescriptionFieldGCS description editor with token and semantic-reference support
GCSSubAssetFieldCompact or card-style selector for registered GCS content
GCSSubAssetListFieldSerialized list editor for GCS content references
GCSFlowGraphFieldFlowGraph button and static entry or enemy-intent summary
GCSDeckEntriesFieldGameDeck card count picker and summary
tip

Use Runtime attributes when they select the required control, register a factory for compound controls, conditional interaction, or project commands

Add project validationโ€‹

Register validators through GCSInspectorValidationRegistry to run them alongside built-in checks, each registration uses a content type and ID, repeated registration replaces that entry, base-type rules also check derived objects

GCSInspectorValidationRegistry.Register<SampleEncounter>(
"sample.encounter.rewards",
encounter => ValidateRewards(encounter));

A validator returns zero or more GCSInspectorValidationResult values:

ValueResult in Workbench
WarningAdds a warning to the current database Issue Badge
ErrorAdds an error to the current database Issue Badge
CategoryDisplays the project-defined issue category
MessageAppears after the content object's display name
PropertyPathExact SerializedProperty.propertyPath used first to jump to and flash one Inspector row
FieldLabelDisplayed-label fallback when PropertyPath is empty or cannot be resolved

Use (severity, message, fieldLabel, category, propertyPath) to report an issue on one field, pass nameof(SampleCard.RequiredLevel) for a top-level field, Stats.BaseDamage for a nested field, or a full Unity path containing Array.data[0] for arrays and List<T>

Workbench resolves PropertyPath first and uses FieldLabel when the path is empty or unresolved, the label comes from [GCSLabel], [InspectorName], or Unity's formatted field name and may differ from the C# name

The four-parameter constructor (severity, message, fieldLabel, category) locates fields through fieldLabel, for an asset-wide issue leave fieldLabel and any applicable propertyPath empty

tip

Return issues from validation and keep the asset unchanged. Workbench saves the asset and runs the remaining checks even if a validator throws. Check the Console for validator exceptions.

Enforce runtime constraints in Runtime code, when overriding OnEnable or OnValidate, call the base implementation to retain GCS identity and base data handling

Build entries for TGDropdownโ€‹

TGDropdown is the Editor popup used by GCS selection controls, entries provide an identifier, label, page path, and callback data

using System.Collections.Generic;
using TinyGiants.Shared.Editor;

private static List<TGDropdownEntry> BuildAbilityEntries()
{
return new List<TGDropdownEntry>
{
new TGDropdownEntry
{
Key = "fireball",
Label = "Fireball",
GroupPath = new List<string> { "Combat", "Magic", "Fire" },
Payload = "fireball",
Tooltip = "Deals fire damage",
Enabled = true
},
new TGDropdownEntry
{
Key = "guard-break",
Label = "Guard Break",
GroupPath = new List<string> { "Combat", "Physical" },
Payload = "guard-break",
Tooltip = "Reduces armor"
}
};
}
Entry memberMeaning
KeyUnique stable row identity for multiple-selection state
LabelVisible row text
GroupPathExplicit page path with any number of segments
PayloadObject returned to callbacks; equal payloads share one Multiple-mode selection state
TooltipOptional row tooltip
EnabledWhether the item can be selected

Each GroupPath element represents one page level, a / within an element is a normal character, Group still supports legacy paths such as Combat/Magic/Fire, a null GroupPath uses Group, and an empty list selects the root page

Search matches Label and the full path, GroupOrder prioritizes child pages at each level while other pages are sorted by name

tip

In Multiple mode, assign a unique, stable Key to each row. The popup replaces missing or duplicate keys with internal identifiers valid only for that instance. Rows with equal Payload values share their selection state, so selecting or clearing one updates the others and invokes the callback once.

Open a single-selection dropdownโ€‹

Single mode passes the selected entry's Payload to OnPicked and closes the popup, with ShowNoneOption enabled, choosing None passes null

using System;
using TinyGiants.GCS.Editor;
using TinyGiants.Shared.Editor;
using UnityEngine.UIElements;

private static void OpenSingle(
Button anchor,
Action<string> commit)
{
TGDropdown.Open(new TGDropdownConfig
{
Title = "Ability",
AnchorRect = GCSVisualKit.ScreenRectOf(anchor),
Entries = BuildAbilityEntries(),
SelectionMode = TGDropdownSelectionMode.Single,
ShowNoneOption = true,
NoneLabel = "No Ability",
OnPicked = payload => commit(payload as string),
GroupOrder = new[] { "Combat", "Utility" },
MinWidth = 260f
});
}

Call context.Modify from OnPicked to save Inspector field changes

Open a multiple-selection dropdownโ€‹

Multiple mode reads initial state through IsSelected and reports each toggle through OnSelectionChanged, the example keeps changes in selection order and commits them when the popup closes

using System;
using System.Collections.Generic;
using TinyGiants.GCS.Editor;
using TinyGiants.Shared.Editor;
using UnityEngine.UIElements;

private static void OpenMultiple(
Button anchor,
IEnumerable<string> initial,
Action<List<string>> commit)
{
var selected = new List<string>();
if (initial != null)
{
foreach (string value in initial)
{
if (!string.IsNullOrEmpty(value) && !selected.Contains(value))
selected.Add(value);
}
}

TGDropdown.Open(new TGDropdownConfig
{
Title = "Granted Abilities",
AnchorRect = GCSVisualKit.ScreenRectOf(anchor),
Entries = BuildAbilityEntries(),
SelectionMode = TGDropdownSelectionMode.Multiple,
ShowNoneOption = false,
IsSelected = payload =>
payload is string id && selected.Contains(id),
OnSelectionChanged = (payload, isSelected) =>
{
if (!(payload is string id)) return;
if (isSelected && !selected.Contains(id)) selected.Add(id);
if (!isSelected) selected.Remove(id);
},
OnClose = () => commit(new List<string>(selected)),
SelectedItemsFirst = true,
ClearSearchOnSelection = true,
MaxHeight = 360f
});
}

For immediate saves, call context.Modify from OnSelectionChanged, for one Undo step, keep local changes and call context.Modify once from OnClose

Open a count dropdownโ€‹

Count mode displays a non-negative count per entry, left-click requests +1 and right-click requests -1

using System;
using System.Collections.Generic;
using TinyGiants.GCS.Editor;
using TinyGiants.Shared.Editor;
using UnityEngine;
using UnityEngine.UIElements;

private static void OpenCounts(
Button anchor,
IDictionary<string, int> initial,
Action<Dictionary<string, int>> commit)
{
var counts = initial != null
? new Dictionary<string, int>(initial, StringComparer.Ordinal)
: new Dictionary<string, int>(StringComparer.Ordinal);

TGDropdown.Open(new TGDropdownConfig
{
Title = "Ability Copies",
AnchorRect = GCSVisualKit.ScreenRectOf(anchor),
Entries = BuildAbilityEntries(),
SelectionMode = TGDropdownSelectionMode.Count,
ShowNoneOption = false,
GetCount = payload =>
{
string id = payload as string;
return id != null && counts.TryGetValue(id, out int count)
? count
: 0;
},
OnCountChanged = (payload, count) =>
{
if (!(payload is string id)) return 0;
if (count == 0) counts.Remove(id);
else counts[id] = count;
return count;
},
OnClose = () =>
commit(new Dictionary<string, int>(counts, StringComparer.Ordinal)),
CountHint = "Left click +1 ยท Right click โˆ’1",
MaxHeight = 520f
});
}

Count mode keeps the popup open after changes, OnCountChanged receives the requested non-negative count and returns the displayed count, clamp the maximum and save the result in this callback

Configure popup behavior consistentlyโ€‹

Use TGDropdownConfig to set titles, sizes, and close callbacks for all three modes

SettingEffect
Title, AnchorRect, EntriesDefines the popup heading, screen anchor, and data
SelectionModeChooses Single, Multiple, or Count interaction
ShowNoneOption, NoneLabelConfigures the optional Single-mode null row
GroupOrderApplies preferred child-page order at every path level
Width, MinWidth, MaxHeightOverrides or constrains popup dimensions
SelectedItemsFirstMoves selected Multiple-mode entries before unselected entries on each page
ClearSearchOnSelectionClears search after a Multiple-mode toggle
CountHintReplaces the Count-mode instruction text
OnCloseRuns once for every popup close path
tip

Write the chosen values through context.Modify to save them, and use RegisterRefresh to update the control after Undo or Redo.

Respect the extension boundaryโ€‹

  • Register controls and validation from an Editor assembly after every Domain Reload.

  • Keep serializable fields, option providers, and typed runtime access in a runtime assembly.

  • Use declarative attributes before replacing a field with a registry factory.

  • Use context.Modify for every persisted custom-control change and RegisterRefresh for Undo/Redo.

  • Keep validators read-only.

  • Use TGDropdown in Editor assemblies.