Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
2c40814
Refactor IconPicker to use single 'All Icons' category
EternalDawn1 Nov 27, 2025
85e3471
Merge branch 'Facepunch:master' into test
EternalDawn1 Dec 3, 2025
9f20b21
Merge branch 'master' into test
EternalDawn1 Dec 4, 2025
8968379
Fix parent selection and emoji array formatting
EternalDawn1 Dec 4, 2025
9c1d07b
Normalize indentation in icon-related editor code
EternalDawn1 Dec 4, 2025
bb7a7c1
Revamp IconPickerWidget with categories and recent icons
EternalDawn1 Dec 4, 2025
aefd35d
Fix indentation in IconCategories dictionary
EternalDawn1 Dec 4, 2025
d7fa715
Convert IconPickerWidget to use tabs instead of spaces
EternalDawn1 Dec 4, 2025
b820385
Add emoji support and favorites to IconPickerWidget
EternalDawn1 Dec 4, 2025
deffa5b
Load emoji categories from external JSON file
EternalDawn1 Dec 4, 2025
8b2b211
Merge branch 'master' into test
EternalDawn1 Dec 4, 2025
23d0eee
Refactor IconPickerWidget emoji loading logic
EternalDawn1 Dec 4, 2025
4949615
Merge branch 'test' of https://github.com/EternalDawn1/sbox-public in…
EternalDawn1 Dec 4, 2025
41d9132
Fix indentation in right-click menu handler
EternalDawn1 Dec 4, 2025
f64d929
Merge branch 'master' into test
EternalDawn1 Dec 7, 2025
93a047b
Fix indentation and whitespace in UI components
EternalDawn1 Dec 7, 2025
0e31365
Merge branch 'test' of https://github.com/EternalDawn1/sbox-public in…
EternalDawn1 Dec 7, 2025
b4c64be
Merge branch 'master' into test
EternalDawn1 Dec 8, 2025
2d0bae8
Merge branch 'master' into test
EternalDawn1 Dec 8, 2025
c38cc00
Merge branch 'master' into test
EternalDawn1 Dec 9, 2025
871480e
Merge branch 'master' into test
EternalDawn1 Dec 10, 2025
a64541e
Merge branch 'master' into test
EternalDawn1 Dec 11, 2025
da0b238
Merge branch 'master' into test
EternalDawn1 Dec 15, 2025
930c949
Merge branch 'Facepunch:master' into test
EternalDawn1 Dec 18, 2025
36f0223
Merge branch 'master' into test
EternalDawn1 Dec 23, 2025
fdf1a2e
Merge branch 'master' into test
EternalDawn1 Dec 26, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
namespace Editor;
using System.Linq;
using System.Text;

namespace Editor;

class GameObjectHeader : Widget
{
Expand Down Expand Up @@ -29,6 +32,7 @@ public GameObjectHeader( Widget parent, SerializedObject targetObject ) : base(
left.Add( new GameObjectIconButton( this ) );
}


// 2 rows right
{
var right = topRow.AddColumn();
Expand Down Expand Up @@ -66,7 +70,6 @@ protected override void OnPaint()
Paint.SetBrush( Theme.SurfaceBackground );
Paint.DrawRect( LocalRect );
}

}

/// <summary>
Expand All @@ -79,7 +82,7 @@ protected override void OnPaint()
private Drag _drag;

public GameObjectIconButton( GameObjectHeader parent )
: base( "📦" )
: base( GetCurrentIcon( parent.Target ) ) // Load icon from tags
{
_parent = parent;

Expand All @@ -88,9 +91,173 @@ public GameObjectIconButton( GameObjectHeader parent )
IconSize = 27;
Background = Color.Transparent;

// Use custom color for the button foreground if one is set on the GameObject
Foreground = GetCurrentColor( parent.Target );

IsDraggable = !parent.Target.IsMultipleTargets;
}

private static string GetCurrentIcon( SerializedObject target )
{
var go = target.Targets.OfType<GameObject>().FirstOrDefault();
if ( go is null ) return "📦";

// Check for persistent icon tag (saved with the scene)
var iconTag = go.Tags.FirstOrDefault( t => t.StartsWith( "icon_" ) );
if ( iconTag is not null )
{
var decoded = Editor.IconTagEncoding.DecodeIconFromTag( iconTag );
if ( !string.IsNullOrEmpty( decoded ) )
return decoded;
}

// Default icon
return "📦";
}

