Feature/make radial menu great again (#32653)
* it works! kinda * so it works now * minor cleanup * central button now is useful too * more cleanup * minor cleanup * more cleanup * refactor: migrated code from toolbox (as it was rejected as too specific) * feat: moved border drawing for radial menu into RadialMenuTextureButton. Radial menu position setting into was moved to OverrideArrange to not being called on every frame * refactor: major reworks! * renamed DrawBagleSector to DrawAnnulusSector * Remove strange indexing * Regularize math * refactor: re-orienting segment elements to be Y-mirrored * refactor: extracted radial menu radius multiplier property, changed color pallet for radial menu button * refactor: removed icon backgrounds on textures used in current radial menu buttons with sectors, RadialContainer Radius renamed and now actually changed control radius. * refactor: in RadialMenuTextureButtonWithSector all sector colors are converted to and from sRGB in property getter-setters * refactor: renamed srgb to include Srgb suffix so devs gonna see that its srgb clearly * fix: enabled any functional keys pressed when pushing radial menu buttons * fix: radial menu sector now scales with UIScale * fix: accept only one event when clicking on radial menu ContextualButton * fix: now radial menu buttons accepts only click/alt-click, now clicks outside menu closes menu always --------- Co-authored-by: pa.pecherskij <pa.pecherskij@interfax.ru> Co-authored-by: Eoin Mcloughlin <helloworld@eoinrul.es>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
<ui:RadialMenu xmlns="https://spacestation14.io"
|
||||
<ui:RadialMenu xmlns="https://spacestation14.io"
|
||||
xmlns:ui="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
BackButtonStyleClass="RadialMenuBackButton"
|
||||
CloseButtonStyleClass="RadialMenuCloseButton"
|
||||
@@ -7,25 +7,25 @@
|
||||
MinSize="450 450">
|
||||
|
||||
<!-- Main -->
|
||||
<ui:RadialContainer Name="Main" VerticalExpand="True" HorizontalExpand="True" Radius="64" ReserveSpaceForHiddenChildren="False">
|
||||
<ui:RadialMenuTextureButton StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'emote-menu-category-general'}" TargetLayer="General" Visible="False">
|
||||
<ui:RadialContainer Name="Main" VerticalExpand="True" HorizontalExpand="True" InitialRadius="100" ReserveSpaceForHiddenChildren="False">
|
||||
<ui:RadialMenuTextureButtonWithSector SetSize="64 64" ToolTip="{Loc 'emote-menu-category-general'}" TargetLayer="General" Visible="False">
|
||||
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Clothing/Head/Soft/mimesoft.rsi/icon.png"/>
|
||||
</ui:RadialMenuTextureButton>
|
||||
<ui:RadialMenuTextureButton StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'emote-menu-category-vocal'}" TargetLayer="Vocal" Visible="False">
|
||||
</ui:RadialMenuTextureButtonWithSector>
|
||||
<ui:RadialMenuTextureButtonWithSector SetSize="64 64" ToolTip="{Loc 'emote-menu-category-vocal'}" TargetLayer="Vocal" Visible="False">
|
||||
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Interface/Emotes/vocal.png"/>
|
||||
</ui:RadialMenuTextureButton>
|
||||
<ui:RadialMenuTextureButton StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'emote-menu-category-hands'}" TargetLayer="Hands" Visible="False">
|
||||
</ui:RadialMenuTextureButtonWithSector>
|
||||
<ui:RadialMenuTextureButtonWithSector SetSize="64 64" ToolTip="{Loc 'emote-menu-category-hands'}" TargetLayer="Hands" Visible="False">
|
||||
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Clothing/Hands/Gloves/latex.rsi/icon.png"/>
|
||||
</ui:RadialMenuTextureButton>
|
||||
</ui:RadialMenuTextureButtonWithSector>
|
||||
</ui:RadialContainer>
|
||||
|
||||
<!-- General -->
|
||||
<ui:RadialContainer Name="General" VerticalExpand="True" HorizontalExpand="True" Radius="64"/>
|
||||
<ui:RadialContainer Name="General" VerticalExpand="True" HorizontalExpand="True" InitialRadius="100"/>
|
||||
|
||||
<!-- Vocal -->
|
||||
<ui:RadialContainer Name="Vocal" VerticalExpand="True" HorizontalExpand="True" Radius="64"/>
|
||||
<ui:RadialContainer Name="Vocal" VerticalExpand="True" HorizontalExpand="True" InitialRadius="100"/>
|
||||
|
||||
<!-- Hands -->
|
||||
<ui:RadialContainer Name="Hands" VerticalExpand="True" HorizontalExpand="True" Radius="64"/>
|
||||
<ui:RadialContainer Name="Hands" VerticalExpand="True" HorizontalExpand="True" InitialRadius="100"/>
|
||||
|
||||
</ui:RadialMenu>
|
||||
|
||||
@@ -50,7 +50,6 @@ public sealed partial class EmotesMenu : RadialMenu
|
||||
|
||||
var button = new EmoteMenuButton
|
||||
{
|
||||
StyleClasses = { "RadialMenuButton" },
|
||||
SetSize = new Vector2(64f, 64f),
|
||||
ToolTip = Loc.GetString(emote.Name),
|
||||
ProtoId = emote.ID,
|
||||
@@ -106,7 +105,7 @@ public sealed partial class EmotesMenu : RadialMenu
|
||||
}
|
||||
|
||||
|
||||
public sealed class EmoteMenuButton : RadialMenuTextureButton
|
||||
public sealed class EmoteMenuButton : RadialMenuTextureButtonWithSector
|
||||
{
|
||||
public ProtoId<EmotePrototype> ProtoId { get; set; }
|
||||
}
|
||||
|
||||
@@ -51,7 +51,6 @@ public sealed partial class GhostRoleRadioMenu : RadialMenu
|
||||
|
||||
var button = new GhostRoleRadioMenuButton()
|
||||
{
|
||||
StyleClasses = { "RadialMenuButton" },
|
||||
SetSize = new Vector2(64, 64),
|
||||
ToolTip = Loc.GetString(ghostRoleProto.Name),
|
||||
ProtoId = ghostRoleProto.ID,
|
||||
@@ -100,7 +99,7 @@ public sealed partial class GhostRoleRadioMenu : RadialMenu
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class GhostRoleRadioMenuButton : RadialMenuTextureButton
|
||||
public sealed class GhostRoleRadioMenuButton : RadialMenuTextureButtonWithSector
|
||||
{
|
||||
public ProtoId<GhostRolePrototype> ProtoId { get; set; }
|
||||
}
|
||||
|
||||
@@ -11,37 +11,37 @@
|
||||
<!-- The radial menu will try to open so that its center is located where the player's cursor is currently -->
|
||||
|
||||
<!-- Entry layer (shows main categories) -->
|
||||
<ui:RadialContainer Name="Main" VerticalExpand="True" HorizontalExpand="True" Radius="64" ReserveSpaceForHiddenChildren="False">
|
||||
<ui:RadialMenuTextureButton StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'rcd-component-walls-and-flooring'}" TargetLayer="WallsAndFlooring" Visible="False">
|
||||
<ui:RadialContainer Name="Main" VerticalExpand="True" HorizontalExpand="True" InitialRadius="100" ReserveSpaceForHiddenChildren="False">
|
||||
<ui:RadialMenuTextureButtonWithSector SetSize="64 64" ToolTip="{Loc 'rcd-component-walls-and-flooring'}" TargetLayer="WallsAndFlooring" Visible="False">
|
||||
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Interface/Radial/RCD/walls_and_flooring.png"/>
|
||||
</ui:RadialMenuTextureButton>
|
||||
<ui:RadialMenuTextureButton StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'rcd-component-windows-and-grilles'}" TargetLayer="WindowsAndGrilles" Visible="False">
|
||||
</ui:RadialMenuTextureButtonWithSector>
|
||||
<ui:RadialMenuTextureButtonWithSector SetSize="64 64" ToolTip="{Loc 'rcd-component-windows-and-grilles'}" TargetLayer="WindowsAndGrilles" Visible="False">
|
||||
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Interface/Radial/RCD/windows_and_grilles.png"/>
|
||||
</ui:RadialMenuTextureButton>
|
||||
<ui:RadialMenuTextureButton StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'rcd-component-airlocks'}" TargetLayer="Airlocks" Visible="False">
|
||||
</ui:RadialMenuTextureButtonWithSector>
|
||||
<ui:RadialMenuTextureButtonWithSector SetSize="64 64" ToolTip="{Loc 'rcd-component-airlocks'}" TargetLayer="Airlocks" Visible="False">
|
||||
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Interface/Radial/RCD/airlocks.png"/>
|
||||
</ui:RadialMenuTextureButton>
|
||||
<ui:RadialMenuTextureButton StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'rcd-component-electrical'}" TargetLayer="Electrical" Visible="False">
|
||||
</ui:RadialMenuTextureButtonWithSector>
|
||||
<ui:RadialMenuTextureButtonWithSector SetSize="64 64" ToolTip="{Loc 'rcd-component-electrical'}" TargetLayer="Electrical" Visible="False">
|
||||
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Interface/Radial/RCD/multicoil.png"/>
|
||||
</ui:RadialMenuTextureButton>
|
||||
<ui:RadialMenuTextureButton StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'rcd-component-lighting'}" TargetLayer="Lighting" Visible="False">
|
||||
</ui:RadialMenuTextureButtonWithSector>
|
||||
<ui:RadialMenuTextureButtonWithSector SetSize="64 64" ToolTip="{Loc 'rcd-component-lighting'}" TargetLayer="Lighting" Visible="False">
|
||||
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Interface/Radial/RCD/lighting.png"/>
|
||||
</ui:RadialMenuTextureButton>
|
||||
</ui:RadialMenuTextureButtonWithSector>
|
||||
</ui:RadialContainer>
|
||||
|
||||
<!-- Walls and flooring -->
|
||||
<ui:RadialContainer Name="WallsAndFlooring" VerticalExpand="True" HorizontalExpand="True" Radius="64"/>
|
||||
<ui:RadialContainer Name="WallsAndFlooring" VerticalExpand="True" HorizontalExpand="True" InitialRadius="100"/>
|
||||
|
||||
<!-- Windows and grilles -->
|
||||
<ui:RadialContainer Name="WindowsAndGrilles" VerticalExpand="True" HorizontalExpand="True" Radius="64"/>
|
||||
<ui:RadialContainer Name="WindowsAndGrilles" VerticalExpand="True" HorizontalExpand="True" InitialRadius="100"/>
|
||||
|
||||
<!-- Airlocks -->
|
||||
<ui:RadialContainer Name="Airlocks" VerticalExpand="True" HorizontalExpand="True" Radius="64"/>
|
||||
<ui:RadialContainer Name="Airlocks" VerticalExpand="True" HorizontalExpand="True" InitialRadius="100"/>
|
||||
|
||||
<!-- Computer and machine frames -->
|
||||
<ui:RadialContainer Name="Electrical" VerticalExpand="True" HorizontalExpand="True" Radius="64"/>
|
||||
<ui:RadialContainer Name="Electrical" VerticalExpand="True" HorizontalExpand="True" InitialRadius="100"/>
|
||||
|
||||
<!-- Lighting -->
|
||||
<ui:RadialContainer Name="Lighting" VerticalExpand="True" HorizontalExpand="True" Radius="64"/>
|
||||
<ui:RadialContainer Name="Lighting" VerticalExpand="True" HorizontalExpand="True" InitialRadius="100"/>
|
||||
|
||||
</ui:RadialMenu>
|
||||
|
||||
@@ -74,7 +74,6 @@ public sealed partial class RCDMenu : RadialMenu
|
||||
|
||||
var button = new RCDMenuButton()
|
||||
{
|
||||
StyleClasses = { "RadialMenuButton" },
|
||||
SetSize = new Vector2(64f, 64f),
|
||||
ToolTip = tooltip,
|
||||
ProtoId = protoId,
|
||||
@@ -99,9 +98,7 @@ public sealed partial class RCDMenu : RadialMenu
|
||||
// is visible in the main radial container (as these all start with Visible = false)
|
||||
foreach (var child in main.Children)
|
||||
{
|
||||
var castChild = child as RadialMenuTextureButton;
|
||||
|
||||
if (castChild is not RadialMenuTextureButton)
|
||||
if (child is not RadialMenuTextureButton castChild)
|
||||
continue;
|
||||
|
||||
if (castChild.TargetLayer == proto.Category)
|
||||
@@ -169,12 +166,7 @@ public sealed partial class RCDMenu : RadialMenu
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class RCDMenuButton : RadialMenuTextureButton
|
||||
public sealed class RCDMenuButton : RadialMenuTextureButtonWithSector
|
||||
{
|
||||
public ProtoId<RCDPrototype> ProtoId { get; set; }
|
||||
|
||||
public RCDMenuButton()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<ui:RadialMenu xmlns="https://spacestation14.io"
|
||||
<ui:RadialMenu xmlns="https://spacestation14.io"
|
||||
xmlns:ui="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
BackButtonStyleClass="RadialMenuBackButton"
|
||||
CloseButtonStyleClass="RadialMenuCloseButton"
|
||||
@@ -7,7 +7,7 @@
|
||||
MinSize="450 450">
|
||||
|
||||
<!-- Main -->
|
||||
<ui:RadialContainer Name="Main" VerticalExpand="True" HorizontalExpand="True" Radius="64" ReserveSpaceForHiddenChildren="False">
|
||||
<ui:RadialContainer Name="Main" VerticalExpand="True" HorizontalExpand="True" InitialRadius="100" ReserveSpaceForHiddenChildren="False">
|
||||
</ui:RadialContainer>
|
||||
|
||||
</ui:RadialMenu>
|
||||
|
||||
@@ -54,7 +54,6 @@ public sealed partial class StationAiMenu : RadialMenu
|
||||
// TODO: This radial boilerplate is quite annoying
|
||||
var button = new StationAiMenuButton(action.Event)
|
||||
{
|
||||
StyleClasses = { "RadialMenuButton" },
|
||||
SetSize = new Vector2(64f, 64f),
|
||||
ToolTip = action.Tooltip != null ? Loc.GetString(action.Tooltip) : null,
|
||||
};
|
||||
@@ -121,7 +120,7 @@ public sealed partial class StationAiMenu : RadialMenu
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class StationAiMenuButton(BaseStationAiAction action) : RadialMenuTextureButton
|
||||
public sealed class StationAiMenuButton(BaseStationAiAction action) : RadialMenuTextureButtonWithSector
|
||||
{
|
||||
public BaseStationAiAction Action = action;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
@@ -8,6 +7,11 @@ namespace Content.Client.UserInterface.Controls;
|
||||
[Virtual]
|
||||
public class RadialContainer : LayoutContainer
|
||||
{
|
||||
/// <summary>
|
||||
/// Increment of radius per child element to be rendered.
|
||||
/// </summary>
|
||||
private const float RadiusIncrement = 5f;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the anglular range, in radians, in which child elements will be placed.
|
||||
/// The first value denotes the angle at which the first element is to be placed, and
|
||||
@@ -49,10 +53,30 @@ public class RadialContainer : LayoutContainer
|
||||
public RAlignment RadialAlignment { get; set; } = RAlignment.Clockwise;
|
||||
|
||||
/// <summary>
|
||||
/// Determines how far from the radial container's center that its child elements will be placed
|
||||
/// Radial menu radius determines how far from the radial container's center its child elements will be placed.
|
||||
/// To correctly display dynamic amount of elements control actually resizes depending on amount of child buttons,
|
||||
/// but uses this property as base value for final radius calculation.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public float Radius { get; set; } = 100f;
|
||||
public float InitialRadius { get; set; } = 100f;
|
||||
|
||||
/// <summary>
|
||||
/// Radial menu radius determines how far from the radial container's center its child elements will be placed.
|
||||
/// This is dynamically calculated (based on child button count) radius, result of <see cref="InitialRadius"/> and
|
||||
/// <see cref="RadiusIncrement"/> multiplied by currently visible child button count.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadOnly)]
|
||||
public float CalculatedRadius { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Determines radial menu button sectors inner radius, is a multiplier of <see cref="InitialRadius"/>.
|
||||
/// </summary>
|
||||
public float InnerRadiusMultiplier { get; set; } = 0.5f;
|
||||
|
||||
/// <summary>
|
||||
/// Determines radial menu button sectors outer radius, is a multiplier of <see cref="InitialRadius"/>.
|
||||
/// </summary>
|
||||
public float OuterRadiusMultiplier { get; set; } = 1.5f;
|
||||
|
||||
/// <summary>
|
||||
/// Sets whether the container should reserve a space on the layout for child which are not currently visible
|
||||
@@ -68,36 +92,73 @@ public class RadialContainer : LayoutContainer
|
||||
|
||||
}
|
||||
|
||||
protected override void Draw(DrawingHandleScreen handle)
|
||||
/// <inheritdoc />
|
||||
protected override Vector2 ArrangeOverride(Vector2 finalSize)
|
||||
{
|
||||
var children = ReserveSpaceForHiddenChildren
|
||||
? Children
|
||||
: Children.Where(x => x.Visible);
|
||||
|
||||
const float baseRadius = 100f;
|
||||
const float radiusIncrement = 5f;
|
||||
|
||||
var children = ReserveSpaceForHiddenChildren ? Children : Children.Where(x => x.Visible);
|
||||
var childCount = children.Count();
|
||||
|
||||
// Add padding from the center at higher child counts so they don't overlap.
|
||||
Radius = baseRadius + (childCount * radiusIncrement);
|
||||
// Add padding from the center at higher child counts so they don't overlap.
|
||||
CalculatedRadius = InitialRadius + (childCount * RadiusIncrement);
|
||||
|
||||
var isAntiClockwise = RadialAlignment == RAlignment.AntiClockwise;
|
||||
|
||||
// Determine the size of the arc, accounting for clockwise and anti-clockwise arrangements
|
||||
var arc = AngularRange.Y - AngularRange.X;
|
||||
arc = (arc < 0) ? MathF.Tau + arc : arc;
|
||||
arc = (RadialAlignment == RAlignment.AntiClockwise) ? MathF.Tau - arc : arc;
|
||||
arc = arc < 0
|
||||
? MathF.Tau + arc
|
||||
: arc;
|
||||
arc = isAntiClockwise
|
||||
? MathF.Tau - arc
|
||||
: arc;
|
||||
|
||||
// Account for both circular arrangements and arc-based arrangements
|
||||
var childMod = MathHelper.CloseTo(arc, MathF.Tau, 0.01f) ? 0 : 1;
|
||||
var childMod = MathHelper.CloseTo(arc, MathF.Tau, 0.01f)
|
||||
? 0
|
||||
: 1;
|
||||
|
||||
// Determine the separation between child elements
|
||||
var sepAngle = arc / (childCount - childMod);
|
||||
sepAngle *= (RadialAlignment == RAlignment.AntiClockwise) ? -1f : 1f;
|
||||
sepAngle *= isAntiClockwise
|
||||
? -1f
|
||||
: 1f;
|
||||
|
||||
var controlCenter = finalSize * 0.5f;
|
||||
|
||||
// Adjust the positions of all the child elements
|
||||
foreach (var (i, child) in children.Select((x, i) => (i, x)))
|
||||
var query = children.Select((x, index) => (index, x));
|
||||
foreach (var (childIndex, child) in query)
|
||||
{
|
||||
var position = new Vector2(Radius * MathF.Sin(AngularRange.X + sepAngle * i) + Width / 2f - child.Width / 2f, -Radius * MathF.Cos(AngularRange.X + sepAngle * i) + Height / 2f - child.Height / 2f);
|
||||
const float angleOffset = MathF.PI * 0.5f;
|
||||
|
||||
var targetAngleOfChild = AngularRange.X + sepAngle * (childIndex + 0.5f) + angleOffset;
|
||||
|
||||
// flooring values for snapping float values to physical grid -
|
||||
// it prevents gaps and overlapping between different button segments
|
||||
var position = new Vector2(
|
||||
MathF.Floor(CalculatedRadius * MathF.Cos(targetAngleOfChild)),
|
||||
MathF.Floor(-CalculatedRadius * MathF.Sin(targetAngleOfChild))
|
||||
) + controlCenter - child.DesiredSize * 0.5f + Position;
|
||||
|
||||
SetPosition(child, position);
|
||||
|
||||
// radial menu buttons with sector need to also know in which sector and around which point
|
||||
// they should be rendered, how much space sector should should take etc.
|
||||
if (child is IRadialMenuItemWithSector tb)
|
||||
{
|
||||
tb.AngleSectorFrom = sepAngle * childIndex;
|
||||
tb.AngleSectorTo = sepAngle * (childIndex + 1);
|
||||
tb.AngleOffset = angleOffset;
|
||||
tb.InnerRadius = CalculatedRadius * InnerRadiusMultiplier;
|
||||
tb.OuterRadius = CalculatedRadius * OuterRadiusMultiplier;
|
||||
tb.ParentCenter = controlCenter;
|
||||
}
|
||||
}
|
||||
|
||||
return base.ArrangeOverride(finalSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -109,4 +170,5 @@ public class RadialContainer : LayoutContainer
|
||||
Clockwise,
|
||||
AntiClockwise,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,6 +3,9 @@ using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Content.Shared.Input;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.Input;
|
||||
|
||||
namespace Content.Client.UserInterface.Controls;
|
||||
|
||||
@@ -12,7 +15,12 @@ public class RadialMenu : BaseWindow
|
||||
/// <summary>
|
||||
/// Contextual button used to traverse through previous layers of the radial menu
|
||||
/// </summary>
|
||||
public TextureButton? ContextualButton { get; set; }
|
||||
public RadialMenuContextualCentralTextureButton ContextualButton { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Button that represents outer area of menu (closes menu on outside clicks).
|
||||
/// </summary>
|
||||
public RadialMenuOuterAreaButton MenuOuterAreaButton { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Set a style class to be applied to the contextual button when it is set to move the user back through previous layers of the radial menu
|
||||
@@ -52,7 +60,7 @@ public class RadialMenu : BaseWindow
|
||||
}
|
||||
}
|
||||
|
||||
private List<Control> _path = new();
|
||||
private readonly List<Control> _path = new();
|
||||
private string? _backButtonStyleClass;
|
||||
private string? _closeButtonStyleClass;
|
||||
|
||||
@@ -78,23 +86,56 @@ public class RadialMenu : BaseWindow
|
||||
}
|
||||
|
||||
// Auto generate a contextual button for moving back through visited layers
|
||||
ContextualButton = new TextureButton()
|
||||
ContextualButton = new RadialMenuContextualCentralTextureButton
|
||||
{
|
||||
HorizontalAlignment = HAlignment.Center,
|
||||
VerticalAlignment = VAlignment.Center,
|
||||
SetSize = new Vector2(64f, 64f),
|
||||
};
|
||||
MenuOuterAreaButton = new RadialMenuOuterAreaButton();
|
||||
|
||||
ContextualButton.OnButtonUp += _ => ReturnToPreviousLayer();
|
||||
MenuOuterAreaButton.OnButtonUp += _ => Close();
|
||||
AddChild(ContextualButton);
|
||||
AddChild(MenuOuterAreaButton);
|
||||
|
||||
// Hide any further add children, unless its promoted to the active layer
|
||||
OnChildAdded += child => child.Visible = (GetCurrentActiveLayer() == child);
|
||||
OnChildAdded += child =>
|
||||
{
|
||||
child.Visible = GetCurrentActiveLayer() == child;
|
||||
SetupContextualButtonData(child);
|
||||
};
|
||||
}
|
||||
|
||||
private void SetupContextualButtonData(Control child)
|
||||
{
|
||||
if (child is RadialContainer { Visible: true } container)
|
||||
{
|
||||
var parentCenter = MinSize * 0.5f;
|
||||
ContextualButton.ParentCenter = parentCenter;
|
||||
MenuOuterAreaButton.ParentCenter = parentCenter;
|
||||
ContextualButton.InnerRadius = container.CalculatedRadius * container.InnerRadiusMultiplier;
|
||||
MenuOuterAreaButton.OuterRadius = container.CalculatedRadius * container.OuterRadiusMultiplier;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Vector2 ArrangeOverride(Vector2 finalSize)
|
||||
{
|
||||
var result = base.ArrangeOverride(finalSize);
|
||||
|
||||
var currentLayer = GetCurrentActiveLayer();
|
||||
if (currentLayer != null)
|
||||
{
|
||||
SetupContextualButtonData(currentLayer);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private Control? GetCurrentActiveLayer()
|
||||
{
|
||||
var children = Children.Where(x => x != ContextualButton);
|
||||
var children = Children.Where(x => x != ContextualButton && x != MenuOuterAreaButton);
|
||||
|
||||
if (!children.Any())
|
||||
return null;
|
||||
@@ -116,7 +157,7 @@ public class RadialMenu : BaseWindow
|
||||
|
||||
foreach (var child in Children)
|
||||
{
|
||||
if (child == ContextualButton)
|
||||
if (child == ContextualButton || child == MenuOuterAreaButton)
|
||||
continue;
|
||||
|
||||
// Hide layers which are not of interest
|
||||
@@ -129,6 +170,7 @@ public class RadialMenu : BaseWindow
|
||||
else
|
||||
{
|
||||
child.Visible = true;
|
||||
SetupContextualButtonData(child);
|
||||
result = true;
|
||||
}
|
||||
}
|
||||
@@ -158,7 +200,7 @@ public class RadialMenu : BaseWindow
|
||||
// Hide all children except the contextual button
|
||||
foreach (var child in Children)
|
||||
{
|
||||
if (child != ContextualButton)
|
||||
if (child != ContextualButton && child != MenuOuterAreaButton)
|
||||
child.Visible = false;
|
||||
}
|
||||
|
||||
@@ -172,49 +214,86 @@ public class RadialMenu : BaseWindow
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Base class for radial menu buttons. Excludes all actions except clicks and alt-clicks
|
||||
/// from interactions.
|
||||
/// </summary>
|
||||
[Virtual]
|
||||
public class RadialMenuButton : Button
|
||||
public class RadialMenuTextureButtonBase : TextureButton
|
||||
{
|
||||
/// <summary>
|
||||
/// Upon clicking this button the radial menu will transition to the named layer
|
||||
/// </summary>
|
||||
public string? TargetLayer { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A simple button that can move the user to a different layer within a radial menu
|
||||
/// </summary>
|
||||
public RadialMenuButton()
|
||||
/// <inheritdoc />
|
||||
protected RadialMenuTextureButtonBase()
|
||||
{
|
||||
OnButtonUp += OnClicked;
|
||||
EnableAllKeybinds = true;
|
||||
}
|
||||
|
||||
private void OnClicked(ButtonEventArgs args)
|
||||
/// <inheritdoc />
|
||||
protected override void KeyBindUp(GUIBoundKeyEventArgs args)
|
||||
{
|
||||
if (TargetLayer == null || TargetLayer == string.Empty)
|
||||
return;
|
||||
|
||||
var parent = FindParentMultiLayerContainer(this);
|
||||
|
||||
if (parent == null)
|
||||
return;
|
||||
|
||||
parent.TryToMoveToNewLayer(TargetLayer);
|
||||
if (args.Function == EngineKeyFunctions.UIClick
|
||||
|| args.Function == ContentKeyFunctions.AltActivateItemInWorld)
|
||||
base.KeyBindUp(args);
|
||||
}
|
||||
}
|
||||
|
||||
private RadialMenu? FindParentMultiLayerContainer(Control control)
|
||||
/// <summary>
|
||||
/// Special button for closing radial menu or going back between radial menu levels.
|
||||
/// Is looking like just <see cref="TextureButton "/> but considers whole space around
|
||||
/// itself (til radial menu buttons) as itself in case of clicking. But this 'effect'
|
||||
/// 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 float InnerRadius { get; set; }
|
||||
|
||||
public Vector2? ParentCenter { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override bool HasPoint(Vector2 point)
|
||||
{
|
||||
foreach (var ancestor in control.GetSelfAndLogicalAncestors())
|
||||
if (ParentCenter == null)
|
||||
{
|
||||
if (ancestor is RadialMenu)
|
||||
return ancestor as RadialMenu;
|
||||
return base.HasPoint(point);
|
||||
}
|
||||
|
||||
return null;
|
||||
var distSquared = (point + Position - ParentCenter.Value).LengthSquared();
|
||||
|
||||
var innerRadiusSquared = InnerRadius * InnerRadius;
|
||||
|
||||
// comparing to squared values is faster then making sqrt
|
||||
return distSquared < innerRadiusSquared;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Menu button for outer area of radial menu (covers everything 'outside').
|
||||
/// </summary>
|
||||
public sealed class RadialMenuOuterAreaButton : RadialMenuTextureButtonBase
|
||||
{
|
||||
public float OuterRadius { get; set; }
|
||||
|
||||
public Vector2? ParentCenter { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override bool HasPoint(Vector2 point)
|
||||
{
|
||||
if (ParentCenter == null)
|
||||
{
|
||||
return base.HasPoint(point);
|
||||
}
|
||||
|
||||
var distSquared = (point + Position - ParentCenter.Value).LengthSquared();
|
||||
|
||||
var outerRadiusSquared = OuterRadius * OuterRadius;
|
||||
|
||||
// comparing to squared values is faster, then making sqrt
|
||||
return distSquared > outerRadiusSquared;
|
||||
}
|
||||
}
|
||||
|
||||
[Virtual]
|
||||
public class RadialMenuTextureButton : TextureButton
|
||||
public class RadialMenuTextureButton : RadialMenuTextureButtonBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Upon clicking this button the radial menu will be moved to the named layer
|
||||
@@ -226,6 +305,7 @@ public class RadialMenuTextureButton : TextureButton
|
||||
/// </summary>
|
||||
public RadialMenuTextureButton()
|
||||
{
|
||||
EnableAllKeybinds = true;
|
||||
OnButtonUp += OnClicked;
|
||||
}
|
||||
|
||||
@@ -246,10 +326,329 @@ public class RadialMenuTextureButton : TextureButton
|
||||
{
|
||||
foreach (var ancestor in control.GetSelfAndLogicalAncestors())
|
||||
{
|
||||
if (ancestor is RadialMenu)
|
||||
return ancestor as RadialMenu;
|
||||
if (ancestor is RadialMenu menu)
|
||||
return menu;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public interface IRadialMenuItemWithSector
|
||||
{
|
||||
/// <summary>
|
||||
/// Angle in radian where button sector should start.
|
||||
/// </summary>
|
||||
public float AngleSectorFrom { set; }
|
||||
|
||||
/// <summary>
|
||||
/// Angle in radian where button sector should end.
|
||||
/// </summary>
|
||||
public float AngleSectorTo { set; }
|
||||
|
||||
/// <summary>
|
||||
/// Outer radius for drawing segment and pointer detection.
|
||||
/// </summary>
|
||||
public float OuterRadius { set; }
|
||||
|
||||
/// <summary>
|
||||
/// Outer radius for drawing segment and pointer detection.
|
||||
/// </summary>
|
||||
public float InnerRadius { set; }
|
||||
|
||||
/// <summary>
|
||||
/// Offset in radian by which menu button should be rotated.
|
||||
/// </summary>
|
||||
public float AngleOffset { set; }
|
||||
|
||||
/// <summary>
|
||||
/// Coordinates of center in parent component - button container.
|
||||
/// </summary>
|
||||
public Vector2 ParentCenter { set; }
|
||||
}
|
||||
|
||||
[Virtual]
|
||||
public class RadialMenuTextureButtonWithSector : RadialMenuTextureButton, IRadialMenuItemWithSector
|
||||
{
|
||||
private Vector2[]? _sectorPointsForDrawing;
|
||||
|
||||
private float _angleSectorFrom;
|
||||
private float _angleSectorTo;
|
||||
private float _outerRadius;
|
||||
private float _innerRadius;
|
||||
private float _angleOffset;
|
||||
|
||||
private bool _isWholeCircle;
|
||||
private Vector2? _parentCenter;
|
||||
|
||||
private Color _backgroundColorSrgb = Color.ToSrgb(new Color(70, 73, 102, 128));
|
||||
private Color _hoverBackgroundColorSrgb = Color.ToSrgb(new Color(87, 91, 127, 128));
|
||||
private Color _borderColorSrgb = Color.ToSrgb(new Color(173, 216, 230, 70));
|
||||
private Color _hoverBorderColorSrgb = Color.ToSrgb(new Color(87, 91, 127, 128));
|
||||
|
||||
/// <summary>
|
||||
/// Marker, that control should render border of segment. Is false by default.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// By default color of border is same as color of background. Use <see cref="BorderColor"/>
|
||||
/// and <see cref="HoverBorderColor"/> to change it.
|
||||
/// </remarks>
|
||||
public bool DrawBorder { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Marker, that control should render background of all sector. Is true by default.
|
||||
/// </summary>
|
||||
public bool DrawBackground { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Marker, that control should render separator lines.
|
||||
/// Separator lines are used to visually separate sector of radial menu items.
|
||||
/// Is true by default
|
||||
/// </summary>
|
||||
public bool DrawSeparators { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Color of background in non-hovered state. Accepts RGB color, works with sRGB for DrawPrimitive internally.
|
||||
/// </summary>
|
||||
public Color BackgroundColor
|
||||
{
|
||||
get => Color.FromSrgb(_backgroundColorSrgb);
|
||||
set => _backgroundColorSrgb = Color.ToSrgb(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Color of background in hovered state. Accepts RGB color, works with sRGB for DrawPrimitive internally.
|
||||
/// </summary>
|
||||
public Color HoverBackgroundColor
|
||||
{
|
||||
get => Color.FromSrgb(_hoverBackgroundColorSrgb);
|
||||
set => _hoverBackgroundColorSrgb = Color.ToSrgb(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Color of button border. Accepts RGB color, works with sRGB for DrawPrimitive internally.
|
||||
/// </summary>
|
||||
public Color BorderColor
|
||||
{
|
||||
get => Color.FromSrgb(_borderColorSrgb);
|
||||
set => _borderColorSrgb = Color.ToSrgb(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Color of button border when button is hovered. Accepts RGB color, works with sRGB for DrawPrimitive internally.
|
||||
/// </summary>
|
||||
public Color HoverBorderColor
|
||||
{
|
||||
get => Color.FromSrgb(_hoverBorderColorSrgb);
|
||||
set => _hoverBorderColorSrgb = Color.ToSrgb(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Color of separator lines.
|
||||
/// Separator lines are used to visually separate sector of radial menu items.
|
||||
/// </summary>
|
||||
public Color SeparatorColor { get; set; } = new Color(128, 128, 128, 128);
|
||||
|
||||
/// <inheritdoc />
|
||||
float IRadialMenuItemWithSector.AngleSectorFrom
|
||||
{
|
||||
set
|
||||
{
|
||||
_angleSectorFrom = value;
|
||||
_isWholeCircle = IsWholeCircle(value, _angleSectorTo);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
float IRadialMenuItemWithSector.AngleSectorTo
|
||||
{
|
||||
set
|
||||
{
|
||||
_angleSectorTo = value;
|
||||
_isWholeCircle = IsWholeCircle(_angleSectorFrom, value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
float IRadialMenuItemWithSector.OuterRadius { set => _outerRadius = value; }
|
||||
|
||||
/// <inheritdoc />
|
||||
float IRadialMenuItemWithSector.InnerRadius { set => _innerRadius = value; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public float AngleOffset { set => _angleOffset = value; }
|
||||
|
||||
/// <inheritdoc />
|
||||
Vector2 IRadialMenuItemWithSector.ParentCenter { set => _parentCenter = value; }
|
||||
|
||||
/// <summary>
|
||||
/// A simple texture button that can move the user to a different layer within a radial menu
|
||||
/// </summary>
|
||||
public RadialMenuTextureButtonWithSector()
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Draw(DrawingHandleScreen handle)
|
||||
{
|
||||
base.Draw(handle);
|
||||
|
||||
if (_parentCenter == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// draw sector where space that button occupies actually is
|
||||
var containerCenter = (_parentCenter.Value - Position) * UIScale;
|
||||
|
||||
var angleFrom = _angleSectorFrom + _angleOffset;
|
||||
var angleTo = _angleSectorTo + _angleOffset;
|
||||
if (DrawBackground)
|
||||
{
|
||||
var segmentColor = DrawMode == DrawModeEnum.Hover
|
||||
? _hoverBackgroundColorSrgb
|
||||
: _backgroundColorSrgb;
|
||||
|
||||
DrawAnnulusSector(handle, containerCenter, _innerRadius * UIScale, _outerRadius * UIScale, angleFrom, angleTo, segmentColor);
|
||||
}
|
||||
|
||||
if (DrawBorder)
|
||||
{
|
||||
var borderColor = DrawMode == DrawModeEnum.Hover
|
||||
? _hoverBorderColorSrgb
|
||||
: _borderColorSrgb;
|
||||
DrawAnnulusSector(handle, containerCenter, _innerRadius * UIScale, _outerRadius * UIScale, angleFrom, angleTo, borderColor, false);
|
||||
}
|
||||
|
||||
if (!_isWholeCircle && DrawSeparators)
|
||||
{
|
||||
DrawSeparatorLines(handle, containerCenter, _innerRadius * UIScale, _outerRadius * UIScale, angleFrom, angleTo, SeparatorColor);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override bool HasPoint(Vector2 point)
|
||||
{
|
||||
if (_parentCenter == null)
|
||||
{
|
||||
return base.HasPoint(point);
|
||||
}
|
||||
|
||||
var outerRadiusSquared = _outerRadius * _outerRadius;
|
||||
var innerRadiusSquared = _innerRadius * _innerRadius;
|
||||
|
||||
var distSquared = (point + Position - _parentCenter.Value).LengthSquared();
|
||||
var isInRadius = distSquared < outerRadiusSquared && distSquared > innerRadiusSquared;
|
||||
if (!isInRadius)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// difference from the center of the parent to the `point`
|
||||
var pointFromParent = point + Position - _parentCenter.Value;
|
||||
|
||||
// Flip Y to get from ui coordinates to natural coordinates
|
||||
var angle = MathF.Atan2(-pointFromParent.Y, pointFromParent.X) - _angleOffset;
|
||||
if (angle < 0)
|
||||
{
|
||||
// atan2 range is -pi->pi, while angle sectors are
|
||||
// 0->2pi, so remap the result into that range
|
||||
angle = MathF.PI * 2 + angle;
|
||||
}
|
||||
|
||||
var isInAngle = angle >= _angleSectorFrom && angle < _angleSectorTo;
|
||||
return isInAngle;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draw segment between two concentrated circles from and to certain angles.
|
||||
/// </summary>
|
||||
/// <param name="drawingHandleScreen">Drawing handle, to which rendering should be delegated.</param>
|
||||
/// <param name="center">Point where circle center should be.</param>
|
||||
/// <param name="radiusInner">Radius of internal circle.</param>
|
||||
/// <param name="radiusOuter">Radius of external circle.</param>
|
||||
/// <param name="angleSectorFrom">Angle in radian, from which sector should start.</param>
|
||||
/// <param name="angleSectorTo">Angle in radian, from which sector should start.</param>
|
||||
/// <param name="color">Color for drawing.</param>
|
||||
/// <param name="filled">Should figure be filled, or have only border.</param>
|
||||
private void DrawAnnulusSector(
|
||||
DrawingHandleScreen drawingHandleScreen,
|
||||
Vector2 center,
|
||||
float radiusInner,
|
||||
float radiusOuter,
|
||||
float angleSectorFrom,
|
||||
float angleSectorTo,
|
||||
Color color,
|
||||
bool filled = true
|
||||
)
|
||||
{
|
||||
const float minimalSegmentSize = MathF.Tau / 128f;
|
||||
|
||||
var requestedSegmentSize = angleSectorTo - angleSectorFrom;
|
||||
var segmentCount = (int)(requestedSegmentSize / minimalSegmentSize) + 1;
|
||||
var anglePerSegment = requestedSegmentSize / (segmentCount - 1);
|
||||
|
||||
var bufferSize = segmentCount * 2;
|
||||
if (_sectorPointsForDrawing == null || _sectorPointsForDrawing.Length != bufferSize)
|
||||
{
|
||||
_sectorPointsForDrawing ??= new Vector2[bufferSize];
|
||||
}
|
||||
|
||||
for (var i = 0; i < segmentCount; i++)
|
||||
{
|
||||
var angle = angleSectorFrom + anglePerSegment * i;
|
||||
|
||||
// Flip Y to get from ui coordinates to natural coordinates
|
||||
var unitPos = new Vector2(MathF.Cos(angle), -MathF.Sin(angle));
|
||||
var outerPoint = center + unitPos * radiusOuter;
|
||||
var innerPoint = center + unitPos * radiusInner;
|
||||
if (filled)
|
||||
{
|
||||
// to make filled sector we need to create strip from triangles
|
||||
_sectorPointsForDrawing[i * 2] = outerPoint;
|
||||
_sectorPointsForDrawing[i * 2 + 1] = innerPoint;
|
||||
}
|
||||
else
|
||||
{
|
||||
// to make border of sector we need points ordered as sequences on radius
|
||||
_sectorPointsForDrawing[i] = outerPoint;
|
||||
_sectorPointsForDrawing[bufferSize - 1 - i] = innerPoint;
|
||||
}
|
||||
}
|
||||
|
||||
var type = filled
|
||||
? DrawPrimitiveTopology.TriangleStrip
|
||||
: DrawPrimitiveTopology.LineStrip;
|
||||
drawingHandleScreen.DrawPrimitives(type, _sectorPointsForDrawing, color);
|
||||
}
|
||||
|
||||
private static void DrawSeparatorLines(
|
||||
DrawingHandleScreen drawingHandleScreen,
|
||||
Vector2 center,
|
||||
float radiusInner,
|
||||
float radiusOuter,
|
||||
float angleSectorFrom,
|
||||
float angleSectorTo,
|
||||
Color color
|
||||
)
|
||||
{
|
||||
var fromPoint = new Angle(-angleSectorFrom).RotateVec(Vector2.UnitX);
|
||||
drawingHandleScreen.DrawLine(
|
||||
center + fromPoint * radiusOuter,
|
||||
center + fromPoint * radiusInner,
|
||||
color
|
||||
);
|
||||
|
||||
var toPoint = new Angle(-angleSectorTo).RotateVec(Vector2.UnitX);
|
||||
drawingHandleScreen.DrawLine(
|
||||
center + toPoint * radiusOuter,
|
||||
center + toPoint * radiusInner,
|
||||
color
|
||||
);
|
||||
}
|
||||
|
||||
private static bool IsWholeCircle(float angleSectorFrom, float angleSectorTo)
|
||||
{
|
||||
return new Angle(angleSectorFrom).EqualsApprox(new Angle(angleSectorTo));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user