Editor/Inspector/Control/LengthControl.cs
using Sandbox;
using Sandbox.Diagnostics;
using Sandbox.UI;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;

namespace Panelize;

public class LengthControl : Widget
{
	public LengthUnit Unit { get; set; }
	public float Length { get; set; }
	public Length Value
	{
		get
		{
			return new()
			{
				Unit = Unit,
				Value = Length
			};
		}
		set
		{
			if ( value.Unit == LengthUnit.Undefined )
				value.Unit = LengthUnit.Auto;

			SetUnit( value.Unit );
			Length = value.Value;
		}
	}
	public Action<Length> OnValueChanged;
	public float UnitControlWidth { get; set; } = 135f;
	public bool UnitControlWrap { get; } = false;
	public float AmountSliderWidth { get; set; } = 90f;
	DropdownEnumControl<LengthUnit> unitControl;
	LineEdit amountControl;
	FloatSlider amountSlider;
	Layout amountRow;
	Layout controlRow;
	public LengthControl( Widget parent = null, bool unitControlWrap = false ) : base(parent)
	{
		Layout = Layout.Row();
		//Layout.Margin = new( 4f, 0 );
		Layout.Spacing = 4f;
		amountRow = Layout.AddRow();
		controlRow = Layout.AddRow();

		unitControl = new( LengthUnit.Auto )
		{
			FixedWidth = UnitControlWidth
		};
		Unit = LengthUnit.Auto;
		BuildUnitControl();

		amountSlider = new( this )
		{
			Minimum = 0f,
			Maximum = 100f,
			Step = 0.1f,
			MinimumWidth = AmountSliderWidth
		};
		amountSlider.OnValueEdited += AmountSliderChanged;
		amountControl = new( this )
		{
			Text = "0",
			RegexValidator = "[0123456789\\.]*", // Only allow numbers and . (for decimals)
			Alignment = TextFlag.RightCenter
		};
		amountControl.TextChanged += AmountTextChanged;

		if( unitControlWrap )
		{
			controlRow.Add( unitControl );
		}
		else
		{
			amountRow.Add( unitControl );
			amountRow.AddSpacingCell( 4f );
		}

		amountRow.Add( amountSlider, 1 );
		amountRow.AddSpacingCell( 4f );
		amountRow.Add( amountControl, 1 );

		UnitControlWrap = unitControlWrap;
	}

	public void Bind(SerializedProperty prop)
	{
		Assert.True( prop != null && prop.IsEditable );

		var length = prop.GetValue<Length>();
		if( length != null)
		{
			unitControl.SetValue(length.Unit);
			amountControl.Text = length.Value.ToString();
		}

		OnValueChanged += (v) =>
		{
			prop.SetValue( v );
		};
	}
	protected override void DoLayout()
	{
		amountSlider.MinimumWidth = AmountSliderWidth;
		unitControl.FixedWidth = UnitControlWidth;
	}
	private void BuildUnitControl()
	{
		//unitControl.CellColumns = 0;
		unitControl.SetOrder( LengthUnit.Pixels, LengthUnit.Percentage, 
			LengthUnit.ViewHeight, LengthUnit.ViewWidth, LengthUnit.Auto );
		unitControl.SetDisplay( LengthUnit.Pixels, "straighten" );
		unitControl.SetDisplay( LengthUnit.Percentage, "aspect_ratio" );
		unitControl.SetDisplay( LengthUnit.ViewHeight, "swap_vert" );
		unitControl.SetDisplay( LengthUnit.ViewWidth, "swap_horiz" );
		unitControl.SetDisplay( LengthUnit.Auto, "open_in_full" );

		unitControl.OnValueChanged += (unit) => SetUnit(unit, false);
	}
	public void SetUnit( LengthUnit unit, bool manual = true )
	{
		Unit = unit;
		OnValueChanged?.Invoke( Value );
		UpdateValueControl( unit );
		if( manual )
		{
			unitControl.SetValue(unit);
		}
	}

	private void UpdateValueControl(LengthUnit unit)
	{
		amountControl.ReadOnly = false;
		amountControl.Text = Length.ToString();

		if ( unit == LengthUnit.Pixels )
		{
			amountSlider.Hidden = true;
		}
		else if ( unit >= LengthUnit.Percentage && unit <= LengthUnit.ViewMax )
		{
			Length = Length.Clamp( 0f, 100f );
			amountSlider.Hidden = false;

			amountControl.Update();
		}
		else
		{
			amountControl.Text = "Auto";
			amountControl.ReadOnly = true;
			amountSlider.Hidden = true;
		}
	}

	private void AmountTextChanged( string value )
	{
		if ( float.TryParse( value, out float number ) )
		{
			Length = number;
		}
		else
		{
			Length = 0f;
		}

		amountSlider.Value = Length;
		OnValueChanged?.Invoke( Value );
	}

	private void AmountSliderChanged()
	{
		float value = amountSlider.Value;
		Length = value;
		amountControl.Text = value.ToString();
		OnValueChanged?.Invoke( Value );
	}
}