diff --git a/src/MrGameEng.UI/EcsInspector.cs b/src/MrGameEng.UI/EcsInspector.cs index 68931d0..9b13ebc 100644 --- a/src/MrGameEng.UI/EcsInspector.cs +++ b/src/MrGameEng.UI/EcsInspector.cs @@ -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 changes. +/// GPU. UI lazily rebuilds when or change. /// public sealed class EcsInspector { @@ -70,8 +70,14 @@ public sealed class EcsInspector /// True while the inspector panel is open. public bool IsOpen { get; private set; } - /// Increments on every change the UI should rebuild for (open/select/edit/refresh). - public int Revision { get; private set; } + /// Increments when the entity/archetype lists or tab state need a UI rebuild. + public int ListRevision { get; private set; } + + /// + /// Increments when the selected entity's field view needs a UI rebuild. Kept separate from + /// so the periodic refresh never recreates a field editor mid-edit. + /// + public int DetailRevision { get; private set; } /// The active tab. public InspectorTab ActiveTab { get; private set; } @@ -93,21 +99,22 @@ public sealed class EcsInspector { IsOpen = !IsOpen; _dirty = true; - Bump(); + BumpList(); + BumpDetail(); } /// Switches the active tab. public void SetTab(InspectorTab tab) { ActiveTab = tab; - Bump(); + BumpList(); } /// Arms (or disarms) world-pick mode. public void ArmPick(bool armed = true) { PickArmed = armed; - Bump(); + BumpList(); } /// Sets the archetype filter and refreshes the list. @@ -115,7 +122,7 @@ public sealed class EcsInspector { Search = search ?? ""; _dirty = true; - Bump(); + BumpList(); } /// Selects an archetype group by index; clears the entity selection. @@ -123,14 +130,16 @@ public sealed class EcsInspector { SelectedArchetype = index; SelectedEntityId = -1; - Bump(); + BumpList(); + BumpDetail(); } /// Selects an entity by runtime id. public void SelectEntity(int entityId) { SelectedEntityId = entityId; - Bump(); + BumpList(); + BumpDetail(); } /// @@ -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() { diff --git a/src/MrGameEng.UI/EcsInspectorUi.cs b/src/MrGameEng.UI/EcsInspectorUi.cs index 0b39769..df424bb 100644 --- a/src/MrGameEng.UI/EcsInspectorUi.cs +++ b/src/MrGameEng.UI/EcsInspectorUi.cs @@ -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 -/// changes. +/// / changes. /// 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;