Skip to content

Commit 2c40814

Browse files
committed
Refactor IconPicker to use single 'All Icons' category
Removed emoji category tabs and consolidated all emojis into a single 'All Icons' category. This simplifies the UI and internal logic by eliminating category selection and related code. Remove icon search filter from IconPickerWidget since its unuseable Eliminated the filter functionality and related UI elements from IconPickerWidget, simplifying pagination and icon retrieval logic. The widget now displays icons by category without search or filter support. Update IconPickerWidget.cs Expand emoji lists in IconPickerWidget Added more symbols and flag emojis to the SymbolsEmojis and FlagsEmojis arrays in IconPickerWidget.cs to provide a broader selection for users. Improve page handling in IconPickerWidget Resets page index when switching categories based on filter state and adds robust bounds checking for page numbers in Rebuild(). Also removes unused emoji generation code for clarity. Handle icon color tags with variable hex length Added support for both AARRGGBB and RRGGBB formats in icon color tags. Now, color changes properly invalidate the node and fallback to full alpha if only RRGGBB is provided. Add emoji icon support and session icon storage Introduces session-only custom icon storage for GameObjects and adds emoji icon support to the icon picker. GameObject icons now prefer persisted tags (with encoding/decoding) and fall back to session storage. The icon picker UI is updated to allow category selection and display emoji icons. Scene tree and inspector logic updated to handle new icon sources and ensure UI updates on icon changes. Update default icon logic for GameObjects Default icon selection now considers visible children in addition to components. If a GameObject has children that should show in the hierarchy, 'folder_open' is used; otherwise, 'layers' is used for objects with components, and 'folder' for empty objects. Update default GameObject icon logic Changed default icon from 'layers' to 'folder' for GameObjects without components in both inspector and scene tree. The icon now reflects whether a GameObject has components, improving visual clarity. Add custom icon and color support for GameObjects Introduces the ability to set both custom icons and colors for GameObjects via tags. Updates the inspector and scene tree to reflect these customizations, and adds a new IconColorPicker popup for selecting icon and color. Add support for custom GameObject icons via tags Introduced the ability to set custom icons for GameObjects using tags prefixed with 'icon_'. The GameObject inspector header now allows users to pick an icon, which updates the tag and refreshes the scene tree. The scene tree node rendering logic was updated to display the custom icon if present.
1 parent 8b1d58d commit 2c40814

5 files changed

Lines changed: 364 additions & 2277 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace Editor;
2+
3+
public static class CustomIconStorage
4+
{
5+
// Session-only storage of custom icons keyed by GameObject reference
6+
public static Dictionary<GameObject, string> Icons = new();
7+
}

game/addons/tools/Code/Scene/GameObjectInspector/GameObjectHeader/GameObjectHeader.cs

Lines changed: 192 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
namespace Editor;
1+
using System.Linq;
2+
using System.Text;
3+
4+
namespace Editor;
25

36
class GameObjectHeader : Widget
47
{
@@ -28,6 +31,7 @@ public GameObjectHeader( Widget parent, SerializedObject targetObject ) : base(
2831
var left = topRow.AddRow();
2932
left.Add( new GameObjectIconButton( this ) );
3033
}
34+
3135

3236
// 2 rows right
3337
{
@@ -66,7 +70,6 @@ protected override void OnPaint()
6670
Paint.SetBrush( Theme.SurfaceBackground );
6771
Paint.DrawRect( LocalRect );
6872
}
69-
7073
}
7174

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

8184
public GameObjectIconButton( GameObjectHeader parent )
82-
: base( "📦" )
85+
: base( GetCurrentIcon( parent.Target ), null )
8386
{
8487
_parent = parent;
8588

@@ -88,9 +91,144 @@ public GameObjectIconButton( GameObjectHeader parent )
8891
IconSize = 27;
8992
Background = Color.Transparent;
9093

94+
// Use custom color for the button foreground if one is set on the GameObject
95+
Foreground = GetCurrentColor( parent.Target );
96+
9197
IsDraggable = !parent.Target.IsMultipleTargets;
9298
}
9399

