1
0

RadialContainer.cs 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. using Robust.Client.UserInterface.Controls;
  2. using System.Linq;
  3. using System.Numerics;
  4. namespace Content.Client.UserInterface.Controls;
  5. [Virtual]
  6. public class RadialContainer : LayoutContainer
  7. {
  8. /// <summary>
  9. /// Increment of radius per child element to be rendered.
  10. /// </summary>
  11. private const float RadiusIncrement = 5f;
  12. /// <summary>
  13. /// Specifies the anglular range, in radians, in which child elements will be placed.
  14. /// The first value denotes the angle at which the first element is to be placed, and
  15. /// the second value denotes the angle at which the last element is to be placed.
  16. /// Both values must be between 0 and 2 PI radians
  17. /// </summary>
  18. /// <remarks>
  19. /// The top of the screen is at 0 radians, and the bottom of the screen is at PI radians
  20. /// </remarks>
  21. [ViewVariables(VVAccess.ReadWrite)]
  22. public Vector2 AngularRange
  23. {
  24. get
  25. {
  26. return _angularRange;
  27. }
  28. set
  29. {
  30. var x = value.X;
  31. var y = value.Y;
  32. x = x > MathF.Tau ? x % MathF.Tau : x;
  33. y = y > MathF.Tau ? y % MathF.Tau : y;
  34. x = x < 0 ? MathF.Tau + x : x;
  35. y = y < 0 ? MathF.Tau + y : y;
  36. _angularRange = new Vector2(x, y);
  37. }
  38. }
  39. private Vector2 _angularRange = new Vector2(0f, MathF.Tau - float.Epsilon);
  40. /// <summary>
  41. /// Determines the direction in which child elements will be arranged
  42. /// </summary>
  43. [ViewVariables(VVAccess.ReadWrite)]
  44. public RAlignment RadialAlignment { get; set; } = RAlignment.Clockwise;
  45. /// <summary>
  46. /// Radial menu radius determines how far from the radial container's center its child elements will be placed.
  47. /// To correctly display dynamic amount of elements control actually resizes depending on amount of child buttons,
  48. /// but uses this property as base value for final radius calculation.
  49. /// </summary>
  50. [ViewVariables(VVAccess.ReadWrite)]
  51. public float InitialRadius { get; set; } = 100f;
  52. /// <summary>
  53. /// Radial menu radius determines how far from the radial container's center its child elements will be placed.
  54. /// This is dynamically calculated (based on child button count) radius, result of <see cref="InitialRadius"/> and
  55. /// <see cref="RadiusIncrement"/> multiplied by currently visible child button count.
  56. /// </summary>
  57. [ViewVariables(VVAccess.ReadOnly)]
  58. public float CalculatedRadius { get; private set; }
  59. /// <summary>
  60. /// Determines radial menu button sectors inner radius, is a multiplier of <see cref="InitialRadius"/>.
  61. /// </summary>
  62. public float InnerRadiusMultiplier { get; set; } = 0.5f;
  63. /// <summary>
  64. /// Determines radial menu button sectors outer radius, is a multiplier of <see cref="InitialRadius"/>.
  65. /// </summary>
  66. public float OuterRadiusMultiplier { get; set; } = 1.5f;
  67. /// <summary>
  68. /// Sets whether the container should reserve a space on the layout for child which are not currently visible
  69. /// </summary>
  70. [ViewVariables(VVAccess.ReadWrite)]
  71. public bool ReserveSpaceForHiddenChildren { get; set; } = true;
  72. /// <summary>
  73. /// This container arranges its children, evenly separated, in a radial pattern
  74. /// </summary>
  75. public RadialContainer()
  76. {
  77. }
  78. /// <inheritdoc />
  79. protected override Vector2 ArrangeOverride(Vector2 finalSize)
  80. {
  81. var children = ReserveSpaceForHiddenChildren
  82. ? Children
  83. : Children.Where(x => x.Visible);
  84. var childCount = children.Count();
  85. // Add padding from the center at higher child counts so they don't overlap.
  86. CalculatedRadius = InitialRadius + (childCount * RadiusIncrement);
  87. var isAntiClockwise = RadialAlignment == RAlignment.AntiClockwise;
  88. // Determine the size of the arc, accounting for clockwise and anti-clockwise arrangements
  89. var arc = AngularRange.Y - AngularRange.X;
  90. arc = arc < 0
  91. ? MathF.Tau + arc
  92. : arc;
  93. arc = isAntiClockwise
  94. ? MathF.Tau - arc
  95. : arc;
  96. // Account for both circular arrangements and arc-based arrangements
  97. var childMod = MathHelper.CloseTo(arc, MathF.Tau, 0.01f)
  98. ? 0
  99. : 1;
  100. // Determine the separation between child elements
  101. var sepAngle = arc / (childCount - childMod);
  102. sepAngle *= isAntiClockwise
  103. ? -1f
  104. : 1f;
  105. var controlCenter = finalSize * 0.5f;
  106. // Adjust the positions of all the child elements
  107. var query = children.Select((x, index) => (index, x));
  108. foreach (var (childIndex, child) in query)
  109. {
  110. const float angleOffset = MathF.PI * 0.5f;
  111. var targetAngleOfChild = AngularRange.X + sepAngle * (childIndex + 0.5f) + angleOffset;
  112. // flooring values for snapping float values to physical grid -
  113. // it prevents gaps and overlapping between different button segments
  114. var position = new Vector2(
  115. MathF.Floor(CalculatedRadius * MathF.Cos(targetAngleOfChild)),
  116. MathF.Floor(-CalculatedRadius * MathF.Sin(targetAngleOfChild))
  117. ) + controlCenter - child.DesiredSize * 0.5f + Position;
  118. SetPosition(child, position);
  119. // radial menu buttons with sector need to also know in which sector and around which point
  120. // they should be rendered, how much space sector should should take etc.
  121. if (child is IRadialMenuItemWithSector tb)
  122. {
  123. tb.AngleSectorFrom = sepAngle * childIndex;
  124. tb.AngleSectorTo = sepAngle * (childIndex + 1);
  125. tb.AngleOffset = angleOffset;
  126. tb.InnerRadius = CalculatedRadius * InnerRadiusMultiplier;
  127. tb.OuterRadius = CalculatedRadius * OuterRadiusMultiplier;
  128. tb.ParentCenter = controlCenter;
  129. }
  130. }
  131. return base.ArrangeOverride(finalSize);
  132. }
  133. /// <summary>
  134. /// Specifies the different radial alignment modes
  135. /// </summary>
  136. /// <seealso cref="RadialAlignment"/>
  137. public enum RAlignment : byte
  138. {
  139. Clockwise,
  140. AntiClockwise,
  141. }
  142. }