Editor/Widgets/SuiDockPanel.cs
using Editor;
using Sandbox;
namespace SboxUiDesigner.EditorUi.Widgets;
/// <summary>
/// Visual imitation of a docked panel with a real Qt-style tab title.
///
/// Shape:
/// ┌─ 1px top gap (root bg shows here) ──────────────────────────┐
/// │ ╔════════════╗ │
/// │ ║ ⛏ Palette × ║ (rest of tab-row is transparent: root bg) │
/// │ ╚════════════╝────────────────────────────────────────────── │
/// │ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ │
/// │ ▓ Body fill #212120, content goes here, bottom corners │
/// │ ▓ rounded, top-left corner is square (covered by tab). │
/// │ ▓ │
/// └─────────────────────────────────────────────────────────────┘
///
/// The tab and body share the same fill color; only the bottom border of the
/// tab is a 2px blue line (#0F3F79) — same accent used on the active editor
/// tab (Designer / Preview / Code).
/// </summary>
public sealed class SuiDockPanel : Widget
{
public string Title { get; private set; }
public string Icon { get; private set; }
private Widget _tabRow;
private SuiDockTab _tab;
private Widget _body;
private Widget _content;
public SuiDockPanel( string title, string icon, Widget content, Widget parent = null ) : base( parent )
{
Title = title;
Icon = icon;
// Outer container is transparent — root bg shows through gaps.
// 1px top margin = the gap above the tab the user asked for.
SetStyles( "background-color: transparent; border: none;" );
Layout = Layout.Column();
Layout.Margin = new Sandbox.UI.Margin( 0, 1, 0, 0 );
Layout.Spacing = 0;
// Tab row — transparent, holds the tab on the left and a stretch
// cell that lets the root bg show on the right.
_tabRow = new Widget( this );
_tabRow.SetStyles( "background-color: transparent; border: none;" );
_tabRow.Layout = Layout.Row();
_tabRow.Layout.Margin = 0;
_tabRow.Layout.Spacing = 0;
_tabRow.FixedHeight = 35;
_tab = new SuiDockTab( title, icon );
_tab.Parent = _tabRow;
_tabRow.Layout.Add( _tab );
_tabRow.Layout.AddStretchCell();
Layout.Add( _tabRow );
// Body — same fill as the tab so they read as one continuous surface.
// Top-left corner is square (covered by tab); the other three corners
// are rounded.
_body = new Widget( this );
_body.SetStyles(
"background-color: rgb(33,33,32);" +
"border: none;" +
"border-top-left-radius: 0;" +
"border-top-right-radius: 4px;" +
"border-bottom-left-radius: 4px;" +
"border-bottom-right-radius: 4px;" );
_body.Layout = Layout.Column();
_body.Layout.Margin = 0;
_body.Layout.Spacing = 0;
if ( content != null )
{
_content = content;
content.Parent = _body;
_body.Layout.Add( content, 1 );
}
Layout.Add( _body, 1 );
}
public Widget Content => _content;
}
/// <summary>
/// The tab pill itself — fill matches the body, rounded top corners, square
/// bottom corners, and a 2px blue underline at the bottom (active accent).
/// </summary>
internal sealed class SuiDockTab : Widget
{
public string Title;
public string Icon;
private bool _sized;
public SuiDockTab( string title, string icon, Widget parent = null ) : base( parent )
{
Title = title ?? "";
Icon = icon;
FixedHeight = 35;
// Initial conservative width — recalibrated on first paint via MeasureText.
var iconW = string.IsNullOrEmpty( icon ) ? 0 : 20;
FixedWidth = 12 + iconW + (Title.Length * 8) + 8 + 22;
SetStyles(
"background-color: rgb(33,33,32);" +
"border: none;" +
"border-top-left-radius: 4px;" +
"border-top-right-radius: 4px;" +
"border-bottom-left-radius: 0;" +
"border-bottom-right-radius: 0;" );
}
protected override void OnPaint()
{
Paint.SetDefaultFont( 11 );
// Calibrate width to actual text on first paint.
if ( !_sized )
{
var titleW0 = string.IsNullOrEmpty( Title ) ? 0 : Paint.MeasureText( Title ).x;
var iconW = string.IsNullOrEmpty( Icon ) ? 0 : 20;
int newW = (int)(12 + iconW + titleW0 + 8 + 22);
_sized = true;
if ( newW != FixedWidth )
{
FixedWidth = newW;
return;
}
}
// Active accent — 2px blue line at the bottom of the tab.
Paint.SetBrushAndPen( new Color( 15 / 255f, 63 / 255f, 121 / 255f ) );
Paint.DrawRect( new Rect( 0, Height - 2, Width, 2 ) );
// Icon + title + × close.
Paint.SetPen( new Color( 220 / 255f, 224 / 255f, 230 / 255f ) );
float x = 12f;
if ( !string.IsNullOrEmpty( Icon ) )
{
Paint.DrawIcon( new Rect( x, (Height - 14) / 2f, 14, 14 ), Icon, 14 );
x += 20f;
}
float titleW = 0;
if ( !string.IsNullOrEmpty( Title ) )
{
titleW = Paint.MeasureText( Title ).x;
Paint.DrawText( new Rect( x, 0, titleW + 2, Height ), Title, TextFlag.LeftCenter );
x += titleW + 8;
}
// × close icon — visual only for now (no click handler wired).
Paint.SetPen( new Color( 150 / 255f, 156 / 255f, 165 / 255f ) );
Paint.DrawIcon( new Rect( x, (Height - 14) / 2f, 14, 14 ), "close", 14 );
}
}