Editor/Prism/Ui/InlineEditors/AssetInlineEditor.cs

Editor UI inline editor that displays an asset reference as a thumbnail chip and lets the user pick or clear an asset. It reads the referenced path from either a node property or the port value, draws a thumbnail or placeholder, opens a filtered asset picker, and writes changes back to the node property or port value.

File AccessNetworking
using Editor.Prism.Core;
using Editor.AssetPickers;
using Editor.Prism.Serialization;
using Editor.Prism.Ui.Adapters;
using System.Reflection;

namespace Editor.Prism.Ui.InlineEditors;

/// <summary>
/// A texture or other asset reference, drawn as a thumbnail chip that opens the asset browser.
/// <para>
/// The chip shows the asset's real thumbnail where the editor has one cached, which turns a wall of
/// identically-named sample nodes into something you can read at a glance. An unset reference is drawn
/// as a dashed placeholder rather than as an empty box, because "nothing chosen yet" and "chose black"
/// must not look the same.
/// </para>
/// </summary>
public sealed class AssetInlineEditor : PrismInlineEditor
{
	Pixmap _thumbnail;
	string _thumbnailFor;

	/// <summary>Bind an asset editor to a port.</summary>
	public AssetInlineEditor( Plug plug, PrismPlugIn port ) : base( plug, port ) { }

	/// <inheritdoc/>
	protected override float PillHeight => 18f;

	/// <summary>The node property this chip edits, when the node keeps its asset in one.</summary>
	PropertyInfo Asset => InlineEditorFactory.AssetProperty( Port );

	/// <summary>The asset path currently referenced, or null.</summary>
	public string Path
	{
		get
		{
			// The node's own property first: a texture node keeps its image there and leaves the port's
			// inline slot empty, so reading only the slot showed "None" over a node that had one.
			if ( Asset is { } property && Port?.Owner?.PrismNode is { } node )
			{
				var stored = PrismLog.Guard<object>( $"Read '{property.Name}'", () => property.GetValue( node ) );

				if ( stored is string text ) return text;
			}

			return Value switch
			{
				TextureValue texture => texture.Path,
				string path => path,
				_ => null
			};
		}
	}

	/// <summary>The file name shown on the chip.</summary>
	public string Label
	{
		get
		{
			var path = Path;

			if ( string.IsNullOrWhiteSpace( path ) ) return "None";

			var name = System.IO.Path.GetFileNameWithoutExtension( path );

			return string.IsNullOrEmpty( name ) ? path : name;
		}
	}

	/// <inheritdoc/>
	protected override float MeasureWidth() =>
		MathF.Min( 150f,
			MathF.Max( 58f, PrismPaint.MeasureText( Label, PrismTheme.InlineValueSize, PrismTheme.InlineValueWeight ) + 34f ) );

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

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

		var thumb = new Rect( pill.Left + 2f, pill.Top + 2f, pill.Height - 4f, pill.Height - 4f );

		if ( empty )
		{
			Paint.ClearBrush();
			Paint.SetPen( TypeColor.WithAlpha( 0.7f ), 1f, PenStyle.Dash );
			Paint.DrawRect( thumb, 2f );
		}
		else
		{
			var pixmap = Thumbnail( path );

			if ( pixmap is not null )
			{
				Paint.Draw( thumb, pixmap, 1f, 2f );
			}
			else
			{
				PrismPaint.Pill( thumb, TypeColor.WithAlpha( 0.35f ), 2f );

				Paint.SetPen( TypeColor );
				Paint.DrawIcon( thumb, PrismIcons.Texture, thumb.Height - 3f, TextFlag.Center );
			}
		}

		PrismPaint.Text( new Rect( thumb.Right + 4f, pill.Top, pill.Right - thumb.Right - 8f, pill.Height ),
			Label,
			empty ? PrismTheme.TextMuted : hovered ? PrismTheme.TextPrimary : PrismTheme.TextSecondary,
			PrismTheme.InlineValueSize, PrismTheme.InlineValueWeight, TextFlag.LeftCenter );
	}

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

		e.Accepted = true;

		if ( e.RightMouseButton )
		{
			Write( null );
			return;
		}

		if ( !e.LeftMouseButton ) return;

		OpenPicker();
	}

	/// <summary>Open the asset browser filtered to the kinds of asset this port can hold.</summary>
	public void OpenPicker()
	{
		PrismLog.Guard( "Open asset picker", () =>
		{
			var parent = Plug?.Node?.Graph;
			var picker = new GenericPicker( parent, new List<AssetType> { AssetType.ImageFile },
				new AssetPicker.PickerOptions() );

			picker.Title = $"Select a texture for {Port?.Label}";

			var current = Path;

			if ( !string.IsNullOrWhiteSpace( current ) ) picker.SetSelection( current );

			picker.OnAssetPicked = assets =>
			{
				var asset = assets?.FirstOrDefault();

				if ( asset is null ) return;

				Write( asset.RelativePath ?? asset.Path );
			};

			picker.Show();
		} );
	}

	void Write( string path )
	{
		var type = Port?.EffectiveType ?? default;

		// Write back where Path read from, or the chip would show one thing and the compiler use another.
		if ( Asset is { } property )
		{
			Port.SetBoundProperty( property.Name, path ?? string.Empty );

			PrismLog.Guard( "Asset reference changed", () =>
			{
				Plug?.Node?.Graph?.ChildValuesChanged( null );
				Plug?.Node?.Update();
			} );

			Update();
			return;
		}

		if ( type.IsTexture )
		{
			var texture = Value as TextureValue ?? new TextureValue();

			Commit( texture with { Path = path ?? string.Empty } );
			return;
		}

		Commit( path ?? string.Empty );
	}

	Pixmap Thumbnail( string path )
	{
		if ( string.Equals( path, _thumbnailFor, StringComparison.OrdinalIgnoreCase ) ) return _thumbnail;

		_thumbnailFor = path;
		_thumbnail = PrismLog.Guard<Pixmap>( "Asset thumbnail",
			() => AssetSystem.FindByPath( path )?.GetAssetThumb(), null );

		return _thumbnail;
	}
}