Editor/OptionalControlWidget.cs
using Editor;
using ExtendedBox.Primitives;
using Sandbox;
using System;
using System.Linq;

namespace ExtendedBox;

[CustomEditor(typeof(Optional<>))]
public class OptionalControlWidget : ControlWidget
{
    public override bool SupportsMultiEdit => true;

    private Type ValueType
    {
        get => SerializedProperty.PropertyType.GetGenericArguments()[0];
    }

    private object? NewValue
    {
        get
        {
            var type = ValueType;

            if(type == typeof(string))
                return string.Empty;

            if(type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
                return Activator.CreateInstance(type.GetGenericArguments()[0], true);

            return Activator.CreateInstance(type, true);
        }
    }

    private object? Value
    {
        get
        {
            return GetValue(SerializedProperty);
        }
        set
        {
            if(Equals(Value, value))
                return;

            var optionalType = SerializedProperty.GetValue<object>().GetType();
            object newValue = Activator.CreateInstance(optionalType, value)!;
            SerializedProperty.SetValue(newValue);
        }
    }

    private bool HasValue
    {
        get => SerializedProperty.MultipleProperties.All(GetHasValue);
        set
        {
            foreach(var serializedProperty in SerializedProperty.MultipleProperties)
            {
                if(GetHasValue(serializedProperty) == value)
                    continue;

                var optional = serializedProperty.GetValue<object>();

                object newValue;
                if(!value)
                    newValue = optional.GetType().GetField(nameof(Optional<int>.Empty))!.GetValue(null)!;
                else
                    newValue = Activator.CreateInstance(optional.GetType(), NewValue)!;

                serializedProperty.SetValue(newValue);
            }
        }
    }

    private bool _hadValue = false;

    public OptionalControlWidget(SerializedProperty property) : base(property)
    {
        Layout = Layout.Row();
        RebuildLayout();
    }

    protected override void OnValueChanged()
    {
        if(HasValue != _hadValue)
            RebuildLayout();
    }

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

        Layout.Add(new PropertyButton(SerializedProperty, () => HasValue, v => HasValue = v));

        var hasValue = HasValue;
        if(hasValue)
        {
            SerializedProperty.TryGetAsObject(out var optionalObject);

            var valueProperty = TypeLibrary.CreateProperty(ValueType, nameof(Optional<int>.Value), () => Value!, v => Value = v, SerializedProperty.GetAttributes().ToArray(), optionalObject);

            if(SerializedProperty.IsMultipleDifferentValues)
                valueProperty = new MultipleValuesSerializedProperty(valueProperty);

            var control = ControlWidget.Create(valueProperty);
            Layout.Add(control);
        }
        else
        {
            var label = new Label($"{ValueType.Name} (Empty)");
            if(SerializedProperty.IsMultipleDifferentValues)
                label.Color = Theme.MultipleValues;
            Layout.Add(label);
        }

        Layout.AddStretchCell();

        _hadValue = hasValue;
    }

    private bool GetHasValue(SerializedProperty serializedProperty)
    {
        var optional = serializedProperty.GetValue<object>();
        return (bool)optional.GetType().GetProperty(nameof(Optional<int>.HasValue))!.GetValue(optional)!;
    }

    private object? GetValue(SerializedProperty serializedProperty)
    {
        var optional = SerializedProperty.GetValue<object>();
        if(!HasValue)
            return null;

        return optional.GetType().GetProperty(nameof(Optional<int>.Value))!.GetValue(optional);
    }
}

file class MultipleValuesSerializedProperty : SerializedProperty.Proxy
{
    protected override SerializedProperty ProxyTarget { get; }

    public override bool IsMultipleDifferentValues => true;

    public MultipleValuesSerializedProperty(SerializedProperty serializedProperty)
    {
        ProxyTarget = serializedProperty;
    }
}

file class PropertyButton : Widget
{
    private SerializedProperty _property;
    private Func<bool> _hasValueGetter;
    private Action<bool> _hasValueSetter;

    public PropertyButton(SerializedProperty property, Func<bool> hasValueGetter, Action<bool> hasValueSetter)
    {
        _property = property;
        _hasValueGetter = hasValueGetter;
        _hasValueSetter = hasValueSetter;

        FixedHeight = Theme.RowHeight;
        FixedWidth = Theme.RowHeight;
        HorizontalSizeMode = SizeMode.Flexible;
        Cursor = CursorShape.Finger;
        ToolTip = "Has Value";
    }

    protected override void OnPaint()
    {
        var icon = "eject";
        var size = 15;
        Color color = Theme.TextControl.WithAlpha(0.3f);
        Paint.TextAntialiasing = true;

        // Nullable - and null
        var isMultiple = _property.IsMultipleDifferentValues;
        if(_hasValueGetter() && !isMultiple)
        {
            icon = "radio_button_unchecked";
            size = 10;
            color = Theme.TextControl.WithAlpha(0.3f);
        }
        else
        {
            icon = "circle";
            size = 10;
            color = isMultiple ? Theme.MultipleValues : Theme.Blue;
            color = color.WithAlpha(0.3f);
        }

        Paint.Pen = color;
        Paint.DrawIcon(LocalRect, icon, size);
    }

    protected override void OnMouseClick(MouseEvent e)
    {
        _hasValueSetter(!_hasValueGetter());
        Update();
    }
}