Inspector: keep field editing usable, clarify editable fields
CI / build-test (push) Successful in 1m7s

Split the inspector's revision into a list revision and a detail
revision. The periodic refresh now only bumps the list revision, and only
when the entity count actually changes, so the field-editor TextBox in the
details pane is no longer recreated (losing focus) twice a second while you
type. Stats and the performance tab update as plain text every frame
without rebuilding widgets.

Editable fields now render with a background, border and padding so they
read as inputs; read-only values and enum hints are muted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Leonid Pershin
2026-06-12 13:05:42 +03:00
co-authored by Claude Opus 4.8
parent 09dbdfad79
commit 44019a706e
2 changed files with 108 additions and 45 deletions
+68 -30
View File
@@ -12,7 +12,7 @@ namespace MrGameEng.Inspector;
/// Entities/Performance tabs and arms world-pick; the Entities tab shows archetypes → entities →
/// the selected entity's components and editable fields; the Performance tab shows renderer
/// timings. A thin border tracks the selected entity in the world each frame. Rebuilds lazily on
/// <see cref="EcsInspector.Revision"/> changes.
/// <see cref="EcsInspector.ListRevision"/>/<see cref="EcsInspector.DetailRevision"/> changes.
/// </summary>
internal sealed class EcsInspectorUi
{
@@ -33,7 +33,8 @@ internal sealed class EcsInspectorUi
private readonly Panel _highlight;
private readonly VerticalStackPanel _panel;
private int _lastRevision = -1;
private int _lastListRevision = -1;
private int _lastDetailRevision = -1;
public EcsInspectorUi(EcsInspector inspector, Renderer2D renderer)
{
@@ -91,10 +92,24 @@ internal sealed class EcsInspectorUi
return;
}
if (_inspector.Revision != _lastRevision)
// Stats and perf are cheap text updates — refresh every frame so they stay live.
var perf = _inspector.Performance();
UpdateStats(perf);
if (_inspector.ActiveTab == InspectorTab.Performance)
{
_lastRevision = _inspector.Revision;
Rebuild();
UpdatePerf(perf);
}
if (_inspector.ListRevision != _lastListRevision)
{
_lastListRevision = _inspector.ListRevision;
RebuildLists();
}
if (_inspector.DetailRevision != _lastDetailRevision)
{
_lastDetailRevision = _inspector.DetailRevision;
RebuildDetails();
}
UpdateHighlight();
@@ -116,39 +131,41 @@ internal sealed class EcsInspectorUi
return section;
}
private void Rebuild()
private void RebuildLists()
{
var entitiesTab = _inspector.ActiveTab == InspectorTab.Entities;
_entitiesSection.Visible = entitiesTab;
_perf.Visible = !entitiesTab;
_pickLabel.Text = _inspector.PickArmed ? "Pick*" : "Pick";
var perf = _inspector.Performance();
_stats.Text =
$"entities {perf.Entities} | archetypes {perf.Archetypes} | {perf.Fps} FPS"
+ (_inspector.PickArmed ? " | click an entity…" : "");
if (entitiesTab)
{
RebuildArchetypes();
RebuildEntities();
RebuildDetails();
}
else
{
_perf.Text =
$"FPS {perf.Fps}\n"
+ $"submit {perf.SubmitMs:0.00} ms\n"
+ $"sort {perf.SortMs:0.00} ms\n"
+ $"build {perf.BuildMs:0.00} ms\n"
+ $"upload {perf.UploadMs:0.00} ms\n"
+ $"draw {perf.DrawMs:0.00} ms\n"
+ $"draw calls {perf.DrawCalls}\n"
+ $"sprites {perf.SubmittedSprites} drawn, {perf.CulledSprites} culled\n"
+ $"entities {perf.Entities}";
}
}
private void UpdateStats(in PerfSnapshot perf)
{
_stats.Text =
$"entities {perf.Entities} | archetypes {perf.Archetypes} | {perf.Fps} FPS"
+ (_inspector.PickArmed ? " | click an entity…" : "");
}
private void UpdatePerf(in PerfSnapshot perf)
{
_perf.Text =
$"FPS {perf.Fps}\n"
+ $"submit {perf.SubmitMs:0.00} ms\n"
+ $"sort {perf.SortMs:0.00} ms\n"
+ $"build {perf.BuildMs:0.00} ms\n"
+ $"upload {perf.UploadMs:0.00} ms\n"
+ $"draw {perf.DrawMs:0.00} ms\n"
+ $"draw calls {perf.DrawCalls}\n"
+ $"sprites {perf.SubmittedSprites} drawn, {perf.CulledSprites} culled\n"
+ $"entities {perf.Entities}";
}
private void RebuildArchetypes()
{
_archetypeList.Widgets.Clear();
@@ -209,16 +226,31 @@ internal sealed class EcsInspectorUi
private Widget BuildFieldRow(Type componentType, FieldRow field)
{
var row = new HorizontalStackPanel { Spacing = 6 };
row.Widgets.Add(new Label { Text = field.Name, Width = 150 });
var row = new HorizontalStackPanel { Spacing = 8 };
row.Widgets.Add(
new Label
{
Text = field.Name,
Width = 140,
TextColor = new Color(170, 178, 190),
}
);
if (!field.Editable)
{
row.Widgets.Add(new Label { Text = field.Value });
row.Widgets.Add(new Label { Text = field.Value, TextColor = new Color(150, 156, 168) });
return row;
}
var editor = new TextBox { Text = field.Value, Width = 220 };
var editor = new TextBox
{
Text = field.Value,
Width = 150,
Padding = new Thickness(4, 2),
Background = new SolidBrush(new Color(28, 33, 42)),
Border = new SolidBrush(new Color(70, 82, 100)),
BorderThickness = new Thickness(1),
};
var entityId = _inspector.SelectedEntityId;
editor.KeyDown += (_, args) =>
{
@@ -230,7 +262,13 @@ internal sealed class EcsInspectorUi
row.Widgets.Add(editor);
if (field.Kind == FieldKind.Enum && field.EnumOptions is { Length: > 0 })
{
row.Widgets.Add(new Label { Text = "(" + string.Join("/", field.EnumOptions) + ")" });
row.Widgets.Add(
new Label
{
Text = string.Join(" / ", field.EnumOptions),
TextColor = new Color(120, 128, 140),
}
);
}
return row;