Files
integration/Source/Integration/RadialMenu.cpp

374 lines
11 KiB
C++
Raw Normal View History

2026-05-08 17:55:11 -04:00
#include "RadialMenu.h"
2026-05-11 00:13:30 -04:00
#include "Rendering/DrawElements.h"
2026-05-15 18:14:38 -04:00
#include "Engine/Texture2D.h"
#include "Styling/SlateBrush.h"
2026-05-11 00:13:30 -04:00
#include "Widgets/SLeafWidget.h"
2026-05-08 17:55:11 -04:00
2026-05-18 02:26:28 -04:00
TArray<FRadialMenuItem> FRadialMenuItem::Calculate(const FRadialMenuConfig &Config)
2026-05-08 17:55:11 -04:00
{
2026-05-18 02:26:28 -04:00
TArray<FRadialMenuItem> Items;
if (Config.NumItems < 0) return Items;
int32 NumRight = (Config.NumItems / 2);
int32 NumLeft = Config.NumItems - NumRight;
Items.SetNum(Config.NumItems);
2026-05-08 17:55:11 -04:00
2026-05-09 03:16:41 -04:00
View LeftItems(Items.GetData(), NumLeft);
View RightItems(Items.GetData() + NumLeft, NumRight);
2026-05-18 02:26:28 -04:00
CalculateSpokes(LeftItems, Config.NumItems, Config.ItemHeight, Config.InnerRadius, Config.MinSpoke);
CalculateSpokes(RightItems, Config.NumItems, Config.ItemHeight, Config.InnerRadius, Config.MinSpoke);
2026-05-09 03:16:41 -04:00
double LeftWidth = WidestSpoke(LeftItems);
double RightWidth = WidestSpoke(RightItems);
2026-05-18 02:26:28 -04:00
double HalfWidth = FMath::Max(LeftWidth, RightWidth) + Config.Spread;
2026-05-09 03:16:41 -04:00
CalculateSpread(LeftItems, HalfWidth - LeftWidth);
CalculateSpread(RightItems, HalfWidth - RightWidth);
2026-05-08 17:55:11 -04:00
FlipHorizontal(LeftItems);
2026-05-18 02:26:28 -04:00
return Items;
2026-05-08 17:55:11 -04:00
}
2026-05-18 02:26:28 -04:00
FVector2D FRadialMenuItem::SpokeVector(int32 I, int32 NSide, int32 NTotal)
2026-05-08 17:55:11 -04:00
{
2026-05-09 03:16:41 -04:00
double SpokeAngle = 1.0 / NTotal;
double OffsetAngle = 0.5 * (0.5 - ((NSide - 1) * SpokeAngle));
double Revolutions = (I * SpokeAngle) + OffsetAngle;
double Radians = (Revolutions * 2.0 * UE_PI);
2026-05-08 17:55:11 -04:00
return FVector2D(FMath::Sin(Radians), -FMath::Cos(Radians));
}
2026-05-18 02:26:28 -04:00
void FRadialMenuItem::FlipHorizontal(View V)
2026-05-08 17:55:11 -04:00
{
for (FRadialMenuItem& Item : V)
{
Item.Point1.X = -Item.Point1.X;
Item.Point2.X = -Item.Point2.X;
Item.Point3.X = -Item.Point3.X;
Item.RightSide = !Item.RightSide;
}
}
2026-05-18 02:26:28 -04:00
double FRadialMenuItem::WidestSpoke(View V)
2026-05-08 17:55:11 -04:00
{
2026-05-09 03:16:41 -04:00
double Result = 0.0;
for (const FRadialMenuItem &Item : V)
{
Result = FMath::Max(Item.Point2.X, Result);
}
return Result;
}
2026-05-18 02:26:28 -04:00
void FRadialMenuItem::CalculateSpread(View V, double Offset)
2026-05-09 03:16:41 -04:00
{
for (FRadialMenuItem &Item : V)
{
Item.Point3 = Item.Point2 + FVector2D(Offset, 0.0);
}
}
2026-05-18 02:26:28 -04:00
void FRadialMenuItem::CalculateSpokes(View V, int32 TotalItems, float ItemHeight, float InnerRadius, float MinSpoke)
2026-05-09 03:16:41 -04:00
{
if (V.Num() == 0) return;
// RightSide is always initialized to true, it may get
// reversed by FlipHorizontal.
2026-05-08 17:55:11 -04:00
for (int32 I = 0; I < V.Num(); I++)
{
V[I].RightSide = true;
2026-05-18 02:26:28 -04:00
V[I].Point1 = SpokeVector(I, V.Num(), TotalItems) * InnerRadius;
2026-05-08 17:55:11 -04:00
}
// Calculate point2 for all spokes.
2026-05-09 03:16:41 -04:00
double NextLineMin = ItemHeight * 0.5;
2026-05-08 17:55:11 -04:00
int32 Mid = (V.Num() / 2);
if (V.Num() & 1)
{
2026-05-09 03:16:41 -04:00
V[Mid].Point2 = FVector2D(InnerRadius + MinSpoke, 0.0);
NextLineMin = ItemHeight;
2026-05-08 17:55:11 -04:00
Mid += 1;
}
for (int32 I = Mid; I < V.Num(); I++)
{
2026-05-18 02:26:28 -04:00
FVector2D UnitVec = SpokeVector(I, V.Num(), TotalItems);
2026-05-09 03:16:41 -04:00
double Y = (UnitVec.Y * (InnerRadius + MinSpoke));
2026-05-08 17:55:11 -04:00
if (Y < NextLineMin) Y = NextLineMin;
2026-05-09 03:16:41 -04:00
NextLineMin = Y + ItemHeight;
2026-05-08 17:55:11 -04:00
FVector2D Point2 = UnitVec * (Y / UnitVec.Y);
V[I].Point2 = Point2;
2026-05-09 03:16:41 -04:00
V[V.Num() - 1 - I].Point2 = Point2 * FVector2D(1.0,-1.0);
2026-05-08 17:55:11 -04:00
}
2026-05-09 03:16:41 -04:00
// The middle spoke is calculated using a different formula,
// which may result in a short spoke. If so, fix it to make
// it at least as long as the adjacent spoke.
2026-05-08 17:55:11 -04:00
if ((V.Num() & 1) && (V.Num() >= 3))
{
Mid = V.Num() / 2;
if (V[Mid].Point2.X < V[Mid + 1].Point2.X)
V[Mid].Point2.X = V[Mid + 1].Point2.X;
}
}
2026-05-11 00:13:30 -04:00
// Slate widget that paints the radial menu's polylines.
class SRadialMenu : public SLeafWidget
{
public:
SLATE_BEGIN_ARGS(SRadialMenu) {}
SLATE_END_ARGS()
2026-05-18 02:26:28 -04:00
void Construct(const FArguments& InArgs, const FRadialMenuConfig& InConfig)
2026-05-11 00:13:30 -04:00
{
2026-05-18 02:26:28 -04:00
Config = InConfig;
2026-05-11 00:13:30 -04:00
}
2026-05-18 02:26:28 -04:00
void SetConfig(const FRadialMenuConfig& NewConfig)
2026-05-11 00:13:30 -04:00
{
2026-05-18 02:26:28 -04:00
Config = NewConfig;
Items = FRadialMenuItem::Calculate(Config);
2026-05-11 00:13:30 -04:00
Invalidate(EInvalidateWidgetReason::Layout);
}
protected:
virtual FVector2D ComputeDesiredSize(float) const override
{
FVector2D Min(0.0, 0.0);
FVector2D Max(0.0, 0.0);
2026-05-18 02:26:28 -04:00
for (const FRadialMenuItem &Item : Items)
2026-05-11 00:13:30 -04:00
{
for (const FVector2D &P : { Item.Point1, Item.Point2, Item.Point3 })
{
Min.X = FMath::Min(Min.X, P.X);
Min.Y = FMath::Min(Min.Y, P.Y);
Max.X = FMath::Max(Max.X, P.X);
Max.Y = FMath::Max(Max.Y, P.Y);
}
}
// Symmetric around the origin so the menu draws centered.
double HalfW = FMath::Max(FMath::Abs(Min.X), FMath::Abs(Max.X));
double HalfH = FMath::Max(FMath::Abs(Min.Y), FMath::Abs(Max.Y));
return FVector2D(HalfW * 2.0, HalfH * 2.0);
}
virtual int32 OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry,
const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements,
int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const override
{
const FVector2D Center = AllottedGeometry.GetLocalSize() * 0.5;
const FPaintGeometry PaintGeom = AllottedGeometry.ToPaintGeometry();
2026-05-18 02:26:28 -04:00
for (int32 I = 0; I < Items.Num(); I++)
2026-05-11 00:13:30 -04:00
{
2026-05-18 02:26:28 -04:00
const FRadialMenuItem& Item = Items[I];
const bool bSelected = (I == Config.SelectedItem);
const FLinearColor& Color = bSelected ? Config.SelectedColor : Config.LineColor;
const float Thickness = bSelected ? Config.SelectedThickness : Config.LineThickness;
2026-05-11 00:13:30 -04:00
TArray<FVector2D> Points;
Points.Add(Center + Item.Point1);
Points.Add(Center + Item.Point2);
Points.Add(Center + Item.Point3);
2026-05-18 02:26:28 -04:00
FSlateDrawElement::MakeLines(OutDrawElements, LayerId, PaintGeom, Points, ESlateDrawEffect::None, Color, true, Thickness);
2026-05-15 18:14:38 -04:00
}
2026-05-18 02:26:28 -04:00
if (Config.DotRadius <= 0.0f) return LayerId;
if (Config.DotTexture == nullptr) return LayerId;
2026-05-15 18:14:38 -04:00
2026-05-18 02:26:28 -04:00
FSlateBrush DotBrush;
DotBrush.SetResourceObject(Config.DotTexture);
const FVector2D DotSize(Config.DotRadius * 2.0f);
for (int32 I = 0; I < Items.Num(); I++)
2026-05-15 18:14:38 -04:00
{
2026-05-18 02:26:28 -04:00
const FRadialMenuItem& Item = Items[I];
const bool bSelected = (I == Config.SelectedItem);
const FLinearColor& Color = bSelected ? Config.SelectedColor : Config.LineColor;
2026-05-15 18:14:38 -04:00
for (const FVector2D& Point : { Item.Point1, Item.Point3 })
{
const FVector2D LocalPoint = Center + Point;
FSlateDrawElement::MakeBox(
OutDrawElements,
LayerId,
2026-05-18 02:26:28 -04:00
AllottedGeometry.ToPaintGeometry(DotSize, FSlateLayoutTransform(LocalPoint - FVector2D(Config.DotRadius))),
2026-05-15 18:14:38 -04:00
&DotBrush,
ESlateDrawEffect::None,
2026-05-18 02:26:28 -04:00
Color);
2026-05-15 18:14:38 -04:00
}
2026-05-11 00:13:30 -04:00
}
return LayerId;
}
private:
2026-05-18 02:26:28 -04:00
FRadialMenuConfig Config;
TArray<FRadialMenuItem> Items;
2026-05-11 00:13:30 -04:00
};
2026-05-15 18:14:38 -04:00
void URadialMenuWidget::SetNumItems(int32 NewNumItems)
{
2026-05-18 02:26:28 -04:00
if (Config.NumItems == NewNumItems) return;
2026-05-15 18:14:38 -04:00
2026-05-18 02:26:28 -04:00
Config.NumItems = NewNumItems;
2026-05-15 18:14:38 -04:00
SynchronizeProperties();
}
void URadialMenuWidget::SetItemHeight(float NewItemHeight)
{
2026-05-18 02:26:28 -04:00
if (Config.ItemHeight == NewItemHeight) return;
2026-05-15 18:14:38 -04:00
2026-05-18 02:26:28 -04:00
Config.ItemHeight = NewItemHeight;
2026-05-15 18:14:38 -04:00
SynchronizeProperties();
}
void URadialMenuWidget::SetInnerRadius(float NewInnerRadius)
{
2026-05-18 02:26:28 -04:00
if (Config.InnerRadius == NewInnerRadius) return;
2026-05-15 18:14:38 -04:00
2026-05-18 02:26:28 -04:00
Config.InnerRadius = NewInnerRadius;
2026-05-15 18:14:38 -04:00
SynchronizeProperties();
}
void URadialMenuWidget::SetMinSpoke(float NewMinSpoke)
{
2026-05-18 02:26:28 -04:00
if (Config.MinSpoke == NewMinSpoke) return;
2026-05-15 18:14:38 -04:00
2026-05-18 02:26:28 -04:00
Config.MinSpoke = NewMinSpoke;
2026-05-15 18:14:38 -04:00
SynchronizeProperties();
}
void URadialMenuWidget::SetSpread(float NewSpread)
{
2026-05-18 02:26:28 -04:00
if (Config.Spread == NewSpread) return;
2026-05-15 18:14:38 -04:00
2026-05-18 02:26:28 -04:00
Config.Spread = NewSpread;
2026-05-15 18:14:38 -04:00
SynchronizeProperties();
}
void URadialMenuWidget::SetLineThickness(float NewLineThickness)
{
2026-05-18 02:26:28 -04:00
if (Config.LineThickness == NewLineThickness) return;
2026-05-15 18:14:38 -04:00
2026-05-18 02:26:28 -04:00
Config.LineThickness = NewLineThickness;
2026-05-15 18:14:38 -04:00
SynchronizeProperties();
}
void URadialMenuWidget::SetLineColor(FLinearColor NewLineColor)
{
2026-05-18 02:26:28 -04:00
if (Config.LineColor == NewLineColor) return;
2026-05-15 18:14:38 -04:00
2026-05-18 02:26:28 -04:00
Config.LineColor = NewLineColor;
2026-05-15 18:14:38 -04:00
SynchronizeProperties();
}
void URadialMenuWidget::SetDotTexture(UTexture2D* NewDotTexture)
{
2026-05-18 02:26:28 -04:00
if (Config.DotTexture == NewDotTexture) return;
2026-05-15 18:14:38 -04:00
2026-05-18 02:26:28 -04:00
Config.DotTexture = NewDotTexture;
2026-05-15 18:14:38 -04:00
SynchronizeProperties();
}
void URadialMenuWidget::SetDotRadius(float NewDotRadius)
2026-05-11 00:13:30 -04:00
{
2026-05-18 02:26:28 -04:00
if (Config.DotRadius == NewDotRadius) return;
Config.DotRadius = NewDotRadius;
SynchronizeProperties();
}
bool URadialMenuWidget::SetSelectedItem(int32 NewSelectedItem)
{
if (Config.SelectedItem == NewSelectedItem) return false;
Config.SelectedItem = NewSelectedItem;
SynchronizeProperties();
return true;
}
void URadialMenuWidget::SetSelectedColor(FLinearColor NewSelectedColor)
{
if (Config.SelectedColor == NewSelectedColor) return;
Config.SelectedColor = NewSelectedColor;
SynchronizeProperties();
}
void URadialMenuWidget::SetSelectedThickness(float NewSelectedThickness)
{
if (Config.SelectedThickness == NewSelectedThickness) return;
2026-05-15 18:14:38 -04:00
2026-05-18 02:26:28 -04:00
Config.SelectedThickness = NewSelectedThickness;
2026-05-15 18:14:38 -04:00
SynchronizeProperties();
}
void URadialMenuWidget::SynchronizeProperties()
{
Super::SynchronizeProperties();
2026-05-18 02:26:28 -04:00
Items = FRadialMenuItem::Calculate(Config);
if (Items.Num() == 0)
2026-05-11 00:13:30 -04:00
{
2026-05-18 02:26:28 -04:00
PointerVector = FVector2D(0,0);
2026-05-11 00:13:30 -04:00
}
if (MySlateWidget.IsValid())
{
2026-05-18 02:26:28 -04:00
MySlateWidget->SetConfig(Config);
2026-05-11 00:13:30 -04:00
}
}
2026-05-18 02:26:28 -04:00
bool URadialMenuWidget::AddPointer(FVector2D Direction, float Scale)
2026-05-11 00:13:30 -04:00
{
2026-05-18 02:26:28 -04:00
if (!IsVisible())
2026-05-11 00:13:30 -04:00
{
2026-05-18 02:26:28 -04:00
return false;
2026-05-11 00:13:30 -04:00
}
2026-05-18 02:26:28 -04:00
if (Items.Num() == 0)
{
return false;
}
PointerVector += (Direction * Scale);
if (PointerVector.Length() < 0.5)
{
return SetSelectedItem(-1);
}
if (PointerVector.Length() > 1.0)
{
PointerVector.Normalize();
}
int32 BestIndex = -1;
double BestDot = -2.0;
for (int32 I = 0; I < Items.Num(); I++)
{
FVector2D SpokeDir = Items[I].Point1;
if (!SpokeDir.Normalize()) continue;
double Dot = FVector2D::DotProduct(SpokeDir, PointerVector);
if (Dot <= BestDot) continue;
BestDot = Dot;
BestIndex = I;
}
return SetSelectedItem(BestIndex);
}
FLinearColor URadialMenuWidget::GetItemColor(int N) const
{
return (N == Config.SelectedItem) ? Config.SelectedColor : Config.LineColor;
}
TSharedRef<SWidget> URadialMenuWidget::RebuildWidget()
{
MySlateWidget = SNew(SRadialMenu, Config);
2026-05-11 00:13:30 -04:00
return MySlateWidget.ToSharedRef();
}
void URadialMenuWidget::ReleaseSlateResources(bool bReleaseChildren)
{
Super::ReleaseSlateResources(bReleaseChildren);
MySlateWidget.Reset();
}