private static Color GetCurrentColor( SerializedObject target )
{
var go = target.Targets.OfType<GameObject>().FirstOrDefault();
if ( go is null ) return Color.White;

var colorTag = go.Tags.FirstOrDefault( t => t.StartsWith( "icon_color_" ) );
if ( colorTag is not null )
{
var hex = colorTag.Substring( 11 ); // Remove "icon_color_"
if ( Color.TryParse( $"#{hex}", out var color ) )
{
return color;
}
}

return Color.White;
}

protected override void OnMousePress( MouseEvent e )
{
if ( e.Button == MouseButtons.Left )
{
OnIconClicked();
e.Accepted = true;
}

base.OnMousePress( e );
}

protected override void OnMouseRightClick( MouseEvent e )
{
base.OnMouseRightClick( e );

var menu = new Menu( this );
menu.AddOption( "Change Icon...", "palette", OnIconClicked );
menu.AddSeparator();
menu.AddOption( "Reset to Default", "refresh", ResetToDefault );
menu.OpenAtCursor();

e.Accepted = true;
}

private void OnIconClicked()
{
var go = _parent.Target.Targets.OfType<GameObject>().FirstOrDefault();
if ( go is null ) return;

var currentIcon = GetCurrentIcon( _parent.Target );
var currentColor = GetCurrentColor( _parent.Target );

IconColorPicker.OpenPopup( this, currentIcon, currentColor, ( selectedIcon, selectedColor ) =>
{
// Prepare new tag values
var hasChildren = go.Children.Where( x => x.ShouldShowInHierarchy() ).Any();
var defaultIcon = "📦";
string newIconTag = selectedIcon != defaultIcon ? IconTagEncoding.EncodeIconToTag( selectedIcon ) : null;
string newColorTag = null;
if ( selectedColor != Color.White )
{
newColorTag = $"icon_color_{((int)(selectedColor.r * 255)):X2}{((int)(selectedColor.g * 255)):X2}{((int)(selectedColor.b * 255)):X2}{((int)(selectedColor.a * 255)):X2}";
}

// Apply to all selected targets to avoid inconsistent state
var targets = _parent.Target.Targets.OfType<GameObject>().ToArray();
foreach ( var targetGo in targets )
{
// Remove existing color tag if needed
var existingColorTag = targetGo.Tags.FirstOrDefault( t => t.StartsWith( "icon_color_" ) );
if ( existingColorTag is not null )
{
if ( newColorTag is null || existingColorTag != newColorTag )
targetGo.Tags.Remove( existingColorTag );
}
if ( newColorTag is not null && !targetGo.Tags.Contains( newColorTag ) )
{
targetGo.Tags.Add( newColorTag );
}

// Remove existing icon tag if needed (but not color tags)
var existingIconTag = targetGo.Tags.FirstOrDefault( t => t.StartsWith( "icon_" ) && !t.StartsWith( "icon_color_" ) );
if ( existingIconTag is not null )
{
if ( newIconTag is null || existingIconTag != newIconTag )
targetGo.Tags.Remove( existingIconTag );
}
if ( newIconTag is not null && !targetGo.Tags.Contains( newIconTag ) )
{
targetGo.Tags.Add( newIconTag );
}
}

// Mark the tree item as dirty so it will update its rendering
if ( SceneTreeWidget.Current?.TreeView is { } tv )
{
foreach ( var t in targets )
{
tv.Dirty( t );
}
tv.UpdateIfDirty();
}

// Update the button icon and foreground color
Icon = selectedIcon;
Foreground = selectedColor;
Update();

// Update the scene tree to reflect the change
SceneTreeWidget.Current?.TreeView?.Update();
} );
}

private void ResetToDefault()
{
var targets = _parent.Target.Targets.OfType<GameObject>().ToArray();
foreach ( var targetGo in targets )
{
// Remove all icon-related tags
var iconTags = targetGo.Tags.Where( t => t.StartsWith( "icon_" ) || t.StartsWith( "icon_color_" ) ).ToList();
foreach ( var tag in iconTags )
{
targetGo.Tags.Remove( tag );
}
}

// Update the tree view
if ( SceneTreeWidget.Current?.TreeView is { } tv )
{
foreach ( var t in targets )
{
tv.Dirty( t );
}
tv.UpdateIfDirty();
}

// Reset button to default state
Icon = "📦";
Foreground = Color.White;
Update();

// Update the scene tree to reflect the change
SceneTreeWidget.Current?.TreeView?.Update();
}

