Editor/HotCodeEditor/Ui/HotCodeStyle.cs
using Sandbox;
using Editor;
using System;

/// <summary>
/// Shared drawing for HotCode's chrome so it matches the rest of the s&amp;box editor.
/// Colours come from <see cref="Theme"/> (s&amp;box's tools/assets/styles/theme.json), so they
/// follow s&amp;box if it changes. Only the code area itself uses its own dark colours (<see cref="SyntaxColors"/>).
/// </summary>
public static class HotCodeStyle
{
	public const float TabHeight = 26;
	public const float FontSize = 8;

	/// <summary>
	/// Draws a tab the way s&amp;box's QTabBar style does: rounded top corners, a 2px Primary line
	/// on the selected tab, light text when inactive, a surface highlight on hover.
	/// Returns the rect left over for content (after padding).
	/// </summary>
	public static Rect DrawTab( Rect rect, bool selected, bool hovered )
	{
		if ( selected || hovered )
		{
			// Extend past the bottom so only the top corners show as rounded
			Paint.ClearPen();
			Paint.SetBrush( selected ? Theme.TabBackground : Theme.SurfaceBackground );
			Paint.DrawRect( new Rect( rect.Left, rect.Top, rect.Width, rect.Height + Theme.ControlRadius ), Theme.ControlRadius );
		}

		if ( selected )
		{
			Paint.SetBrush( Theme.Primary );
			Paint.DrawRect( new Rect( rect.Left + 1, rect.Top, rect.Width - 2, 2 ), 1 );
		}

		return rect.Shrink( 12, 2, 8, 0 );
	}

	public static Color TabTextColor( bool selected, bool hovered ) => selected || hovered ? Theme.Text : Theme.TextLight;

	public static void SetTabFont( bool italic = false ) => Paint.SetDefaultFont( FontSize, 500, italic );

	/// <summary>
	/// Material icon for a file, by extension.
	/// </summary>
	public static string FileIcon( string path ) => System.IO.Path.GetExtension( path ).ToLowerInvariant() switch
	{
		".cs" => "code",
		".razor" => "web",
		".scss" or ".css" => "palette",
		".json" => "data_object",
		".sbproj" or ".csproj" => "settings",
		".sln" or ".slnx" => "account_tree",
		".shader" or ".hlsl" => "gradient",
		".md" or ".txt" => "notes",
		_ => "description"
	};

	public static Color FileIconColor( string path ) => System.IO.Path.GetExtension( path ).ToLowerInvariant() switch
	{
		".cs" => Theme.Blue,
		".razor" => Theme.Pink,
		".scss" or ".css" => Theme.Pink,
		".json" => Theme.Yellow,
		".sbproj" or ".csproj" or ".sln" or ".slnx" => Theme.Green,
		".shader" or ".hlsl" => Theme.Green,
		_ => Theme.TextLight
	};
}

/// <summary>
/// A row of panel tabs (like "Problems" / "Output") painted to match s&amp;box's tabs, with an optional
/// count badge per tab.
/// </summary>
public class PanelTabBar : Widget
{
	private readonly System.Collections.Generic.List<(string Key, string Text, string Icon)> _tabs = new();
	private readonly System.Collections.Generic.Dictionary<string, (string Text, Color Color)> _badges = new();
	private int _hovered = -1;

	public string Selected { get; private set; }
	public Action<string> OnSelected;

	public PanelTabBar( Widget parent ) : base( parent )
	{
		FixedHeight = HotCodeStyle.TabHeight;
		MouseTracking = true;
	}

	public void AddTab( string key, string text, string icon )
	{
		_tabs.Add( (key, text, icon) );
		Selected ??= key;
		Update();
	}

	public void Select( string key )
	{
		if ( Selected == key ) return;
		Selected = key;
		Update();
		OnSelected?.Invoke( key );
	}

	/// <summary>
	/// A small count shown after a tab's name (null or empty clears it).
	/// </summary>
	public void SetBadge( string key, string text, Color color )
	{
		if ( string.IsNullOrEmpty( text ) ) _badges.Remove( key );
		else _badges[key] = (text, color);
		Update();
	}

	private float TabWidth( int i )
	{
		HotCodeStyle.SetTabFont();
		var width = Paint.MeasureText( _tabs[i].Text ).x + 20 + 22;
		if ( _badges.TryGetValue( _tabs[i].Key, out var badge ) )
		{
			Paint.SetDefaultFont( 7, 600 );
			width += Paint.MeasureText( badge.Text ).x + 14;
		}
		return width;
	}

	private int TabAt( Vector2 pos )
	{
		float x = 4;
		for ( int i = 0; i < _tabs.Count; i++ )
		{
			var w = TabWidth( i );
			if ( pos.x >= x && pos.x < x + w ) return i;
			x += w + 2;
		}
		return -1;
	}

	protected override void OnMouseMove( MouseEvent e )
	{
		base.OnMouseMove( e );
		var i = TabAt( e.LocalPosition );
		if ( i != _hovered ) { _hovered = i; Update(); }
	}

