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