A UI control that displays selectable resource thumbnails in a grid for a ResourceSelect property. It builds cells from ResourceLibrary, sizes them to fit the panel, updates selection state, and shows tooltips and thumbnails.
using Sandbox.UI;
namespace Sandbox;
/// <summary>
/// A resource property that shows all available options in a grid
/// </summary>
public class UIResourceGrid : BaseControl
{
readonly List<Cell> _cells = new();
string _extension;
string _rendered;
public override bool SupportsMultiEdit => false;
public override void Rebuild()
{
DeleteChildren( true );
_cells.Clear();
if ( Property is null ) return;
if ( !Property.TryGetAttribute<ResourceSelectAttribute>( out var attribute ) ) return;
_extension = string.IsNullOrEmpty( attribute.Extension ) ? null : $".{attribute.Extension}";
var resources = ResourceLibrary.GetAll<Resource>()
.Where( x => HasExtension( x.ResourcePath ) )
.OrderBy( x => x.ResourcePath, StringComparer.OrdinalIgnoreCase )
.ToList();
foreach ( var resource in resources )
{
var cell = AddChild( new Cell( resource, OnPicked ) );
_cells.Add( cell );
}
SetClass( "empty", _cells.Count == 0 );
_rendered = null;
_fittedWidth = 0f;
_fittedCount = -1;
UpdateSelection();
}
bool HasExtension( string path )
{
if ( string.IsNullOrEmpty( path ) ) return false;
if ( string.IsNullOrEmpty( _extension ) ) return true;
return System.IO.Path.GetExtension( path ).Equals( _extension, StringComparison.OrdinalIgnoreCase );
}
void OnPicked( Resource resource )
{
Property.As.String = resource.ResourcePath;
UpdateSelection();
}
public override void Tick()
{
base.Tick();
if ( Property is null ) return;
FitCells();
if ( Property.As.String != _rendered )
UpdateSelection();
}
float _fittedWidth;
int _fittedCount = -1;
// the grid's border and padding top and bottom
const float Frame = 2f + 2f;
const float BarRoom = 9f;
// minimum/maximum amount of rows that can be displayed in a resource grid panel
const int MinRows = 1;
const int MaxRows = 5;
/// <summary>
/// Make sure all cells fit in grid space properly regardless of screen resolution
/// </summary>
void FitCells()
{
// inside the border, and then inside the 1px of padding each side (at least a pixel on screen)
var inner = Box.RectInner.Width - 2f * MathF.Max( 1f, MathF.Round( ScaleToScreen ) );
if ( inner <= 0f ) return;
if ( MathF.Abs( inner - _fittedWidth ) < 0.5f && _fittedCount == _cells.Count ) return;
_fittedWidth = inner;
_fittedCount = _cells.Count;
var toDesign = ScaleFromScreen;
var rows = (int)MathF.Ceiling( _cells.Count / 3f );
var shownRows = rows.Clamp( MinRows, MaxRows );
var cell = CellPixels( inner );
var scrolls = rows > MaxRows;
var bar = scrolls ? MathF.Round( BarRoom * ScaleToScreen ) : 0f;
if ( scrolls )
cell = CellPixels( inner - bar );
// the bar is drawn over the right edge of the grid, so its room is padding on that side
Style.PaddingRight = 1f + (scrolls ? BarRoom : 0f);
// the bar only shows when some of the rows are out of view
SetClass( "scrolls", scrolls );
// whole pixels for the gaps too, the last pixel of slack stays at the right
var gap = MathF.Max( 1f, MathF.Floor( (inner - bar - 3f * cell - 1f) / 2f ) );
Style.ColumnGap = gap * toDesign;
// an empty grid keeps the short height of its style
var height = _cells.Count == 0 ? (float?)null : Frame + (shownRows * (cell * toDesign + 1f)) + (shownRows - 1);
Style.Height = height;
Style.MinHeight = height;
Style.MaxHeight = height;
foreach ( var c in _cells )
{
c.Style.Width = cell * toDesign;
c.Style.Height = cell * toDesign + 1f;
}
}
static float CellPixels( float room ) => MathF.Floor( (room - 3f) / 3f );
void UpdateSelection()
{
_rendered = Property?.As.String;
foreach ( var cell in _cells )
cell.SetClass( "selected", cell.Matches( _rendered ) );
}
/// <summary>
/// One pickable resource
/// </summary>
class Cell : Panel
{
readonly Resource _resource;
readonly Action<Resource> _onPicked;
readonly string _title;
readonly string _description;
public Cell( Resource resource, Action<Resource> onPicked )
{
_resource = resource;
_onPicked = onPicked;
_title = resource.ResourceName;
if ( ResourceLibrary.Get<GameResource>( resource.ResourcePath ) is IDefinitionResource definition )
{
_title = definition.Title;
_description = definition.Description;
}
AddClass( "resource-cell" );
var thumb = Add.Panel( "thumb" );
thumb.Style.Set( "background-image", $"url( thumb:{resource.ResourcePath} )" );
}
public bool Matches( string path ) => !string.IsNullOrEmpty( path ) && path.Equals( _resource.ResourcePath, StringComparison.OrdinalIgnoreCase );
protected override void OnClick( MousePanelEvent e )
{
base.OnClick( e );
_onPicked?.Invoke( _resource );
}
public override bool HasTooltip => true;
protected override Panel CreateTooltipPanel()
{
var description = string.IsNullOrWhiteSpace( _description ) ? "#ui.resource.no_description" : _description;
var tip = new UIHintPanel( _title, description );
tip.Parent = FindRootPanel();
return tip;
}
}
}