Editor/Prism/Ui/InlineEditors/EnumInlineEditor.cs

An inline editor UI component for enum-valued ports in the Editor. It builds a list of enum options from a CLR enum type, draws a compact dropdown chip showing the current option label, and opens a context menu to pick a different enum value.

Reflection
using Editor.Prism.Core;
using Editor.Prism.Ui.Adapters;

namespace Editor.Prism.Ui.InlineEditors;

/// <summary>
/// An enum-valued port, drawn as a compact dropdown chip.
/// <para>
/// The shader type system has no enums — a sampler filter or a blend mode is an <c>int</c> by the time
/// it reaches the compiler — so the choice is discovered from the CLR property the port's inline value
/// is bound to. That is also where the pretty names come from, so <c>[Title]</c> on an enum member
/// shows up on the card for free.
/// </para>
/// </summary>
public sealed class EnumInlineEditor : PrismInlineEditor
{
	readonly List<(object Value, string Label)> _options = new();

	/// <summary>Bind an enum editor to a port and the enum type behind it.</summary>
	public EnumInlineEditor( Plug plug, PrismPlugIn port, Type enumType ) : base( plug, port )
	{
		EnumType = enumType;

		BuildOptions();
	}

	/// <summary>The enum type the bound property declares.</summary>
	public Type EnumType { get; }

	/// <summary>The label of the current value.</summary>
	public string Text
	{
		get
		{
			var current = Value;

			foreach ( var option in _options )
			{
				if ( Equals( option.Value, current ) ) return option.Label;
			}

			return current?.ToString() ?? ( _options.Count > 0 ? _options[0].Label : "—" );
		}
	}

	/// <inheritdoc/>
	protected override float MeasureWidth() =>
		MathF.Min( 140f,
			MathF.Max( 42f, PrismPaint.MeasureText( Text, PrismTheme.InlineValueSize, PrismTheme.InlineValueWeight ) + 24f ) );

	/// <inheritdoc/>
	protected override void OnPaintPill( Rect pill )
	{
		var hovered = Paint.HasMouseOver;

		PrismPaint.Pill( pill, hovered ? PrismTheme.Elevated.Lighten( 0.14f ) : PrismTheme.Elevated,
			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 );

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

		Paint.SetPen( PrismTheme.TextMuted );
		Paint.DrawIcon( pill.Shrink( 0f, 0f, 2f, 0f ), "expand_more", 12f, TextFlag.RightCenter );
	}

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

		e.Accepted = true;

		PrismLog.Guard( "Open enum menu", () =>
		{
			if ( _options.Count == 0 ) return;

			var menu = new Menu();
			var current = Value;

			foreach ( var option in _options )
			{
				var value = option.Value;
				var entry = menu.AddOption( option.Label, null, () => Commit( value ) );

				entry.Checkable = true;
				entry.Checked = Equals( value, current );
			}

			var view = Plug?.Node?.GraphicsView;
			var pill = PillRect;
			var screen = view is null
				? Application.CursorPosition
				: view.ToScreen( view.FromScene( ToScene( new Vector2( pill.Left, pill.Bottom + 2f ) ) ) );

			menu.OpenAt( screen );
		} );
	}

	void BuildOptions()
	{
		if ( EnumType is null || !EnumType.IsEnum ) return;

		PrismLog.Guard( "Read enum values", () =>
		{
			var values = Enum.GetValues( EnumType );
			var names = Enum.GetNames( EnumType );

			DisplayInfo[] display = null;

			PrismLog.Guard( "Read enum display info", () => display = DisplayInfo.ForEnumValues( EnumType ) );

			for ( int i = 0; i < values.Length; i++ )
			{
				var label = display is not null && i < display.Length && !string.IsNullOrEmpty( display[i].Name )
					? display[i].Name
					: names[i];

				_options.Add( (values.GetValue( i ), label) );
			}
		} );
	}
}