#include "RadialMenu.h" #include "Rendering/DrawElements.h" #include "Engine/Texture2D.h" #include "Styling/SlateBrush.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); } void SetLineThickness(float NewLineThickness) { LineThickness = NewLineThickness; Invalidate(EInvalidateWidgetReason::Paint); } void SetLineColor(FLinearColor NewLineColor) { LineColor = NewLineColor; Invalidate(EInvalidateWidgetReason::Paint); } void SetDotTexture(UTexture2D* NewDotTexture) { DotTexture = NewDotTexture; DotBrush.SetResourceObject(NewDotTexture); Invalidate(EInvalidateWidgetReason::Paint); } void SetDotRadius(float NewDotRadius) { DotRadius = NewDotRadius; Invalidate(EInvalidateWidgetReason::Paint); } 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 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, LineColor, true, LineThickness); } if (DotRadius <= 0.0f) return LayerId; if (!DotTexture.IsValid()) return LayerId; const FVector2D DotSize(DotRadius * 2.0f); for (const FRadialMenuItem &Item : Layout->GetItems()) { for (const FVector2D& Point : { Item.Point1, Item.Point3 }) { const FVector2D LocalPoint = Center + Point; FSlateDrawElement::MakeBox( OutDrawElements, LayerId, AllottedGeometry.ToPaintGeometry(DotSize, FSlateLayoutTransform(LocalPoint - FVector2D(DotRadius))), &DotBrush, ESlateDrawEffect::None, LineColor); } } return LayerId; } private: TWeakObjectPtr Layout; float LineThickness = 1.0f; FLinearColor LineColor = FLinearColor::White; TWeakObjectPtr DotTexture; FSlateBrush DotBrush; float DotRadius = 0.0f; }; void URadialMenuWidget::SetNumItems(int32 NewNumItems) { if (NumItems == NewNumItems) return; NumItems = NewNumItems; SynchronizeProperties(); } void URadialMenuWidget::SetItemHeight(float NewItemHeight) { if (ItemHeight == NewItemHeight) return; ItemHeight = NewItemHeight; SynchronizeProperties(); } void URadialMenuWidget::SetInnerRadius(float NewInnerRadius) { if (InnerRadius == NewInnerRadius) return; InnerRadius = NewInnerRadius; SynchronizeProperties(); } void URadialMenuWidget::SetMinSpoke(float NewMinSpoke) { if (MinSpoke == NewMinSpoke) return; MinSpoke = NewMinSpoke; SynchronizeProperties(); } void URadialMenuWidget::SetSpread(float NewSpread) { if (Spread == NewSpread) return; Spread = NewSpread; SynchronizeProperties(); } void URadialMenuWidget::SetLineThickness(float NewLineThickness) { if (LineThickness == NewLineThickness) return; LineThickness = NewLineThickness; SynchronizeProperties(); } void URadialMenuWidget::SetLineColor(FLinearColor NewLineColor) { if (LineColor == NewLineColor) return; LineColor = NewLineColor; SynchronizeProperties(); } void URadialMenuWidget::SetDotTexture(UTexture2D* NewDotTexture) { if (DotTexture == NewDotTexture) return; DotTexture = NewDotTexture; SynchronizeProperties(); } void URadialMenuWidget::SetDotRadius(float NewDotRadius) { if (DotRadius == NewDotRadius) return; DotRadius = NewDotRadius; SynchronizeProperties(); } void URadialMenuWidget::SynchronizeProperties() { Super::SynchronizeProperties(); if (!Layout) { Layout = NewObject(this); } Layout->Configure(NumItems, ItemHeight, InnerRadius, MinSpoke, Spread); if (MySlateWidget.IsValid()) { MySlateWidget->SetLineThickness(LineThickness); MySlateWidget->SetLineColor(LineColor); MySlateWidget->SetDotTexture(DotTexture); MySlateWidget->SetDotRadius(DotRadius); MySlateWidget->Refresh(); } } TSharedRef URadialMenuWidget::RebuildWidget() { if (!Layout) { Layout = NewObject(this); } MySlateWidget = SNew(SRadialMenu, Layout); SynchronizeProperties(); return MySlateWidget.ToSharedRef(); } void URadialMenuWidget::ReleaseSlateResources(bool bReleaseChildren) { Super::ReleaseSlateResources(bReleaseChildren); MySlateWidget.Reset(); }