Editor/Inspector/LengthControlWidget.cs
using Editor;
using Grains.RazorDesigner.Document;
using Sandbox;

namespace Grains.RazorDesigner.Inspector;

[CustomEditor( typeof( Length ) )]
public sealed class LengthControlWidget : ControlWidget
{
	private const string LogPrefix = "[Grains.RazorDesigner]";

	private const int ValueEditWidthSolo    = 60;
	private const int ValueEditWidthCompact = 32;
	private const int UnitWidthSolo         = 96;
	private const int UnitWidthCompact      = 72;

	public override bool SupportsMultiEdit => true;

	private LineEdit _valueEdit;
	private EnumControlWidget _unitWidget;
	private UnitProxy _unitProxy;
	private SerializedObject _unitSerialized;
	// Synchronous change events on both sides; without this guard SetValue would loop.
	private bool _syncing;

	private sealed class UnitProxy
	{
		public LengthUnit Unit { get; set; }
	}

	// Engine-instantiated solo ctor. Width/Height/etc inspector rows go through this.
	public LengthControlWidget( SerializedProperty property ) : this( property, icon: null ) { }

	public LengthControlWidget( SerializedProperty property, string icon ) : base( property )
	{
		Log.Info( $"{LogPrefix} LengthControlWidget ctor for {property.Name} icon={icon ?? "(none)"}" );

		var compact = !string.IsNullOrEmpty( icon );

		Layout = Layout.Row();
		Layout.Spacing = 2;

		if ( compact )
		{
			var iconBox = new IconBoxWidget( this, icon )
			{
				FixedSize = new Vector2( Theme.RowHeight, Theme.RowHeight ),
			};
			Layout.Add( iconBox );
		}

		_valueEdit = new LineEdit( this );
		_valueEdit.MinimumSize = new Vector2( compact ? ValueEditWidthCompact : ValueEditWidthSolo, Theme.RowHeight );
		_valueEdit.MaximumSize = new Vector2( 4096, Theme.RowHeight );
		_valueEdit.SetStyles( "background-color: transparent;" );
		_valueEdit.EditingStarted += OnValueEditStarted;
		_valueEdit.TextEdited += OnValueEditTextEdited;
		_valueEdit.EditingFinished += OnValueEditFinished;
		Layout.Add( _valueEdit, 1 );

		_unitProxy = new UnitProxy { Unit = LengthUnit.Auto };
		_unitSerialized = EditorTypeLibrary.GetSerializedObject( _unitProxy );
		var unitProp = _unitSerialized.GetProperty( nameof( UnitProxy.Unit ) );
		_unitWidget = new EnumControlWidget( unitProp );
		_unitWidget.MinimumWidth = compact ? UnitWidthCompact : UnitWidthSolo;
		_unitWidget.MaximumWidth = compact ? UnitWidthCompact : UnitWidthSolo;
		_unitSerialized.OnPropertyChanged += OnUnitProxyChanged;
		Layout.Add( _unitWidget );

		SyncFromProperty();
	}

	private sealed class IconBoxWidget : Widget
	{
		private readonly string _icon;

		public IconBoxWidget( Widget parent, string icon ) : base( parent )
		{
			_icon = icon;
		}

		protected override void OnPaint()
		{
			var rect = new Rect( 0, Size );
			Paint.SetPen( Theme.TextControl.WithAlpha( 0.7f ) );
			Paint.DrawIcon( rect, _icon, Theme.RowHeight - 4, TextFlag.Center );
		}
	}

	private void SyncFromProperty()
	{
		if ( _syncing ) return;
		_syncing = true;

		try
		{
			var len = SerializedProperty.GetValue<Length>( Length.Auto );

			if ( !_valueEdit.IsFocused )
				_valueEdit.Text = len.Value.ToString( "0.###" );

			// Push through SerializedProperty so EnumControlWidget repaints; direct field assignment skips the event.
			var unitProp = _unitSerialized.GetProperty( nameof( UnitProxy.Unit ) );
			unitProp.SetValue( len.Unit );

			_valueEdit.Enabled = len.Unit != LengthUnit.Auto;
		}
		finally
		{
			_syncing = false;
		}
	}

	private void OnUnitProxyChanged( SerializedProperty property )
	{
		if ( _syncing ) return;
		if ( ReadOnly || !SerializedProperty.IsEditable )
			return;

		_syncing = true;
		try
		{
			var current = SerializedProperty.GetValue<Length>( Length.Auto );
			var newValue = new Length( current.Value, _unitProxy.Unit );

			Log.Info( $"{LogPrefix} LengthControlWidget OnUnitChanged {newValue}" );

			PropertyStartEdit();
			SerializedProperty.SetValue( newValue );
			SignalValuesChanged();
			PropertyFinishEdit();

			_valueEdit.Enabled = _unitProxy.Unit != LengthUnit.Auto;
		}
		finally
		{
			_syncing = false;
		}
	}

	private void OnValueEditStarted()
	{
		if ( ReadOnly || !SerializedProperty.IsEditable )
			return;
		PropertyStartEdit();
	}

	private void OnValueEditTextEdited( string text )
	{
		if ( _syncing ) return;
		if ( ReadOnly || !SerializedProperty.IsEditable )
			return;

		if ( !float.TryParse( text, out var parsed ) )
			return;

		var current = SerializedProperty.GetValue<Length>( Length.Auto );
		var newValue = new Length( parsed, current.Unit );
		if ( newValue == current )
			return;

		_syncing = true;
		try
		{
			SerializedProperty.SetValue( newValue );
			SignalValuesChanged();
		}
		finally
		{
			_syncing = false;
		}
	}

	private void OnValueEditFinished()
	{
		if ( _syncing ) return;
		if ( ReadOnly || !SerializedProperty.IsEditable )
		{
			PropertyFinishEdit();
			return;
		}

		var current = SerializedProperty.GetValue<Length>( Length.Auto );

		if ( !float.TryParse( _valueEdit.Text, out var parsed ) )
		{
			// Restore on bad input; don't commit (avoids spurious ValueChanged).
			_valueEdit.Text = current.Value.ToString( "0.###" );
			Log.Info( $"{LogPrefix} LengthControlWidget OnValueEditFinished: parse failed, restored to {current.Value}" );
			PropertyFinishEdit();
			return;
		}

		var formatted = parsed.ToString( "0.###" );
		if ( _valueEdit.Text != formatted )
			_valueEdit.Text = formatted;

		var newValue = new Length( parsed, current.Unit );
		if ( newValue != current )
		{
			Log.Info( $"{LogPrefix} LengthControlWidget OnValueChanged {newValue}" );
			_syncing = true;
			try
			{
				SerializedProperty.SetValue( newValue );
				SignalValuesChanged();
			}
			finally
			{
				_syncing = false;
			}
		}

		PropertyFinishEdit();
	}

	protected override void OnValueChanged()
	{
		base.OnValueChanged();
		SyncFromProperty();
	}
}