feat: SimpleRadial menu support for sprite-view and more extensibility (#39223)

This commit is contained in:
Fildrance
2025-09-10 11:11:15 +03:00
committed by GitHub
parent c7406f65ab
commit 35d69e0f33
12 changed files with 275 additions and 265 deletions

View 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 JetBrains.Annotations;
using Robust.Client.UserInterface; using Robust.Client.UserInterface;
@@ -7,28 +10,58 @@ namespace Content.Client.Changeling.UI;
[UsedImplicitly] [UsedImplicitly]
public sealed partial class ChangelingTransformBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey) 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() protected override void Open()
{ {
base.Open(); base.Open();
_window = this.CreateWindow<ChangelingTransformMenu>(); _menu = this.CreateWindow<SimpleRadialMenu>();
Update();
_window.OnIdentitySelect += SendIdentitySelect; _menu.OpenOverMouseScreenPosition();
_window.Update(Owner);
} }
public override void Update() public override void Update()
{ {
if (_window == null) if (_menu == null)
return; return;
_window.Update(Owner); if (!EntMan.TryGetComponent<ChangelingIdentityComponent>(Owner, out var lingIdentity))
return;
var models = ConvertToButtons(lingIdentity.ConsumedIdentities, lingIdentity?.CurrentIdentity);
_menu.SetButtons(models);
} }
public void SendIdentitySelect(NetEntity identityId) 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;
}
private void SendIdentitySelect(NetEntity identityId)
{ {
SendPredictedMessage(new ChangelingTransformIdentitySelectMessage(identityId)); SendPredictedMessage(new ChangelingTransformIdentitySelectMessage(identityId));
} }

View File

@@ -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>

View File

@@ -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;

View File

@@ -1,25 +1,58 @@
using Content.Client.UserInterface.Controls;
using Content.Shared.Ghost.Roles; using Content.Shared.Ghost.Roles;
using Content.Shared.Ghost.Roles.Components;
using Robust.Client.UserInterface; using Robust.Client.UserInterface;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
namespace Content.Client.Ghost; 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) private SimpleRadialMenu? _ghostRoleRadioMenu;
{
IoCManager.InjectDependencies(this);
}
protected override void Open() protected override void Open()
{ {
base.Open(); base.Open();
_ghostRoleRadioMenu = this.CreateWindow<GhostRoleRadioMenu>(); _ghostRoleRadioMenu = this.CreateWindow<SimpleRadialMenu>();
_ghostRoleRadioMenu.SetEntity(Owner);
_ghostRoleRadioMenu.SendGhostRoleRadioMessageAction += SendGhostRoleRadioMessage; // 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) private void SendGhostRoleRadioMessage(ProtoId<GhostRolePrototype> protoId)

View File

@@ -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>

View File

@@ -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; }
}

View File

