Compare commits

..
1 Commits
Author SHA1 Message Date
Leonid PershinandClaude Opus 4.8 44019a706e 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>
2026-06-12 13:05:42 +03:00
2 changed files with 108 additions and 45 deletions
+39 -14
View File
@@ -42,7 +42,7 @@ public readonly record struct PerfSnapshot(
/// store's entities grouped by archetype, reflects a selected entity's components and fields, /// 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 /// 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 /// 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> /// </summary>
public sealed class EcsInspector public sealed class EcsInspector
{ {
@@ -70,8 +70,14 @@ public sealed class EcsInspector
/// <summary>True while the inspector panel is open.</summary> /// <summary>True while the inspector panel is open.</summary>
public bool IsOpen { get; private set; } public bool IsOpen { get; private set; }
/// <summary>Increments on every change the UI should rebuild for (open/select/edit/refresh).</summary> /// <summary>Increments when the entity/archetype lists or tab state need a UI rebuild.</summary>
public int Revision { get; private set; } 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> /// <summary>The active tab.</summary>
public InspectorTab ActiveTab { get; private set; } public InspectorTab ActiveTab { get; private set; }
@@ -93,21 +99,22 @@ public sealed class EcsInspector
{ {
IsOpen = !IsOpen; IsOpen = !IsOpen;
_dirty = true; _dirty = true;
Bump(); BumpList();
BumpDetail();
} }
/// <summary>Switches the active tab.</summary> /// <summary>Switches the active tab.</summary>
public void SetTab(InspectorTab tab) public void SetTab(InspectorTab tab)
{ {
ActiveTab = tab; ActiveTab = tab;
Bump(); BumpList();
} }
/// <summary>Arms (or disarms) world-pick mode.</summary> /// <summary>Arms (or disarms) world-pick mode.</summary>
public void ArmPick(bool armed = true) public void ArmPick(bool armed = true)
{ {
PickArmed = armed; PickArmed = armed;
Bump(); BumpList();
} }
/// <summary>Sets the archetype filter and refreshes the list.</summary> /// <summary>Sets the archetype filter and refreshes the list.</summary>
@@ -115,7 +122,7 @@ public sealed class EcsInspector
{ {
Search = search ?? ""; Search = search ?? "";
_dirty = true; _dirty = true;
Bump(); BumpList();
} }
/// <summary>Selects an archetype group by index; clears the entity selection.</summary> /// <summary>Selects an archetype group by index; clears the entity selection.</summary>
@@ -123,14 +130,16 @@ public sealed class EcsInspector
{ {
SelectedArchetype = index; SelectedArchetype = index;
SelectedEntityId = -1; SelectedEntityId = -1;
Bump(); BumpList();
BumpDetail();
} }
/// <summary>Selects an entity by runtime id.</summary> /// <summary>Selects an entity by runtime id.</summary>
public void SelectEntity(int entityId) public void SelectEntity(int entityId)
{ {
SelectedEntityId = entityId; SelectedEntityId = entityId;
Bump(); BumpList();
BumpDetail();
} }
/// <summary> /// <summary>
@@ -145,11 +154,24 @@ public sealed class EcsInspector
} }
_refreshTimer += deltaSeconds; _refreshTimer += deltaSeconds;
if (_refreshTimer >= 0.5f) if (_refreshTimer < 0.5f)
{ {
return;
}
_refreshTimer = 0f; _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)
{
_dirty = true; _dirty = true;
Bump(); BumpList();
} }
} }
@@ -231,7 +253,7 @@ public sealed class EcsInspector
// Adding an existing component type replaces it with the edited value. // Adding an existing component type replaces it with the edited value.
AddComponentMethod.MakeGenericMethod(componentType).Invoke(entity, [boxed]); AddComponentMethod.MakeGenericMethod(componentType).Invoke(entity, [boxed]);
Bump(); BumpDetail();
return true; return true;
} }
@@ -281,7 +303,8 @@ public sealed class EcsInspector
{ {
SelectedEntityId = best; SelectedEntityId = best;
PickArmed = false; PickArmed = false;
Bump(); BumpList();
BumpDetail();
} }
return best; return best;
@@ -345,7 +368,9 @@ public sealed class EcsInspector
private static object BoxedValue(EntityComponent component) => component.Value; private static object BoxedValue(EntityComponent component) => component.Value;
#pragma warning restore CS0618 #pragma warning restore CS0618
private void Bump() => Revision++; private void BumpList() => ListRevision++;
private void BumpDetail() => DetailRevision++;
private void RebuildIfNeeded() private void RebuildIfNeeded()
{ {
+57 -19
View File
@@ -12,7 +12,7 @@ namespace MrGameEng.Inspector;
/// Entities/Performance tabs and arms world-pick; the Entities tab shows archetypes → entities → /// 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 /// 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 /// 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> /// </summary>
internal sealed class EcsInspectorUi internal sealed class EcsInspectorUi
{ {
@@ -33,7 +33,8 @@ internal sealed class EcsInspectorUi
private readonly Panel _highlight; private readonly Panel _highlight;
private readonly VerticalStackPanel _panel; private readonly VerticalStackPanel _panel;
private int _lastRevision = -1; private int _lastListRevision = -1;
private int _lastDetailRevision = -1;
public EcsInspectorUi(EcsInspector inspector, Renderer2D renderer) public EcsInspectorUi(EcsInspector inspector, Renderer2D renderer)
{ {
@@ -91,10 +92,24 @@ internal sealed class EcsInspectorUi
return; 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; UpdatePerf(perf);
Rebuild(); }
if (_inspector.ListRevision != _lastListRevision)
{
_lastListRevision = _inspector.ListRevision;
RebuildLists();
}
if (_inspector.DetailRevision != _lastDetailRevision)
{
_lastDetailRevision = _inspector.DetailRevision;
RebuildDetails();
} }
UpdateHighlight(); UpdateHighlight();
@@ -116,25 +131,28 @@ internal sealed class EcsInspectorUi
return section; return section;
} }
private void Rebuild() private void RebuildLists()
{ {
var entitiesTab = _inspector.ActiveTab == InspectorTab.Entities; var entitiesTab = _inspector.ActiveTab == InspectorTab.Entities;
_entitiesSection.Visible = entitiesTab; _entitiesSection.Visible = entitiesTab;
_perf.Visible = !entitiesTab; _perf.Visible = !entitiesTab;
_pickLabel.Text = _inspector.PickArmed ? "Pick*" : "Pick"; _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) if (entitiesTab)
{ {
RebuildArchetypes(); RebuildArchetypes();
RebuildEntities(); RebuildEntities();
RebuildDetails();
} }
else }
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 = _perf.Text =
$"FPS {perf.Fps}\n" $"FPS {perf.Fps}\n"
@@ -147,7 +165,6 @@ internal sealed class EcsInspectorUi
+ $"sprites {perf.SubmittedSprites} drawn, {perf.CulledSprites} culled\n" + $"sprites {perf.SubmittedSprites} drawn, {perf.CulledSprites} culled\n"
+ $"entities {perf.Entities}"; + $"entities {perf.Entities}";
} }
}
private void RebuildArchetypes() private void RebuildArchetypes()
{ {
@@ -209,16 +226,31 @@ internal sealed class EcsInspectorUi
private Widget BuildFieldRow(Type componentType, FieldRow field) private Widget BuildFieldRow(Type componentType, FieldRow field)
{ {
var row = new HorizontalStackPanel { Spacing = 6 }; var row = new HorizontalStackPanel { Spacing = 8 };
row.Widgets.Add(new Label { Text = field.Name, Width = 150 }); row.Widgets.Add(
new Label
{
Text = field.Name,
Width = 140,
TextColor = new Color(170, 178, 190),
}
);
if (!field.Editable) 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; 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; var entityId = _inspector.SelectedEntityId;
editor.KeyDown += (_, args) => editor.KeyDown += (_, args) =>
{ {
@@ -230,7 +262,13 @@ internal sealed class EcsInspectorUi
row.Widgets.Add(editor); row.Widgets.Add(editor);
if (field.Kind == FieldKind.Enum && field.EnumOptions is { Length: > 0 }) 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; return row;