Editor/ColorPaletteDock.cs
using Sandbox;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Editor;
[Dock( "Editor", "Color Palette", "palette" )]
public class ColorPaletteDock : Widget
{
const int MaxColors = 24;
const int PaletteColumns = 6;
readonly List<Color> _paletteColors = new();
readonly ColorSlotWidget[] _slots;
readonly List<string> _paletteNames = new();
string _paletteId = "Default";
Widget HeaderBar;
Label PaletteNameLabel;
public string PaletteId
{
get => _paletteId;
set
{
if ( string.IsNullOrEmpty( value ) || _paletteId == value )
return;
_paletteId = value;
SaveActivePalette();
LoadPaletteFromCookie();
UpdateHeader();
}
}
public ColorPaletteDock( Widget parent ) : base( parent )
{
WindowTitle = "Color Palette";
Size = new Vector2( 400, 500 );
FixedWidth = 220;
Layout = Layout.Column();
Layout.Spacing = 0;
CreateHeader();
Layout.AddStretchCell();
var grid = Layout.Grid();
grid.Spacing = 4;
grid.Margin = 8;
Layout.Add( grid );
OnPaintOverride = () =>
{
Paint.SetBrushAndPen( Theme.ControlBackground );
Paint.DrawRect( Paint.LocalRect, 0 );
return false;
};
_slots = new ColorSlotWidget[MaxColors];
for ( int i = 0; i < MaxColors; i++ )
{
var row = i / PaletteColumns;
var col = i % PaletteColumns;
var slot = new ColorSlotWidget( this )
{
FixedSize = 48
};
_slots[i] = slot;
grid.AddCell( row, col, slot );
}
Layout.AddStretchCell();
LoadPalettes();
LoadPaletteFromCookie();
}
void CreateHeader()
{
HeaderBar = new Widget( this );
HeaderBar.Layout = Layout.Row();
HeaderBar.Layout.Spacing = 8;
HeaderBar.Layout.Margin = 8;
HeaderBar.FixedHeight = 36;
HeaderBar.OnPaintOverride = () =>
{
Paint.SetBrushAndPen( Theme.ButtonBackground );
Paint.DrawRect( HeaderBar.LocalRect, 0 );
return false;
};
Layout.Add( HeaderBar );
PaletteNameLabel = new Label( "Default" );
PaletteNameLabel.ToolTip = "Current palette";
HeaderBar.Layout.Add( PaletteNameLabel, 1 );
var menuButton = new Button();
menuButton.Text = "≡";
menuButton.FixedWidth = 36;
menuButton.ToolTip = "Palette menu";
menuButton.Pressed += ShowPaletteMenu;
HeaderBar.Layout.Add( menuButton );
var addButton = new Button();
addButton.Text = "+";
addButton.FixedWidth = 36;
addButton.ToolTip = "Add color from picker";
addButton.Pressed += AddColorFromPicker;
HeaderBar.Layout.Add( addButton );
}
void UpdateHeader()
{
PaletteNameLabel.Text = _paletteId;
}
void ShowPaletteMenu()
{
var m = new ContextMenu();
AddPaletteMenu( m );
m.OpenAtCursor( false );
}
void AddColorFromPicker()
{
// Find the first empty slot
ColorSlotWidget firstEmptySlot = null;
for ( int i = 0; i < _slots.Length; i++ )
{
if ( !IsColorValid( _slots[i].SlotColor ) )
{
firstEmptySlot = _slots[i];
break;
}
}
if ( firstEmptySlot != null )
{
SlotAssignColor( firstEmptySlot );
}
else
{
Log.Warning( "Color palette is full. Please clear a slot first." );
}
}
void LoadPalettes()
{
_paletteNames.Clear();
string rawNames;
try { rawNames = ProjectCookie.Get( "ColorPalette.Names", string.Empty ); }
catch { rawNames = string.Empty; }
if ( string.IsNullOrWhiteSpace( rawNames ) )
{
_paletteNames.Add( "Default" );
}
else
{
foreach ( var name in rawNames.Split( ';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries ) )
{
if ( !_paletteNames.Contains( name ) )
_paletteNames.Add( name );
}
if ( _paletteNames.Count == 0 )
_paletteNames.Add( "Default" );
}
try { _paletteId = ProjectCookie.Get( "ColorPalette.Active", _paletteNames[0] ); }
catch { _paletteId = _paletteNames[0]; }
if ( !_paletteNames.Contains( _paletteId ) )
_paletteId = _paletteNames[0];
}
void SavePalettes()
{
ProjectCookie.Set( "ColorPalette.Names", string.Join( ";", _paletteNames ) );
SaveActivePalette();
}
void SaveActivePalette()
{
ProjectCookie.Set( "ColorPalette.Active", _paletteId ?? string.Empty );
}
internal void AddPaletteMenu( ContextMenu m )
{
LoadPalettes();
var p = m.AddMenu( "Palettes", "palette" );
foreach ( var name in _paletteNames )
{
var localName = name;
var icon = (localName == _paletteId) ? "check" : "palette";
p.AddOption( localName, icon, () => PaletteId = localName );
}
p.AddSeparator();
p.AddOption( "New Palette…", "add", ShowCreatePalettePopup );
p.AddOption( "Rename Palette…", "edit", () => ShowRenamePalettePopup( _paletteId ) ).Enabled = _paletteNames.Count > 0;
p.AddOption( "Duplicate Palette", "content_copy", () => DuplicatePalette( _paletteId ) ).Enabled = _paletteNames.Count > 0;
var del = p.AddOption( "Delete Palette", "delete", () => DeletePalette( _paletteId ) );
del.Enabled = _paletteNames.Count > 1;
m.AddSeparator();
m.AddOption( "Clear All Colors", "clear", ClearAllColors ).Enabled = _paletteColors.Any( c => IsColorValid( c ) );
}
void ShowCreatePalettePopup()
{
var popup = new PopupWidget( this );
popup.FixedWidth = 220;
popup.Layout = Layout.Column();
popup.Layout.Margin = 8;
popup.Layout.Spacing = 4;
_ = popup.Layout.Add( new Label.Small( "New palette" ) );
var entry = popup.Layout.Add( new LineEdit( popup ) );
entry.FixedHeight = Theme.RowHeight;
entry.PlaceholderText = "Palette name…";
void Commit()
{
var name = entry.Value?.Trim();
if ( string.IsNullOrEmpty( name ) ) { popup.Destroy(); return; }
if ( _paletteNames.Contains( name ) ) { popup.Destroy(); return; }
_paletteNames.Add( name );
_paletteId = name;
SavePalettes();
LoadPaletteFromCookie();
UpdateHeader();
popup.Destroy();
}
entry.ReturnPressed += Commit;
popup.OpenAtCursor();
entry.Focus();
}
void ShowRenamePalettePopup( string oldName )
{
if ( string.IsNullOrEmpty( oldName ) )
return;
var popup = new PopupWidget( this );
popup.FixedWidth = 220;
popup.Layout = Layout.Column();
popup.Layout.Margin = 8;
popup.Layout.Spacing = 4;
_ = popup.Layout.Add( new Label.Small( "Rename palette" ) );
var entry = popup.Layout.Add( new LineEdit( popup ) );
entry.FixedHeight = Theme.RowHeight;
entry.Value = oldName;
void Commit()
{
var newName = entry.Value?.Trim();
if ( string.IsNullOrEmpty( newName ) || newName == oldName ) { popup.Destroy(); return; }
if ( _paletteNames.Contains( newName ) ) { popup.Destroy(); return; }
RenamePalette( oldName, newName );
popup.Destroy();
}
entry.ReturnPressed += Commit;
popup.OpenAtCursor();
entry.Focus();
}
void RenamePalette( string oldName, string newName )
{
var idx = _paletteNames.IndexOf( oldName );
if ( idx < 0 ) return;
_paletteNames[idx] = newName;
var oldKey = $"ColorPalette.{oldName}";
var newKey = $"ColorPalette.{newName}";
try
{
var data = ProjectCookie.Get( oldKey, string.Empty );
ProjectCookie.Set( newKey, data );
ProjectCookie.Set( oldKey, string.Empty );
}
catch { }
if ( _paletteId == oldName )
_paletteId = newName;
SavePalettes();
LoadPaletteFromCookie();
UpdateHeader();
}
void DuplicatePalette( string sourceName )
{
if ( string.IsNullOrEmpty( sourceName ) )
return;
var baseName = $"{sourceName} Copy";
var newName = baseName;
int counter = 2;
while ( _paletteNames.Contains( newName ) )
newName = $"{baseName} {counter++}";
_paletteNames.Add( newName );
var srcKey = $"ColorPalette.{sourceName}";
var dstKey = $"ColorPalette.{newName}";
try
{
var data = ProjectCookie.Get( srcKey, string.Empty );
ProjectCookie.Set( dstKey, data );
}
catch { }
_paletteId = newName;
SavePalettes();
LoadPaletteFromCookie();
UpdateHeader();
}
void DeletePalette( string name )
{
if ( _paletteNames.Count <= 1 )
return;
var idx = _paletteNames.IndexOf( name );
if ( idx < 0 )
return;
_paletteNames.RemoveAt( idx );
var key = $"ColorPalette.{name}";
try { ProjectCookie.Set( key, string.Empty ); }
catch { }
_paletteId = _paletteNames[Math.Clamp( idx - 1, 0, _paletteNames.Count - 1 )];
SavePalettes();
LoadPaletteFromCookie();
UpdateHeader();
}
void ClearAllColors()
{
_paletteColors.Clear();
UpdateSlots();
SavePaletteToCookie();
}
public void AddColor( Color color )
{
// Remove duplicates
_paletteColors.RemoveAll( c => ColorsEqual( c, color ) );
// Add to the first empty slot or append
var firstEmptyIndex = -1;
for ( int i = 0; i < _slots.Length; i++ )
{
if ( i >= _paletteColors.Count || !IsColorValid( _paletteColors[i] ) )
{
firstEmptyIndex = i;
break;
}
}
if ( firstEmptyIndex >= 0 )
{
while ( _paletteColors.Count <= firstEmptyIndex )
_paletteColors.Add( default );
_paletteColors[firstEmptyIndex] = color;
}
else if ( _paletteColors.Count < _slots.Length )
{
_paletteColors.Add( color );
}
UpdateSlots();
SavePaletteToCookie();
}
internal bool IsColorValid( Color color )
{
// Check if color has been set (not default)
return color.a > 0;
}
bool ColorsEqual( Color a, Color b, float tolerance = 0.001f )
{
return Math.Abs( a.r - b.r ) < tolerance &&
Math.Abs( a.g - b.g ) < tolerance &&
Math.Abs( a.b - b.b ) < tolerance &&
Math.Abs( a.a - b.a ) < tolerance;
}
void UpdateSlots()
{
for ( int i = 0; i < _slots.Length; i++ )
{
if ( i < _paletteColors.Count && IsColorValid( _paletteColors[i] ) )
_slots[i].SlotColor = _paletteColors[i];
else
_slots[i].SlotColor = default;
}
}
internal void SlotClicked( Color color )
{
if ( !IsColorValid( color ) ) return;
// Copy to clipboard
var hexColor = ColorToHex( color );
EditorUtility.Clipboard.Copy( hexColor );
// Log feedback (Toast doesn't exist in s&box editor)
Log.Info( $"Color copied to clipboard: {hexColor}" );
}
internal string ColorToHex( Color color )
{
int r = (int)(color.r * 255);
int g = (int)(color.g * 255);
int b = (int)(color.b * 255);
return $"#{r:X2}{g:X2}{b:X2}";
}
private void SwapColors( ColorSlotWidget targetSlot, Color draggedColor )
{
var targetIndex = Array.IndexOf( _slots, targetSlot );
if ( targetIndex < 0 ) return;
// Find the source slot with the dragged color
var sourceIndex = -1;
for ( int i = 0; i < _paletteColors.Count; i++ )
{
if ( IsColorValid( _paletteColors[i] ) && ColorsEqual( _paletteColors[i], draggedColor ) )
{
sourceIndex = i;
break;
}
}
if ( sourceIndex < 0 ) return;
// Ensure both indices are within bounds
while ( _paletteColors.Count <= Math.Max( sourceIndex, targetIndex ) )
_paletteColors.Add( default );
// Swap the colors
var temp = _paletteColors[targetIndex];
_paletteColors[targetIndex] = _paletteColors[sourceIndex];
_paletteColors[sourceIndex] = temp;
UpdateSlots();
SavePaletteToCookie();
}
private void SlotSetColor( ColorSlotWidget slot, Color color )
{
if ( slot is null ) return;
var index = Array.IndexOf( _slots, slot );
if ( index < 0 ) return;
if ( index >= _paletteColors.Count )
{
while ( _paletteColors.Count <= index )
_paletteColors.Add( default );
}
_paletteColors[index] = color;
UpdateSlots();
SavePaletteToCookie();
}
private void SlotAssignColor( ColorSlotWidget slot )
{
var popup = new PopupWidget( this );
popup.Layout = Layout.Column();
popup.Layout.Margin = 12;
popup.Layout.Spacing = 8;
var label = new Label.Small( "Select a color:" );
popup.Layout.Add( label );
var picker = new ColorPicker( popup );
picker.FixedSize = new Vector2( 300, 400 );
picker.Value = IsColorValid( slot.SlotColor ) ? slot.SlotColor : Color.White;
popup.Layout.Add( picker );
var buttonLayout = popup.Layout.AddRow();
buttonLayout.Spacing = 8;
var cancelButton = new Button( "Cancel" );
cancelButton.Pressed += () => popup.Destroy();
buttonLayout.Add( cancelButton, 1 );
var setButton = new Button( "Set Color" );
setButton.Pressed += () =>
{
SlotSetColor( slot, picker.Value );
popup.Destroy();
};
buttonLayout.Add( setButton, 1 );
popup.OpenAtCursor();
}
private void SlotClear( ColorSlotWidget slot ) => SlotSetColor( slot, default );
void SavePaletteToCookie()
{
// Ensure we have exactly MaxColors entries
while ( _paletteColors.Count < MaxColors )
_paletteColors.Add( default );
var parts = _paletteColors
.Take( MaxColors )
.Select( c => IsColorValid( c ) ? ColorToHex( c ) : string.Empty );
ProjectCookie.Set( $"ColorPalette.{_paletteId}", string.Join( ";", parts ) );
}
void LoadPaletteFromCookie()
{
string data;
try { data = ProjectCookie.Get( $"ColorPalette.{_paletteId}", string.Empty ); }
catch { data = string.Empty; }
_paletteColors.Clear();
if ( string.IsNullOrEmpty( data ) )
{
UpdateSlots();
return;
}
var parts = data.Split( ';' );
for ( int i = 0; i < MaxColors; i++ )
{
if ( i >= parts.Length || string.IsNullOrWhiteSpace( parts[i] ) )
{
_paletteColors.Add( default );
continue;
}
var hexString = parts[i].Trim();
if ( TryParseHex( hexString, out var color ) )
{
_paletteColors.Add( color );
}
else
{
_paletteColors.Add( default );
}
}
UpdateSlots();
}
bool TryParseHex( string hex, out Color color )
{
color = default;
if ( string.IsNullOrEmpty( hex ) )
return false;
hex = hex.TrimStart( '#' );
if ( hex.Length != 6 && hex.Length != 8 )
return false;
try
{
int r = Convert.ToInt32( hex.Substring( 0, 2 ), 16 );
int g = Convert.ToInt32( hex.Substring( 2, 2 ), 16 );
int b = Convert.ToInt32( hex.Substring( 4, 2 ), 16 );
int a = hex.Length == 8 ? Convert.ToInt32( hex.Substring( 6, 2 ), 16 ) : 255;
color = new Color( r / 255f, g / 255f, b / 255f, a / 255f );
return true;
}
catch
{
return false;
}
}
class ColorSlotWidget : Widget
{
readonly ColorPaletteDock _dock;
bool _isValidDropHover;
public Color SlotColor
{
get => field;
set
{
if ( field == value ) return;
field = value;
Update();
}
}
public ColorSlotWidget( ColorPaletteDock dock ) : base( dock )
{
_dock = dock;
Cursor = CursorShape.Finger;
ToolTip = "Left click to copy, drag to reorder, right click for options";
IsDraggable = true;
AcceptDrops = true;
}
protected override void OnMouseClick( MouseEvent e )
{
base.OnMouseClick( e );
if ( e.LeftMouseButton )
{
if ( _dock.IsColorValid( SlotColor ) )
{
_dock.SlotClicked( SlotColor );
}
else
{
_dock.SlotAssignColor( this );
}
}
}
protected override void OnDragStart()
{
if ( !_dock.IsColorValid( SlotColor ) )
return;
var drag = new Drag( this );
drag.Data.Object = SlotColor;
drag.Execute();
}
public override void OnDragLeave()
{
base.OnDragLeave();
_isValidDropHover = false;
Update();
}
public override void OnDragHover( DragEvent ev )
{
if ( ev.Data.Object is Color )
{
ev.Action = DropAction.Move;
_isValidDropHover = true;
Update();
}
}
public override void OnDragDrop( DragEvent ev )
{
base.OnDragDrop( ev );
if ( ev.Data.Object is Color droppedColor )
{
_dock.SwapColors( this, droppedColor );
ev.Action = DropAction.Move;
}
_isValidDropHover = false;
Update();
}
protected override void OnContextMenu( ContextMenuEvent e )
{
var m = new ContextMenu();
bool hasColor = _dock.IsColorValid( SlotColor );
if ( hasColor )
{
var hexColor = _dock.ColorToHex( SlotColor );
m.AddOption( $"Copy {hexColor}", "content_copy", () => _dock.SlotClicked( SlotColor ) );
m.AddSeparator();
}
var text = hasColor ? "Change Color" : "Set Color";
m.AddOption( text, "palette", () => _dock.SlotAssignColor( this ) );
m.AddSeparator();
_dock.AddPaletteMenu( m );
m.AddSeparator();
m.AddOption( "Clear", "backspace", () => _dock.SlotClear( this ) ).Enabled = hasColor;
m.OpenAtCursor( false );
e.Accepted = true;
}
protected override void OnPaint()
{
Paint.ClearPen();
Paint.ClearBrush();
var controlRect = Paint.LocalRect;
controlRect = controlRect.Shrink( 2 );
Paint.Antialiasing = true;
Paint.TextAntialiasing = true;
if ( _dock.IsColorValid( SlotColor ) )
{
// Draw color
Paint.SetBrush( SlotColor );
Paint.DrawRect( controlRect );
// Draw border
if ( _isValidDropHover )
{
Paint.SetPen( Theme.Green );
Paint.DrawRect( controlRect );
}
else if ( Paint.HasMouseOver )
{
Paint.SetPen( Color.White, 3, PenStyle.Dot );
Paint.DrawRect( controlRect );
}
else
{
Paint.SetPen( SlotColor.Darken( 0.3f ), 2, PenStyle.Dot );
Paint.DrawRect( controlRect );
}
}
else
{
// Empty slot
var baseFill = Theme.Text.WithAlpha( 0.01f );
var baseLine = Theme.Text.WithAlpha( 0.1f );
var iconColor = Theme.Text.WithAlpha( 0.1f );
if ( _isValidDropHover )
{
baseFill = Theme.Green.WithAlpha( 0.05f );
baseLine = Theme.Green.WithAlpha( 0.8f );
iconColor = Theme.Green;
}
else if ( Paint.HasMouseOver )
{
baseFill = Theme.Text.WithAlpha( 0.04f );
baseLine = Theme.Text.WithAlpha( 0.2f );
iconColor = Theme.Text.WithAlpha( 0.2f );
}
Paint.SetBrushAndPen( baseFill, baseLine, style: _isValidDropHover ? PenStyle.Solid : PenStyle.Dot );
Paint.DrawRect( controlRect, 4 );
Paint.SetPen( iconColor );
Paint.DrawIcon( LocalRect.Shrink( 2 ), "add", 16 );
}
}
}
}