Files
tbd-station-14/Content.Client/UserInterface/Controls/SimpleRadialMenu.xaml.cs

390 lines
12 KiB
C#

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<RadialMenuOptionBase> models, SimpleRadialMenuSettings? settings = null)
{
ClearExistingChildrenRadialButtons();
var sprites = _entManager.System<SpriteSystem>();
Fill(models, sprites, Children, settings ?? new SimpleRadialMenuSettings());
}
public void OpenOverMouseScreenPosition()
{
var vpSize = _clyde.ScreenSize;
OpenCenteredAt(_inputManager.MouseScreenPosition.Position / vpSize);
}
private void Fill(
IEnumerable<RadialMenuOptionBase> models,
SpriteSystem sprites,
ICollection<Control> 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<Control> 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<Control>(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<SpriteSystem>().GetSpriteScreenCoordinates((_attachMenuToEntity.Value, null, xform));
if (!coords.IsValid)
{
Close();
return;
}
OpenScreenAt(coords.Position, _clyde);
}
#endregion
}
/// <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;
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; }
/// <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;
}
/// <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));
/// <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;
/// <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>
/// Default container draw radius. Is going to be further affected by per sector increment.
/// </summary>
public int DefaultContainerRadius = 100;
/// <summary>
/// Marker, if sector-buttons should be used.
/// </summary>
public bool UseSectors = true;
/// <summary>
/// Marker, if border of buttons should be rendered. Can only be used when <see cref="UseSectors"/> = true.
/// </summary>
public bool DisplayBorders = true;
/// <summary>
/// Marker, if sector background should not be rendered. Can only be used when <see cref="UseSectors"/> = true.
/// </summary>
public bool NoBackground = false;
}