protected override void OnDragStart()
{
base.OnDragStart();
Expand Down Expand Up @@ -143,3 +310,69 @@ protected override void OnDragStart()
drag.Execute();
}
}

/// <summary>
/// Custom popup for selecting icon and color.
/// </summary>
file static class IconColorPicker
{
public static void OpenPopup( Widget parent, string currentIcon, Color currentColor, Action<string, Color> onSelected )
{
var popup = new PopupWidget( parent );
popup.Visible = false;
popup.FixedWidth = 300;
popup.Layout = Layout.Column();
popup.Layout.Margin = 8;
popup.Layout.Spacing = 8;

// Track current color
Color selectedColor = currentColor;

// Icon Picker with color button
var iconPicker = popup.Layout.Add( new IconPickerWidget( popup ), 1 );
iconPicker.Icon = currentIcon;

// Update color when changed via the color button
iconPicker.ColorChanged = ( c ) =>
{
selectedColor = c;
onSelected?.Invoke( iconPicker.Icon, selectedColor );
};

// Live update when icon changes
iconPicker.ValueChanged = ( v ) =>
{
onSelected?.Invoke( v, selectedColor );
};

// Buttons
var buttonRow = popup.Layout.AddRow();
buttonRow.Spacing = 4;

var resetButton = buttonRow.Add( new Button( "Reset" ) );
resetButton.Icon = "refresh";
resetButton.Clicked += () =>
{
iconPicker.Icon = "📦";
selectedColor = Color.White;
onSelected?.Invoke( "📦", Color.White );
};

buttonRow.AddStretchCell();

var cancelButton = buttonRow.Add( new Button( "Cancel" ) );
cancelButton.Clicked += () => popup.Destroy();

var okButton = buttonRow.Add( new Button.Primary( "OK" ) );
okButton.Clicked += () =>
{
onSelected?.Invoke( iconPicker.Icon, selectedColor );
popup.Destroy();
};

// Position with offset from cursor to avoid covering the icon
popup.Position = Application.CursorPosition + new Vector2( 10, 10 );
popup.Show();
popup.ConstrainToScreen();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using System;
using System.Text;

namespace Editor
{
public static class IconTagEncoding
{
public static string EncodeIconToTag( string icon )
{
if ( string.IsNullOrEmpty( icon ) ) return null;
var bytes = Encoding.UTF8.GetBytes( icon );
var sb = new StringBuilder();
sb.Append( "icon_" );
foreach ( var b in bytes ) sb.Append( b.ToString( "X2" ) );
return sb.ToString();
}

public static string DecodeIconFromTag( string tag )
{
if ( string.IsNullOrEmpty( tag ) ) return null;
if ( !tag.StartsWith( "icon_" ) ) return null;
var hex = tag.Substring( 5 );
if ( hex.Length == 0 || hex.Length % 2 != 0 ) return null;
try
{
var bytes = new byte[hex.Length / 2];
for ( int i = 0; i < bytes.Length; i++ ) bytes[i] = Convert.ToByte( hex.Substring( i * 2, 2 ), 16 );
return Encoding.UTF8.GetString( bytes );
}
catch
{
return null;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ protected override void OnValueChanged()
var isGameTags = tagset is GameTags;
foreach ( var tag in tagset )
{
// Hide internal icon tags
if ( tag.StartsWith( "icon_" ) || tag.StartsWith( "icon_color_" ) )
continue;

if ( tagCounts.ContainsKey( tag ) )
{
tagCounts[tag]++;
Expand All @@ -78,6 +82,10 @@ protected override void OnValueChanged()
{
foreach ( var tag in gameset.TryGetAll( false ) )
{
// Hide internal icon tags
if ( tag.StartsWith( "icon_" ) || tag.StartsWith( "icon_color_" ) )
continue;

if ( !ownTags.Contains( tag ) )
{
ownTags.Add( tag );
Expand All @@ -93,13 +101,21 @@ protected override void OnValueChanged()

foreach ( var tag in tags )
{
// Hide internal icon tags
if ( tag.StartsWith( "icon_" ) || tag.StartsWith( "icon_color_" ) )
continue;

tagCounts[tag] = 1;
}

if ( tags is GameTags gTags )
{
foreach ( var tag in gTags.TryGetAll( false ) )
{
// Hide internal icon tags
if ( tag.StartsWith( "icon_" ) || tag.StartsWith( "icon_color_" ) )
continue;

if ( !ownTags.Contains( tag ) )
{
ownTags.Add( tag );
Expand Down
Loading