Editor/ControlWidgets/FoldableControlWidget.cs
using Editor;
using ExtendedEditor.Attributes;
using ExtendedEditor.TypeLibraryFixes;
using Sandbox;
using Sandbox.UI;
using System;
using System.Collections;
using System.Linq;

namespace ExtendedEditor.ControlWidgets;

[CustomEditor(typeof(object), WithAllAttributes = [typeof(FoldableEditorAttribute)])]
public class FoldableControlWidget : ControlWidget
{
    private Widget _body = null!;
    private Header _header = null!;

    private bool IsFolded => _body?.Hidden ?? true;


    public FoldableControlWidget(SerializedProperty property) : base(property.Fix())
    {
        Layout = Layout.Column();
        Layout.Margin = 0;
        Layout.Spacing = 0;

        RebuildLayout();
    }

    private void RebuildLayout()
    {
        Layout.Clear(true);

        if(SerializedProperty.TryGetAsObject(out var serializedObject) && serializedObject is SerializedCollection)
        {
            this.Skip();
            return;
        }

        var startFolded = true;
        if(SerializedProperty.TryGetAttribute<FoldableEditorAttribute>(out var foldableEditorAttribute))
            startFolded = foldableEditorAttribute.StartFolded;

        var attributes = SerializedProperty.GetAttributes()
            .Where(x => x is not FoldableEditorAttribute)
            .Append(new InlineEditorAttribute() { Label = false });

        if(SerializedProperty.Parent is SerializedCollection)
            attributes = attributes.Where(x => x is not InspectorVisibilityAttribute);

        var property = SerializedProperty.CreateProxy(attributes);

        _body = new Widget
        {
            Hidden = true,
            VerticalSizeMode = SizeMode.CanGrow,
            HorizontalSizeMode = SizeMode.Flexible,
            ReadOnly = ReadOnly,
            Layout = Layout.Column()
        };
        _body.Layout.Margin = new Margin(12, 4, 0, 4);
        _body.Layout.Spacing = 0;

        var bodyControlWidget = ControlWidget.Create(property);
        bodyControlWidget.PaintBackground = false;
        _body.Layout.Add(bodyControlWidget);

        _header = new();
        _header.OnToggled += opened => _body.Hidden = !opened;

        Layout.Add(_header);
        Layout.Add(_body);

        UpdateHeaderName();

        if(_header.Opened == startFolded)
            _header.Toggle();
    }

    protected override void OnValueChanged()
    {
        RebuildLayout();
    }

    public override void ChildValuesChanged(Widget source)
    {
        base.ChildValuesChanged(source);
        UpdateHeaderName();
    }

    private void UpdateHeaderName()
    {
        if(_header is null)
            return;

        if(SerializedProperty.TryGetAttribute<FoldableEditorAttribute>(out var foldableEditorAttribute) &&
             !string.IsNullOrEmpty(foldableEditorAttribute.Label))
        {
            _header.Title = foldableEditorAttribute.Label;
            return;
        }

        var folderName = SerializedProperty.GetValue<object>()?.ToString()?.Replace('\n', ' ') ??
            $"None {SerializedProperty.PropertyType}";

        if(SerializedProperty.TryGetAsObject(out var serializedObject))
        {
            var keyProperty = serializedObject.FirstOrDefault(x => x.HasAttribute<FoldableObjectKeyAttribute>());
            if(keyProperty is not null)
            {
                var value = keyProperty.GetValue<object>();
                if(value is IEnumerable enumerable)
                {
                    var enumerableObj = enumerable.Cast<object>().Where(x => x is not null);
                    if(enumerableObj.Any())
                        folderName = $"{string.Join(", ", enumerable.Cast<object>().Take(3).Select(x => $"'{x}'"))}{(enumerableObj.Count() > 3 ? $" ... +{enumerableObj.Count() - 3} more" : string.Empty)}";
                }
                else
                {
                    var str = value?.ToString();
                    if(str is not null && (value is not IValid valid || valid.IsValid))
                        folderName = str;
                }
            }
        }

        _header.Title = folderName;
    }

