#include "RadialMenu.h" #include "Rendering/DrawElements.h" #include "Widgets/SLeafWidget.h" void URadialMenuLayout::Configure(int32 NItems, float ItemHeight, float InnerRadius, float MinSpoke, float Spread) { NumRight = (NItems / 2); NumLeft = NItems - NumRight; Items.SetNum(NItems); View LeftItems(Items.GetData(), NumLeft); View RightItems(Items.GetData() + NumLeft, NumRight); CalculateSpokes(LeftItems, ItemHeight, InnerRadius, MinSpoke); CalculateSpokes(RightItems, ItemHeight, InnerRadius, MinSpoke); double LeftWidth = WidestSpoke(LeftItems); double RightWidth = WidestSpoke(RightItems); double HalfWidth = FMath::Max(LeftWidth, RightWidth) + Spread; CalculateSpread(LeftItems, HalfWidth - LeftWidth); CalculateSpread(RightItems, HalfWidth - RightWidth); FlipHorizontal(LeftItems); } FVector2D URadialMenuLayout::SpokeVector(int32 I, int32 NSide, int32 NTotal) { 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); return FVector2D(FMath::Sin(Radians), -FMath::Cos(Radians)); } void URadialMenuLayout::FlipHorizontal(View V) { 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; } } double URadialMenuLayout::WidestSpoke(View V) { double Result = 0.0; for (const FRadialMenuItem &Item : V) { Result = FMath::Max(Item.Point2.X, Result); } return Result; } void URadialMenuLayout::CalculateSpread(View V, double Offset) { for (FRadialMenuItem &Item : V) { Item.Point3 = Item.Point2 + FVector2D(Offset, 0.0); } } void URadialMenuLayout::CalculateSpokes(View V, float ItemHeight, float InnerRadius, float MinSpoke) { if (V.Num() == 0) return; // RightSide is always initialized to true, it may get // reversed by FlipHorizontal. for (int32 I = 0; I < V.Num(); I++) { V[I].RightSide = true; V[I].Point1 = SpokeVector(I, V.Num(), Items.Num()) * InnerRadius; } // Calculate point2 for all spokes. double NextLineMin = ItemHeight * 0.5; int32 Mid = (V.Num() / 2); if (V.Num() & 1) { V[Mid].Point2 = FVector2D(InnerRadius + MinSpoke, 0.0); NextLineMin = ItemHeight; Mid += 1; } for (int32 I = Mid; I < V.Num(); I++) { FVector2D UnitVec = SpokeVector(I, V.Num(), Items.Num()); double Y = (UnitVec.Y * (InnerRadius + MinSpoke)); if (Y < NextLineMin) Y = NextLineMin; NextLineMin = Y + ItemHeight; FVector2D Point2 = UnitVec * (Y / UnitVec.Y); V[I].Point2 = Point2; V[V.Num() - 1 - I].Point2 = Point2 * FVector2D(1.0,-1.0); } // 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. 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; } } // Slate widget that paints the radial menu's polylines. class SRadialMenu : public SLeafWidget { public: SLATE_BEGIN_ARGS(SRadialMenu) {} SLATE_END_ARGS() void Construct(const FArguments& InArgs, URadialMenuLayout* InLayout) { Layout = InLayout; } void Refresh() { Invalidate(EInvalidateWidgetReason::Layout); } protected: virtual FVector2D ComputeDesiredSize(float) const override { if (!Layout.IsValid()) return FVector2D::ZeroVector; FVector2D Min(0.0, 0.0); FVector2D Max(0.0, 0.0); for (const FRadialMenuItem &Item : Layout->GetItems()) { 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 { if (!Layout.IsValid()) return LayerId; const FVector2D Center = AllottedGeometry.GetLocalSize() * 0.5; const FLinearColor Color = InWidgetStyle.GetColorAndOpacityTint(); const FPaintGeometry PaintGeom = AllottedGeometry.ToPaintGeometry(); for (const FRadialMenuItem &Item : Layout->GetItems()) { TArray Points; Points.Add(Center + Item.Point1); Points.Add(Center + Item.Point2); Points.Add(Center + Item.Point3); FSlateDrawElement::MakeLines(OutDrawElements, LayerId, PaintGeom, Points, ESlateDrawEffect::None, Color, true, 1.0f); } return LayerId; } private: TWeakObjectPtr Layout; }; void URadialMenuWidget::Configure(int32 NItems, float ItemHeight, float InnerRadius, float MinSpoke, float Spread) { if (!Layout) { Layout = NewObject(this); } Layout->Configure(NItems, ItemHeight, InnerRadius, MinSpoke, Spread); if (MySlateWidget.IsValid()) { MySlateWidget->Refresh(); } } TSharedRef URadialMenuWidget::RebuildWidget() { if (!Layout) { Layout = NewObject(this); } MySlateWidget = SNew(SRadialMenu, Layout); return MySlateWidget.ToSharedRef(); } void URadialMenuWidget::ReleaseSlateResources(bool bReleaseChildren) { Super::ReleaseSlateResources(bReleaseChildren); MySlateWidget.Reset(); }