Editor/Inspectors/AttackCasterInspector.cs
using System;
using System.Linq;
using Editor;
using Sandbox;
using Sandbox.UI;
using Application = Editor.Application;
using Button = Editor.Button;
using ControlSheet = Editor.ControlSheet;
using Label = Editor.Label;

namespace MANIFOLD.BHLib.Editor {
    [CustomEditor(typeof(AttackCaster))]
    public class AttackCasterInspector : ComponentEditorWidget {
        public class AttackProgress : Widget {
            public const float BAR_MARGIN = 2;
            public const float BAR_SIZE = 30;

            private readonly AttackCaster caster;
            private readonly AttackCaster.Instance instance;
            private readonly Label label;

            public AttackProgress(AttackCaster caster, AttackCaster.Instance instance, Widget parent = null) : base(parent) {
                this.caster = caster;
                this.instance = instance;
                Layout = Layout.Column();
                Layout.Margin = new Margin(8, 0);
                
                FixedHeight = 40;

                var row = Layout.AddRow();
                label = row.Add(new Label(instance.Attack.ResourceName ?? "Embedded Resource"));
                row.AddStretchCell();
                row.Add(new Button("Stop") { Clicked = () => {
                    caster.StopAttack(instance);
                }});
                
                Layout.AddStretchCell();

                VerticalSizeMode = SizeMode.CanGrow;
                HorizontalSizeMode = SizeMode.Default;
            }

            [EditorEvent.Frame]
            private void OnFrame() {
                Update();
            }
            
            protected override void OnPaint() {
                base.OnPaint();
                
                var rect = LocalRect;
                rect.Shrink(Layout.Margin);
                rect.Top = label.Height + BAR_MARGIN;
                
                Paint.SetBrushAndPen(Theme.ControlBackground);
                Paint.DrawRect(rect);
                
                rect.Width *= instance.NormalizedTime;
                
                Paint.SetBrushAndPen(Theme.Green);
                Paint.DrawRect(rect);
                
                Paint.ClearBrush();
                Paint.SetPen(Theme.TextDark);
                Paint.DrawText(rect.Shrink(1), $"{(instance.NormalizedTime * 100).FloorToInt()}%", TextFlag.Right);
            }
        }
        
        private ControlSheet sheet;
        private Widget runtimeCanvas;
        private Layout attackListLayout;
        private Layout instanceListLayout;
        
        private int lastInstanceCount;
        
        public AttackCaster Target => SerializedObject.Targets.FirstOrDefault() as AttackCaster;
        
        public AttackCasterInspector(SerializedObject obj) : base(obj) {
            Layout = Layout.Column();
            Layout.Margin = 0;
            Layout.Spacing = 4;

            sheet = new ControlSheet();
            Layout.Add(sheet);
            RebuildSheet();
            
            runtimeCanvas = Layout.Add(new Widget());
            runtimeCanvas.Layout = Layout.Column();
            runtimeCanvas.Layout.Margin = new Margin(8, 0);

            runtimeCanvas.Layout.AddSeparator();
            
            // ATTACK LIST
            runtimeCanvas.Layout.Add(new Label.Header("Attacks"));
            attackListLayout = runtimeCanvas.Layout.AddColumn();
            // RebuildAttackList();
            
            // INSTANCE LIST
            runtimeCanvas.Layout.Add(new Label.Header("Instances"));
            instanceListLayout = runtimeCanvas.Layout.AddColumn();
            // RebuildInstanceList();
            
            if (Target.Scene.IsEditor) OnSceneStop();
            else OnScenePlay();
        }
        
        [Event("scene.play")]
        private void OnScenePlay() {
            runtimeCanvas.Visible = true;
            lastInstanceCount = 0;
            RebuildAttackList();
        }

        [Event("scene.stop")]
        private void OnSceneStop() {
            runtimeCanvas.Visible = false;
        }

        [EditorEvent.Frame]
        private void OnFrame() {
            if (Target == null) return;
            if (Target.Scene.IsEditor) return;
            if (Target.Instances.Count != lastInstanceCount) {
                RebuildInstanceList();
                lastInstanceCount = Target.Instances.Count;
            }
        }

        private void RebuildSheet() {
            sheet.Clear(true);
            sheet.AddObject(SerializedObject, PropertyFilter);
        }
        
        private void RebuildAttackList() {
            attackListLayout.Clear(true);
            attackListLayout.Margin = new Margin(8, 0);
            attackListLayout.Spacing = 4;

            for (int i = 0; i < Target.Attacks.Count; i++) {
                var attack = Target.Attacks[i];
                var row = attackListLayout.AddRow();
                row.Add(new Label(attack.ResourceName ?? "Embedded Resource"));
                row.AddStretchCell();
                row.Add(new Button("Cast") {
                    Clicked = () => {
                        Target.PlayAttack(attack);
                    }
                });
            }
        }

        private void RebuildInstanceList() {
            instanceListLayout.Clear(true);
            instanceListLayout.Margin = new Margin(8, 0);
            instanceListLayout.Spacing = 4;

            for (int i = 0; i < Target.Instances.Count; i++) {
                var inst = Target.Instances[i];
                instanceListLayout.Add(new AttackProgress(Target, inst));
            }
        }

        private bool PropertyFilter(SerializedProperty o) {
            var hideInEventTab = o.PropertyType.IsAssignableTo( typeof( Delegate ) ) && o.Name.StartsWith( "OnComponent" );
            if (hideInEventTab) return false;
            if (!Target.Scene.IsEditor && o.Name == nameof(AttackCaster.Attacks)) return false;
            if (!o.HasAttribute<PropertyAttribute>()) return false;
            return true;
        }
    }
}