diff --git a/Content.Client/Chat/UI/EmotesMenu.xaml b/Content.Client/Chat/UI/EmotesMenu.xaml
index cc4d5bb77e..845b631617 100644
--- a/Content.Client/Chat/UI/EmotesMenu.xaml
+++ b/Content.Client/Chat/UI/EmotesMenu.xaml
@@ -1,4 +1,4 @@
-
-
-
+
+
-
-
+
+
-
-
+
+
-
+
-
+
-
+
-
+
diff --git a/Content.Client/Chat/UI/EmotesMenu.xaml.cs b/Content.Client/Chat/UI/EmotesMenu.xaml.cs
index f3b7837f21..80daa405a6 100644
--- a/Content.Client/Chat/UI/EmotesMenu.xaml.cs
+++ b/Content.Client/Chat/UI/EmotesMenu.xaml.cs
@@ -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 ProtoId { get; set; }
}
diff --git a/Content.Client/Ghost/GhostRoleRadioMenu.xaml.cs b/Content.Client/Ghost/GhostRoleRadioMenu.xaml.cs
index 3897b1b949..1b65eac6ed 100644
--- a/Content.Client/Ghost/GhostRoleRadioMenu.xaml.cs
+++ b/Content.Client/Ghost/GhostRoleRadioMenu.xaml.cs
@@ -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 ProtoId { get; set; }
}
diff --git a/Content.Client/RCD/RCDMenu.xaml b/Content.Client/RCD/RCDMenu.xaml
index b3d5367a5f..d8ab0ac8f4 100644
--- a/Content.Client/RCD/RCDMenu.xaml
+++ b/Content.Client/RCD/RCDMenu.xaml
@@ -11,37 +11,37 @@
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
+
-
+
-
+
-
+
-
+
-
+
diff --git a/Content.Client/RCD/RCDMenu.xaml.cs b/Content.Client/RCD/RCDMenu.xaml.cs
index f0d27d6b1f..7ea9894e41 100644
--- a/Content.Client/RCD/RCDMenu.xaml.cs
+++ b/Content.Client/RCD/RCDMenu.xaml.cs
@@ -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 ProtoId { get; set; }
-
- public RCDMenuButton()
- {
-
- }
}
diff --git a/Content.Client/Silicons/StationAi/StationAiMenu.xaml b/Content.Client/Silicons/StationAi/StationAiMenu.xaml
index d56fc83289..cfa0b93234 100644
--- a/Content.Client/Silicons/StationAi/StationAiMenu.xaml
+++ b/Content.Client/Silicons/StationAi/StationAiMenu.xaml
@@ -1,4 +1,4 @@
-
-
+
diff --git a/Content.Client/Silicons/StationAi/StationAiMenu.xaml.cs b/Content.Client/Silicons/StationAi/StationAiMenu.xaml.cs
index b152f5ead8..a536d911f3 100644
--- a/Content.Client/Silicons/StationAi/StationAiMenu.xaml.cs
+++ b/Content.Client/Silicons/StationAi/StationAiMenu.xaml.cs
@@ -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;
}
diff --git a/Content.Client/UserInterface/Controls/RadialContainer.cs b/Content.Client/UserInterface/Controls/RadialContainer.cs
index be9b8817a0..72555aab5f 100644
--- a/Content.Client/UserInterface/Controls/RadialContainer.cs
+++ b/Content.Client/UserInterface/Controls/RadialContainer.cs
@@ -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
{
+ ///
+ /// Increment of radius per child element to be rendered.
+ ///
+ private const float RadiusIncrement = 5f;
+
///
/// 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;
///
- /// 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.
///
[ViewVariables(VVAccess.ReadWrite)]
- public float Radius { get; set; } = 100f;
+ public float InitialRadius { get; set; } = 100f;
+
+ ///
+ /// 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 and
+ /// multiplied by currently visible child button count.
+ ///
+ [ViewVariables(VVAccess.ReadOnly)]
+ public float CalculatedRadius { get; private set; }
+
+ ///
+ /// Determines radial menu button sectors inner radius, is a multiplier of .
+ ///
+ public float InnerRadiusMultiplier { get; set; } = 0.5f;
+
+ ///
+ /// Determines radial menu button sectors outer radius, is a multiplier of .
+ ///
+ public float OuterRadiusMultiplier { get; set; } = 1.5f;
///
/// Sets whether the container should reserve a space on the layout for child which are not currently visible
@@ -67,37 +91,74 @@ public class RadialContainer : LayoutContainer
{
}
-
- protected override void Draw(DrawingHandleScreen handle)
+
+ ///
+ protected override Vector2 ArrangeOverride(Vector2 finalSize)
{
-
- const float baseRadius = 100f;
- const float radiusIncrement = 5f;
-
- var children = ReserveSpaceForHiddenChildren ? Children : Children.Where(x => x.Visible);
+ 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);
}
///
@@ -109,4 +170,5 @@ public class RadialContainer : LayoutContainer
Clockwise,
AntiClockwise,
}
+
}
diff --git a/Content.Client/UserInterface/Controls/RadialMenu.cs b/Content.Client/UserInterface/Controls/RadialMenu.cs
index 5f56ad7f86..1b7f07aa2c 100644
--- a/Content.Client/UserInterface/Controls/RadialMenu.cs
+++ b/Content.Client/UserInterface/Controls/RadialMenu.cs
@@ -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,11 +15,16 @@ public class RadialMenu : BaseWindow
///
/// Contextual button used to traverse through previous layers of the radial menu
///
- public TextureButton? ContextualButton { get; set; }
+ public RadialMenuContextualCentralTextureButton ContextualButton { get; }
+
+ ///
+ /// Button that represents outer area of menu (closes menu on outside clicks).
+ ///
+ public RadialMenuOuterAreaButton MenuOuterAreaButton { get; }
///
/// 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
- ///
+ ///
public string? BackButtonStyleClass
{
get
@@ -52,7 +60,7 @@ public class RadialMenu : BaseWindow
}
}
- private List _path = new();
+ private readonly List _path = new();
private string? _backButtonStyleClass;
private string? _closeButtonStyleClass;
@@ -60,8 +68,8 @@ public class RadialMenu : BaseWindow
/// A free floating menu which enables the quick display of one or more radial containers
///
///
- /// Only one radial container is visible at a time (each container forming a separate 'layer' within
- /// the menu), along with a contextual button at the menu center, which will either return the user
+ /// Only one radial container is visible at a time (each container forming a separate 'layer' within
+ /// the menu), along with a contextual button at the menu center, which will either return the user
/// to the previous layer or close the menu if there are no previous layers left to traverse.
/// To create a functional radial menu, simply parent one or more named radial containers to it,
/// and populate the radial containers with RadialMenuButtons. Setting the TargetLayer field of these
@@ -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;
+ }
+ }
+
+ ///
+ 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
}
}
+///
+/// Base class for radial menu buttons. Excludes all actions except clicks and alt-clicks
+/// from interactions.
+///
[Virtual]
-public class RadialMenuButton : Button
+public class RadialMenuTextureButtonBase : TextureButton
{
- ///
- /// Upon clicking this button the radial menu will transition to the named layer
- ///
- public string? TargetLayer { get; set; }
-
- ///
- /// A simple button that can move the user to a different layer within a radial menu
- ///
- public RadialMenuButton()
+ ///
+ protected RadialMenuTextureButtonBase()
{
- OnButtonUp += OnClicked;
+ EnableAllKeybinds = true;
}
- private void OnClicked(ButtonEventArgs args)
+ ///
+ 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)
+///
+/// Special button for closing radial menu or going back between radial menu levels.
+/// Is looking like just 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.
+///
+public sealed class RadialMenuContextualCentralTextureButton : RadialMenuTextureButtonBase
+{
+ public float InnerRadius { get; set; }
+
+ public Vector2? ParentCenter { get; set; }
+
+ ///
+ 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;
+ }
+}
+
+///
+/// Menu button for outer area of radial menu (covers everything 'outside').
+///
+public sealed class RadialMenuOuterAreaButton : RadialMenuTextureButtonBase
+{
+ public float OuterRadius { get; set; }
+
+ public Vector2? ParentCenter { get; set; }
+
+ ///
+ 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
{
///
/// Upon clicking this button the radial menu will be moved to the named layer
@@ -226,6 +305,7 @@ public class RadialMenuTextureButton : TextureButton
///
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
+{
+ ///
+ /// Angle in radian where button sector should start.
+ ///
+ public float AngleSectorFrom { set; }
+
+ ///
+ /// Angle in radian where button sector should end.
+ ///
+ public float AngleSectorTo { set; }
+
+ ///
+ /// Outer radius for drawing segment and pointer detection.
+ ///
+ public float OuterRadius { set; }
+
+ ///
+ /// Outer radius for drawing segment and pointer detection.
+ ///
+ public float InnerRadius { set; }
+
+ ///
+ /// Offset in radian by which menu button should be rotated.
+ ///
+ public float AngleOffset { set; }
+
+ ///
+ /// Coordinates of center in parent component - button container.
+ ///
+ 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));
+
+ ///
+ /// Marker, that control should render border of segment. Is false by default.
+ ///
+ ///
+ /// By default color of border is same as color of background. Use
+ /// and to change it.
+ ///
+ public bool DrawBorder { get; set; } = false;
+
+ ///
+ /// Marker, that control should render background of all sector. Is true by default.
+ ///
+ public bool DrawBackground { get; set; } = true;
+
+ ///
+ /// Marker, that control should render separator lines.
+ /// Separator lines are used to visually separate sector of radial menu items.
+ /// Is true by default
+ ///
+ public bool DrawSeparators { get; set; } = true;
+
+ ///
+ /// Color of background in non-hovered state. Accepts RGB color, works with sRGB for DrawPrimitive internally.
+ ///
+ public Color BackgroundColor
+ {
+ get => Color.FromSrgb(_backgroundColorSrgb);
+ set => _backgroundColorSrgb = Color.ToSrgb(value);
+ }
+
+ ///
+ /// Color of background in hovered state. Accepts RGB color, works with sRGB for DrawPrimitive internally.
+ ///
+ public Color HoverBackgroundColor
+ {
+ get => Color.FromSrgb(_hoverBackgroundColorSrgb);
+ set => _hoverBackgroundColorSrgb = Color.ToSrgb(value);
+ }
+
+ ///
+ /// Color of button border. Accepts RGB color, works with sRGB for DrawPrimitive internally.
+ ///
+ public Color BorderColor
+ {
+ get => Color.FromSrgb(_borderColorSrgb);
+ set => _borderColorSrgb = Color.ToSrgb(value);
+ }
+
+ ///
+ /// Color of button border when button is hovered. Accepts RGB color, works with sRGB for DrawPrimitive internally.
+ ///
+ public Color HoverBorderColor
+ {
+ get => Color.FromSrgb(_hoverBorderColorSrgb);
+ set => _hoverBorderColorSrgb = Color.ToSrgb(value);
+ }
+
+ ///
+ /// Color of separator lines.
+ /// Separator lines are used to visually separate sector of radial menu items.
+ ///
+ public Color SeparatorColor { get; set; } = new Color(128, 128, 128, 128);
+
+ ///
+ float IRadialMenuItemWithSector.AngleSectorFrom
+ {
+ set
+ {
+ _angleSectorFrom = value;
+ _isWholeCircle = IsWholeCircle(value, _angleSectorTo);
+ }
+ }
+
+ ///
+ float IRadialMenuItemWithSector.AngleSectorTo
+ {
+ set
+ {
+ _angleSectorTo = value;
+ _isWholeCircle = IsWholeCircle(_angleSectorFrom, value);
+ }
+ }
+
+ ///
+ float IRadialMenuItemWithSector.OuterRadius { set => _outerRadius = value; }
+
+ ///
+ float IRadialMenuItemWithSector.InnerRadius { set => _innerRadius = value; }
+
+ ///
+ public float AngleOffset { set => _angleOffset = value; }
+
+ ///
+ Vector2 IRadialMenuItemWithSector.ParentCenter { set => _parentCenter = value; }
+
+ ///
+ /// A simple texture button that can move the user to a different layer within a radial menu
+ ///
+ public RadialMenuTextureButtonWithSector()
+ {
+ }
+
+ ///
+ 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);
+ }
+ }
+
+ ///
+ 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;
+ }
+
+ ///
+ /// Draw segment between two concentrated circles from and to certain angles.
+ ///
+ /// Drawing handle, to which rendering should be delegated.
+ /// Point where circle center should be.
+ /// Radius of internal circle.
+ /// Radius of external circle.
+ /// Angle in radian, from which sector should start.
+ /// Angle in radian, from which sector should start.
+ /// Color for drawing.
+ /// Should figure be filled, or have only border.
+ 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));
+ }
+}