Editor/HotCodeEditor/CodeView/SignaturePopup.cs
using Sandbox;
using Editor;
using System;
using System.Collections.Generic;
using System.Linq;
/// <summary>
/// Parameter hints shown above the line while typing a call's arguments:
/// <code>
/// [1 of 2] void Medic.Heal(Player p, int amount) <- current parameter highlighted
/// Heals by an amount. <- method docs
/// amount: How much to heal. <- current parameter's docs
/// </code>
/// Like <see cref="CompletionPopup"/> it never takes focus; the editor drives it.
/// </summary>
public class SignaturePopup : Widget
{
const float MaxWidth = 640;
const float Padding = 6;
const float SignatureFontSize = 9;
const float DocFontSize = 8;
const int MaxDocLines = 3;
private SignatureHelpResult _result;
private int _index;
private int _activeParameter;
// Laid out in SetResult, drawn in OnPaint
private record struct Piece( string Text, Vector2 Position, bool Highlight, bool Dim );
private readonly List<Piece> _signature = new();
private readonly List<string> _summaryLines = new();
private readonly List<string> _parameterLines = new();
private string _parameterName;
private float _signatureLineHeight;
private float _docLineHeight;
private float _signatureHeight;
public SignaturePopup( Widget parent ) : base( parent )
{
FocusMode = FocusMode.None;
TransparentForMouseEvents = true;
Visible = false;
}
public int Index => _index;
public int Count => _result?.Signatures.Count ?? 0;
public void SetResult( SignatureHelpResult result, int index )
{
_result = result;
_index = Math.Clamp( index, 0, result.Signatures.Count - 1 );
// The argument index applies to whichever overload is showing, as long as it has that many parameters
var signature = result.Signatures[_index];
_activeParameter = result.ActiveParameter;
if ( _activeParameter >= signature.Parameters.Count )
_activeParameter = signature.Parameters.LastOrDefault()?.Display.StartsWith( "params " ) == true ? signature.Parameters.Count - 1 : -1;
LayoutContent();
Update();
}
/// <summary>
/// Step through overloads (Up/Down).
/// </summary>
public void Cycle( int delta ) => SetResult( _result, (_index + delta + Count) % Count );
private void LayoutContent()
{
var signature = _result.Signatures[_index];
// Signature: pieces flow left to right and wrap (indented) when the line is full
Paint.SetFont( HotCodeSettings.CodeFont, SignatureFontSize );
_signatureLineHeight = MathF.Ceiling( Paint.MeasureText( "M" ).y + 2 );
_signature.Clear();
var pieces = new List<(string Text, bool Highlight, bool Dim)>();
if ( Count > 1 ) pieces.Add( ($"{_index + 1} of {Count} ", false, true) );
pieces.Add( (signature.Prefix, false, false) );
for ( int i = 0; i < signature.Parameters.Count; i++ )
pieces.Add( (signature.Parameters[i].Display + (i < signature.Parameters.Count - 1 ? ", " : ""), i == _activeParameter, false) );
pieces.Add( (signature.Suffix, false, false) );
var x = Padding;
var y = Padding;
var right = Padding;
var indent = Padding + Paint.MeasureText( " " ).x;
foreach ( var (text, highlight, dim) in pieces )
{
var width = Paint.MeasureText( text ).x;
if ( x + width > MaxWidth - Padding && x > indent )
{
x = indent;
y += _signatureLineHeight;
}
_signature.Add( new Piece( text, new Vector2( x, y ), highlight, dim ) );
x += width;
right = MathF.Max( right, x );
}
_signatureHeight = y + _signatureLineHeight;
// Docs: method summary, then the current parameter's description
Paint.SetDefaultFont( DocFontSize );
_docLineHeight = MathF.Ceiling( Paint.MeasureText( "M" ).y + 2 );
var parameter = _activeParameter >= 0 ? signature.Parameters[_activeParameter] : null;
_parameterName = string.IsNullOrEmpty( parameter?.Documentation ) ? null : parameter.Name;
var docWidth = MathF.Max( right, 320 ) - Padding;
_summaryLines.Clear();
_summaryLines.AddRange( Wrap( signature.Documentation, docWidth, MaxDocLines ) );
_parameterLines.Clear();
if ( _parameterName is not null )
_parameterLines.AddRange( Wrap( $"{_parameterName}: {parameter.Documentation}", docWidth, MaxDocLines ) );
var docLines = _summaryLines.Count + _parameterLines.Count;
var height = _signatureHeight + (docLines > 0 ? 4 + docLines * _docLineHeight : 0) + Padding;
var widest = MathF.Max( right, docLines > 0 ? docWidth + Padding : 0 );
Size = new Vector2( MathF.Min( MaxWidth, widest + Padding ), height );
}
/// <summary>
/// Greedy word wrap with the current font; the last line gets "..." if there's more.
/// </summary>
private static List<string> Wrap( string text, float width, int maxLines )
{
var lines = new List<string>();
if ( string.IsNullOrWhiteSpace( text ) ) return lines;
var line = "";
foreach ( var word in text.Split( ' ', StringSplitOptions.RemoveEmptyEntries ) )
{
var candidate = line.Length == 0 ? word : line + " " + word;
if ( Paint.MeasureText( candidate ).x <= width || line.Length == 0 )
{
line = candidate;
continue;
}
lines.Add( line );
line = word;
if ( lines.Count == maxLines )
{
lines[^1] = Paint.GetElidedText( lines[^1] + " " + line + " ...", width, ElideMode.Right );
return lines;
}
}
if ( line.Length > 0 ) lines.Add( line );
return lines;
}
protected override void OnPaint()
{
if ( _result is null ) return;
Paint.ClearPen();
Paint.SetBrush( SyntaxColors.PopupBorder );
Paint.DrawRect( LocalRect );
Paint.SetBrush( SyntaxColors.PopupBackground );
Paint.DrawRect( LocalRect.Shrink( 1 ) );
foreach ( var piece in _signature )
{
Paint.SetFont( HotCodeSettings.CodeFont, SignatureFontSize, piece.Highlight ? 700 : 400 );
Paint.SetPen( piece.Highlight ? SyntaxColors.Link : piece.Dim ? SyntaxColors.Text.WithAlpha( 0.5f ) : SyntaxColors.Text );
Paint.DrawText( new Rect( piece.Position, new Vector2( MaxWidth, _signatureLineHeight ) ), piece.Text, TextFlag.LeftCenter | TextFlag.SingleLine );
}
var y = _signatureHeight + 4;
Paint.SetDefaultFont( DocFontSize );
Paint.SetPen( SyntaxColors.Text.WithAlpha( 0.8f ) );
foreach ( var line in _summaryLines )
{
Paint.DrawText( new Rect( Padding, y, Width - Padding * 2, _docLineHeight ), line, TextFlag.LeftCenter | TextFlag.SingleLine );
y += _docLineHeight;
}
// "amount: How much to heal." with the name picked out
for ( int i = 0; i < _parameterLines.Count; i++ )
{
var rect = new Rect( Padding, y, Width - Padding * 2, _docLineHeight );
var line = _parameterLines[i];
if ( i == 0 && line.StartsWith( _parameterName + ":" ) )
{
var name = _parameterName + ":";
Paint.SetDefaultFont( DocFontSize, 700 );
Paint.SetPen( SyntaxColors.Link );
Paint.DrawText( rect, name, TextFlag.LeftCenter | TextFlag.SingleLine );
var nameWidth = Paint.MeasureText( name ).x;
Paint.SetDefaultFont( DocFontSize );
Paint.SetPen( SyntaxColors.Text.WithAlpha( 0.8f ) );
Paint.DrawText( new Rect( rect.Left + nameWidth, rect.Top, rect.Width - nameWidth, rect.Height ), line[name.Length..], TextFlag.LeftCenter | TextFlag.SingleLine );
}
else
{
Paint.DrawText( rect, line, TextFlag.LeftCenter | TextFlag.SingleLine );
}
y += _docLineHeight;
}
}
}