Editor/AssetBrowserAddon/AssetTypeSelector.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Editor;
using Sandbox;
namespace Sandbox.AssetBrowserAddon;
/// <summary>
/// Searchable list of asset types that supports multi-selection with icons.
/// </summary>
internal sealed class AssetTypeSelector : Widget
{
private readonly LineEdit _searchInput;
private readonly ScrollArea _scrollArea;
private readonly Widget _list;
private readonly Action<LineEdit> _styleInput;
private readonly List<OptionData> _options = new();
private readonly List<AssetTypeRow> _rows = new();
private readonly HashSet<string> _selected = new(StringComparer.OrdinalIgnoreCase);
public event Action<IReadOnlyCollection<string>> SelectionChanged;
public AssetTypeSelector(Widget parent, Action<LineEdit> styleInput) : base(parent)
{
_styleInput = styleInput;
Layout = Layout.Column();
Layout.Spacing = 6f;
_searchInput = Layout.Add(new LineEdit(this));
_searchInput.MinimumHeight = Theme.RowHeight;
_searchInput.PlaceholderText = "Search asset types...";
_styleInput?.Invoke(_searchInput);
_searchInput.TextEdited += _ => RefreshList();
_scrollArea = Layout.Add(new ScrollArea(this));
_scrollArea.MinimumHeight = 220f;
_list = new Widget(_scrollArea);
_scrollArea.Canvas = _list;
_list.Layout = Layout.Column();
_list.Layout.Spacing = 2f;
_list.VerticalSizeMode = SizeMode.CanShrink;
BuildOptions();
RefreshList();
}
public IReadOnlyCollection<string> SelectedTags => _selected;
public void SetSelected(IEnumerable<string> tags)
{
_selected.Clear();
if (tags != null)
{
foreach (var tag in tags)
{
var normalized = NormalizeExtension(tag);
if (!string.IsNullOrWhiteSpace(normalized))
_selected.Add(normalized);
}
}
RefreshList();
}
private void BuildOptions()
{
_options.Clear();
foreach (var assetType in AssetType.All.OrderBy(x => x.FriendlyName ?? x.FileExtension))
{
var extensions = (assetType.FileExtensions?.Count ?? 0) > 0
? assetType.FileExtensions
: new[] { assetType.FileExtension };
foreach (var extension in extensions ?? Array.Empty<string>())
{
var normalized = NormalizeExtension(extension);
if (string.IsNullOrWhiteSpace(normalized))
continue;
if (_options.Any(x => x.Tag.Equals(normalized, StringComparison.OrdinalIgnoreCase)))
continue;
var title = assetType.FriendlyName ?? normalized.ToUpperInvariant();
var subtitle = string.IsNullOrWhiteSpace(extension) ? null : $".{normalized}";
_options.Add(new OptionData(normalized, title, subtitle, assetType.Icon16));
}
}
}
private void RefreshList()
{
var filter = _searchInput.Text?.Trim() ?? string.Empty;
_list.DestroyChildren();
_rows.Clear();
IEnumerable<OptionData> data = _options;
if (!string.IsNullOrWhiteSpace(filter))
{
data = data.Where(option =>
Contains(option.Tag, filter) ||
Contains(option.Title, filter) ||
Contains(option.Subtitle, filter));
}
foreach (var option in data)
{
var row = new AssetTypeRow(_list, option, _selected.Contains(option.Tag), ToggleSelection);
_list.Layout.Add(row);
_rows.Add(row);
}
if (!_rows.Any())
{
var empty = _list.Layout.Add(new Label(_list)
{
Text = "No asset types match your search.",
Alignment = TextFlag.Center
});
empty.SetStyles($"color: {Theme.Text.WithAlpha(0.6f).Hex}; padding: 12px;");
}
}
private void ToggleSelection(string tag, bool isSelected)
{
if (isSelected)
_selected.Add(tag);
else
_selected.Remove(tag);
SelectionChanged?.Invoke(_selected);
}
private static bool Contains(string value, string filter)
{
if (string.IsNullOrWhiteSpace(value))
return false;
return value.IndexOf(filter, StringComparison.OrdinalIgnoreCase) >= 0;
}
private static string NormalizeExtension(string value)
{
if (string.IsNullOrWhiteSpace(value))
return string.Empty;
return value.Trim().TrimStart('.').ToLowerInvariant();
}
private sealed record OptionData(string Tag, string Title, string Subtitle, Pixmap Icon);
private sealed class AssetTypeRow : Widget
{
private readonly OptionData _option;
private readonly Action<string, bool> _onToggle;
private bool _isSelected;
public AssetTypeRow(Widget parent, OptionData option, bool selected, Action<string, bool> onToggle) : base(parent)
{
_option = option;
_onToggle = onToggle;
_isSelected = selected;
MinimumHeight = 26f;
MaximumHeight = 26f;
MinimumWidth = 50f;
MaximumWidth = 10000f;
VerticalSizeMode = SizeMode.CanShrink;
HorizontalSizeMode = SizeMode.CanGrow;
}
protected override void OnMousePress(MouseEvent e)
{
if ( e.LeftMouseButton )
{
Toggle();
e.Accepted = true;
}
base.OnMousePress( e );
}
private void Toggle()
{
_isSelected = !_isSelected;
_onToggle?.Invoke( _option.Tag, _isSelected );
Update();
}
protected override void OnPaint()
{
var rect = LocalRect;
if ( Paint.HasMouseOver || _isSelected )
{
var color = _isSelected ? Theme.Primary.WithAlpha( 0.15f ) : Theme.ControlBackground.WithAlpha( 0.5f );
Paint.SetBrush( color );
Paint.ClearPen();
Paint.DrawRect( rect );
}
var iconRect = new Rect( rect.Left, rect.Top, Theme.RowHeight, Theme.RowHeight );
if ( _option.Icon != null )
{
Paint.Draw( iconRect.Shrink( 4 ), _option.Icon, 1f );
}
var textRect = rect;
textRect.Left = iconRect.Right + 8f;
textRect.Right -= 20f;
Rect? extensionRect = null;
if ( !string.IsNullOrWhiteSpace( _option.Subtitle ) )
{
var width = Math.Clamp( textRect.Width * 0.3f, 60f, 140f );
extensionRect = new Rect( textRect.Right - width, rect.Top, width, rect.Height );
textRect.Right = extensionRect.Value.Left - 8f;
}
Paint.SetPen( Theme.Text );
Paint.DrawText( textRect, _option.Title, TextFlag.LeftCenter );
if ( extensionRect.HasValue )
{
Paint.SetPen( Theme.Text.WithAlpha( 0.8f ) );
Paint.DrawText( extensionRect.Value, _option.Subtitle, TextFlag.RightCenter );
}
}
}
}