using Robust.Client.UserInterface.Controls;
using System.Linq;
using System.Numerics;
+using Robust.Client.Graphics;
+using Robust.Shared.Prototypes;
namespace Content.Client.UserInterface.Controls;
[Virtual]
public class RadialContainer : LayoutContainer
{
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ [Dependency] private readonly IClyde _clyde= default!;
+ private readonly ShaderInstance _shader;
+
+ private readonly float[] _angles = new float[64];
+ private readonly float[] _sectorMedians = new float[64];
+ private readonly Color[] _sectorColors = new Color[64];
+ private readonly Color[] _borderColors = new Color[64];
+
/// <summary>
/// Increment of radius per child element to be rendered.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public Vector2 AngularRange
{
- get
- {
- return _angularRange;
- }
-
+ get => _angularRange;
set
{
var x = value.X;
/// </summary>
public RadialContainer()
{
-
+ IoCManager.InjectDependencies(this);
+ _shader = _prototypeManager.Index<ShaderPrototype>("RadialMenu")
+ .InstanceUnique();
}
/// <inheritdoc />
return base.ArrangeOverride(finalSize);
}
+ /// <inheritdoc />
+ protected override void Draw(DrawingHandleScreen handle)
+ {
+ base.Draw(handle);
+
+ float selectedFrom = 0;
+ float selectedTo = 0;
+
+ var i = 0;
+ foreach (var child in Children)
+ {
+ if (child is not IRadialMenuItemWithSector menuWithSector)
+ {
+ continue;
+ }
+
+ _angles[i] = menuWithSector.AngleSectorTo;
+ _sectorMedians[i] = (menuWithSector.AngleSectorTo + menuWithSector.AngleSectorFrom) / 2;
+
+ if (menuWithSector.IsHovered)
+ {
+ // menuWithSector.DrawBackground;
+ // menuWithSector.DrawBorder;
+ _sectorColors[i] = menuWithSector.HoverBackgroundColor;
+ _borderColors[i] = menuWithSector.HoverBorderColor;
+ selectedFrom = menuWithSector.AngleSectorFrom;
+ selectedTo = menuWithSector.AngleSectorTo;
+ }
+ else
+ {
+ _sectorColors[i] = menuWithSector.BackgroundColor;
+ _borderColors[i] = menuWithSector.BorderColor;
+ }
+
+ i++;
+ }
+
+ var screenSize = _clyde.ScreenSize;
+
+ var menuCenter = new Vector2(
+ ScreenCoordinates.X + (Size.X / 2) * UIScale,
+ screenSize.Y - ScreenCoordinates.Y - (Size.Y / 2) * UIScale
+ );
+
+ _shader.SetParameter("separatorAngles", _angles);
+ _shader.SetParameter("sectorMedianAngles", _sectorMedians);
+ _shader.SetParameter("selectedFrom", selectedFrom);
+ _shader.SetParameter("selectedTo", selectedTo);
+ _shader.SetParameter("childCount", i);
+ _shader.SetParameter("sectorColors", _sectorColors);
+ _shader.SetParameter("borderColors", _borderColors);
+ _shader.SetParameter("centerPos", menuCenter);
+ _shader.SetParameter("screenSize", screenSize);
+ _shader.SetParameter("innerRadius", CalculatedRadius * InnerRadiusMultiplier * UIScale);
+ _shader.SetParameter("outerRadius", CalculatedRadius * OuterRadiusMultiplier * UIScale);
+
+ handle.UseShader(_shader);
+ handle.DrawRect(new UIBox2(0, 0, screenSize.X, screenSize.Y), Color.White);
+ handle.UseShader(null);
+ }
+
/// <summary>
/// Specifies the different radial alignment modes
/// </summary>
/// <summary>
/// Angle in radian where button sector should start.
/// </summary>
- public float AngleSectorFrom { set; }
+ public float AngleSectorFrom { set; get; }
/// <summary>
/// Angle in radian where button sector should end.
/// </summary>
- public float AngleSectorTo { set; }
+ public float AngleSectorTo { set; get; }
/// <summary>
/// Outer radius for drawing segment and pointer detection.
/// Coordinates of center in parent component - button container.
/// </summary>
public Vector2 ParentCenter { set; }
+
+ /// <summary>
+ /// Marker, is menu item hovered currently.
+ /// </summary>
+ public bool IsHovered { get; }
+
+ /// <summary>
+ /// Color for menu item background when it is hovered over.
+ /// </summary>
+ Color HoverBackgroundColor { get; }
+
+ /// <summary>
+ /// Color for menu item default state.
+ /// </summary>
+ Color BackgroundColor { get; }
+
+ /// <summary>
+ /// Color for menu item border when item is hovered over.
+ /// </summary>
+ Color HoverBorderColor { get; }
+
+ /// <summary>
+ /// Color for menu item border default state.
+ /// </summary>
+ Color BorderColor { get; }
+
+ /// <summary>
+ /// Marker, if menu item background should be drawn.
+ /// </summary>
+ public bool DrawBackground { get; }
+
+ /// <summary>
+ /// Marker, if menu item borders should be drawn.
+ /// </summary>
+ public bool DrawBorder { get; }
}
[Virtual]
/// Marker, that controls if border of segment should be rendered. Is false by default.
/// </summary>
/// <remarks>
- /// By default color of border is same as color of background. Use <see cref="BorderColor"/>
+ /// 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;
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
{
_angleSectorFrom = value;
_isWholeCircle = IsWholeCircle(value, _angleSectorTo);
}
+ get => _angleSectorFrom;
}
/// <inheritdoc />
_angleSectorTo = value;
_isWholeCircle = IsWholeCircle(_angleSectorFrom, value);
}
+ get => _angleSectorTo;
}
/// <inheritdoc />
{
}
- /// <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 && DrawBorder)
- {
- DrawSeparatorLines(handle, containerCenter, _innerRadius * UIScale, _outerRadius * UIScale, angleFrom, angleTo, SeparatorColor);
- }
- }
-
/// <inheritdoc />
protected override bool HasPoint(Vector2 point)
{
using Robust.Shared.Timing;
using Robust.Client.UserInterface.XAML;
using Robust.Client.Input;
+using Robust.Shared.Collections;
namespace Content.Client.UserInterface.Controls;
private void ClearExistingChildrenRadialButtons()
{
- var toRemove = new List<Control>(ChildCount);
+ var toRemove = new ValueList<Control>(ChildCount);
foreach (var child in Children)
{
if (child != ContextualButton && child != MenuOuterAreaButton)
- type: shader
id: Hologram
kind: source
- path: "/Textures/Shaders/hologram.swsl"
\ No newline at end of file
+ path: "/Textures/Shaders/hologram.swsl"
+
+- type: shader
+ id: RadialMenu
+ kind: source
+ path: "/Textures/Shaders/radial-menu.swsl"
--- /dev/null
+const highp float Thickness = 0.002;
+
+const highp float pi = 3.14159265;
+const highp float twopi = 2.0 * pi;
+const highp float halfpi = 0.5 * pi;
+const highp float invpi = 1.0 / pi;
+
+uniform highp float innerRadius;
+uniform highp float outerRadius;
+
+uniform highp vec4[64] sectorColors;
+uniform highp vec4[64] borderColors;
+
+uniform highp float[64] separatorAngles;
+uniform highp float[64] sectorMedianAngles;
+uniform highp int childCount;
+uniform highp vec2 centerPos;
+
+uniform highp float selectedFrom;
+uniform highp float selectedTo;
+uniform highp vec2 screenSize;
+
+highp float SMOOTH(highp float r, highp float R)
+{
+ return 1.0 - smoothstep(R - 1.0, R + 1.0, r);
+}
+
+// line from center of circle to radius (outer arg) on theta0 angle (radian,)
+highp float separator(highp vec2 d, highp float r, highp float outer, highp float theta0, highp float thickness)
+{
+ // rotate due to difference in coordinate spaces between shaders and ui
+ highp float theta1 = theta0 - halfpi;
+ highp vec2 p = outer * vec2(cos(theta1), -sin(theta1));
+ highp float l = length(d - p * clamp(dot(d, p) / dot(p, p), 0.0, 1.0));
+ return SMOOTH(l, thickness);
+}
+
+highp float circle(highp float r, highp float radius, highp float width)
+{
+ return SMOOTH(r - width / 2.0, radius) - SMOOTH(r + width / 2.0, radius);
+}
+
+// get angle between current point and circle center
+highp float getAngle(highp vec2 d)
+{
+ highp vec2 n = normalize(d);
+ highp float angle = acos(n.x);
+ int isNegativeY = int(n.y < 0.0);
+ angle = angle + (twopi - angle * 2) * isNegativeY;
+ // rotate
+ angle = mod(angle - halfpi, twopi);
+ return angle;
+}
+
+highp float pcurve(highp float x, highp float a, highp float b )
+{
+ highp float k = pow(a + b,a + b) / (pow(a, a) * pow(b, b));
+ return k * pow(x, a) * pow(1.0 - x, b);
+}
+
+// gets alpha for radial gradients based on pcurve
+highp float fillGradient(highp float r, highp float inner, highp float outer)
+{
+ highp float nInner = inner / outer;
+ highp float nR = r / outer;
+ return pcurve(nR, nInner, 1.0);
+}
+
+void fragment()
+{
+ highp vec4 col = vec4(0.0);
+
+ //angle of the line
+ highp vec2 d = FRAGCOORD.xy - centerPos;
+ highp float angle = getAngle(d);
+
+ highp float r = length(FRAGCOORD.xy - centerPos);
+ // fill sectors
+ int isInsideRange = int(r > innerRadius && r < outerRadius);
+ highp float g = fillGradient(r, innerRadius, outerRadius);
+
+ // trying to mix in color per button
+
+ highp float from = 0;
+ for (int i = 0; i < childCount; i++)
+ {
+ highp float to = separatorAngles[i];
+ int isInSector = int(angle > from && angle < to);
+ col += isInsideRange
+ * vec4(sectorColors[i].xyz , g)* isInSector;
+
+ from = to;
+ }
+
+ // get step of radial menu buttons in radian
+ highp float halfSectorAngleSize = (separatorAngles[1] - separatorAngles[0]) * 0.5;
+
+ for (int i = 0; i < childCount; i++)
+ {
+ highp float sectorMedian = sectorMedianAngles[i];
+ highp float sectorMedianToAngleDiff = abs(sectorMedian - angle);
+ highp vec4 borderColor = borderColors[i];
+ highp vec4 borderColorLight = borderColor * 0.6;
+ int isInInnerRadius = int(r > innerRadius);
+ highp float iAngle = twopi - separatorAngles[i];
+ // button separators
+ highp float sectorFromAngle;
+ int isCurrentZero = int(i > 0);
+ sectorFromAngle = separatorAngles[i - 1] * isCurrentZero;
+
+ col += isInInnerRadius
+ * separator(d, r, outerRadius, sectorFromAngle, 0.5) * borderColor;
+ col += isInInnerRadius
+ * separator(d, r, outerRadius, iAngle, 0.5) * borderColor;
+
+ // set up decorations
+ // inner button 'square' decoration
+ int isInInnerBorderRange = int(r > innerRadius + 15);
+ col += isInInnerRadius * isInInnerBorderRange
+ * separator(d, r, outerRadius - 15, twopi - sectorMedian - halfSectorAngleSize * 0.8, 1.0) * 0.2 * borderColorLight;
+ col += isInInnerRadius * isInInnerBorderRange
+ * separator(d, r, outerRadius - 15, twopi - sectorMedian + halfSectorAngleSize * 0.8, 1.0) * 0.2 * borderColorLight;
+
+ int isInInnerBorderSector = int(sectorMedianToAngleDiff < halfSectorAngleSize * 0.8);
+ col += isInInnerRadius * isInInnerBorderRange * isInInnerBorderSector
+ * circle(r, innerRadius + 15, 2.0) * 0.2 * borderColorLight;
+ col += isInInnerRadius * isInInnerBorderRange * isInInnerBorderSector
+ * circle(r, outerRadius - 15, 2.0) * 0.2 * borderColorLight;
+
+ // outer button decorative elements
+ int isInOuterBorderOuterSector = int(sectorMedianToAngleDiff < halfSectorAngleSize * 0.2);
+ col += isInOuterBorderOuterSector
+ * circle(r, innerRadius - 5, 4.0) * 0.4 * borderColor;
+
+ int isInOuterBorderInnerSector = int(sectorMedianToAngleDiff < halfSectorAngleSize * 0.6);
+ col += isInOuterBorderInnerSector
+ * circle(r, outerRadius + 10, 4.0) * 0.4 * borderColor;
+
+ // outer and inner circle of sectors
+ int isOnSectorBorder = int(sectorMedianToAngleDiff < halfSectorAngleSize);
+ col += isOnSectorBorder
+ * circle(r, innerRadius, 2.0) * borderColor;
+ col += isOnSectorBorder
+ * circle(r, outerRadius, 2.0) * borderColor;
+ }
+
+ COLOR = col;
+}