@@ -51,10 +51,10 @@ public sealed class RCDMenuBoundUserInterface : BoundUserInterface
_menu.OpenOverMouseScreenPosition(); _menu.OpenOverMouseScreenPosition();
} }
private IEnumerable<RadialMenuOption> ConvertToButtons(HashSet<ProtoId<RCDPrototype>> prototypes) private IEnumerable<RadialMenuOptionBase> ConvertToButtons(HashSet<ProtoId<RCDPrototype>> prototypes)
{ {
Dictionary<string, List<RadialMenuActionOption>> buttonsByCategory = new(); Dictionary<string, List<RadialMenuActionOptionBase>> buttonsByCategory = new();
ValueList<RadialMenuActionOption> topLevelActions = new(); ValueList<RadialMenuActionOptionBase> topLevelActions = new();
foreach (var protoId in prototypes) foreach (var protoId in prototypes)
{ {
var prototype = _prototypeManager.Index(protoId); var prototype = _prototypeManager.Index(protoId);
@@ -62,7 +62,7 @@ public sealed class RCDMenuBoundUserInterface : BoundUserInterface
{ {
var topLevelActionOption = new RadialMenuActionOption<RCDPrototype>(HandleMenuOptionClick, prototype) var topLevelActionOption = new RadialMenuActionOption<RCDPrototype>(HandleMenuOptionClick, prototype)
{ {
Sprite = prototype.Sprite, IconSpecifier = RadialMenuIconSpecifier.With(prototype.Sprite),
ToolTip = GetTooltip(prototype) ToolTip = GetTooltip(prototype)
}; };
topLevelActions.Add(topLevelActionOption); topLevelActions.Add(topLevelActionOption);
@@ -74,26 +74,26 @@ public sealed class RCDMenuBoundUserInterface : BoundUserInterface
if (!buttonsByCategory.TryGetValue(prototype.Category, out var list)) if (!buttonsByCategory.TryGetValue(prototype.Category, out var list))
{ {
list = new List<RadialMenuActionOption>(); list = new List<RadialMenuActionOptionBase>();
buttonsByCategory.Add(prototype.Category, list); buttonsByCategory.Add(prototype.Category, list);
} }
var actionOption = new RadialMenuActionOption<RCDPrototype>(HandleMenuOptionClick, prototype) var actionOption = new RadialMenuActionOption<RCDPrototype>(HandleMenuOptionClick, prototype)
{ {
Sprite = prototype.Sprite, IconSpecifier = RadialMenuIconSpecifier.With(prototype.Sprite),
ToolTip = GetTooltip(prototype) ToolTip = GetTooltip(prototype)
}; };
list.Add(actionOption); list.Add(actionOption);
} }
var models = new RadialMenuOption[buttonsByCategory.Count + topLevelActions.Count]; var models = new RadialMenuOptionBase[buttonsByCategory.Count + topLevelActions.Count];
var i = 0; var i = 0;
foreach (var (key, list) in buttonsByCategory) foreach (var (key, list) in buttonsByCategory)
{ {
var groupInfo = PrototypesGroupingInfo[key]; var groupInfo = PrototypesGroupingInfo[key];
models[i] = new RadialMenuNestedLayerOption(list) models[i] = new RadialMenuNestedLayerOption(list)
{ {
Sprite = groupInfo.Sprite, IconSpecifier = RadialMenuIconSpecifier.With(groupInfo.Sprite),
ToolTip = Loc.GetString(groupInfo.Tooltip) ToolTip = Loc.GetString(groupInfo.Tooltip)
}; };
i++; i++;

View File

@@ -23,15 +23,15 @@ public sealed class StationAiBoundUserInterface(EntityUid owner, Enum uiKey) : B
_menu.Open(); _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++) for (int i = 0; i < actions.Count; i++)
{ {
var action = actions[i]; var action = actions[i];
models[i] = new RadialMenuActionOption<BaseStationAiAction>(HandleRadialMenuClick, action.Event) models[i] = new RadialMenuActionOption<BaseStationAiAction>(HandleRadialMenuClick, action.Event)
{ {
Sprite = action.Sprite, IconSpecifier = RadialMenuIconSpecifier.With(action.Sprite),
ToolTip = action.Tooltip ToolTip = action.Tooltip
}; };
} }

View File

@@ -229,10 +229,10 @@ public class RadialMenu : BaseWindow
/// from interactions. /// from interactions.
/// </summary> /// </summary>
[Virtual] [Virtual]
public class RadialMenuTextureButtonBase : TextureButton public abstract class RadialMenuButtonBase : BaseButton
{ {
/// <inheritdoc /> /// <inheritdoc />
protected RadialMenuTextureButtonBase() protected RadialMenuButtonBase()
{ {
EnableAllKeybinds = true; EnableAllKeybinds = true;
} }
@@ -242,8 +242,10 @@ public class RadialMenuTextureButtonBase : TextureButton
{ {
if (args.Function == EngineKeyFunctions.UIClick if (args.Function == EngineKeyFunctions.UIClick
|| args.Function == ContentKeyFunctions.AltActivateItemInWorld) || args.Function == ContentKeyFunctions.AltActivateItemInWorld)
{
base.KeyBindUp(args); base.KeyBindUp(args);
} }
}
} }
/// <summary> /// <summary>
@@ -253,8 +255,14 @@ public class RadialMenuTextureButtonBase : TextureButton
/// works only if control have parent, and ActiveContainer property is set. /// works only if control have parent, and ActiveContainer property is set.
/// Also considers all space outside of radial menu buttons as itself for clicking. /// Also considers all space outside of radial menu buttons as itself for clicking.
/// </summary> /// </summary>
public sealed class RadialMenuContextualCentralTextureButton : RadialMenuTextureButtonBase public sealed class RadialMenuContextualCentralTextureButton : TextureButton
{ {
/// <inheritdoc />
public RadialMenuContextualCentralTextureButton()
{
EnableAllKeybinds = true;
}
public float InnerRadius { get; set; } public float InnerRadius { get; set; }
public Vector2? ParentCenter { get; set; } public Vector2? ParentCenter { get; set; }
@@ -271,15 +279,25 @@ public sealed class RadialMenuContextualCentralTextureButton : RadialMenuTexture
var innerRadiusSquared = InnerRadius * InnerRadius; 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; return distSquared < innerRadiusSquared;
} }
/// <inheritdoc />
protected override void KeyBindUp(GUIBoundKeyEventArgs args)
{
if (args.Function == EngineKeyFunctions.UIClick
|| args.Function == ContentKeyFunctions.AltActivateItemInWorld)
{
base.KeyBindUp(args);
}
}
} }
/// <summary> /// <summary>
/// Menu button for outer area of radial menu (covers everything 'outside'). /// Menu button for outer area of radial menu (covers everything 'outside').
/// </summary> /// </summary>
public sealed class RadialMenuOuterAreaButton : RadialMenuTextureButtonBase public sealed class RadialMenuOuterAreaButton : RadialMenuButtonBase
{ {
public float OuterRadius { get; set; } public float OuterRadius { get; set; }
@@ -303,7 +321,7 @@ public sealed class RadialMenuOuterAreaButton : RadialMenuTextureButtonBase
} }
[Virtual] [Virtual]
public class RadialMenuTextureButton : RadialMenuTextureButtonBase public class RadialMenuButton : RadialMenuButtonBase
{ {
/// <summary> /// <summary>
/// Upon clicking this button the radial menu will be moved to the layer of this control. /// 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> /// <summary>
/// A simple texture button that can move the user to a different layer within a radial menu /// A simple texture button that can move the user to a different layer within a radial menu
/// </summary> /// </summary>
public RadialMenuTextureButton() public RadialMenuButton()
{ {
EnableAllKeybinds = true;
OnButtonUp += OnClicked; OnButtonUp += OnClicked;
} }
@@ -391,7 +408,7 @@ public interface IRadialMenuItemWithSector
} }
[Virtual] [Virtual]
public class RadialMenuTextureButtonWithSector : RadialMenuTextureButton, IRadialMenuItemWithSector public class RadialMenuButtonWithSector : RadialMenuButton, IRadialMenuItemWithSector
{ {
private Vector2[]? _sectorPointsForDrawing; private Vector2[]? _sectorPointsForDrawing;
@@ -500,7 +517,7 @@ public class RadialMenuTextureButtonWithSector : RadialMenuTextureButton, IRadia
/// <summary> /// <summary>
/// A simple texture button that can move the user to a different layer within a radial menu /// A simple texture button that can move the user to a different layer within a radial menu
/// </summary> /// </summary>
public RadialMenuTextureButtonWithSector() public RadialMenuButtonWithSector()
{ {
} }

View File

@@ -7,6 +7,8 @@ using Robust.Client.GameObjects;
using Robust.Shared.Timing; using Robust.Shared.Timing;
using Robust.Client.UserInterface.XAML; using Robust.Client.UserInterface.XAML;
using Robust.Client.Input; using Robust.Client.Input;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Prototypes;
namespace Content.Client.UserInterface.Controls; namespace Content.Client.UserInterface.Controls;
@@ -30,7 +32,7 @@ public sealed partial class SimpleRadialMenu : RadialMenu
_attachMenuToEntity = owner; _attachMenuToEntity = owner;
} }
public void SetButtons(IEnumerable<RadialMenuOption> models, SimpleRadialMenuSettings? settings = null) public void SetButtons(IEnumerable<RadialMenuOptionBase> models, SimpleRadialMenuSettings? settings = null)
{ {
ClearExistingChildrenRadialButtons(); ClearExistingChildrenRadialButtons();
@@ -45,7 +47,7 @@ public sealed partial class SimpleRadialMenu : RadialMenu
} }
private void Fill( private void Fill(
IEnumerable<RadialMenuOption> models, IEnumerable<RadialMenuOptionBase> models,
SpriteSystem sprites, SpriteSystem sprites,
ICollection<Control> rootControlChildren, ICollection<Control> rootControlChildren,
SimpleRadialMenuSettings settings SimpleRadialMenuSettings settings
@@ -77,7 +79,7 @@ public sealed partial class SimpleRadialMenu : RadialMenu
} }
} }
private RadialMenuTextureButton RecursiveContainerExtraction( private RadialMenuButton RecursiveContainerExtraction(
SpriteSystem sprites, SpriteSystem sprites,
ICollection<Control> rootControlChildren, ICollection<Control> rootControlChildren,
RadialMenuNestedLayerOption model, RadialMenuNestedLayerOption model,
@@ -112,8 +114,8 @@ public sealed partial class SimpleRadialMenu : RadialMenu
return thisLayerLinkButton; return thisLayerLinkButton;
} }
private RadialMenuTextureButton ConvertToButton( private RadialMenuButton ConvertToButton(
RadialMenuOption model, RadialMenuOptionBase model,
SpriteSystem sprites, SpriteSystem sprites,
SimpleRadialMenuSettings settings, SimpleRadialMenuSettings settings,
bool haveNested bool haveNested
@@ -121,29 +123,26 @@ public sealed partial class SimpleRadialMenu : RadialMenu
{ {
var button = settings.UseSectors var button = settings.UseSectors
? ConvertToButtonWithSector(model, settings) ? ConvertToButtonWithSector(model, settings)
: new RadialMenuTextureButton(); : new RadialMenuButton();
button.SetSize = new Vector2(64f, 64f); button.SetSize = new Vector2(64f, 64f);
button.ToolTip = model.ToolTip; button.ToolTip = model.ToolTip;
if (model.Sprite != null) var imageControl = model.IconSpecifier switch
{ {
var scale = Vector2.One; RadialMenuTextureIconSpecifier textureSpecifier => CreateTexture(textureSpecifier.Sprite, sprites),
RadialMenuEntityIconSpecifier entitySpecifier => CreateSpriteView(entitySpecifier.Entity),
RadialMenuEntityPrototypeIconSpecifier entProtoSpecifier => CreateEntityPrototypeView(entProtoSpecifier.ProtoId),
_ => null
};
var texture = sprites.Frame0(model.Sprite); if(imageControl != null)
if (texture.Width <= 32) button.AddChild(imageControl);
{
scale *= 2;
}
button.TextureNormal = texture; if (model is RadialMenuActionOptionBase actionOption)
button.Scale = scale;
}
if (model is RadialMenuActionOption actionOption)
{ {
button.OnPressed += _ => button.OnPressed += _ =>
{ {
actionOption.OnPressed?.Invoke(); actionOption.OnPressed?.Invoke();
if(!haveNested) if (!haveNested)
Close(); Close();
}; };
} }
@@ -151,9 +150,53 @@ public sealed partial class SimpleRadialMenu : RadialMenu
return button; return button;
} }
private static RadialMenuTextureButtonWithSector ConvertToButtonWithSector(RadialMenuOption model, SimpleRadialMenuSettings settings) private Control CreateEntityPrototypeView(EntProtoId protoId)
{ {
var button = new RadialMenuTextureButtonWithSector 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 RadialMenuButtonWithSector
{ {
DrawBorder = settings.DisplayBorders, DrawBorder = settings.DisplayBorders,
DrawBackground = !settings.NoBackground DrawBackground = !settings.NoBackground
@@ -228,32 +271,99 @@ public sealed partial class SimpleRadialMenu : RadialMenu
} }
/// <summary>
public abstract class RadialMenuOption /// Abstract representation of a way to specify icon in radial menu.
/// </summary>
public abstract record RadialMenuIconSpecifier
{ {
public string? ToolTip { get; init; } /// <summary> Use entity prototype viewer. </summary>
public static RadialMenuIconSpecifier? With(EntProtoId? protoId)
{
if (protoId is null)
return null;
public SpriteSpecifier? Sprite { get; init; } return new RadialMenuEntityPrototypeIconSpecifier(protoId.Value);
public Color? BackgroundColor { get; set; } }
public Color? HoverBackgroundColor { get; set; }
/// <summary> Use simple texture icon. </summary>
public static RadialMenuIconSpecifier? With(SpriteSpecifier? sprite)
{
if (sprite == null)
return null;
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);
}
} }
public abstract class RadialMenuActionOption(Action onPressed) : RadialMenuOption /// <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; }
/// <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; }
}
/// <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 Action OnPressed { get; } = onPressed;
} }
public sealed class RadialMenuActionOption<T>(Action<T> onPressed, T data) /// <summary> Strong-typed model for radial menu button with action, stores provided data to be used upon button press. </summary>
: RadialMenuActionOption(onPressed: () => onPressed(data)); public sealed class RadialMenuActionOption<T>(Action<T> onPressed, T data) : RadialMenuActionOptionBase(onPressed: () => onPressed(data));
public sealed class RadialMenuNestedLayerOption(IReadOnlyCollection<RadialMenuOption> nested, float containerRadius = 100) /// <summary>
: RadialMenuOption /// 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 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 public sealed class SimpleRadialMenuSettings
{ {
/// <summary> /// <summary>

View File

@@ -132,12 +132,12 @@ public sealed class EmotesUIController : UIController, IOnStateChanged<GameplayS
_menu = null; _menu = null;
} }
private IEnumerable<RadialMenuOption> ConvertToButtons(IEnumerable<EmotePrototype> emotePrototypes) private IEnumerable<RadialMenuOptionBase> ConvertToButtons(IEnumerable<EmotePrototype> emotePrototypes)
{ {
var whitelistSystem = EntitySystemManager.GetEntitySystem<EntityWhitelistSystem>(); var whitelistSystem = EntitySystemManager.GetEntitySystem<EntityWhitelistSystem>();
var player = _playerManager.LocalSession?.AttachedEntity; var player = _playerManager.LocalSession?.AttachedEntity;
Dictionary<EmoteCategory, List<RadialMenuOption>> emotesByCategory = new(); Dictionary<EmoteCategory, List<RadialMenuOptionBase>> emotesByCategory = new();
foreach (var emote in emotePrototypes) foreach (var emote in emotePrototypes)
{ {
if(emote.Category == EmoteCategory.Invalid) if(emote.Category == EmoteCategory.Invalid)
@@ -157,19 +157,19 @@ public sealed class EmotesUIController : UIController, IOnStateChanged<GameplayS
if (!emotesByCategory.TryGetValue(emote.Category, out var list)) if (!emotesByCategory.TryGetValue(emote.Category, out var list))
{ {
list = new List<RadialMenuOption>(); list = new List<RadialMenuOptionBase>();
emotesByCategory.Add(emote.Category, list); emotesByCategory.Add(emote.Category, list);
} }
var actionOption = new RadialMenuActionOption<EmotePrototype>(HandleRadialButtonClick, emote) var actionOption = new RadialMenuActionOption<EmotePrototype>(HandleRadialButtonClick, emote)
{ {
Sprite = emote.Icon, IconSpecifier = RadialMenuIconSpecifier.With(emote.Icon),
ToolTip = Loc.GetString(emote.Name) ToolTip = Loc.GetString(emote.Name)
}; };
list.Add(actionOption); list.Add(actionOption);
} }
var models = new RadialMenuOption[emotesByCategory.Count]; var models = new RadialMenuOptionBase[emotesByCategory.Count];
var i = 0; var i = 0;
foreach (var (key, list) in emotesByCategory) foreach (var (key, list) in emotesByCategory)
{ {
@@ -177,7 +177,7 @@ public sealed class EmotesUIController : UIController, IOnStateChanged<GameplayS
models[i] = new RadialMenuNestedLayerOption(list) models[i] = new RadialMenuNestedLayerOption(list)
{ {
Sprite = tuple.Sprite, IconSpecifier = RadialMenuIconSpecifier.With(tuple.Sprite),
ToolTip = Loc.GetString(tuple.Tooltip) ToolTip = Loc.GetString(tuple.Tooltip)
}; };
i++; i++;

View File

@@ -24,6 +24,6 @@ namespace Content.Shared.Ghost.Roles.Components
/// If this ghostrole spawner has multiple selectable ghostrole prototypes. /// If this ghostrole spawner has multiple selectable ghostrole prototypes.
/// </summary> /// </summary>
[DataField] [DataField]
public List<string> SelectablePrototypes = []; public List<ProtoId<GhostRolePrototype>> SelectablePrototypes = [];
} }
} }