Editor/Inspectors/EntityDataInspector.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Editor;
using Sandbox;
using Application = Editor.Application;

namespace MANIFOLD.BHLib.Editor {
    [Inspector(typeof(EntityData))]
    public class EntityDataInspector : InspectorWidget {
        public class ComponentView : Widget {
            private readonly ComponentDefinition component;

            private InspectorHeader header;
            private ControlSheet controlSheet;

            public Action OnPropertyChanged;
            public Action<ComponentDefinition> OnDeleteRequested;
            
            public ComponentView(ComponentDefinition component, Widget parent = null) : base(parent) {
                this.component = component;
                
                Layout = Layout.Column();
                header = Layout.Add(new InspectorHeader());
                
                var type = EditorTypeLibrary.GetType(component.GetType());
                header.Title = type.Title;
                header.Icon = type.Icon;
                header.Color = Theme.Primary;
                header.IsExpanded = true;
                header.ContextMenu = BuildMenu;
                header.BuildUI();
                
                controlSheet = new ControlSheet();
                Layout.Add(controlSheet);
                
                RebuildSheet();
            }

            [EditorEvent.Hotload]
            private void RebuildSheet() {
                controlSheet.Clear(true);
                
                var so = component.GetSerialized();
                controlSheet.AddObject(so);
                so.OnPropertyChanged = PropertyChange;
            }

            private void BuildMenu(Menu menu) {
                menu.AddOption("Remove", "remove", RequestDelete);
            }

            private void PropertyChange(SerializedProperty prop) {
                OnPropertyChanged?.Invoke();
            }
            
            private void RequestDelete() {
                OnDeleteRequested?.Invoke(component);
            }
        }
        
        private EntityData target;

        public EntityDataInspector(SerializedObject so) : base(so) {
            Layout = Layout.Column();
            Layout.Margin = 8;

            if (so.Targets.FirstOrDefault() is not EntityData resource) return;
            target = resource;
            
            CreateUI();
        }

        private void CreateUI() {
            Layout.Clear(true);
            
            Layout.Add(new Label($"Component count: {target.Components.Count}"));
            Layout.AddSpacingCell(20);
            
            foreach (var component in target.Components) {
                var view = Layout.Add(new ComponentView(component, this));
                view.OnDeleteRequested = RemoveComponent;
                view.OnPropertyChanged = ComponentPropertyChanged;
            }
            
            Layout.AddSpacingCell(20);
            var finalRow = Layout.AddRow();
            finalRow.AddStretchCell();
            finalRow.Add(new Button.Primary("Add Component", "add") { Clicked = NewComponentPopup });
            finalRow.AddStretchCell();
        }

        private void NewComponentPopup() {
            Menu menu = new Menu();
            menu.IsPopup = true;

            foreach (var type in GetAllComponentTypes()) {
                Menu localMenu;
                if (!string.IsNullOrEmpty(type.Group)) localMenu = menu.FindOrCreateMenu(type.Group);
                else localMenu = menu;
                
                var option = localMenu.AddOption(type.Title, type.Icon);
                option.ToolTip = type.Description;
                option.Triggered = () => {
                    AddNewComponent(type);
                };
            }
            
            menu.Position = Application.CursorPosition;
            menu.Show();
        }

        private void AddNewComponent(TypeDescription type) {
            var component = type.Create<ComponentDefinition>();
            target.Components.Add(component);
            target.StateHasChanged();

            CreateUI();
        }

        private void RemoveComponent(ComponentDefinition component) {
            target.Components.Remove(component);
            target.StateHasChanged();
            CreateUI();
        }

        private void ComponentPropertyChanged() {
            target.StateHasChanged();
        }
        
        private IEnumerable<TypeDescription> GetAllComponentTypes() {
            return EditorTypeLibrary.GetTypes<ComponentDefinition>().Where(x => !x.IsAbstract);
        }
    }
}