    protected override void PaintUnder()
    {
        if(!PaintBackground)
            return;

        var backgroundColor = Theme.WidgetBackground;

        var parent = Parent;
        while(parent is not null)
        {
            if(parent is FoldableControlWidget)
                backgroundColor = backgroundColor.Darken(0.1f);

            parent = parent.Parent;
        }

        Paint.ClearPen();
        Paint.SetBrush(backgroundColor);
        Paint.DrawRect(LocalRect, Theme.ControlRadius);
    }

    public class Header : Widget
    {
        public Action<bool>? OnToggled;

        public bool Opened { get; private set; } = false;
        public string Title { get; set; } = string.Empty;

        public string CookieName
        {
            get
            {
                return _cookieName;
            }

            set
            {
                _cookieName = value;

                var newState = ProjectCookie.Get(_cookieName, Opened);
                if(newState == Opened)
                    return;

                Toggle();
            }
        }

        private string _cookieName = string.Empty;
        private readonly Layout _toggleLayout;

        public Header() : base(null)
        {
            FixedHeight = Theme.RowHeight;
            VerticalSizeMode = SizeMode.CanGrow;
            HorizontalSizeMode = SizeMode.Flexible;
            Cursor = CursorShape.Finger;
            Layout = Layout.Row();
            Layout.Spacing = 5;
            Layout.Margin = new Margin(16, 0, 0, 0);
            Layout.AddSpacingCell(10);

            _toggleLayout = Layout.AddColumn();

            Layout.AddStretchCell();
        }

        protected override void OnMousePress(MouseEvent e)
        {
            base.OnMousePress(e);

            if(e.Button == MouseButtons.Left)
                Toggle();
        }

        protected override void OnDoubleClick(MouseEvent e)
        {
            e.Accepted = false;
        }

        protected override void OnPaint()
        {
            float spacing = 0;

            var textRect = Paint.MeasureText(LocalRect.Shrink(_toggleLayout.OuterRect.Right + spacing, 0, 0, 0), Title, TextFlag.LeftCenter);

            var backgroundRect = LocalRect.Shrink(3, 4, 4, 4);
            backgroundRect.Height = 22 - 8;
            backgroundRect.Right = textRect.Right + 8;
            backgroundRect.Width = backgroundRect.Height;

            // Background
            {
                var backgroundColor = Theme.WindowBackground.Lighten(0.2f).WithAlphaMultiplied(Opened ? 1 : 0.5f);

                Paint.SetBrushAndPen(backgroundColor);
                Paint.DrawRect(backgroundRect, 6);
            }

            Paint.ClearBrush();

            var iconIntensity = IsUnderMouse ? 1.5f : 1f;
            if(Opened)
            {
                Paint.Pen = Theme.TextControl.WithAlpha(0.2f * iconIntensity);
                Paint.DrawIcon(backgroundRect, "remove", 12, TextFlag.Center);
            }
            else
            {
                Paint.Pen = Theme.TextControl.WithAlpha(0.4f * iconIntensity);
                Paint.DrawIcon(backgroundRect, "add", 12, TextFlag.Center);
            }

            Paint.Pen = Theme.TextControl.WithAlpha(Opened ? 1 : 0.8f);

            Paint.SetDefaultFont(11, weight: 400, sizeInPixels: true);
            Paint.DrawText(LocalRect.Shrink(_toggleLayout.OuterRect.Right + spacing, 0, 0, 0), Title, TextFlag.LeftCenter);
        }

        public void Toggle()
        {
            Opened = !Opened;
            OnToggled?.Invoke(Opened);

            if(CookieName is not null)
            {
                ProjectCookie.Set(CookieName, Opened);
            }
        }
    }
}