using Robust.Client.UserInterface; using System.Numerics; using Robust.Client.AutoGenerated; using Robust.Client.Graphics; using Robust.Shared.Utility; 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; [GenerateTypedNameReferences] public sealed partial class SimpleRadialMenu : RadialMenu { private EntityUid? _attachMenuToEntity; [Dependency] private readonly IClyde _clyde = default!; [Dependency] private readonly IEntityManager _entManager = default!; [Dependency] private readonly IInputManager _inputManager = default!; public SimpleRadialMenu() { IoCManager.InjectDependencies(this); RobustXamlLoader.Load(this); } public void Track(EntityUid owner) { _attachMenuToEntity = owner; } public void SetButtons(IEnumerable models, SimpleRadialMenuSettings? settings = null) { ClearExistingChildrenRadialButtons(); var sprites = _entManager.System(); Fill(models, sprites, Children, settings ?? new SimpleRadialMenuSettings()); } public void OpenOverMouseScreenPosition() { var vpSize = _clyde.ScreenSize; OpenCenteredAt(_inputManager.MouseScreenPosition.Position / vpSize); } private void Fill( IEnumerable models, SpriteSystem sprites, ICollection rootControlChildren, SimpleRadialMenuSettings settings ) { var rootContainer = new RadialContainer { HorizontalExpand = true, VerticalExpand = true, InitialRadius = settings.DefaultContainerRadius, ReserveSpaceForHiddenChildren = false, Visible = true }; rootControlChildren.Add(rootContainer); foreach (var model in models) { if (model is RadialMenuNestedLayerOption nestedMenuModel) { var linkButton = RecursiveContainerExtraction(sprites, rootControlChildren, nestedMenuModel, settings); linkButton.Visible = true; rootContainer.AddChild(linkButton); } else { var rootButtons = ConvertToButton(model, sprites, settings, false); rootContainer.AddChild(rootButtons); } } } private RadialMenuButton RecursiveContainerExtraction( SpriteSystem sprites, ICollection rootControlChildren, RadialMenuNestedLayerOption model, SimpleRadialMenuSettings settings ) { var container = new RadialContainer { HorizontalExpand = true, VerticalExpand = true, InitialRadius = model.ContainerRadius!.Value, ReserveSpaceForHiddenChildren = false, Visible = false }; foreach (var nested in model.Nested) { if (nested is RadialMenuNestedLayerOption nestedMenuModel) { var linkButton = RecursiveContainerExtraction(sprites, rootControlChildren, nestedMenuModel, settings); container.AddChild(linkButton); } else { var button = ConvertToButton(nested, sprites, settings, false); container.AddChild(button); } } rootControlChildren.Add(container); var thisLayerLinkButton = ConvertToButton(model, sprites, settings, true); thisLayerLinkButton.TargetLayer = container; return thisLayerLinkButton; } private RadialMenuButton ConvertToButton( RadialMenuOptionBase model, SpriteSystem sprites, SimpleRadialMenuSettings settings, bool haveNested ) { var button = settings.UseSectors ? ConvertToButtonWithSector(model, settings) : new RadialMenuButton(); button.SetSize = new Vector2(64f, 64f); button.ToolTip = model.ToolTip; var imageControl = model.IconSpecifier switch { RadialMenuTextureIconSpecifier textureSpecifier => CreateTexture(textureSpecifier.Sprite, sprites), RadialMenuEntityIconSpecifier entitySpecifier => CreateSpriteView(entitySpecifier.Entity), RadialMenuEntityPrototypeIconSpecifier entProtoSpecifier => CreateEntityPrototypeView(entProtoSpecifier.ProtoId), _ => null }; if(imageControl != null) button.AddChild(imageControl); if (model is RadialMenuActionOptionBase actionOption) { button.OnPressed += _ => { actionOption.OnPressed?.Invoke(); if (!haveNested) Close(); }; } return button; } 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 RadialMenuButtonWithSector { DrawBorder = settings.DisplayBorders, DrawBackground = !settings.NoBackground }; if (model.BackgroundColor.HasValue) { button.BackgroundColor = model.BackgroundColor.Value; } if (model.HoverBackgroundColor.HasValue) { button.HoverBackgroundColor = model.HoverBackgroundColor.Value; } return button; } private void ClearExistingChildrenRadialButtons() { var toRemove = new List(ChildCount); foreach (var child in Children) { if (child != ContextualButton && child != MenuOuterAreaButton) { toRemove.Add(child); } } foreach (var control in toRemove) { Children.Remove(control); } } #region target entity tracking protected override void FrameUpdate(FrameEventArgs args) { base.FrameUpdate(args); if (_attachMenuToEntity != null) { UpdatePosition(); } } private void UpdatePosition() { if (!_entManager.TryGetComponent(_attachMenuToEntity, out TransformComponent? xform)) { Close(); return; } if (!xform.Coordinates.IsValid(_entManager)) { Close(); return; } var coords = _entManager.System().GetSpriteScreenCoordinates((_attachMenuToEntity.Value, null, xform)); if (!coords.IsValid) { Close(); return; } OpenScreenAt(coords.Position, _clyde); } #endregion } /// /// Abstract representation of a way to specify icon in radial menu. /// public abstract record RadialMenuIconSpecifier { /// Use entity prototype viewer. public static RadialMenuIconSpecifier? With(EntProtoId? protoId) { if (protoId is null) return null; return new RadialMenuEntityPrototypeIconSpecifier(protoId.Value); } /// Use simple texture icon. public static RadialMenuIconSpecifier? With(SpriteSpecifier? sprite) { if (sprite == null) return null; return new RadialMenuTextureIconSpecifier(sprite); } /// Use entity sprite viewer. public static RadialMenuIconSpecifier? With(EntityUid? entity) { if (entity == null) return null; return new RadialMenuEntityIconSpecifier(entity.Value); } } /// Marker that should be used to display radial menu icon. public sealed record RadialMenuEntityIconSpecifier(EntityUid Entity) : RadialMenuIconSpecifier; /// Marker that should be used to display radial menu icon. public sealed record RadialMenuTextureIconSpecifier(SpriteSpecifier Sprite) : RadialMenuIconSpecifier; /// Marker that should be used to display radial menu icon. public sealed record RadialMenuEntityPrototypeIconSpecifier(EntProtoId ProtoId) : RadialMenuIconSpecifier; /// Container for common options for radial menu button. public abstract class RadialMenuOptionBase { /// Tooltip to be displayed when button is hovered. public string? ToolTip { get; init; } /// /// Color for button background. /// Is used only with sector radial (). /// public Color? BackgroundColor { get; set; } /// /// Color for button background when it is hovered. /// Is used only with sector radial (). /// public Color? HoverBackgroundColor { get; set; } /// /// Specifier that describes icon to be used for radial menu button. /// public RadialMenuIconSpecifier? IconSpecifier { get; set; } } /// Base type for model of radial menu button with some action on button pressed. /// public abstract class RadialMenuActionOptionBase(Action onPressed) : RadialMenuOptionBase { /// Action to be executed on button press. public Action OnPressed { get; } = onPressed; } /// Strong-typed model for radial menu button with action, stores provided data to be used upon button press. public sealed class RadialMenuActionOption(Action onPressed, T data) : RadialMenuActionOptionBase(onPressed: () => onPressed(data)); /// /// Model for radial menu button that represents reference for next layer of radial buttons. /// /// List of button models for next layer of menu. /// Radius for radial menu buttons of next layer. public sealed class RadialMenuNestedLayerOption(IReadOnlyCollection nested, float containerRadius = 100) : RadialMenuOptionBase { /// Radius for radial menu buttons of next layer. public float? ContainerRadius { get; } = containerRadius; /// List of button models for next layer of menu. public IReadOnlyCollection Nested { get; } = nested; } /// /// Additional settings for radial menu render. /// public sealed class SimpleRadialMenuSettings { /// /// Default container draw radius. Is going to be further affected by per sector increment. /// public int DefaultContainerRadius = 100; /// /// Marker, if sector-buttons should be used. /// public bool UseSectors = true; /// /// Marker, if border of buttons should be rendered. Can only be used when = true. /// public bool DisplayBorders = true; /// /// Marker, if sector background should not be rendered. Can only be used when = true. /// public bool NoBackground = false; }