Editor/Prism/Ui/InlineEditors/FloatInlineEditor.cs

An inline editor UI component for a single float or integer port in the Prism editor. It draws a value “pill”, supports range-based mini-slider rendering, click-drag scrubbing, integer snapping, formatted display text, and commits values through the port codec.

Native Interop
using Editor.Prism.Core;
using Editor.Prism.Serialization;
using Editor.Prism.Ui.Adapters;

namespace Editor.Prism.Ui.InlineEditors;

/// <summary>
/// A single scalar, drawn as a value pill.
/// <para>
/// A port with a declared <c>[Range]</c> becomes a mini-slider — the fill is the value, so a whole
/// column of them reads at a glance — and a port without one becomes a drag field, where horizontal
/// movement scrubs the number. Both are far more useful in a node card than a text box you cannot
/// see the caret in at 60 % zoom.
/// </para>
/// </summary>
public sealed class FloatInlineEditor : PrismInlineEditor
{
	bool _dragging;
	bool _integer;
	float _lastX;
	float _accumulated;

	/// <summary>Bind a scalar editor to a port.</summary>
	public FloatInlineEditor( Plug plug, PrismPlugIn port ) : base( plug, port )
	{
		_integer = port?.EffectiveType.IsIntegral ?? false;
		Cursor = CursorShape.SizeH;
	}

	/// <summary>The value as it is written on the pill.</summary>
	public string Text
	{
		get
		{
			var value = AsFloat();

			if ( _integer ) return ( (int)MathF.Round( value ) ).ToString();

			var magnitude = MathF.Abs( value );

			if ( magnitude >= 1000f ) return value.ToString( "0" );
			if ( magnitude >= 100f ) return value.ToString( "0.#" );

			return value.ToString( "0.###" );
		}
	}

	/// <inheritdoc/>
	protected override float MeasureWidth()
	{
		var range = Range;
		var text = PrismPaint.MeasureText( Text, PrismTheme.InlineValueSize, PrismTheme.InlineValueWeight ) + 14f;

		return range.Defined ? MathF.Max( 54f, text ) : MathF.Max( 34f, text );
	}

	/// <inheritdoc/>
	protected override void OnPaintPill( Rect pill )
	{
		var range = Range;
		var hovered = Paint.HasMouseOver || _dragging;
		var background = hovered ? PrismTheme.Elevated.Lighten( 0.12f ) : PrismTheme.Elevated;

		PrismPaint.Pill( pill, background, PrismTheme.BorderSubtle.WithAlpha( hovered ? 0.9f : 0.55f ),
			PrismTheme.RadiusChip );

		// The type-coloured left edge is what ties a floating pill back to the socket it belongs to.
		Paint.ClearPen();
		Paint.SetBrush( TypeColor.WithAlpha( hovered ? 1f : 0.8f ) );
		Paint.DrawRect( new Rect( pill.Left, pill.Top, 2f, pill.Height ), 1f );

		if ( range.Defined )
		{
			var track = pill.Shrink( 5f, 3f, 5f, 3f );

			PrismPaint.Track( track, range.Fraction( AsFloat() ),
				PrismTheme.Canvas.WithAlpha( 0.75f ), TypeColor.WithAlpha( 0.55f ) );
		}

		PrismPaint.Text( pill.Shrink( 6f, 0f, 5f, 0f ), Text,
			hovered ? PrismTheme.TextPrimary : PrismTheme.TextSecondary,
			PrismTheme.InlineValueSize, PrismTheme.InlineValueWeight, TextFlag.Center );
	}

	/// <inheritdoc/>
	protected override void OnMousePressed( GraphicsMouseEvent e )
	{
		if ( !ShouldDraw || !e.LeftMouseButton ) return;
		if ( !PillRect.Grow( 3f ).IsInside( e.LocalPosition ) ) return;

		_dragging = true;
		_lastX = e.ScreenPosition.x;
		_accumulated = 0f;

		if ( Range.Defined ) SetFromPosition( e.LocalPosition );

		e.Accepted = true;
	}

	/// <inheritdoc/>
	protected override void OnMouseMove( GraphicsMouseEvent e )
	{
		if ( !_dragging ) return;

		var range = Range;

		if ( range.Defined )
		{
			SetFromPosition( e.LocalPosition );
		}
		else
		{
			var delta = e.ScreenPosition.x - _lastX;

			_lastX = e.ScreenPosition.x;

			var scale = e.HasCtrl ? 0.001f : e.HasShift ? 0.1f : 0.01f;

			if ( _integer )
			{
				_accumulated += delta * ( e.HasShift ? 0.5f : 0.1f );

				var steps = MathF.Truncate( _accumulated );

				if ( MathF.Abs( steps ) >= 1f )
				{
					_accumulated -= steps;
					Write( AsFloat() + steps );
				}
			}
			else
			{
				Write( AsFloat() + delta * scale );
			}
		}

		e.Accepted = true;
	}

	/// <inheritdoc/>
	protected override void OnMouseReleased( GraphicsMouseEvent e )
	{
		if ( !_dragging ) return;

		_dragging = false;
		e.Accepted = true;

		Update();
	}

	void SetFromPosition( Vector2 local )
	{
		var pill = PillRect;

		if ( pill.Width <= 1f ) return;

		var fraction = ( local.x - pill.Left - 5f ) / MathF.Max( 1f, pill.Width - 10f );

		Write( Range.Value( fraction ) );
	}

	void Write( float value )
	{
		value = Range.Snap( value );

		object boxed = _integer ? (int)MathF.Round( value ) : value;

		// Round-trip through the codec so the port keeps whatever shape its type demands.
		Commit( Shape( boxed ) );
	}
}