An editor UI widget that lists project TilesetResource assets, shows thumbnails, name, brush type and path, allows hovering and selecting a tileset, and ensures project tileset GUIDs are unique by regenerating and saving assets when needed.
using Editor;
using Sandbox;
using System;
using System.Collections.Generic;
using System.Linq;
using Saandy.Tilemapper;
namespace Saandy.Editor.Tilemapper;
public sealed class TilemapTilesetPickerWidget : Widget
{
private sealed class TilesetEntry
{
public TilesetResource Tileset;
public Asset Asset;
public Pixmap Thumbnail;
public string Name;
public string ResourcePath;
}
private sealed class ProjectTilesetEntry
{
public TilesetResource Tileset;
public Asset Asset;
public string ResourcePath;
}
private const float HeaderHeight = 34.0f;
private const float RowHeight = 74.0f;
private const float Padding = 8.0f;
private const float ThumbSize = 52.0f;
private readonly List<TilesetEntry> _entries = new();
private TilesetResource _selectedTileset;
private string _selectedResourcePath;
private int _hoveredIndex = -1;
public Action<TilesetResource> OnTilesetSelected { get; set; }
public TilemapTilesetPickerWidget( Widget parent ) : base( parent )
{
MouseTracking = true;
HorizontalSizeMode = SizeMode.Flexible;
VerticalSizeMode = SizeMode.CanGrow;
MinimumHeight = 420;
RefreshTilesets();
}
public void RefreshTilesets()
{
_entries.Clear();
var projectTilesets = GetProjectTilesetEntries();
EnsureUniqueProjectTilesetGuids( projectTilesets );
foreach ( var projectEntry in projectTilesets )
{
var tileset = projectEntry.Tileset;
var asset = projectEntry.Asset;
string resourcePath = projectEntry.ResourcePath;
_entries.Add( new TilesetEntry
{
Tileset = tileset,
Asset = asset,
Thumbnail = asset.GetAssetThumb( true ),
Name = GetTilesetDisplayName( tileset, asset, resourcePath ),
ResourcePath = resourcePath
} );
}
_entries.Sort( ( a, b ) => string.Compare( a.Name, b.Name, StringComparison.OrdinalIgnoreCase ) );
MinimumHeight = Math.Max( 220.0f, HeaderHeight + Padding + _entries.Count * RowHeight + Padding );
Update();
}
public void SetSelectedTileset( TilesetResource tileset )
{
_selectedTileset = tileset;
_selectedResourcePath = GetTilesetResourcePath( tileset );
Update();
}
protected override void OnMouseMove( MouseEvent e )
{
base.OnMouseMove( e );
_hoveredIndex = GetEntryIndexAtPosition( e.LocalPosition );
Update();
}
protected override void OnMouseLeave()
{
base.OnMouseLeave();
_hoveredIndex = -1;
Update();
}
protected override void OnMousePress( MouseEvent e )
{
base.OnMousePress( e );
if ( !e.LeftMouseButton )
return;
int index = GetEntryIndexAtPosition( e.LocalPosition );
if ( index < 0 || index >= _entries.Count )
return;
var entry = _entries[index];
if ( entry?.Tileset == null || !entry.Tileset.IsValid() )
return;
SetSelectedTileset( entry.Tileset );
OnTilesetSelected?.Invoke( entry.Tileset );
}
protected override void OnPaint()
{
base.OnPaint();
Paint.ClearPen();
Paint.SetBrush( Color.Black.WithAlpha( 0.18f ) );
Paint.DrawRect( LocalRect );
DrawHeader();
if ( _entries.Count == 0 )
{
Paint.SetPen( Color.White.WithAlpha( 0.75f ) );
Paint.DrawText(
new Rect( Padding, HeaderHeight + Padding, LocalRect.Width - Padding * 2.0f, 80.0f ),
"No TilesetResource assets found. Save a .tileset asset, then press Refresh Tilesets.",
TextFlag.LeftTop
);
return;
}
for ( int i = 0; i < _entries.Count; i++ )
{
DrawEntry( i, _entries[i] );
}
}
private void DrawHeader()
{
Rect headerRect = new Rect( 0.0f, 0.0f, LocalRect.Width, HeaderHeight );
Paint.ClearPen();
Paint.SetBrush( Color.Black.WithAlpha( 0.30f ) );
Paint.DrawRect( headerRect );
Paint.SetPen( Color.White );
Paint.DrawText(
new Rect( Padding, headerRect.Top, Math.Max( 1.0f, headerRect.Width - Padding * 2.0f ), headerRect.Height ),
$"Tilesets ({_entries.Count})",
TextFlag.LeftCenter
);
}
private void DrawEntry( int index, TilesetEntry entry )
{
Rect row = GetEntryRect( index );
bool selected = IsSelected( entry );
bool hovered = index == _hoveredIndex;
Color background = selected
? Color.Yellow.WithAlpha( 0.24f )
: hovered
? Color.White.WithAlpha( 0.09f )
: Color.White.WithAlpha( 0.035f );
Color border = selected
? Color.Yellow
: Color.White.WithAlpha( 0.16f );
Paint.SetBrush( background );
Paint.SetPen( border, selected ? 2.0f : 1.0f );
Paint.DrawRect( row );
Rect thumbRect = new Rect(
row.Left + Padding,
row.Top + (row.Height - ThumbSize) * 0.5f,
ThumbSize,
ThumbSize
);
DrawThumbnail( entry, thumbRect );
float textX = thumbRect.Right + Padding;
float textW = Math.Max( 1.0f, row.Right - textX - Padding );
Rect nameRect = new Rect( textX, row.Top + 8.0f, textW, 20.0f );
Rect brushRect = new Rect( textX, row.Top + 30.0f, textW, 18.0f );
Rect pathRect = new Rect( textX, row.Top + 50.0f, textW, 16.0f );
Paint.SetPen( selected ? Color.Yellow : Color.White );
Paint.DrawText( nameRect, entry.Name ?? "Tileset", TextFlag.LeftCenter );
string brushName = TileBrush.GetBrush( entry.Tileset?.BrushType ?? BrushType.None )?.Name ?? "No Brush";
Paint.SetPen( Color.White.WithAlpha( 0.70f ) );
Paint.DrawText( brushRect, brushName, TextFlag.LeftCenter );
Paint.SetPen( Color.White.WithAlpha( 0.45f ) );
Paint.DrawText( pathRect, entry.ResourcePath ?? "", TextFlag.LeftCenter );
}
private void DrawThumbnail( TilesetEntry entry, Rect rect )
{
Paint.ClearPen();
Paint.SetBrush( Color.Black.WithAlpha( 0.45f ) );
Paint.DrawRect( rect );
if ( entry?.Thumbnail == null )
{
Paint.SetPen( Color.White.WithAlpha( 0.45f ) );
Paint.DrawText( rect, "?", TextFlag.Center );
return;
}
DrawPixmapInsideRect( entry.Thumbnail, rect, 4.0f );
}
private void DrawPixmapInsideRect( Pixmap pixmap, Rect rect, float padding )
{
if ( pixmap == null )
return;
float availableW = Math.Max( 1.0f, rect.Width - padding * 2.0f );
float availableH = Math.Max( 1.0f, rect.Height - padding * 2.0f );
float scale = Math.Min(
availableW / Math.Max( 1.0f, pixmap.Width ),
availableH / Math.Max( 1.0f, pixmap.Height )
);
// Large thumbnails must shrink into the square. Small thumbnails may scale up a bit.
scale = Math.Max( 0.01f, scale );
float w = pixmap.Width * scale;
float h = pixmap.Height * scale;
Rect drawRect = new Rect(
rect.Left + (rect.Width - w) * 0.5f,
rect.Top + (rect.Height - h) * 0.5f,
w,
h
);
Paint.Draw( drawRect, pixmap );
}
private int GetEntryIndexAtPosition( Vector2 position )
{
if ( position.y < HeaderHeight + Padding )
return -1;
int index = (int)MathF.Floor( (position.y - HeaderHeight - Padding) / RowHeight );
if ( index < 0 || index >= _entries.Count )
return -1;
Rect row = GetEntryRect( index );
if ( !row.IsInside( position ) )
return -1;
return index;
}
private Rect GetEntryRect( int index )
{
return new Rect(
Padding,
HeaderHeight + Padding + index * RowHeight,
Math.Max( 1.0f, LocalRect.Width - Padding * 2.0f ),
RowHeight - 6.0f
);
}
private bool IsSelected( TilesetEntry entry )
{
if ( entry == null )
return false;
string entryPath = NormalizePath( entry.ResourcePath );
string selectedPath = NormalizePath( _selectedResourcePath );
if ( !string.IsNullOrWhiteSpace( entryPath ) && !string.IsNullOrWhiteSpace( selectedPath ) )
return entryPath == selectedPath;
return entry.Tileset == _selectedTileset;
}
public static TilesetResource GetFirstProjectTileset()
{
var projectTilesets = GetProjectTilesetEntries();
EnsureUniqueProjectTilesetGuids( projectTilesets );
return projectTilesets.FirstOrDefault()?.Tileset;
}
private static List<ProjectTilesetEntry> GetProjectTilesetEntries()
{
List<ProjectTilesetEntry> result = new();
HashSet<string> seenResourcePaths = new();
foreach ( var tileset in ResourceLibrary.GetAll<TilesetResource>() )
{
if ( tileset == null || !tileset.IsValid() )
continue;
string resourcePath = GetTilesetResourcePath( tileset );
string normalizedPath = NormalizePath( resourcePath );
if ( string.IsNullOrWhiteSpace( normalizedPath ) )
continue;
if ( !normalizedPath.EndsWith( ".tileset", StringComparison.OrdinalIgnoreCase ) )
continue;
if ( !seenResourcePaths.Add( normalizedPath ) )
continue;
Asset asset = FindAsset( resourcePath );
// If the resource does not resolve to an asset-browser entry, it is probably
// a stale/deleted/cached resource. Do not show it in the painter list.
if ( asset == null )
continue;
result.Add( new ProjectTilesetEntry
{
Tileset = tileset,
Asset = asset,
ResourcePath = resourcePath
} );
}
result.Sort( ( a, b ) => string.Compare(
NormalizePath( a.ResourcePath ),
NormalizePath( b.ResourcePath ),
StringComparison.OrdinalIgnoreCase ) );
return result;
}
private static void EnsureUniqueProjectTilesetGuids( List<ProjectTilesetEntry> projectTilesets )
{
if ( projectTilesets == null )
return;
HashSet<Guid> usedGuids = new();
foreach ( var entry in projectTilesets )
{
if ( entry?.Tileset == null || !entry.Tileset.IsValid() || entry.Asset == null )
continue;
bool changed = false;
if ( entry.Tileset.TilesetGuid == Guid.Empty || usedGuids.Contains( entry.Tileset.TilesetGuid ) )
{
do
{
entry.Tileset.RegenerateTilesetGuid();
}
while ( entry.Tileset.TilesetGuid == Guid.Empty || usedGuids.Contains( entry.Tileset.TilesetGuid ) );
changed = true;
}
usedGuids.Add( entry.Tileset.TilesetGuid );
if ( changed )
{
entry.Asset.SaveToDisk( entry.Tileset );
}
}
}
private static string GetTilesetDisplayName( TilesetResource tileset, Asset asset, string resourcePath )
{
if ( asset != null && !string.IsNullOrWhiteSpace( asset.Name ) )
return asset.Name;
if ( !string.IsNullOrWhiteSpace( resourcePath ) )
{
string fileName = System.IO.Path.GetFileNameWithoutExtension( resourcePath );
if ( !string.IsNullOrWhiteSpace( fileName ) )
return fileName;
}
return tileset?.FilePath ?? "Tileset";
}
private static Asset FindAsset( string resourcePath )
{
if ( string.IsNullOrWhiteSpace( resourcePath ) )
return null;
return AssetSystem.FindByPath( resourcePath );
}
private static string GetTilesetResourcePath( TilesetResource tileset )
{
if ( tileset == null )
return string.Empty;
return tileset.ResourcePath ?? string.Empty;
}
private static string NormalizePath( string path )
{
return (path ?? string.Empty)
.Replace( '\\', '/' )
.Trim()
.ToLowerInvariant();
}
}