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&box editor.
/// Colours come from <see cref="Theme"/> (s&box's tools/assets/styles/theme.json), so they
/// follow s&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&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&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 );
}
}