100+
private static string GetCurrentIcon( SerializedObject target )
101+
{
102+
var go = target.Targets.OfType<GameObject>().FirstOrDefault();
103+
if ( go is null ) return "folder";
104+
105+
// Check for persistent icon tag first (saved with the scene)
106+
var iconTag = go.Tags.FirstOrDefault( t => t.StartsWith( "icon_" ) );
107+
if ( iconTag is not null )
108+
{
109+
var decoded = Editor.IconTagEncoding.DecodeIconFromTag( iconTag );
110+
if ( !string.IsNullOrEmpty( decoded ) )
111+
return decoded;
112+
}
113+
114+
// Fallback to session-only storage
115+
if ( CustomIconStorage.Icons.TryGetValue( go, out var customIcon ) )
116+
{
117+
return customIcon;
118+
}
119+
120+
// Default icon based on children and components
121+
return go.Children.Where( x => x.ShouldShowInHierarchy() ).Any() ? "📂" : (go.Components.Count > 0 ? "layers" : "📁");
122+
}
123+
124+
private static Color GetCurrentColor( SerializedObject target )
125+
{
126+
var go = target.Targets.OfType<GameObject>().FirstOrDefault();
127+
if ( go is null ) return Color.White;
128+
129+
var colorTag = go.Tags.FirstOrDefault( t => t.StartsWith( "icon_color_" ) );
130+
if ( colorTag is not null )
131+
{
132+
var hex = colorTag.Substring( 11 ); // Remove "icon_color_"
133+
if ( Color.TryParse( $"#{hex}", out var color ) )
134+
{
135+
return color;
136+
}
137+
}
138+
139+
return Color.White;
140+
}
141+
142+
protected override void OnMousePress( MouseEvent e )
143+
{
144+
if ( e.Button == MouseButtons.Left )
145+
{
146+
OnIconClicked();
147+
e.Accepted = true;
148+
}
149+
150+
base.OnMousePress( e );
151+
}
152+
153+
private void OnIconClicked()
154+
{
155+
var go = _parent.Target.Targets.OfType<GameObject>().FirstOrDefault();
156+
if ( go is null ) return;
157+
158+
var currentIcon = GetCurrentIcon( _parent.Target );
159+
var currentColor = GetCurrentColor( _parent.Target );
160+
161+
IconColorPicker.OpenPopup( this, currentIcon, currentColor, ( selectedIcon, selectedColor ) =>
162+
{
163+
// Prepare new tag values
164+
var hasChildren = go.Children.Where( x => x.ShouldShowInHierarchy() ).Any();
165+
var defaultIcon = hasChildren ? "folder_open" : (go.Components.Count > 0 ? "layers" : "folder");
166+
string newIconTag = selectedIcon != defaultIcon ? IconTagEncoding.EncodeIconToTag( selectedIcon ) : null;
167+
string newColorTag = null;
168+
if ( selectedColor != Color.White )
169+
{
170+
newColorTag = $"icon_color_{((int)(selectedColor.r * 255)):X2}{((int)(selectedColor.g * 255)):X2}{((int)(selectedColor.b * 255)):X2}{((int)(selectedColor.a * 255)):X2}";
171+
}
172+
173+
// Apply to all selected targets to avoid inconsistent state
174+
var targets = _parent.Target.Targets.OfType<GameObject>().ToArray();
175+
foreach ( var targetGo in targets )
176+
{
177+
// Icon (store by Id)
178+
if ( selectedIcon == defaultIcon )
179+
{
180+
CustomIconStorage.Icons.Remove( targetGo );
181+
}
182+
else
183+
{
184+
CustomIconStorage.Icons[targetGo] = selectedIcon;
185+
}
186+
187+
// Color tag (keep using tags for color)
188+
var existingColorTag = targetGo.Tags.FirstOrDefault( t => t.StartsWith( "icon_color_" ) );
189+
if ( existingColorTag is not null )
190+
{
191+
if ( newColorTag is null || existingColorTag != newColorTag )
192+
targetGo.Tags.Remove( existingColorTag );
193+
}
194+
if ( newColorTag is not null && !targetGo.Tags.Contains( newColorTag ) )
195+
{
196+
targetGo.Tags.Add( newColorTag );
197+
}
198+
199+
// Icon tag (persisted with the scene)
200+
var existingIconTag = targetGo.Tags.FirstOrDefault( t => t.StartsWith( "icon_" ) );
201+
if ( existingIconTag is not null )
202+
{
203+
if ( newIconTag is null || existingIconTag != newIconTag )
204+
targetGo.Tags.Remove( existingIconTag );
205+
}
206+
if ( newIconTag is not null && !targetGo.Tags.Contains( newIconTag ) )
207+
{
208+
targetGo.Tags.Add( newIconTag );
209+
}
210+
}
211+
212+
// Mark the tree item as dirty so it will update its rendering
213+
if ( SceneTreeWidget.Current?.TreeView is { } tv )
214+
{
215+
foreach ( var t in targets )
216+
{
217+
tv.Dirty( t );
218+
}
219+
tv.UpdateIfDirty();
220+
}
221+
222+
// Update the button icon and foreground color
223+
Icon = selectedIcon;
224+
Foreground = selectedColor;
225+
Update();
226+
227+
// Update the scene tree to reflect the change
228+
SceneTreeWidget.Current?.TreeView?.Update();
229+
} );
230+
}
231+
94232
protected override void OnDragStart()
95233
{
96234
base.OnDragStart();
@@ -143,3 +281,54 @@ protected override void OnDragStart()
143281
drag.Execute();
144282
}
145283
}
284+
285+
/// <summary>
286+
/// Custom popup for selecting icon and color.
287+
/// </summary>
288+
file static class IconColorPicker
289+
{
290+
public static void OpenPopup( Widget parent, string currentIcon, Color currentColor, Action<string, Color> onSelected )
291+
{
292+
var popup = new PopupWidget( parent );
293+
popup.Visible = false;
294+
popup.FixedWidth = 300;
295+
popup.Layout = Layout.Column();
296+
popup.Layout.Margin = 8;
297+
popup.Layout.Spacing = 8;
298+
299+
// Icon Picker
300+
var iconPicker = popup.Layout.Add( new IconPickerWidget( popup ), 1 );
301+
iconPicker.Icon = currentIcon;
302+
303+
// Color Picker
304+
var colorPicker = popup.Layout.Add( new ColorPicker( popup ) );
305+
colorPicker.Value = currentColor;
306+
307+
// Live update when icon or color changes
308+
iconPicker.ValueChanged = ( v ) =>
309+
{
310+
onSelected?.Invoke( v, colorPicker.Value );
311+
};
312+
313+
colorPicker.ValueChanged = ( c ) =>
314+
{
315+
onSelected?.Invoke( iconPicker.Icon, c );
316+
};
317+
318+
// Buttons
319+
var buttonRow = popup.Layout.AddRow();
320+
buttonRow.Spacing = 4;
321+
322+
var cancelButton = buttonRow.Add( new Button( "Cancel" ) );
323+
cancelButton.Clicked += () => popup.Destroy();
324+
325+
var okButton = buttonRow.Add( new Button.Primary( "OK" ) );
326+
okButton.Clicked += () =>
327+
{
328+
onSelected?.Invoke( iconPicker.Icon, colorPicker.Value );
329+
popup.Destroy();
330+
};
331+
332+
popup.OpenAtCursor();
333+
}
334+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
using System;
2+
using System.Text;
3+
4+
namespace Editor
5+
{
6+
public static class IconTagEncoding
7+
{
8+
public static string EncodeIconToTag( string icon )
9+
{
10+
if ( string.IsNullOrEmpty( icon ) ) return null;
11+
var bytes = Encoding.UTF8.GetBytes( icon );
12+
var sb = new StringBuilder();
13+
sb.Append( "icon_" );
14+
foreach ( var b in bytes ) sb.Append( b.ToString( "X2" ) );
15+
return sb.ToString();
16+
}
17+
18+
public static string DecodeIconFromTag( string tag )
19+
{
20+
if ( string.IsNullOrEmpty( tag ) ) return null;
21+
if ( !tag.StartsWith( "icon_" ) ) return null;
22+
var hex = tag.Substring( 5 );
23+
if ( hex.Length == 0 || hex.Length % 2 != 0 ) return null;
24+
try
25+
{
26+
var bytes = new byte[hex.Length / 2];
27+
for ( int i = 0; i < bytes.Length; i++ ) bytes[i] = Convert.ToByte( hex.Substring( i * 2, 2 ), 16 );
28+
return Encoding.UTF8.GetString( bytes );
29+
}
30+
catch
31+
{
32+
return null;
33+
}
34+
}
35+
}
36+
}

game/addons/tools/Code/Scene/SceneTree/GameObjectNode.cs

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,27 @@ public override int ValueHash
3434
hc.Add( Value.Network.IsOwner );
3535
hc.Add( Value.IsProxy );
3636
hc.Add( Value.Active );
37+
hc.Add( Value.Tags ); // Include tags for custom icon and color changes
38+
39+
// Include custom icon (from session storage) so changes trigger a node update
40+
// Prefer persisted icon tag (saved with scene) so icons survive reloads
41+
var iconTag = Value.Tags.FirstOrDefault( t => t.StartsWith( "icon_" ) );
42+
if ( iconTag is not null )
43+
{
44+
var decoded = Editor.IconTagEncoding.DecodeIconFromTag( iconTag );
45+
if ( !string.IsNullOrEmpty( decoded ) ) hc.Add( decoded );
46+
}
47+
else if ( CustomIconStorage.Icons.TryGetValue( Value, out var sessionIcon ) )
48+
{
49+
hc.Add( sessionIcon );
50+
}
51+
52+
// Also include persisted color tag so color changes invalidate the node
53+
var colorTag = Value.Tags.FirstOrDefault( t => t.StartsWith( "icon_color_" ) );
54+
if ( colorTag is not null )
55+
{
56+
hc.Add( colorTag );
57+
}
3758

3859
foreach ( var val in Value.Children )
3960
{
@@ -73,7 +94,7 @@ public override void OnPaint( VirtualWidget item )
7394
if ( !Value.Active ) opacity *= 0.5f;
7495

7596
Color pen = Theme.TextControl;
76-
string icon = "layers";
97+
string icon = Value.Children.Where( x => x.ShouldShowInHierarchy() ).Any() ? "📂" : (Value.Components.Count > 0 ? "layers" : "📁");
7798
Color iconColor = Theme.TextControl.WithAlpha( 0.6f );
7899
Color overlayIconColor = iconColor;
79100

@@ -165,7 +186,6 @@ public override void OnPaint( VirtualWidget item )
165186
pen = Theme.Yellow.WithAlpha( 0.6f );
166187
}
167188

168-
169189
//
170190
// If there's a drag and drop happening, fade out nodes that aren't possible
171191
//
@@ -230,8 +250,48 @@ public override void OnPaint( VirtualWidget item )
230250

231251
var iconSize = 16;
232252

233-
Paint.Pen = iconColor.WithAlphaMultiplied( opacity );
234-
Paint.DrawIcon( r, icon, iconSize, TextFlag.LeftCenter );
253+
// Apply custom icon and color overrides (after all default conditions)
254+
255+
256+
// Prefer persisted icon tag (saved with scene)
257+
var iconTag = Value.Tags.FirstOrDefault( t => t.StartsWith( "icon_" ) );
258+
if ( iconTag is not null )
259+
{
260+
var decoded = Editor.IconTagEncoding.DecodeIconFromTag( iconTag );
261+
if ( !string.IsNullOrEmpty( decoded ) ) icon = decoded;
262+
}
263+
else if ( CustomIconStorage.Icons.TryGetValue( Value, out var sessionIconValue ) )
264+
{
265+
icon = sessionIconValue;
266+
}
267+
268+
// Prefer persisted color tag (saved with scene)
269+
var colorTag = Value.Tags.FirstOrDefault( t => t.StartsWith( "icon_color_" ) );
270+
if ( colorTag is not null )
271+
{
272+
var hex = colorTag.Substring( 11 ); // Remove "icon_color_"
273+
// Expecting AARRGGBB
274+
if ( hex.Length == 8 )
275+
{
276+
if ( Color.TryParse( $"#{hex}", out var parsedColor ) )
277+
{
278+
iconColor = parsedColor;
279+
overlayIconColor = parsedColor;
280+
}
281+
}
282+
else if ( hex.Length == 6 )
283+
{
284+
// fallback RRGGBB -> assume full alpha
285+
if ( Color.TryParse( $"#FF{hex}", out var parsedColor ) )
286+
{
287+
iconColor = parsedColor;
288+
overlayIconColor = parsedColor;
289+
}
290+
}
291+
}
292+
293+
Paint.Pen = iconColor.WithAlphaMultiplied( opacity );
294+
Paint.DrawIcon( r, icon, iconSize, TextFlag.LeftCenter );
235295
if ( !string.IsNullOrEmpty( overlayIcon ) )
236296
{
237297
var overlayIconRect = r;
@@ -277,6 +337,11 @@ public override void OnPaint( VirtualWidget item )
277337

278338

279339
}
340+
// Apply custom icon and color overrides (after all default conditions)
341+
if ( CustomIconStorage.Icons.TryGetValue( Value, out var customIcon ) )
342+
{
343+
icon = customIcon;
344+
}
280345
}
281346

282347
public override void OnRename( VirtualWidget item, string text, List<TreeNode> selection = null )

0 commit comments

Comments
 (0)