An inline UI editor component for vector-like ports in the editor. It draws a pill with 2–4 scrubbable numeric fields (X,Y,Z,W), handles mouse drag to change component values, formats and reads/writes values via a ValueCodec and commits changes to the bound plug/port.
using Editor.Prism.Core;
using Editor.Prism.Serialization;
using Editor.Prism.Ui.Adapters;
namespace Editor.Prism.Ui.InlineEditors;
/// <summary>
/// Two to four scalars in one pill, each field individually scrubbable.
/// <para>
/// The component letters are drawn in the type colour rather than as plain text, because a
/// <c>float3</c> pill next to a <c>float2</c> pill has to be distinguishable without counting fields.
/// </para>
/// </summary>
public sealed class VectorInlineEditor : PrismInlineEditor
{
static readonly string[] s_labels = { "X", "Y", "Z", "W" };
int _dragging = -1;
float _lastX;
/// <summary>Bind a vector editor to a port.</summary>
public VectorInlineEditor( Plug plug, PrismPlugIn port ) : base( plug, port )
{
Cursor = CursorShape.SizeH;
}
/// <summary>How many components the port's resolved type has, clamped to what we can draw.</summary>
public int Components => Math.Clamp( Port?.EffectiveType.Components ?? 2, 2, 4 );
/// <summary>True when the components are whole numbers.</summary>
public bool IsInteger => Port?.EffectiveType.IsIntegral ?? false;
/// <summary>Width of one component field.</summary>
float FieldWidth => IsInteger ? 30f : 38f;
/// <inheritdoc/>
protected override float MeasureWidth() => Components * FieldWidth + 4f;
/// <inheritdoc/>
protected override void OnPaintPill( Rect pill )
{
var hovered = Paint.HasMouseOver || _dragging >= 0;
var background = hovered ? PrismTheme.Elevated.Lighten( 0.12f ) : PrismTheme.Elevated;
PrismPaint.Pill( pill, background, PrismTheme.BorderSubtle.WithAlpha( hovered ? 0.9f : 0.55f ),
PrismTheme.RadiusChip );
Paint.ClearPen();
Paint.SetBrush( TypeColor.WithAlpha( hovered ? 1f : 0.8f ) );
Paint.DrawRect( new Rect( pill.Left, pill.Top, 2f, pill.Height ), 1f );
var components = Read();
var count = Components;
for ( int i = 0; i < count; i++ )
{
var field = FieldRect( pill, i );
if ( i > 0 )
{
Paint.ClearBrush();
Paint.SetPen( PrismTheme.BorderSubtle.WithAlpha( 0.7f ), 1f );
Paint.DrawLine( new Vector2( field.Left, pill.Top + 3f ), new Vector2( field.Left, pill.Bottom - 3f ) );
}
var active = _dragging == i;
var label = field.Shrink( 3f, 0f, 0f, 0f );
PrismPaint.Text( label, s_labels[i], TypeColor.WithAlpha( active ? 1f : 0.7f ),
PrismTheme.InlineValueSize - 1, 600, TextFlag.LeftCenter );
PrismPaint.Text( field.Shrink( 10f, 0f, 4f, 0f ), Format( components, i ),
active || hovered ? PrismTheme.TextPrimary : PrismTheme.TextSecondary,
PrismTheme.InlineValueSize, PrismTheme.InlineValueWeight, TextFlag.RightCenter );
}
}
/// <inheritdoc/>
protected override void OnMousePressed( GraphicsMouseEvent e )
{
if ( !ShouldDraw || !e.LeftMouseButton ) return;
var pill = PillRect;
if ( !pill.Grow( 3f ).IsInside( e.LocalPosition ) ) return;
_dragging = IndexAt( pill, e.LocalPosition );
_lastX = e.ScreenPosition.x;
e.Accepted = true;
}
/// <inheritdoc/>
protected override void OnMouseMove( GraphicsMouseEvent e )
{
if ( _dragging < 0 ) return;
var delta = e.ScreenPosition.x - _lastX;
_lastX = e.ScreenPosition.x;
var scale = e.HasCtrl ? 0.001f : e.HasShift ? 0.1f : 0.01f;
var components = Read();
if ( _dragging >= components.Length ) return;
components[_dragging] += IsInteger ? MathF.Round( delta * 0.25f ) : delta * scale;
if ( IsInteger ) components[_dragging] = MathF.Round( components[_dragging] );
Write( components );
e.Accepted = true;
}
/// <inheritdoc/>
protected override void OnMouseReleased( GraphicsMouseEvent e )
{
if ( _dragging < 0 ) return;
_dragging = -1;
e.Accepted = true;
Update();
}
Rect FieldRect( Rect pill, int index )
{
var width = ( pill.Width - 3f ) / Components;
return new Rect( pill.Left + 3f + width * index, pill.Top, width, pill.Height );
}
int IndexAt( Rect pill, Vector2 local )
{
var width = ( pill.Width - 3f ) / Components;
var index = (int)MathF.Floor( ( local.x - pill.Left - 3f ) / MathF.Max( 1f, width ) );
return Math.Clamp( index, 0, Components - 1 );
}
string Format( float[] components, int index )
{
if ( components is null || index >= components.Length ) return "0";
var value = components[index];
if ( IsInteger ) return ( (int)MathF.Round( value ) ).ToString();
return MathF.Abs( value ) >= 100f ? value.ToString( "0.#" ) : value.ToString( "0.##" );
}
float[] Read()
{
var count = Components;
var source = ValueCodec.ToFloats( Value ) ?? Array.Empty<float>();
var result = new float[count];
for ( int i = 0; i < count; i++ )
{
result[i] = i < source.Length ? source[i] : 0f;
}
return result;
}
void Write( float[] components ) =>
Commit( Shape( ValueCodec.ToComponents( components, Components ) ) );
}