#include "RadialMenu.h" #include "Rendering/DrawElements.h" #include "Engine/Texture2D.h" #include "Styling/SlateBrush.h" #include "Widgets/SLeafWidget.h" TArray FRadialMenuItem::Calculate(const FRadialMenuConfig &Config) { TArray Items; if (Config.NumItems < 0) return Items; int32 NumRight = (Config.NumItems / 2); int32 NumLeft = Config.NumItems - NumRight; Items.SetNum(Config.NumItems); View LeftItems(Items.GetData(), NumLeft); View RightItems(Items.GetData() + NumLeft, NumRight); CalculateSpokes(LeftItems, Config.NumItems, Config.ItemHeight, Config.InnerRadius, Config.MinSpoke); CalculateSpokes(RightItems, Config.NumItems, Config.ItemHeight, Config.InnerRadius, Config.MinSpoke); double LeftWidth = WidestSpoke(LeftItems); double RightWidth = WidestSpoke(RightItems); double HalfWidth = FMath::Max(LeftWidth, RightWidth) + Config.Spread; CalculateSpread(LeftItems, HalfWidth - LeftWidth); CalculateSpread(RightItems, HalfWidth - RightWidth); FlipHorizontal(LeftItems); return Items; } FVector2D FRadialMenuItem::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 FRadialMenuItem::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 FRadialMenuItem::WidestSpoke(View V) { double Result = 0.0; for (const FRadialMenuItem &Item : V) { Result = FMath::Max(Item.Point2.X, Result); } return Result; } void FRadialMenuItem::CalculateSpread(View V, double Offset) { for (FRadialMenuItem &Item : V) { Item.Point3 = Item.Point2 + FVector2D(Offset, 0.0); } } void FRadialMenuItem::CalculateSpokes(View V, int32 TotalItems, 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(), TotalItems) * 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(), TotalItems); 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, const FRadialMenuConfig& InConfig) { Config = InConfig; } void SetConfig(const FRadialMenuConfig& NewConfig) { Config = NewConfig; Items = FRadialMenuItem::Calculate(Config); Invalidate(EInvalidateWidgetReason::Layout); } protected: virtual FVector2D ComputeDesiredSize(float) const override { FVector2D Min(0.0, 0.0); FVector2D Max(0.0, 0.0); for (const FRadialMenuItem &Item : Items) { 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(); for (int32 I = 0; I < Items.Num(); I++) { 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; 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, Thickness); } if (Config.DotRadius <= 0.0f) return LayerId; if (Config.DotTexture == nullptr) return LayerId; FSlateBrush DotBrush; DotBrush.SetResourceObject(Config.DotTexture); const FVector2D DotSize(Config.DotRadius * 2.0f); for (int32 I = 0; I < Items.Num(); I++) { const FRadialMenuItem& Item = Items[I]; const bool bSelected = (I == Config.SelectedItem); const FLinearColor& Color = bSelected ? Config.SelectedColor : Config.LineColor; for (const FVector2D& Point : { Item.Point1, Item.Point3 }) { const FVector2D LocalPoint = Center + Point; FSlateDrawElement::MakeBox( OutDrawElements, LayerId, AllottedGeometry.ToPaintGeometry(DotSize, FSlateLayoutTransform(LocalPoint - FVector2D(Config.DotRadius))), &DotBrush, ESlateDrawEffect::None, Color); } } return LayerId; } private: FRadialMenuConfig Config; TArray Items; }; void URadialMenuWidget::SetNumItems(int32 NewNumItems) { if (Config.NumItems == NewNumItems) return; Config.NumItems = NewNumItems; SynchronizeProperties(); } void URadialMenuWidget::SetItemHeight(float NewItemHeight) { if (Config.ItemHeight == NewItemHeight) return; Config.ItemHeight = NewItemHeight; SynchronizeProperties(); } void URadialMenuWidget::SetInnerRadius(float NewInnerRadius) { if (Config.InnerRadius == NewInnerRadius) return; Config.InnerRadius = NewInnerRadius; SynchronizeProperties(); } void URadialMenuWidget::SetMinSpoke(float NewMinSpoke) { if (Config.MinSpoke == NewMinSpoke) return; Config.MinSpoke = NewMinSpoke; SynchronizeProperties(); } void URadialMenuWidget::SetSpread(float NewSpread) { if (Config.Spread == NewSpread) return; Config.Spread = NewSpread; SynchronizeProperties(); } void URadialMenuWidget::SetLineThickness(float NewLineThickness) { if (Config.LineThickness == NewLineThickness) return; Config.LineThickness = NewLineThickness; SynchronizeProperties(); } void URadialMenuWidget::SetLineColor(FLinearColor NewLineColor) { if (Config.LineColor == NewLineColor) return; Config.LineColor = NewLineColor; SynchronizeProperties(); } void URadialMenuWidget::SetDotTexture(UTexture2D* NewDotTexture) { if (Config.DotTexture == NewDotTexture) return; Config.DotTexture = NewDotTexture; SynchronizeProperties(); } void URadialMenuWidget::SetDotRadius(float NewDotRadius) { 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; Config.SelectedThickness = NewSelectedThickness; SynchronizeProperties(); } void URadialMenuWidget::SynchronizeProperties() { Super::SynchronizeProperties(); Items = FRadialMenuItem::Calculate(Config); if (Items.Num() == 0) { PointerVector = FVector2D(0,0); } if (MySlateWidget.IsValid()) { MySlateWidget->SetConfig(Config); } } bool URadialMenuWidget::AddPointer(FVector2D Direction, float Scale) { if (!IsVisible()) { return false; } 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 URadialMenuWidget::RebuildWidget() { MySlateWidget = SNew(SRadialMenu, Config); return MySlateWidget.ToSharedRef(); } void URadialMenuWidget::ReleaseSlateResources(bool bReleaseChildren) { Super::ReleaseSlateResources(bReleaseChildren); MySlateWidget.Reset(); }