]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
feat: SimpleRadial menu support for sprite-view and more extensibility (#39223)
authorFildrance <fildrance@gmail.com>
Wed, 10 Sep 2025 08:11:15 +0000 (11:11 +0300)
committerGitHub <noreply@github.com>
Wed, 10 Sep 2025 08:11:15 +0000 (11:11 +0300)
12 files changed:
Content.Client/Changeling/UI/ChangelingTransformBoundUserInterface.cs
Content.Client/Changeling/UI/ChangelingTransformMenu.xaml [deleted file]
Content.Client/Changeling/UI/ChangelingTransformMenu.xaml.cs [deleted file]
Content.Client/Ghost/GhostRoleRadioBoundUserInterface.cs
Content.Client/Ghost/GhostRoleRadioMenu.xaml [deleted file]
Content.Client/Ghost/GhostRoleRadioMenu.xaml.cs [deleted file]
Content.Client/RCD/RCDMenuBoundUserInterface.cs
Content.Client/Silicons/StationAi/StationAiBoundUserInterface.cs
Content.Client/UserInterface/Controls/RadialMenu.cs
Content.Client/UserInterface/Controls/SimpleRadialMenu.xaml.cs
Content.Client/UserInterface/Systems/Emotes/EmotesUIController.cs
Content.Shared/Ghost/Roles/Components/GhostRoleMobSpawnerComponent.cs

index 8220e1870891631a59e0c863f0c0936e6477cfc4..97c07dd8c95d8c4f33186fa8098edc1a5a65bc3d 100644 (file)
@@ -1,4 +1,7 @@
-using Content.Shared.Changeling.Systems;
+using Content.Client.Stylesheets;
+using Content.Client.UserInterface.Controls;
+using Content.Shared.Changeling.Components;
+using Content.Shared.Changeling.Systems;
 using JetBrains.Annotations;
 using Robust.Client.UserInterface;
 
@@ -7,28 +10,58 @@ namespace Content.Client.Changeling.UI;
 [UsedImplicitly]
 public sealed partial class ChangelingTransformBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
 {
-    private ChangelingTransformMenu? _window;
+    private SimpleRadialMenu? _menu;
+    private static readonly Color SelectedOptionBackground = StyleNano.ButtonColorGoodDefault.WithAlpha(128);
+    private static readonly Color SelectedOptionHoverBackground = StyleNano.ButtonColorGoodHovered.WithAlpha(128);
 
     protected override void Open()
     {
         base.Open();
 
-        _window = this.CreateWindow<ChangelingTransformMenu>();
-
-        _window.OnIdentitySelect += SendIdentitySelect;
-
-        _window.Update(Owner);
+        _menu = this.CreateWindow<SimpleRadialMenu>();
+        Update();
+        _menu.OpenOverMouseScreenPosition();
     }
 
+
     public override void Update()
     {
-        if (_window == null)
+        if (_menu == null)
             return;
 
-        _window.Update(Owner);
+        if (!EntMan.TryGetComponent<ChangelingIdentityComponent>(Owner, out var lingIdentity))
+            return;
+
+        var models = ConvertToButtons(lingIdentity.ConsumedIdentities, lingIdentity?.CurrentIdentity);
+
+        _menu.SetButtons(models);
+    }
+
+    private IEnumerable<RadialMenuOptionBase> ConvertToButtons(
+        IEnumerable<EntityUid> identities,
+        EntityUid? currentIdentity
+    )
+    {
+        var buttons = new List<RadialMenuOptionBase>();
+        foreach (var identity in identities)
+        {
+            if (!EntMan.TryGetComponent<MetaDataComponent>(identity, out var metadata))
+                continue;
+
+            var option = new RadialMenuActionOption<NetEntity>(SendIdentitySelect, EntMan.GetNetEntity(identity))
+            {
+                IconSpecifier = RadialMenuIconSpecifier.With(identity),
+                ToolTip = metadata.EntityName,
+                BackgroundColor = (currentIdentity == identity) ? SelectedOptionBackground : null,
+                HoverBackgroundColor = (currentIdentity == identity) ? SelectedOptionHoverBackground : null
+            };
+            buttons.Add(option);
+        }
+
+        return buttons;
     }
 
-    public void SendIdentitySelect(NetEntity identityId)
+    private void SendIdentitySelect(NetEntity identityId)
     {
         SendPredictedMessage(new ChangelingTransformIdentitySelectMessage(identityId));
     }
diff --git a/Content.Client/Changeling/UI/ChangelingTransformMenu.xaml b/Content.Client/Changeling/UI/ChangelingTransformMenu.xaml
deleted file mode 100644 (file)
index 38ae0ec..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-<ui:RadialMenu
-    xmlns:ui="clr-namespace:Content.Client.UserInterface.Controls"
-    CloseButtonStyleClass="RadialMenuCloseButton"
-    VerticalExpand="True"
-    HorizontalExpand="True">
-    <ui:RadialContainer Name="Main">
-    </ui:RadialContainer>
-</ui:RadialMenu>
diff --git a/Content.Client/Changeling/UI/ChangelingTransformMenu.xaml.cs b/Content.Client/Changeling/UI/ChangelingTransformMenu.xaml.cs
deleted file mode 100644 (file)
index ebd4e90..0000000
+++ /dev/null
@@ -1,62 +0,0 @@
-using System.Numerics;
-using Content.Client.UserInterface.Controls;
-using Content.Shared.Changeling.Components;
-using Robust.Client.AutoGenerated;
-using Robust.Client.UserInterface.Controls;
-using Robust.Client.UserInterface.XAML;
-
-namespace Content.Client.Changeling.UI;
-
-[GenerateTypedNameReferences]
-public sealed partial class ChangelingTransformMenu : RadialMenu
-{
-    [Dependency] private readonly IEntityManager _entity = default!;
-    public event Action<NetEntity>? OnIdentitySelect;
-
-    public ChangelingTransformMenu()
-    {
-        RobustXamlLoader.Load(this);
-        IoCManager.InjectDependencies(this);
-    }
-
-    public void Update(EntityUid uid)
-    {
-        Main.DisposeAllChildren();
-
-        if (!_entity.TryGetComponent<ChangelingIdentityComponent>(uid, out var identityComp))
-            return;
-
-        foreach (var identityUid in identityComp.ConsumedIdentities)
-        {
-            if (!_entity.TryGetComponent<MetaDataComponent>(identityUid, out var metadata))
-                continue;
-
-            var identityName = metadata.EntityName;
-
-            var button = new ChangelingTransformMenuButton()
-            {
-                StyleClasses = { "RadialMenuButton" },
-                SetSize = new Vector2(64, 64),
-                ToolTip = identityName,
-            };
-
-            var entView = new SpriteView()
-            {
-                SetSize = new Vector2(48, 48),
-                VerticalAlignment = VAlignment.Center,
-                HorizontalAlignment = HAlignment.Center,
-                Stretch = SpriteView.StretchMode.Fill,
-            };
-            entView.SetEntity(identityUid);
-            button.OnButtonUp += _ =>
-            {
-                OnIdentitySelect?.Invoke(_entity.GetNetEntity(identityUid));
-                Close();
-            };
-            button.AddChild(entView);
-            Main.AddChild(button);
-        }
-    }
-}
-
-public sealed class ChangelingTransformMenuButton : RadialMenuTextureButtonWithSector;
index 52ea835f4a86022ed6aacaef9b76743cb34ec2c8..9334c8553648da2a430298c80c04b9f6da0225f1 100644 (file)
@@ -1,25 +1,58 @@
+using Content.Client.UserInterface.Controls;
 using Content.Shared.Ghost.Roles;
+using Content.Shared.Ghost.Roles.Components;
 using Robust.Client.UserInterface;
 using Robust.Shared.Prototypes;
 
 namespace Content.Client.Ghost;
 
-public sealed class GhostRoleRadioBoundUserInterface : BoundUserInterface
+public sealed class GhostRoleRadioBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
 {
-    private GhostRoleRadioMenu? _ghostRoleRadioMenu;
+    [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
 
-    public GhostRoleRadioBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
-    {
-        IoCManager.InjectDependencies(this);
-    }
+    private SimpleRadialMenu? _ghostRoleRadioMenu;
 
     protected override void Open()
     {
         base.Open();
 
-        _ghostRoleRadioMenu = this.CreateWindow<GhostRoleRadioMenu>();
-        _ghostRoleRadioMenu.SetEntity(Owner);
-        _ghostRoleRadioMenu.SendGhostRoleRadioMessageAction += SendGhostRoleRadioMessage;
+        _ghostRoleRadioMenu = this.CreateWindow<SimpleRadialMenu>();
+
+        // The purpose of this radial UI is for ghost role radios that allow you to select
+        // more than one potential option, such as with kobolds/lizards.
+        // This means that it won't show anything if SelectablePrototypes is empty.
+        if (!EntMan.TryGetComponent<GhostRoleMobSpawnerComponent>(Owner, out var comp))
+            return;
+
+        var list = ConvertToButtons(comp.SelectablePrototypes);
+
+        _ghostRoleRadioMenu.SetButtons(list);
+    }
+
+    private IEnumerable<RadialMenuOptionBase> ConvertToButtons(List<ProtoId<GhostRolePrototype>> protoIds)
+    {
+        var list = new List<RadialMenuOptionBase>();
+        foreach (var ghostRoleProtoId in protoIds)
+        {
+            // For each prototype we find we want to create a button that uses the name of the ghost role
+            // as the hover tooltip, and the icon is taken from either the ghost role entityprototype
+            // or the indicated icon entityprototype.
+            if (!_prototypeManager.Resolve(ghostRoleProtoId, out var ghostRoleProto))
+                continue;
+
+            var option = new RadialMenuActionOption<ProtoId<GhostRolePrototype>>(SendGhostRoleRadioMessage, ghostRoleProtoId)
+            {
+                ToolTip = Loc.GetString(ghostRoleProto.Name),
+                // pick the icon if it exists, otherwise fallback to the ghost role's entity
+                IconSpecifier = ghostRoleProto.IconPrototype != null
+                                && _prototypeManager.Resolve(ghostRoleProto.IconPrototype, out var iconProto)
+                    ? RadialMenuIconSpecifier.With(iconProto)
+                    : RadialMenuIconSpecifier.With(ghostRoleProto.EntityPrototype)
+            };
+            list.Add(option);
+        }
+
+        return list;
     }
 
     private void SendGhostRoleRadioMessage(ProtoId<GhostRolePrototype> protoId)
diff --git a/Content.Client/Ghost/GhostRoleRadioMenu.xaml b/Content.Client/Ghost/GhostRoleRadioMenu.xaml
deleted file mode 100644 (file)
index c35ee12..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-<ui:RadialMenu
-    xmlns:ui="clr-namespace:Content.Client.UserInterface.Controls"
-    CloseButtonStyleClass="RadialMenuCloseButton"
-    VerticalExpand="True"
-    HorizontalExpand="True">
-    <ui:RadialContainer Name="Main">
-    </ui:RadialContainer>
-</ui:RadialMenu>
diff --git a/Content.Client/Ghost/GhostRoleRadioMenu.xaml.cs b/Content.Client/Ghost/GhostRoleRadioMenu.xaml.cs
deleted file mode 100644 (file)
index 718b6c4..0000000
+++ /dev/null
@@ -1,105 +0,0 @@
-using Content.Client.UserInterface.Controls;
-using Content.Shared.Ghost.Roles;
-using Content.Shared.Ghost.Roles.Components;
-using Robust.Client.UserInterface;
-using Robust.Client.UserInterface.Controls;
-using Robust.Client.UserInterface.XAML;
-using Robust.Shared.Prototypes;
-using System.Numerics;
-
-namespace Content.Client.Ghost;
-
-public sealed partial class GhostRoleRadioMenu : RadialMenu
-{
-    [Dependency] private readonly EntityManager _entityManager = default!;
-    [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
-
-    public event Action<ProtoId<GhostRolePrototype>>? SendGhostRoleRadioMessageAction;
-
-    public EntityUid Entity { get; set; }
-
-    public GhostRoleRadioMenu()
-    {
-        IoCManager.InjectDependencies(this);
-        RobustXamlLoader.Load(this);
-    }
-
-    public void SetEntity(EntityUid uid)
-    {
-        Entity = uid;
-        RefreshUI();
-    }
-
-    private void RefreshUI()
-    {
-        // The main control that will contain all the clickable options
-        var main = FindControl<RadialContainer>("Main");
-
-        // The purpose of this radial UI is for ghost role radios that allow you to select
-        // more than one potential option, such as with kobolds/lizards.
-        // This means that it won't show anything if SelectablePrototypes is empty.
-        if (!_entityManager.TryGetComponent<GhostRoleMobSpawnerComponent>(Entity, out var comp))
-            return;
-
-        foreach (var ghostRoleProtoString in comp.SelectablePrototypes)
-        {
-            // For each prototype we find we want to create a button that uses the name of the ghost role
-            // as the hover tooltip, and the icon is taken from either the ghost role entityprototype
-            // or the indicated icon entityprototype.
-            if (!_prototypeManager.TryIndex<GhostRolePrototype>(ghostRoleProtoString, out var ghostRoleProto))
-                continue;
-
-            var button = new GhostRoleRadioMenuButton()
-            {
-                SetSize = new Vector2(64, 64),
-                ToolTip = Loc.GetString(ghostRoleProto.Name),
-                ProtoId = ghostRoleProto.ID,
-            };
-
-            var entProtoView = new EntityPrototypeView()
-            {
-                SetSize = new Vector2(48, 48),
-                VerticalAlignment = VAlignment.Center,
-                HorizontalAlignment = HAlignment.Center,
-                Stretch = SpriteView.StretchMode.Fill
-            };
-
-            // pick the icon if it exists, otherwise fallback to the ghost role's entity
-            if (_prototypeManager.Resolve(ghostRoleProto.IconPrototype, out var iconProto))
-                entProtoView.SetPrototype(iconProto);
-            else
-                entProtoView.SetPrototype(ghostRoleProto.EntityPrototype);
-
-            button.AddChild(entProtoView);
-            main.AddChild(button);
-            AddGhostRoleRadioMenuButtonOnClickActions(main);
-        }
-    }
-
-    private void AddGhostRoleRadioMenuButtonOnClickActions(Control control)
-    {
-        var mainControl = control as RadialContainer;
-
-        if (mainControl == null)
-            return;
-
-        foreach (var child in mainControl.Children)
-        {
-            var castChild = child as GhostRoleRadioMenuButton;
-
-            if (castChild == null)
-                continue;
-
-            castChild.OnButtonUp += _ =>
-            {
-                SendGhostRoleRadioMessageAction?.Invoke(castChild.ProtoId);
-                Close();
-            };
-        }
-    }
-}
-
-public sealed class GhostRoleRadioMenuButton : RadialMenuTextureButtonWithSector
-{
-    public ProtoId<GhostRolePrototype> ProtoId { get; set; }
-}
index 3c9d5d1e55aedd03f1dbefc2161f284349344c0b..6aa32892cfd84a2d4b826ce93712087f9eff031d 100644 (file)
@@ -51,10 +51,10 @@ public sealed class RCDMenuBoundUserInterface : BoundUserInterface
         _menu.OpenOverMouseScreenPosition();
     }
 
-    private IEnumerable<RadialMenuOption> ConvertToButtons(HashSet<ProtoId<RCDPrototype>> prototypes)
+    private IEnumerable<RadialMenuOptionBase> ConvertToButtons(HashSet<ProtoId<RCDPrototype>> prototypes)
     {
-        Dictionary<string, List<RadialMenuActionOption>> buttonsByCategory = new();
-        ValueList<RadialMenuActionOption> topLevelActions = new();
+        Dictionary<string, List<RadialMenuActionOptionBase>> buttonsByCategory = new();
+        ValueList<RadialMenuActionOptionBase> topLevelActions = new();
         foreach (var protoId in prototypes)
         {
             var prototype = _prototypeManager.Index(protoId);
@@ -62,7 +62,7 @@ public sealed class RCDMenuBoundUserInterface : BoundUserInterface
             {
                 var topLevelActionOption = new RadialMenuActionOption<RCDPrototype>(HandleMenuOptionClick, prototype)
                 {
-                    Sprite = prototype.Sprite,
+                    IconSpecifier = RadialMenuIconSpecifier.With(prototype.Sprite),
                     ToolTip = GetTooltip(prototype)
                 };
                 topLevelActions.Add(topLevelActionOption);
@@ -74,26 +74,26 @@ public sealed class RCDMenuBoundUserInterface : BoundUserInterface
 
             if (!buttonsByCategory.TryGetValue(prototype.Category, out var list))
             {
-                list = new List<RadialMenuActionOption>();
+                list = new List<RadialMenuActionOptionBase>();
                 buttonsByCategory.Add(prototype.Category, list);
             }
 
             var actionOption = new RadialMenuActionOption<RCDPrototype>(HandleMenuOptionClick, prototype)
             {
-                Sprite = prototype.Sprite,
+                IconSpecifier = RadialMenuIconSpecifier.With(prototype.Sprite),
                 ToolTip = GetTooltip(prototype)
             };
             list.Add(actionOption);
         }
 
-        var models = new RadialMenuOption[buttonsByCategory.Count + topLevelActions.Count];
+        var models = new RadialMenuOptionBase[buttonsByCategory.Count + topLevelActions.Count];
         var i = 0;
         foreach (var (key, list) in buttonsByCategory)
         {
             var groupInfo = PrototypesGroupingInfo[key];
             models[i] = new RadialMenuNestedLayerOption(list)
             {
-                Sprite = groupInfo.Sprite,
+                IconSpecifier = RadialMenuIconSpecifier.With(groupInfo.Sprite),
                 ToolTip = Loc.GetString(groupInfo.Tooltip)
             };
             i++;
index 77ac13c972fd568be15595067fe7e5ec7e82953c..2ada6e4b0158d8b2afb934018c2b83e5deb030f5 100644 (file)
@@ -23,15 +23,15 @@ public sealed class StationAiBoundUserInterface(EntityUid owner, Enum uiKey) : B
         _menu.Open();
     }
 
-    private IEnumerable<RadialMenuActionOption> ConvertToButtons(IReadOnlyList<StationAiRadial> actions)
+    private IEnumerable<RadialMenuActionOptionBase> ConvertToButtons(IReadOnlyList<StationAiRadial> actions)
     {
-        var models = new RadialMenuActionOption[actions.Count];
+        var models = new RadialMenuActionOptionBase[actions.Count];
         for (int i = 0; i < actions.Count; i++)
         {
             var action = actions[i];
             models[i] = new RadialMenuActionOption<BaseStationAiAction>(HandleRadialMenuClick, action.Event)
             {
-                Sprite = action.Sprite,
+                IconSpecifier = RadialMenuIconSpecifier.With(action.Sprite),
                 ToolTip = action.Tooltip
             };
         }
index 9734cf2960119a1e03aa7560173e570d7d947efb..959a60ef4f88fca5ac3b315164912bc455080476 100644 (file)
@@ -229,10 +229,10 @@ public class RadialMenu : BaseWindow
 /// from interactions.
 /// </summary>
 [Virtual]
-public class RadialMenuTextureButtonBase : TextureButton
+public abstract class RadialMenuButtonBase : BaseButton
 {
     /// <inheritdoc />
-    protected RadialMenuTextureButtonBase()
+    protected RadialMenuButtonBase()
     {
         EnableAllKeybinds = true;
     }
@@ -242,7 +242,9 @@ public class RadialMenuTextureButtonBase : TextureButton
     {
         if (args.Function == EngineKeyFunctions.UIClick
             || args.Function == ContentKeyFunctions.AltActivateItemInWorld)
+        {
             base.KeyBindUp(args);
+        }
     }
 }
 
@@ -253,8 +255,14 @@ public class RadialMenuTextureButtonBase : TextureButton
 /// works only if control have parent, and ActiveContainer property is set.
 /// Also considers all space outside of radial menu buttons as itself for clicking.
 /// </summary>
-public sealed class RadialMenuContextualCentralTextureButton : RadialMenuTextureButtonBase
+public sealed class RadialMenuContextualCentralTextureButton : TextureButton
 {
+    /// <inheritdoc />
+    public RadialMenuContextualCentralTextureButton()
+    {
+        EnableAllKeybinds = true;
+    }
+
     public float InnerRadius { get; set; }
 
     public Vector2? ParentCenter { get; set; }
@@ -271,15 +279,25 @@ public sealed class RadialMenuContextualCentralTextureButton : RadialMenuTexture
 
         var innerRadiusSquared = InnerRadius * InnerRadius;
 
-        // comparing to squared values is faster then making sqrt
+        // comparing to squared values is faster, then making sqrt
         return distSquared < innerRadiusSquared;
     }
+
+    /// <inheritdoc />
+    protected override void KeyBindUp(GUIBoundKeyEventArgs args)
+    {
+        if (args.Function == EngineKeyFunctions.UIClick
+            || args.Function == ContentKeyFunctions.AltActivateItemInWorld)
+        {
+            base.KeyBindUp(args);
+        }
+    }
 }
 
 /// <summary>
 /// Menu button for outer area of radial menu (covers everything 'outside').
 /// </summary>
-public sealed class RadialMenuOuterAreaButton : RadialMenuTextureButtonBase
+public sealed class RadialMenuOuterAreaButton : RadialMenuButtonBase
 {
     public float OuterRadius { get; set; }
 
@@ -303,7 +321,7 @@ public sealed class RadialMenuOuterAreaButton : RadialMenuTextureButtonBase
 }
 
 [Virtual]
-public class RadialMenuTextureButton : RadialMenuTextureButtonBase
+public class RadialMenuButton : RadialMenuButtonBase
 {
     /// <summary>
     /// Upon clicking this button the radial menu will be moved to the layer of this control.
@@ -319,9 +337,8 @@ public class RadialMenuTextureButton : RadialMenuTextureButtonBase
     /// <summary>
     /// A simple texture button that can move the user to a different layer within a radial menu
     /// </summary>
-    public RadialMenuTextureButton()
+    public RadialMenuButton()
     {
-        EnableAllKeybinds = true;
         OnButtonUp += OnClicked;
     }
 
@@ -391,7 +408,7 @@ public interface IRadialMenuItemWithSector
 }
 
 [Virtual]
-public class RadialMenuTextureButtonWithSector : RadialMenuTextureButton, IRadialMenuItemWithSector
+public class RadialMenuButtonWithSector : RadialMenuButton, IRadialMenuItemWithSector
 {
     private Vector2[]? _sectorPointsForDrawing;
 
@@ -500,7 +517,7 @@ public class RadialMenuTextureButtonWithSector : RadialMenuTextureButton, IRadia
     /// <summary>
     /// A simple texture button that can move the user to a different layer within a radial menu
     /// </summary>
-    public RadialMenuTextureButtonWithSector()
+    public RadialMenuButtonWithSector()
     {
     }
 
index 31d7eab3400d5bd8f7302d485c43bf5d4a9bfb7e..ec7dcbbb5af4b000181dc7175d2d09ad84691941 100644 (file)
@@ -7,6 +7,8 @@ using Robust.Client.GameObjects;
 using Robust.Shared.Timing;
 using Robust.Client.UserInterface.XAML;
 using Robust.Client.Input;
+using Robust.Client.UserInterface.Controls;
+using Robust.Shared.Prototypes;
 
 namespace Content.Client.UserInterface.Controls;
 
@@ -30,7 +32,7 @@ public sealed partial class SimpleRadialMenu : RadialMenu
         _attachMenuToEntity = owner;
     }
 
-    public void SetButtons(IEnumerable<RadialMenuOption> models, SimpleRadialMenuSettings? settings = null)
+    public void SetButtons(IEnumerable<RadialMenuOptionBase> models, SimpleRadialMenuSettings? settings = null)
     {
         ClearExistingChildrenRadialButtons();
 
@@ -45,7 +47,7 @@ public sealed partial class SimpleRadialMenu : RadialMenu
     }
 
     private void Fill(
-        IEnumerable<RadialMenuOption> models,
+        IEnumerable<RadialMenuOptionBase> models,
         SpriteSystem sprites,
         ICollection<Control> rootControlChildren,
         SimpleRadialMenuSettings settings
@@ -77,7 +79,7 @@ public sealed partial class SimpleRadialMenu : RadialMenu
         }
     }
 
-    private RadialMenuTextureButton RecursiveContainerExtraction(
+    private RadialMenuButton RecursiveContainerExtraction(
         SpriteSystem sprites,
         ICollection<Control> rootControlChildren,
         RadialMenuNestedLayerOption model,
@@ -112,8 +114,8 @@ public sealed partial class SimpleRadialMenu : RadialMenu
         return thisLayerLinkButton;
     }
 
-    private RadialMenuTextureButton ConvertToButton(
-        RadialMenuOption model,
+    private RadialMenuButton ConvertToButton(
+        RadialMenuOptionBase model,
         SpriteSystem sprites,
         SimpleRadialMenuSettings settings,
         bool haveNested
@@ -121,29 +123,26 @@ public sealed partial class SimpleRadialMenu : RadialMenu
     {
         var button = settings.UseSectors
             ? ConvertToButtonWithSector(model, settings)
-            : new RadialMenuTextureButton();
+            : new RadialMenuButton();
         button.SetSize = new Vector2(64f, 64f);
         button.ToolTip = model.ToolTip;
-        if (model.Sprite != null)
+        var imageControl = model.IconSpecifier switch
         {
-            var scale = Vector2.One;
-
-            var texture = sprites.Frame0(model.Sprite);
-            if (texture.Width <= 32)
-            {
-                scale *= 2;
-            }
+            RadialMenuTextureIconSpecifier textureSpecifier => CreateTexture(textureSpecifier.Sprite, sprites),
+            RadialMenuEntityIconSpecifier entitySpecifier => CreateSpriteView(entitySpecifier.Entity),
+            RadialMenuEntityPrototypeIconSpecifier entProtoSpecifier => CreateEntityPrototypeView(entProtoSpecifier.ProtoId),
+            _ => null
+        };
 
-            button.TextureNormal = texture;
-            button.Scale = scale;
-        }
+        if(imageControl != null)
+            button.AddChild(imageControl);
 
-        if (model is RadialMenuActionOption actionOption)
+        if (model is RadialMenuActionOptionBase actionOption)
         {
             button.OnPressed += _ =>
             {
                 actionOption.OnPressed?.Invoke();
-                if(!haveNested)
+                if (!haveNested)
                     Close();
             };
         }
@@ -151,9 +150,53 @@ public sealed partial class SimpleRadialMenu : RadialMenu
         return button;
     }
 
-    private static RadialMenuTextureButtonWithSector ConvertToButtonWithSector(RadialMenuOption model, SimpleRadialMenuSettings settings)
+    private Control CreateEntityPrototypeView(EntProtoId protoId)
+    {
+        var entProtoView = new EntityPrototypeView
+        {
+            SetSize = new Vector2(48, 48),
+            VerticalAlignment = VAlignment.Center,
+            HorizontalAlignment = HAlignment.Center,
+            Stretch = SpriteView.StretchMode.Fill,
+        };
+        entProtoView.SetPrototype(protoId);
+        return entProtoView;
+    }
+
+    private static Control CreateSpriteView(EntityUid entityForSpriteView)
+    {
+        var entView = new SpriteView
+        {
+            SetSize = new Vector2(48, 48),
+            VerticalAlignment = VAlignment.Center,
+            HorizontalAlignment = HAlignment.Center,
+            Stretch = SpriteView.StretchMode.Fill,
+        };
+        entView.SetEntity(entityForSpriteView);
+        return entView;
+    }
+
+    private static Control CreateTexture(SpriteSpecifier spriteSpecifier, SpriteSystem sprites)
+    {
+        var scale = Vector2.One;
+
+        var texture = sprites.Frame0(spriteSpecifier);
+        if (texture.Width <= 32)
+        {
+            scale *= 2;
+        }
+
+        var imageControl = new TextureRect()
+        {
+            Texture = texture,
+            TextureScale = scale
+        };
+        return imageControl;
+    }
+
+    private static RadialMenuButtonWithSector ConvertToButtonWithSector(RadialMenuOptionBase model, SimpleRadialMenuSettings settings)
     {
-        var button = new RadialMenuTextureButtonWithSector
+        var button = new RadialMenuButtonWithSector
         {
             DrawBorder = settings.DisplayBorders,
             DrawBackground = !settings.NoBackground
@@ -228,32 +271,99 @@ public sealed partial class SimpleRadialMenu : RadialMenu
 
 }
 
+/// <summary>
+/// Abstract representation of a way to specify icon in radial menu.
+/// </summary>
+public abstract record RadialMenuIconSpecifier
+{
+    /// <summary> Use entity prototype viewer. </summary>
+    public static RadialMenuIconSpecifier? With(EntProtoId? protoId)
+    {
+        if (protoId is null)
+            return null;
+
+        return new RadialMenuEntityPrototypeIconSpecifier(protoId.Value);
+    }
+
+    /// <summary> Use simple texture icon. </summary>
+    public static RadialMenuIconSpecifier? With(SpriteSpecifier? sprite)
+    {
+        if (sprite == null)
+            return null;
 
-public abstract class RadialMenuOption
+        return new RadialMenuTextureIconSpecifier(sprite);
+    }
+
+    /// <summary> Use entity sprite viewer. </summary>
+    public static RadialMenuIconSpecifier? With(EntityUid? entity)
+    {
+        if (entity == null)
+            return null;
+
+        return new RadialMenuEntityIconSpecifier(entity.Value);
+    }
+}
+
+/// <summary> Marker that <see cref="SpriteView"/> should be used to display radial menu icon. </summary>
+public sealed record RadialMenuEntityIconSpecifier(EntityUid Entity) : RadialMenuIconSpecifier;
+
+/// <summary> Marker that <see cref="TextureRect"/> should be used to display radial menu icon. </summary>
+public sealed record RadialMenuTextureIconSpecifier(SpriteSpecifier Sprite) : RadialMenuIconSpecifier;
+
+/// <summary> Marker that <see cref="EntityPrototypeView"/> should be used to display radial menu icon. </summary>
+public sealed record RadialMenuEntityPrototypeIconSpecifier(EntProtoId ProtoId) : RadialMenuIconSpecifier;
+
+/// <summary> Container for common options for radial menu button. </summary>
+public abstract class RadialMenuOptionBase
 {
+    /// <summary> Tooltip to be displayed when button is hovered. </summary>
     public string? ToolTip { get; init; }
 
-    public SpriteSpecifier? Sprite { get; init; }
+    /// <summary>
+    /// Color for button background.
+    /// Is used only with sector radial (<see cref="SimpleRadialMenuSettings.UseSectors"/>).
+    /// </summary>
     public Color? BackgroundColor { get; set; }
+    /// <summary>
+    /// Color for button background when it is hovered.
+    /// Is used only with sector radial (<see cref="SimpleRadialMenuSettings.UseSectors"/>).
+    /// </summary>
     public Color? HoverBackgroundColor { get; set; }
+
+    /// <summary>
+    /// Specifier that describes icon to be used for radial menu button.
+    /// </summary>
+    public RadialMenuIconSpecifier? IconSpecifier { get; set; }
 }
 
-public abstract class RadialMenuActionOption(Action onPressed) : RadialMenuOption
+/// <summary> Base type for model of radial menu button with some action on button pressed. </summary>
+/// <param name="onPressed"></param>
+public abstract class RadialMenuActionOptionBase(Action onPressed) : RadialMenuOptionBase
 {
+    /// <summary> Action to be executed on button press. </summary>
     public Action OnPressed { get; } = onPressed;
 }
 
-public sealed class RadialMenuActionOption<T>(Action<T> onPressed, T data)
-    : RadialMenuActionOption(onPressed: () => onPressed(data));
+/// <summary> Strong-typed model for radial menu button with action, stores provided data to be used upon button press. </summary>
+public sealed class RadialMenuActionOption<T>(Action<T> onPressed, T data) : RadialMenuActionOptionBase(onPressed: () => onPressed(data));
 
-public sealed class RadialMenuNestedLayerOption(IReadOnlyCollection<RadialMenuOption> nested, float containerRadius = 100)
-    : RadialMenuOption
+/// <summary>
+/// Model for radial menu button that represents reference for next layer of radial buttons.
+/// </summary>
+/// <param name="nested">List of button models for next layer of menu.</param>
+/// <param name="containerRadius">Radius for radial menu buttons of next layer.</param>
+public sealed class RadialMenuNestedLayerOption(IReadOnlyCollection<RadialMenuOptionBase> nested, float containerRadius = 100) : RadialMenuOptionBase
 {
+    /// <summary> Radius for radial menu buttons of next layer. </summary>
     public float? ContainerRadius { get; } = containerRadius;
 
-    public IReadOnlyCollection<RadialMenuOption> Nested { get; } = nested;
+    /// <summary> List of button models for next layer of menu. </summary>
+    public IReadOnlyCollection<RadialMenuOptionBase> Nested { get; } = nested;
 }
 
+/// <summary>
+/// Additional settings for radial menu render.
+/// </summary>
 public sealed class SimpleRadialMenuSettings
 {
     /// <summary>
index c1bb5b5630d18938ad8897efab49b17fe3f82d41..fdcc3c45a2fdd2c6b9353fd09195a0244648be16 100644 (file)
@@ -132,12 +132,12 @@ public sealed class EmotesUIController : UIController, IOnStateChanged<GameplayS
         _menu = null;
     }
 
-    private IEnumerable<RadialMenuOption> ConvertToButtons(IEnumerable<EmotePrototype> emotePrototypes)
+    private IEnumerable<RadialMenuOptionBase> ConvertToButtons(IEnumerable<EmotePrototype> emotePrototypes)
     {
         var whitelistSystem = EntitySystemManager.GetEntitySystem<EntityWhitelistSystem>();
         var player = _playerManager.LocalSession?.AttachedEntity;
 
-        Dictionary<EmoteCategory, List<RadialMenuOption>> emotesByCategory = new();
+        Dictionary<EmoteCategory, List<RadialMenuOptionBase>> emotesByCategory = new();
         foreach (var emote in emotePrototypes)
         {
             if(emote.Category == EmoteCategory.Invalid)
@@ -157,19 +157,19 @@ public sealed class EmotesUIController : UIController, IOnStateChanged<GameplayS
 
             if (!emotesByCategory.TryGetValue(emote.Category, out var list))
             {
-                list = new List<RadialMenuOption>();
+                list = new List<RadialMenuOptionBase>();
                 emotesByCategory.Add(emote.Category, list);
             }
 
             var actionOption = new RadialMenuActionOption<EmotePrototype>(HandleRadialButtonClick, emote)
             {
-                Sprite = emote.Icon,
+                IconSpecifier = RadialMenuIconSpecifier.With(emote.Icon),
                 ToolTip = Loc.GetString(emote.Name)
             };
             list.Add(actionOption);
         }
 
-        var models = new RadialMenuOption[emotesByCategory.Count];
+        var models = new RadialMenuOptionBase[emotesByCategory.Count];
         var i = 0;
         foreach (var (key, list) in emotesByCategory)
         {
@@ -177,7 +177,7 @@ public sealed class EmotesUIController : UIController, IOnStateChanged<GameplayS
 
             models[i] = new RadialMenuNestedLayerOption(list)
             {
-                Sprite = tuple.Sprite,
+                IconSpecifier = RadialMenuIconSpecifier.With(tuple.Sprite),
                 ToolTip = Loc.GetString(tuple.Tooltip)
             };
             i++;
index 2e44effad96542061ee623856844ca1d0ea8db78..6984be91f96926cf370eecfc07326a47aacc6e11 100644 (file)
@@ -3,7 +3,7 @@ using Robust.Shared.Prototypes;
 namespace Content.Shared.Ghost.Roles.Components
 {
     /// <summary>
-    ///     Allows a ghost to take this role, spawning a new entity.
+    /// Allows a ghost to take this role, spawning a new entity.
     /// </summary>
     [RegisterComponent, EntityCategory("Spawner")]
     public sealed partial class GhostRoleMobSpawnerComponent : Component
@@ -21,9 +21,9 @@ namespace Content.Shared.Ghost.Roles.Components
         public EntProtoId? Prototype;
 
         /// <summary>
-        ///     If this ghostrole spawner has multiple selectable ghostrole prototypes.
+        /// If this ghostrole spawner has multiple selectable ghostrole prototypes.
         /// </summary>
         [DataField]
-        public List<string> SelectablePrototypes = [];
+        public List<ProtoId<GhostRolePrototype>> SelectablePrototypes = [];
     }
 }