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