| name | debug-widget |
| description | DebugContainer debug widget implementation and updates. Use when adding a new debug widget to the debug panel, creating or modifying debug categories, wiring ElementBinding or DebugWidgetVisibilityBinding, using IDebugContainerBuilder, calling TryAddWidget or AddCustomMarker, displaying live runtime data in the debug panel, or adding colored status indicators to the profiling/debug UI. Also applies when passing IDebugContainerBuilder through StaticContainer to a plugin or system. |
| user-invocable | false |
Debug Container Widget Implementation
Sources
docs/debug-container-and-widgets.md — Full architecture reference (file structure, lifecycle, all element types, builder API)
Read the doc above for detailed type inventories and UXML structure. This skill focuses on the implementation workflow and patterns you need to get a widget working.
Quick Reference: Key Files
| Purpose | Path |
|---|
| Categories + interface | Assets/DCL/PerformanceAndDiagnostics/DebugUtilities/Builders/IDebugContainerBuilder.cs |
| Fluent builder API | Assets/DCL/PerformanceAndDiagnostics/DebugUtilities/Builders/DebugWidgetBuilder.cs |
| Convenience shortcuts | Assets/DCL/PerformanceAndDiagnostics/DebugUtilities/Builders/BuilderExtensions.cs |
| ElementBinding<T> | Assets/DCL/PerformanceAndDiagnostics/DebugUtilities/UIBindings/ElementBinding.cs |
| Visibility binding | Assets/DCL/PerformanceAndDiagnostics/DebugUtilities/UIBindings/DebugWidgetVisibilityBinding.cs |
| Null builder (no-op) | Assets/DCL/PerformanceAndDiagnostics/DebugUtilities/Builders/NullDebugContainerBuilder.cs |
| StaticContainer (DI root) | Assets/DCL/Infrastructure/Global/StaticContainer.cs |
Implementation Checklist
Step 1: Add a Category (if needed)
Check if an existing category in IDebugContainerBuilder.Categories fits. If not, add one to the Categories static class inside IDebugContainerBuilder:
public static readonly WidgetName MY_WIDGET = "My Widget".AsWidgetName();
The type must be WidgetName, not string or const string. Every existing category uses this exact pattern — public static readonly WidgetName. The AsWidgetName() extension method (defined in the same file) wraps the string in the type-safe WidgetName struct. Using const string or plain string will not match the GetOrAddWidget(WidgetName) overload and breaks the convention used by all other categories in the class.
Step 2: Choose an Integration Pattern
There are three patterns. Pick the one that matches your situation:
Pattern A — System creates widget directly.
The system receives IDebugContainerBuilder as a constructor parameter (source-generated into InjectToWorld). Widget creation happens in the constructor. Best when the system owns both the data and the display.
Pattern B — Plugin creates bindings, system consumes.
The plugin creates ElementBinding<T> + DebugWidgetVisibilityBinding in InitializeAsync, builds the widget, then passes the bindings to the system via InjectToWorld. Best when the plugin already manages the lifecycle and the system just writes values.
Pattern C — Non-system helper class.
A standalone class builds and updates the widget. Best for complex multi-widget scenarios (e.g. multiple room widgets).
Step 3: Create Bindings and Build the Widget
Regardless of pattern, the widget construction looks the same:
private readonly ElementBinding<string> myBinding = new (string.Empty);
private readonly DebugWidgetVisibilityBinding myVisibility = new (true);
debugBuilder.TryAddWidget(IDebugContainerBuilder.Categories.MY_WIDGET)
?.SetVisibilityBinding(myVisibility)
.AddCustomMarker("Label:", myBinding);
TryAddWidget returns null when debug is disabled (NullDebugContainerBuilder), so the ?. chain is essential — never remove it.
Available Builder Methods
Read BuilderExtensions.cs for the full list. The most common:
| Method | Use Case |
|---|
AddCustomMarker(string label, ElementBinding<string>) | Live text with optional rich text color tags |
AddMarker(string label, ElementBinding<ulong>, Unit) | Formatted numeric (time ns, bytes, bits) |
AddToggleField(string label, EventCallback, bool) | Toggle a runtime setting |
AddSingleButton(string text, Action) | Trigger an action |
AddFloatSliderField(string, ElementBinding<float>, min, max) | Tune a float parameter |
AddIntSliderField(string, ElementBinding<int>, min, max) | Tune an int parameter |
AddControlWithLabel(string, IDebugElementDef?, DebugHintDef?) | Custom left-label + right-element row |
AddControl(new DebugLineChartDef(...), null) | Full-width sparkline chart (auto-scaled, LineChartBuffer ring binding, current value formatted via DebugLongMarkerDef.Unit) |
For the complete element definition types, check the Declarations/ folder — one Debug*Def.cs per element kind. Line charts in particular: see DebugLineChartDef / DebugLineChartElement and use ElementBinding<LineChartBuffer> with a reused float[] ring (re-pushing the same backing array each frame is allocation-free).
Step 4: Wire IDebugContainerBuilder Through DI
IDebugContainerBuilder is a global resource on StaticContainer. It does not flow through InjectToWorld's shared dependencies. You must pass it explicitly:
-
Add it to your plugin's constructor:
public MyPlugin(..., IDebugContainerBuilder debugBuilder)
{
this.debugBuilder = debugBuilder;
}
-
Pass it in StaticContainer where the plugin is instantiated (search for your plugin name in StaticContainer.cs, usually in the ECSWorldPlugins array):
new MyPlugin(..., container.DebugContainerBuilder),
If using Pattern A (system creates widget), add IDebugContainerBuilder as a constructor parameter on the system. The source generator will include it in the generated InjectToWorld method. Then pass it from the plugin's InjectToWorld:
MyDebugSystem.InjectToWorld(ref builder, debugBuilder);
Step 5: Update Bindings at Runtime
In your system's Update(), write values to bindings behind a visibility guard:
protected override void Update(float t)
{
if (visibilityBinding.IsExpanded)
{
string color = currentValue >= threshold ? "red" : "green";
myBinding.Value = $"<color={color}>{currentValue} / {threshold}</color>";
}
}
The visibility guard skips string formatting when the widget is collapsed or the debug panel is closed. This matters because Update() runs every frame across multiple worlds.
Step 6: Required Usings
using DCL.DebugUtilities;
using DCL.DebugUtilities.UIBindings;
Both namespaces live in the DebugUtilities assembly (GUID:4725c02394ab4ce19f889e4e8001f989). The DCL.Plugins assembly already references it, so any code compiled into that assembly (including .asmref files pointing to DCL.Plugins) can use these without asmdef changes. If your system lives in a different assembly, add the GUID to that assembly's references.
Performance Rules
- Always guard with
visibilityBinding.IsExpanded before updating bindings. String interpolation and $"<color=...>" formatting allocate — skip them when nobody is looking.
- Use
IsExpanded (not IsConnectedAndExpanded) unless the widget creation was conditional and the binding might not be connected.
- No LINQ in Update — standard ECS performance constraint applies here too.
- Binding.Value set is lazy — it marks a dirty flag and the UI picks it up on the next UIElements binding pass. No need to call
.SetAndUpdate() from a system's Update() — that's for one-shot updates outside the frame loop.
Colored Status Indicators
The established pattern for budget/threshold indicators uses Unity rich text:
string color = value >= limit ? "red" : "green";
binding.Value = $"<color={color}>{value} / {limit}</color>";
string color = value >= high ? "green" : value >= low ? "yellow" : "red";
This works because AddCustomMarker renders ElementBinding<string> through a label that supports rich text tags.
Panel Layout States
The container has three runtime states; widgets must render correctly in all three. The state is driven entirely by USS classes on #Panel — never hardcode pixel dimensions in widget UXML/USS.
| State | USS class on #Panel | Trigger | What changes |
|---|
| Minimized (default) | (none) | Initial state, or close→reopen with maximized pref off | width: 300px, max-height: 500px |
| Maximized | debug-panel--maximized | User clicks the square header button (between title and ✖) | width: 450px baseline, max-height: 90%, top margin clears the connection panel; non-header text +30%; chart plot height +50%; DebugControl columns shift to 65/35 with label wrapping enabled; left-edge resize handle becomes visible |
| Wide | debug-panel--maximized debug-panel--wide | Drag width past 2 × baselineMaximizedWidth | #Parent switches to flex-direction: row; flex-wrap: wrap; with each DebugWidget at width: 50%. Resize is clamped to Screen.width * 0.7. |
Persisted state lives in DCLPrefKeys.DEBUG_PANEL_MAXIMIZED (bool) and DEBUG_PANEL_MAXIMIZED_WIDTH (float). The baseline width is read at runtime from mainPanel.resolvedStyle.width after the maximized class is applied — don't duplicate that 450 px constant in C#.
Implications when adding a widget:
- Use percent or
flex layouts, not fixed width: NNNpx. Existing elements all stretch via align-self: stretch or percent flex-basis — match that.
- Don't hardcode font sizes on text labels you create — inherit from
.unity-text-element (12 px → 16 px when maximized via the .debug-panel--maximized .unity-text-element rule).
- If your widget contains a category foldout, use class
debug-text--header so it stays readable at 12 px in maximized mode (the rule re-pins headers).
- A widget should look reasonable at 300, 450, 600, and 900+ px panel widths.
Reference Implementations
When in doubt, read these existing implementations:
| Pattern | File | What it shows |
|---|
| A (system-direct) | PerformanceAndDiagnostics/Profiling/ECS/DebugViewProfilingSystem.cs | Multiple widgets, visibility guards, colored markers, sliders, toggles |
| A (system-direct) | Rendering/GPUInstancing/Systems/DebugGPUInstancingSystem.cs | Toggle + slider, settings sync, cleanup on dispose |
| B (plugin-creates) | PluginSystem/World/ParticleSystemPlugin.cs + SDKComponents/ParticleSystem/Systems/ParticleSystemBudgetSystem.cs | Plugin creates bindings in InitializeAsync, system writes them |
| C (helper class) | Multiplayer/Connections/Systems/Debug/DebugWidgetRoomDisplay.cs | Non-system class, decorator pattern, multiple widget instances |
| Charts + appended widget | PerformanceAndDiagnostics/Profiling/ECS/DebugViewCurrentSceneSystem.cs | DebugLineChartDef/LineChartBuffer usage, appending rows to an existing category, gating Update on widgetEnabled (skip all work when TryAddWidget returned null), reactive subscription to IScenesCache.CurrentScene |