Editor/FloatRangeControlWidget.cs
using Editor;
using ExtendedBox.General;
using Sandbox;
using System;

namespace ExtendedBox;

[CustomEditor(typeof(FloatRange))]
public sealed class FloatRangeControlWidget : ControlWidget
{
    private readonly SerializedObject _serializedObject;

    private readonly ControlWidget _minControl = null!;
    private readonly ControlWidget _maxControl = null!;

    private readonly LimitedFloatProperty _minProperty = null!;
    private readonly LimitedFloatProperty _maxProperty = null!;

    private readonly Layout _fieldsLayout = null!;

    private readonly FloatRange? _range;
    private readonly bool _rangeClamp;
    private readonly float _rangeStep;
    private readonly bool _isSlider;

    private readonly DoubleFloatSlider? _slider;

    private float Min
    {
        get => SerializedProperty.GetValue<FloatRange>().Min;
        set => SerializedProperty.SetValue(SerializedProperty.GetValue<FloatRange>() with { Min = value });
    }

    private float Max
    {
        get => SerializedProperty.GetValue<FloatRange>().Max;
        set => SerializedProperty.SetValue(SerializedProperty.GetValue<FloatRange>() with { Max = value });
    }


    public FloatRangeControlWidget(SerializedProperty property) : base(property)
    {
        property.TryGetAsObject(out _serializedObject);

        if(!_serializedObject.IsValid())
        {
            Log.Warning($"Error when trying to get {property} as object");
            return;
        }

        Layout = Layout.Column();

        _fieldsLayout = Layout.Add(Layout.Row());
        _fieldsLayout.Spacing = 2f;

        if(property.TryGetAttribute<RangeAttribute>(out var rangeAttribute))
        {
            _range = new(rangeAttribute.Min, rangeAttribute.Max);
            _rangeClamp = rangeAttribute.Clamped;
#pragma warning disable CS0618 // Type or member is obsolete
            _rangeStep = rangeAttribute.Step;
#pragma warning restore CS0618 // Type or member is obsolete
            _isSlider = rangeAttribute.Slider;
        }

        if(property.TryGetAttribute<StepAttribute>(out var stepAttribute))
        {
            _rangeStep = stepAttribute.Step;
        }

        (_minControl, _minProperty) = AddField(nameof(FloatRange.Min), "Min");
        (_maxControl, _maxProperty) = AddField(nameof(FloatRange.Max), "Max");

        if(_rangeClamp)
        {
            _minProperty.Min = _range!.Value.Min;
            _maxProperty.Max = _range!.Value.Max;
        }

        if(_isSlider)
        {
            _slider = new(this)
            {
                Minimum = _range!.Value.Min,
                Maximum = _range!.Value.Max,
                ValueA = Min,
                ValueB = Max,
                Step = _rangeStep,
            };
            _slider.OnValueEdited += OnSliderValueEdited;
            Layout.Add(_slider);
        }
    }

    private void OnSliderValueEdited()
    {
        var min = Math.Min(_slider!.ValueA, _slider.ValueB);
        var max = Math.Max(_slider!.ValueA, _slider.ValueB);

        Min = min;
        Max = max;
    }

    private void OnChildValueChanged(Widget widget)
    {
        if(widget == _minControl || widget.IsDescendantOf(_minControl) ||
            widget == _maxControl || widget.IsDescendantOf(_maxControl))
        {
            if(_slider.IsValid())
            {
                _slider.ValueA = Min;
                _slider.ValueB = Max;
            }
        }
    }

    public override void OnDestroyed()
    {
        _minControl.OnChildValuesChanged -= OnChildValueChanged;
        _maxControl.OnChildValuesChanged -= OnChildValueChanged;
    }

    private (ControlWidget, LimitedFloatProperty) AddField(string propertyName, string text)
    {
        LimitedFloatProperty property = new(_serializedObject.GetProperty(propertyName));

        var controlWidget = Create(property);
        controlWidget.OnChildValuesChanged += OnChildValueChanged;
        controlWidget.MinimumWidth = Theme.RowHeight;
        controlWidget.HorizontalSizeMode = (SizeMode)3;

        _fieldsLayout.Add(controlWidget);
        return (controlWidget, property);
    }

    public override void StartEditing()
    {
        _minControl.StartEditing();
    }

    protected override void OnPaint()
    {
    }

    private class LimitedFloatProperty : SerializedProperty.Proxy
    {
        protected override SerializedProperty ProxyTarget { get; }

        public float? Min { get; set; }
        public float? Max { get; set; }

        public LimitedFloatProperty(SerializedProperty proxyTarget)
        {
            ProxyTarget = proxyTarget;
        }

        public override void SetValue<T>(T value)
        {
            var realValue = (float)(object)value;
            if(Min.HasValue)
                realValue = Math.Max(Min.Value, realValue);
            if(Max.HasValue)
                realValue = Math.Min(Max.Value, realValue);

            base.SetValue(realValue);
        }
    }
}