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