	protected override void OnMouseLeave()
	{
		base.OnMouseLeave();
		_hovered = -1;
		Update();
	}

	protected override void OnMousePress( MouseEvent e )
	{
		base.OnMousePress( e );
		var i = TabAt( e.LocalPosition );
		if ( i >= 0 && e.LeftMouseButton ) Select( _tabs[i].Key );
	}

	protected override void OnPaint()
	{
		Paint.ClearPen();
		Paint.SetBrush( Theme.TabBarBackground );
		Paint.DrawRect( LocalRect );

		float x = 4;
		for ( int i = 0; i < _tabs.Count; i++ )
		{
			var tab = _tabs[i];
			var width = TabWidth( i );
			var selected = tab.Key == Selected;
			var hovered = i == _hovered;

			var content = HotCodeStyle.DrawTab( new Rect( x, 2, width, Height - 2 ), selected, hovered );
			var color = HotCodeStyle.TabTextColor( selected, hovered );

			Paint.SetPen( color );
			Paint.DrawIcon( new Rect( content.Left, content.Top, 16, content.Height ), tab.Icon, 14, TextFlag.LeftCenter );

			HotCodeStyle.SetTabFont();
			var textRect = new Rect( content.Left + 20, content.Top, content.Width - 20, content.Height );
			Paint.DrawText( textRect, tab.Text, TextFlag.LeftCenter );

			if ( _badges.TryGetValue( tab.Key, out var badge ) )
			{
				var textWidth = Paint.MeasureText( tab.Text ).x;
				Paint.SetDefaultFont( 7, 600 );
				var badgeWidth = Paint.MeasureText( badge.Text ).x + 10;
				var badgeRect = new Rect( textRect.Left + textWidth + 6, content.Center.y - 7, badgeWidth, 14 );

				Paint.ClearPen();
				Paint.SetBrush( badge.Color.WithAlpha( 0.25f ) );
				Paint.DrawRect( badgeRect, 7 );
				Paint.SetPen( badge.Color );
				Paint.DrawText( badgeRect, badge.Text, TextFlag.Center );
			}

			x += width + 2;
		}
	}
}

/// <summary>
/// A short vertical line between status bar items.
/// </summary>
public class StatusDivider : Widget
{
	public StatusDivider( Widget parent ) : base( parent )
	{
		FixedWidth = 9;
		FixedHeight = 20;
	}

	protected override void OnPaint()
	{
		Paint.SetPen( Theme.Border );
		Paint.DrawLine( new Vector2( 4, 4 ), new Vector2( 4, Height - 4 ) );
	}
}

/// <summary>
/// A clickable piece of text (with an optional icon) for the status bar.
/// </summary>
public class StatusItem : Widget
{
	private string _text = "";
	private string _icon;
	private Color? _iconColor;

	public Action Clicked;

	public StatusItem( Widget parent, string tooltip = null ) : base( parent )
	{
		ToolTip = tooltip;
		MouseTracking = true;
		FixedHeight = 20;
	}

	public void Set( string text, string icon = null, Color? iconColor = null )
	{
		if ( _text == text && _icon == icon && Nullable.Equals( _iconColor, iconColor ) ) return;

		_text = text ?? "";
		_icon = icon;
		_iconColor = iconColor;

		Paint.SetDefaultFont( HotCodeStyle.FontSize );
		FixedWidth = Paint.MeasureText( _text ).x + (icon is null ? 0 : 18) + 14;
		Visible = _text.Length > 0 || icon is not null;
		Update();
	}

	protected override void OnMouseEnter()
	{
		base.OnMouseEnter();
		Cursor = Clicked is null ? CursorShape.Arrow : CursorShape.Finger;
		Update();
	}

	protected override void OnMouseLeave()
	{
		base.OnMouseLeave();
		Update();
	}

	protected override void OnMousePress( MouseEvent e )
	{
		base.OnMousePress( e );
		if ( e.LeftMouseButton ) Clicked?.Invoke();
	}

	protected override void OnPaint()
	{
		if ( Clicked is not null && IsUnderMouse )
		{
			Paint.ClearPen();
			Paint.SetBrush( Theme.SurfaceBackground );
			Paint.DrawRect( LocalRect, Theme.ControlRadius );
		}

		var x = 7f;
		if ( _icon is not null )
		{
			Paint.SetPen( _iconColor ?? Theme.TextLight );
			Paint.DrawIcon( new Rect( x, 0, 16, Height ), _icon, 13, TextFlag.LeftCenter );
			x += 18;
		}

		Paint.SetDefaultFont( HotCodeStyle.FontSize );
		Paint.SetPen( IsUnderMouse && Clicked is not null ? Theme.Text : Theme.TextLight );
		Paint.DrawText( new Rect( x, 0, Width - x, Height ), _text, TextFlag.LeftCenter );
	}
}