using System; using Microsoft.Xna.Framework; using MrGameEng.Graphics; using Myra.Graphics2D; using Myra.Graphics2D.Brushes; using Myra.Graphics2D.UI; namespace MrGameEng.Inspector; /// /// Myra overlay of the ECS inspector — a Chrome-DevTools-style side panel. The top bar switches the /// 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. /// internal sealed class EcsInspectorUi { private const int PanelWidth = 540; private static readonly Color Accent = new(120, 200, 255); private readonly EcsInspector _inspector; private readonly Renderer2D _renderer; private readonly Desktop _desktop; private readonly VerticalStackPanel _archetypeList = Stack(); private readonly VerticalStackPanel _entityList = Stack(); private readonly VerticalStackPanel _detailList = Stack(); private readonly Label _stats = new(); private readonly Label _perf = new() { Wrap = false }; private readonly Label _pickLabel = new() { Text = "Pick" }; private readonly Widget _entitiesSection; private readonly Panel _highlight; private readonly VerticalStackPanel _panel; private int _lastListRevision = -1; private int _lastDetailRevision = -1; public EcsInspectorUi(EcsInspector inspector, Renderer2D renderer) { _inspector = inspector; _renderer = renderer; var tabEntities = MakeButton("Entities", () => _inspector.SetTab(InspectorTab.Entities)); var tabPerf = MakeButton("Performance", () => _inspector.SetTab(InspectorTab.Performance)); var pick = new Button { Content = _pickLabel }; pick.Click += (_, _) => _inspector.ArmPick(!_inspector.PickArmed); var search = new TextBox { HintText = "filter components", Width = 160 }; search.TextChanged += (_, _) => _inspector.SetSearch(search.Text ?? ""); var bar = new HorizontalStackPanel { Spacing = 6 }; bar.Widgets.Add(tabEntities); bar.Widgets.Add(tabPerf); bar.Widgets.Add(pick); bar.Widgets.Add(search); _entitiesSection = BuildEntitiesSection(); _panel = new VerticalStackPanel { Spacing = 6, Padding = new Thickness(8), Width = PanelWidth, HorizontalAlignment = HorizontalAlignment.Right, VerticalAlignment = VerticalAlignment.Stretch, Background = new SolidBrush(new Color(8, 10, 14, 235)), }; _panel.Widgets.Add(bar); _panel.Widgets.Add(_stats); _panel.Widgets.Add(new HorizontalSeparator()); _panel.Widgets.Add(_entitiesSection); _panel.Widgets.Add(_perf); _highlight = new Panel { Border = new SolidBrush(Accent), BorderThickness = new Thickness(2), Visible = false, }; var root = new Panel(); root.Widgets.Add(_highlight); root.Widgets.Add(_panel); _desktop = new Desktop { Root = root }; } public void Render() { if (!_inspector.IsOpen) { return; } // 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) { UpdatePerf(perf); } if (_inspector.ListRevision != _lastListRevision) { _lastListRevision = _inspector.ListRevision; RebuildLists(); } if (_inspector.DetailRevision != _lastDetailRevision) { _lastDetailRevision = _inspector.DetailRevision; RebuildDetails(); } UpdateHighlight(); _desktop.Render(); } /// True when the physical screen point is over the panel (so a pick click is ignored). public bool IsOverPanel(Point screen) => _panel.Bounds.Contains(screen); private Widget BuildEntitiesSection() { var section = new VerticalStackPanel { Spacing = 4 }; section.Widgets.Add(Header("Archetypes")); section.Widgets.Add(new ScrollViewer { Content = _archetypeList, Height = 150 }); section.Widgets.Add(Header("Entities")); section.Widgets.Add(new ScrollViewer { Content = _entityList, Height = 150 }); section.Widgets.Add(Header("Selected")); section.Widgets.Add(new ScrollViewer { Content = _detailList, Height = 320 }); return section; } private void RebuildLists() { var entitiesTab = _inspector.ActiveTab == InspectorTab.Entities; _entitiesSection.Visible = entitiesTab; _perf.Visible = !entitiesTab; _pickLabel.Text = _inspector.PickArmed ? "Pick*" : "Pick"; if (entitiesTab) { RebuildArchetypes(); RebuildEntities(); } } 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(); foreach (var row in _inspector.Archetypes()) { var index = row.Index; var button = MakeButton( $"[{row.Count}] {row.Signature}", () => _inspector.SelectArchetype(index) ); if (index == _inspector.SelectedArchetype) { Highlight(button); } _archetypeList.Widgets.Add(button); } } private void RebuildEntities() { _entityList.Widgets.Clear(); if (_inspector.SelectedArchetype < 0) { return; } foreach (var id in _inspector.EntitiesOf(_inspector.SelectedArchetype)) { var entityId = id; var button = MakeButton($"entity {id}", () => _inspector.SelectEntity(entityId)); if (entityId == _inspector.SelectedEntityId) { Highlight(button); } _entityList.Widgets.Add(button); } } private void RebuildDetails() { _detailList.Widgets.Clear(); if (_inspector.SelectedEntityId < 0) { return; } foreach (var component in _inspector.Inspect(_inspector.SelectedEntityId)) { _detailList.Widgets.Add(new Label { Text = component.TypeName, TextColor = Accent }); foreach (var field in component.Fields) { _detailList.Widgets.Add(BuildFieldRow(component.Type, field)); } } } private Widget BuildFieldRow(Type componentType, FieldRow field) { 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, TextColor = new Color(150, 156, 168) }); return row; } 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) => { if (args.Data == Microsoft.Xna.Framework.Input.Keys.Enter) { _inspector.SetField(entityId, componentType, field.Name, editor.Text ?? ""); } }; row.Widgets.Add(editor); if (field.Kind == FieldKind.Enum && field.EnumOptions is { Length: > 0 }) { row.Widgets.Add( new Label { Text = string.Join(" / ", field.EnumOptions), TextColor = new Color(120, 128, 140), } ); } return row; } private void UpdateHighlight() { if (!_inspector.TryGetSelectedBounds(out var center, out var radius)) { _highlight.Visible = false; return; } var screenCenter = _renderer.WorldToScreen(center); var edge = _renderer.WorldToScreen(center + new Vector2(radius, 0f)); var screenRadius = MathF.Max(4f, MathF.Abs(edge.X - screenCenter.X)); _highlight.Visible = true; _highlight.Left = (int)(screenCenter.X - screenRadius); _highlight.Top = (int)(screenCenter.Y - screenRadius); _highlight.Width = (int)(screenRadius * 2f); _highlight.Height = (int)(screenRadius * 2f); } private static void Highlight(Button button) { if (button.Content is Label label) { label.TextColor = Accent; } } private static VerticalStackPanel Stack() => new() { Spacing = 2 }; private static Label Header(string text) => new() { Text = text, TextColor = new Color(150, 160, 175) }; private static Button MakeButton(string text, Action onClick) { var button = new Button { Content = new Label { Text = text } }; button.Click += (_, _) => onClick(); return button; } }