Editor/HotCodeEditor/CodeView/CompletionPopup.cs
using Sandbox;
using Editor;
using System;
using System.Collections.Generic;

/// <summary>
/// The autocomplete list shown under the caret. It never takes focus: the code editor keeps
/// handling keys and tells it to move, and it reports clicks back through <see cref="Committed"/>.
/// </summary>
public class CompletionPopup : Widget
{
	const int MaxRows = 10;
	const float PopupWidth = 460;

	private List<CompletionItem> _items = new();
	private int _selected;
	private int _top;
	private float _rowHeight = 20;

	public Action<CompletionItem> Committed;

	public CompletionItem Selected => _items.Count > 0 ? _items[_selected] : null;

	public CompletionPopup( Widget parent ) : base( parent )
	{
		FocusMode = FocusMode.None;
		MouseTracking = true;
		Cursor = CursorShape.Arrow;
		Visible = false;
	}

	public void SetItems( List<CompletionItem> items, float rowHeight )
	{
		_items = items;
		_rowHeight = rowHeight;

		// Every refilter starts from the best match, so typing more always shows the top hit
		_selected = 0;
		_top = 0;

		Size = new Vector2( PopupWidth, Math.Min( MaxRows, _items.Count ) * _rowHeight + 2 );
		Update();
	}

	public void Move( int delta )
	{
		if ( _items.Count == 0 ) return;

		// Wrap around at the ends when stepping one at a time, like other editors
		if ( Math.Abs( delta ) == 1 )
			_selected = (_selected + delta + _items.Count) % _items.Count;
		else
			_selected = Math.Clamp( _selected + delta, 0, _items.Count - 1 );

		if ( _selected < _top ) _top = _selected;
		if ( _selected >= _top + MaxRows ) _top = _selected - MaxRows + 1;
		Update();
	}

	public static int PageSize => MaxRows - 1;

	private int RowAt( Vector2 pos )
	{
		var row = _top + (int)((pos.y - 1) / _rowHeight);
		return row >= 0 && row < _items.Count ? row : -1;
	}

	protected override void OnMousePress( MouseEvent e )
	{
		base.OnMousePress( e );
		var row = RowAt( e.LocalPosition );
		if ( row < 0 || !e.LeftMouseButton ) return;

		_selected = row;
		Committed?.Invoke( _items[row] );
		e.Accepted = true;
	}

	protected override void OnMouseWheel( WheelEvent e )
	{
		var maxTop = Math.Max( 0, _items.Count - MaxRows );
		_top = Math.Clamp( _top + (e.Delta > 0 ? -3 : 3), 0, maxTop );
		Update();
		e.Accept();
	}

	protected override void OnPaint()
	{
		Paint.ClearPen();
		Paint.SetBrush( SyntaxColors.PopupBorder );
		Paint.DrawRect( LocalRect );
		Paint.SetBrush( SyntaxColors.PopupBackground );
		Paint.DrawRect( LocalRect.Shrink( 1 ) );

		Paint.SetFont( HotCodeSettings.CodeFont, 9 );

		for ( int i = _top; i < Math.Min( _items.Count, _top + MaxRows ); i++ )
		{
			var item = _items[i];
			var rect = new Rect( 1, 1 + (i - _top) * _rowHeight, Width - 2, _rowHeight );

			if ( i == _selected )
			{
				Paint.ClearPen();
				Paint.SetBrush( SyntaxColors.PopupSelection );
				Paint.DrawRect( rect );
			}

			Paint.SetPen( IconColor( item.Kind ) );
			Paint.DrawIcon( new Rect( rect.Left + 4, rect.Top, 18, rect.Height ), Icon( item.Kind ), 14, TextFlag.Center );

			// Name on the left, detail (type / signature) dimmed on the right
			var nameRect = new Rect( rect.Left + 26, rect.Top, rect.Width - 30, rect.Height );
			Paint.SetPen( SyntaxColors.Text );
			var nameWidth = Paint.MeasureText( item.Name ).x;
			Paint.DrawText( nameRect, item.Name, TextFlag.LeftCenter );

			var detailRect = new Rect( nameRect.Left + nameWidth + 16, rect.Top, rect.Right - nameRect.Left - nameWidth - 24, rect.Height );
			if ( detailRect.Width > 30 && !string.IsNullOrEmpty( item.Detail ) )
			{
				Paint.SetPen( SyntaxColors.Text.WithAlpha( i == _selected ? 0.75f : 0.45f ) );
				Paint.DrawText( detailRect, Paint.GetElidedText( item.Detail, detailRect.Width, ElideMode.Right ), TextFlag.RightCenter );
			}
		}

		// Scroll indicator when there's more than fits
		if ( _items.Count > MaxRows )
		{
			var trackHeight = Height - 2;
			var thumbHeight = MathF.Max( 12, trackHeight * MaxRows / _items.Count );
			var thumbTop = 1 + (trackHeight - thumbHeight) * _top / Math.Max( 1, _items.Count - MaxRows );
			Paint.ClearPen();
			Paint.SetBrush( SyntaxColors.Text.WithAlpha( 0.2f ) );
			Paint.DrawRect( new Rect( Width - 5, thumbTop, 3, thumbHeight ) );
		}
	}

	private static string Icon( CompletionKind kind ) => kind switch
	{
		CompletionKind.Keyword => "key",
		CompletionKind.Local => "data_object",
		CompletionKind.Field => "crop_square",
		CompletionKind.Property => "build",
		CompletionKind.Method => "functions",
		CompletionKind.Event => "bolt",
		CompletionKind.Class => "category",
		CompletionKind.Struct => "view_in_ar",
		CompletionKind.Interface => "api",
		CompletionKind.Enum => "list",
		CompletionKind.EnumMember => "looks_one",
		CompletionKind.Delegate => "call_split",
		CompletionKind.Namespace => "folder",
		CompletionKind.TypeParameter => "text_fields",
		_ => "circle"
	};

	private static Color IconColor( CompletionKind kind ) => kind switch
	{
		CompletionKind.Method or CompletionKind.Delegate => SyntaxColors.ControlKeyword,
		CompletionKind.Field or CompletionKind.Property or CompletionKind.Local or CompletionKind.Event => SyntaxColors.Property,
		CompletionKind.Class or CompletionKind.Struct or CompletionKind.Interface or CompletionKind.TypeParameter => SyntaxColors.Type,
		CompletionKind.Enum or CompletionKind.EnumMember => SyntaxColors.Method,
		_ => SyntaxColors.Text.WithAlpha( 0.7f )
	};
}