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.
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 ) );
}
}