Editor/MaterialIconPickerWidget.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Sandbox;
namespace Editor;
[Dock( "Editor", "Material Icon Picker", "view_cozy" )]
public sealed class MaterialIconPickerWidget : Widget
{
public static string DataUrl { get; set; } = "https://fonts.google.com/metadata/icons";
public static IconMetadata IconData { get; set; } = null;
LineEdit Search { get; set; }
Widget IconList { get; set; }
public float IconScale { get; set; } = 1.5f;
public Action<MouseEvent, IconMetadata.Icon> OnIconClicked { get; set; } = ( e, icon ) =>
{
var text = icon.Name;
if ( e.KeyboardModifiers != KeyboardModifiers.None )
{
text = $"[Icon(\"{text}\")]";
}
EditorUtility.Clipboard.Copy( text );
var snd = EditorUtility.PlaySound( "sounds/kenney/ui/select_002.vsnd" );
snd.Pitch = 0.6f + Random.Shared.Float( 0.3f );
snd.Volume = 0.15f;
Log.Info( $"\"{text}\" copied to clipboard!" );
};
public MaterialIconPickerWidget( Widget parent ) : base( parent, false )
{
if ( IconData == null )
{
RequestIconData().ContinueWith( async delegate
{
await MainThread.Wait();
RebuildIconList();
} );
}
Layout = Layout.Column();
Layout.Margin = 4;
Layout.Spacing = 4;
Layout.Alignment = TextFlag.Top;
Search = Layout.Add( new LineEdit( this ) );
IconList = Layout.Add( new ScrollArea( this ) );
IconList.Layout = Layout.Grid();
IconList.Layout.Spacing = 4;
IconList.Layout.Alignment = TextFlag.Top;
RebuildIconList();
Search.TextChanged += ( _ ) => RebuildIconList();
}
protected override void OnResize()
{
base.OnResize();
RebuildIconList( false );
}
List<IconMetadata.Icon> _query = [];
public void RebuildIconList( bool refresh = true )
{
IconList.Layout.Clear( true );
static int GetDistance( string s, string q )
{
int len = Math.Min( s.Length, q.Length );
return s[..len].ToLowerInvariant().Distance( q );
}
if ( refresh && IconData != null )
{
var searchText = Search.Text.ToLowerInvariant();
var query = IconData.Icons
.Select( icon => (
Icon: icon,
SearchTerms: icon.Categories
.Concat( icon.Tags )
) )
.Select( x => (
x.Icon,
Distance: x.SearchTerms.Min( s => GetDistance( s, searchText ) - (GetDistance( x.Icon.Name, searchText ) == 0 ? 100 : 0) )
) )
.Where( x => x.Distance <= 3 )
.OrderBy( x => x.Distance )
.ThenByDescending( x => x.Icon.Popularity )
.Select( x => x.Icon )
.ToList();
_query = query;
}
var columns = (int)(Size.x / (26f * IconScale));
var rows = (int)((Size.y - 42f) / (28f * IconScale));
var i = 0;
foreach ( var icon in _query )
{
if ( i / columns > rows ) { break; }
if ( IconList.Layout is GridLayout grid )
{
var iconButton = new IconPickButton( icon.Name, ( e ) => OnIconClicked( e, icon ), IconList )
{
ToolTip = icon.Name
};
iconButton.FixedWidth *= IconScale;
iconButton.FixedHeight *= IconScale;
iconButton.IconSize *= IconScale;
grid.AddCell( i % columns, i / columns, iconButton );
}
i += 1;
}
}
public class IconPickButton : IconButton
{
public new Action<MouseEvent> OnClick { get; set; }
public IconPickButton( string icon, Action<MouseEvent> onClick = null, Widget parent = null ) : base( icon, null, parent )
{
OnClick = onClick;
}
protected override void OnMouseClick( MouseEvent e )
{
base.OnMouseClick( e );
if ( IsToggle )
{
IsActive = !IsActive;
}
OnClick?.Invoke( e );
e.Accepted = true;
}
}
static async Task RequestIconData()
{
var result = await Http.RequestAsync( DataUrl );
result.EnsureSuccessStatusCode();
{ // API has garbage symbols at start...
// IconData = await result.Content.ReadFromJsonAsync<IconMetadata>();
}
{ // Doesn't compile???
// var stream = await result.Content.ReadAsStreamAsync();
// stream.Position = 4; // Skip garbage symbols
// IconData = await JsonSerializer.DeserializeAsync<IconMetadata>( stream );
}
{
var strJson = await result.Content.ReadAsStringAsync();
strJson = strJson[4..];
IconData = Json.Deserialize<IconMetadata>( strJson );
}
Log.Info( $"Fetched {IconData.Icons.Count} material icons successfully." );
// Log.Info( string.Join( ", ", IconData.Icons.Select( x => x.Name ) ) );
}
public class IconMetadata
{
public string Host { get; set; }
public List<Icon> Icons { get; set; } = [];
public class Icon
{
public string Name { get; set; }
public int Version { get; set; }
public int Popularity { get; set; }
public int Codepoint { get; set; }
public List<string> Categories { get; set; }
public List<string> Tags { get; set; }
}
}
}