Editor/Widgets/SuiTabStrip.cs
using System;
using System.Collections.Generic;
using Editor;
using Sandbox;
namespace SboxUiDesigner.EditorUi.Widgets;
/// <summary>
/// Custom tab strip — pure widget, paint-only. Designer/Preview/Code style:
/// active tab has a solid blue fill (#0F3F79 — user spec), inactive tabs
/// are flat with grey text. Subtle vertical separators between tabs match
/// the top-bar look.
///
/// Background: #141414 (canvas dark area).
/// </summary>
public sealed class SuiTabStrip : Widget
{
private readonly List<SuiTabStripItem> _tabs = new();
private int _active = 0;
public event Action<int> ActiveTabChanged;
public int ActiveIndex
{
get => _active;
set
{
if ( _active == value ) return;
_active = value;
foreach ( var t in _tabs ) t.Update();
ActiveTabChanged?.Invoke( _active );
}
}
public SuiTabStrip( Widget parent = null ) : base( parent )
{
FixedHeight = 36;
Layout = Layout.Row();
Layout.Margin = new Sandbox.UI.Margin( 0, 0, 0, 0 );
Layout.Spacing = 0;
}
public void AddTab( string label, string icon = null )
{
var idx = _tabs.Count;
// Insert vertical separator between tabs (not before first).
if ( _tabs.Count > 0 )
{
var sep = new SuiTabStripSeparator();
Layout.Add( sep );
}
var tab = new SuiTabStripItem( this, idx, label, icon );
_tabs.Add( tab );
Layout.Add( tab );
}
public void AddStretch() => Layout.AddStretchCell();
/// <summary>Push remaining items to the right by inserting a stretch
/// cell after the last tab. Called automatically by parent so tabs
/// stay anchored on the left and don't expand to fill the row.</summary>
public void FinishLeftAligned() => Layout.AddStretchCell();
internal bool IsActive( int idx ) => idx == _active;
internal void OnTabClicked( int idx )
{
ActiveIndex = idx;
}
protected override void OnPaint()
{
// Background: #141414 — canvas dark area.
Paint.SetBrushAndPen( new Color( 20 / 255f, 20 / 255f, 20 / 255f ) );
Paint.DrawRect( LocalRect );
}
}
internal sealed class SuiTabStripItem : Widget
{
private readonly SuiTabStrip _strip;
private readonly int _index;
public string Label;
public string Icon;
private bool _hover;
private bool _sized;
private const int FontSize = 11;
private const int PadH = 14;
private const int IconSize = 12;
private const int IconGap = 6;
public SuiTabStripItem( SuiTabStrip strip, int index, string label, string icon ) : base( strip )
{
_strip = strip;
_index = index;
Label = label ?? "";
Icon = icon;
Cursor = CursorShape.Finger;
FixedHeight = 36;
// Conservative initial width.
var iconBlock = string.IsNullOrEmpty( icon ) ? 0 : IconSize + IconGap;
FixedWidth = PadH + iconBlock + (Label.Length * 10) + PadH;
}
protected override void OnPaint()
{
Paint.SetDefaultFont( FontSize );
if ( !_sized )
{
var labelW = string.IsNullOrEmpty( Label ) ? 0 : Paint.MeasureText( Label ).x;
var iconBlock = string.IsNullOrEmpty( Icon ) ? 0 : IconSize + IconGap;
int newW = (int)(PadH + iconBlock + labelW + 2 + PadH);
_sized = true;
if ( newW != FixedWidth )
{
FixedWidth = newW;
return;
}
}
var rect = LocalRect;
bool active = _strip.IsActive( _index );
// Active tab: solid blue #0F3F79.
if ( active )
{
Paint.SetBrushAndPen( new Color( 15 / 255f, 63 / 255f, 121 / 255f ) );
Paint.DrawRect( rect );
}
else if ( _hover )
{
Paint.SetBrushAndPen( Color.White.WithAlpha( 0.04f ) );
Paint.DrawRect( rect );
}
// Text + icon.
var textColor = active
? new Color( 240 / 255f, 244 / 255f, 250 / 255f )
: new Color( 165 / 255f, 170 / 255f, 178 / 255f );
Paint.SetPen( textColor );
float x = PadH;
if ( !string.IsNullOrEmpty( Icon ) )
{
var iconRect = new Rect( x, (Height - IconSize) / 2f, IconSize, IconSize );
Paint.DrawIcon( iconRect, Icon, IconSize );
x += IconSize + IconGap;
}
if ( !string.IsNullOrEmpty( Label ) )
{
var labelW = Paint.MeasureText( Label ).x;
var labelRect = new Rect( x, 0, labelW + 2, Height );
Paint.DrawText( labelRect, Label, TextFlag.LeftCenter );
}
}
protected override void OnMouseEnter() { _hover = true; Update(); }
protected override void OnMouseLeave() { _hover = false; Update(); }
protected override void OnMousePress( MouseEvent e )
{
if ( e.LeftMouseButton ) _strip.OnTabClicked( _index );
}
}
/// <summary>
/// Subtle 1px vertical line between tab items — same look as the top-bar
/// group separators (rgba(255,255,255,0.08), 55% of widget height, centered).
/// </summary>
internal sealed class SuiTabStripSeparator : Widget
{
public SuiTabStripSeparator( Widget parent = null ) : base( parent )
{
FixedWidth = 11;
FixedHeight = 36;
}
protected override void OnPaint()
{
float lineHeight = Height * 0.55f;
float yTop = (Height - lineHeight) / 2f;
float xCenter = Width / 2f;
Paint.SetPen( Color.White.WithAlpha( 0.08f ) );
Paint.DrawLine( new Vector2( xCenter, yTop ), new Vector2( xCenter, yTop + lineHeight ) );
}
}