Inspector: keep field editing usable, clarify editable fields
CI / build-test (push) Successful in 1m7s
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:
co-authored by
Claude Opus 4.8
parent
09dbdfad79
commit
44019a706e
@@ -42,7 +42,7 @@ public readonly record struct PerfSnapshot(
|
||||
/// store's entities grouped by archetype, reflects a selected entity's components and fields,
|
||||
/// writes simple field edits back, and (when a renderer is supplied) picks entities under the
|
||||
/// cursor and reports frame metrics. Holds no Myra state, so its logic is unit-testable without a
|
||||
/// GPU. UI lazily rebuilds when <see cref="Revision"/> changes.
|
||||
/// GPU. UI lazily rebuilds when <see cref="ListRevision"/> or <see cref="DetailRevision"/> change.
|
||||
/// </summary>
|
||||
public sealed class EcsInspector
|
||||
{
|
||||
@@ -70,8 +70,14 @@ public sealed class EcsInspector
|
||||
/// <summary>True while the inspector panel is open.</summary>
|
||||
public bool IsOpen { get; private set; }
|
||||
|
||||
/// <summary>Increments on every change the UI should rebuild for (open/select/edit/refresh).</summary>
|
||||
public int Revision { get; private set; }
|
||||
/// <summary>Increments when the entity/archetype lists or tab state need a UI rebuild.</summary>
|
||||
public int ListRevision { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Increments when the selected entity's field view needs a UI rebuild. Kept separate from
|
||||
/// <see cref="ListRevision"/> so the periodic refresh never recreates a field editor mid-edit.
|
||||
/// </summary>
|
||||
public int DetailRevision { get; private set; }
|
||||
|
||||
/// <summary>The active tab.</summary>
|
||||
public InspectorTab ActiveTab { get; private set; }
|
||||
@@ -93,21 +99,22 @@ public sealed class EcsInspector
|
||||
{
|
||||
IsOpen = !IsOpen;
|
||||
_dirty = true;
|
||||
Bump();
|
||||
BumpList();
|
||||
BumpDetail();
|
||||
}
|
||||
|
||||
/// <summary>Switches the active tab.</summary>
|
||||
public void SetTab(InspectorTab tab)
|
||||
{
|
||||
ActiveTab = tab;
|
||||
Bump();
|
||||
BumpList();
|
||||
}
|
||||
|
||||
/// <summary>Arms (or disarms) world-pick mode.</summary>
|
||||
public void ArmPick(bool armed = true)
|
||||
{
|
||||
PickArmed = armed;
|
||||
Bump();
|
||||
BumpList();
|
||||
}
|
||||
|
||||
/// <summary>Sets the archetype filter and refreshes the list.</summary>
|
||||
@@ -115,7 +122,7 @@ public sealed class EcsInspector
|
||||
{
|
||||
Search = search ?? "";
|
||||
_dirty = true;
|
||||
Bump();
|
||||
BumpList();
|
||||
}
|
||||
|
||||
/// <summary>Selects an archetype group by index; clears the entity selection.</summary>
|
||||
@@ -123,14 +130,16 @@ public sealed class EcsInspector
|
||||
{
|
||||
SelectedArchetype = index;
|
||||
SelectedEntityId = -1;
|
||||
Bump();
|
||||
BumpList();
|
||||
BumpDetail();
|
||||
}
|
||||
|
||||
/// <summary>Selects an entity by runtime id.</summary>
|
||||
public void SelectEntity(int entityId)
|
||||
{
|
||||
SelectedEntityId = entityId;
|
||||
Bump();
|
||||
BumpList();
|
||||
BumpDetail();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -145,11 +154,24 @@ public sealed class EcsInspector
|
||||
}
|
||||
|
||||
_refreshTimer += deltaSeconds;
|
||||
if (_refreshTimer >= 0.5f)
|
||||
if (_refreshTimer < 0.5f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_refreshTimer = 0f;
|
||||
// Only rebuild the lists when entities were actually added or removed — never on a plain
|
||||
// tick, so a field editor in the details pane is not destroyed while it has focus.
|
||||
var count = 0;
|
||||
foreach (var _ in _store.Entities)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
|
||||
if (count != _entityCount)
|
||||
{
|
||||
_refreshTimer = 0f;
|
||||
_dirty = true;
|
||||
Bump();
|
||||
BumpList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,7 +253,7 @@ public sealed class EcsInspector
|
||||
|
||||
// Adding an existing component type replaces it with the edited value.
|
||||
AddComponentMethod.MakeGenericMethod(componentType).Invoke(entity, [boxed]);
|
||||
Bump();
|
||||
BumpDetail();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -281,7 +303,8 @@ public sealed class EcsInspector
|
||||
{
|
||||
SelectedEntityId = best;
|
||||
PickArmed = false;
|
||||
Bump();
|
||||
BumpList();
|
||||
BumpDetail();
|
||||
}
|
||||
|
||||
return best;
|
||||
@@ -345,7 +368,9 @@ public sealed class EcsInspector
|
||||
private static object BoxedValue(EntityComponent component) => component.Value;
|
||||
#pragma warning restore CS0618
|
||||
|
||||
private void Bump() => Revision++;
|
||||
private void BumpList() => ListRevision++;
|
||||
|
||||
private void BumpDetail() => DetailRevision++;
|
||||
|
||||
private void RebuildIfNeeded()
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user