From 0b23c82e73fe1db3ed497193cc5436007b63bc73 Mon Sep 17 00:00:00 2001 From: jyelon Date: Mon, 18 May 2026 02:26:28 -0400 Subject: [PATCH] Progress on radial menus --- Content/Widgets/WB_Menu.uasset | 4 +- Content/Widgets/WB_Menu_Item.uasset | 4 +- Source/Integration/RadialMenu.cpp | 228 +++++++++++++++++----------- Source/Integration/RadialMenu.h | 139 +++++++++-------- 4 files changed, 216 insertions(+), 159 deletions(-) diff --git a/Content/Widgets/WB_Menu.uasset b/Content/Widgets/WB_Menu.uasset index 5e3fd5d9..1ef194c3 100644 --- a/Content/Widgets/WB_Menu.uasset +++ b/Content/Widgets/WB_Menu.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:be1bb8179b743dbf679277ae1e24d495f5c3989bde30d412f3615b895c3a2228 -size 252653 +oid sha256:d69e87fe955c4d10512477e624e8c59e56142fbd205be7a21bbf27617088508b +size 280353 diff --git a/Content/Widgets/WB_Menu_Item.uasset b/Content/Widgets/WB_Menu_Item.uasset index cc036174..f81127cb 100644 --- a/Content/Widgets/WB_Menu_Item.uasset +++ b/Content/Widgets/WB_Menu_Item.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:12fcddbda10c43d75ffa1e69ef965e788e73601151ed8beb68bf8153a485185a -size 49715 +oid sha256:045443642d50d9e8fe191bf6464137c46d50b405d50daaa74f193cff57a031e2 +size 57301 diff --git a/Source/Integration/RadialMenu.cpp b/Source/Integration/RadialMenu.cpp index ed98f4af..93508629 100644 --- a/Source/Integration/RadialMenu.cpp +++ b/Source/Integration/RadialMenu.cpp @@ -5,27 +5,31 @@ #include "Widgets/SLeafWidget.h" -void URadialMenuLayout::Configure(int32 NItems, float ItemHeight, float InnerRadius, float MinSpoke, float Spread) +TArray FRadialMenuItem::Calculate(const FRadialMenuConfig &Config) { - NumRight = (NItems / 2); - NumLeft = NItems - NumRight; - Items.SetNum(NItems); + 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, ItemHeight, InnerRadius, MinSpoke); - CalculateSpokes(RightItems, ItemHeight, InnerRadius, MinSpoke); + 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) + Spread; + double HalfWidth = FMath::Max(LeftWidth, RightWidth) + Config.Spread; CalculateSpread(LeftItems, HalfWidth - LeftWidth); CalculateSpread(RightItems, HalfWidth - RightWidth); FlipHorizontal(LeftItems); + return Items; } -FVector2D URadialMenuLayout::SpokeVector(int32 I, int32 NSide, int32 NTotal) +FVector2D FRadialMenuItem::SpokeVector(int32 I, int32 NSide, int32 NTotal) { double SpokeAngle = 1.0 / NTotal; double OffsetAngle = 0.5 * (0.5 - ((NSide - 1) * SpokeAngle)); @@ -34,7 +38,7 @@ FVector2D URadialMenuLayout::SpokeVector(int32 I, int32 NSide, int32 NTotal) return FVector2D(FMath::Sin(Radians), -FMath::Cos(Radians)); } -void URadialMenuLayout::FlipHorizontal(View V) +void FRadialMenuItem::FlipHorizontal(View V) { for (FRadialMenuItem& Item : V) { @@ -45,7 +49,7 @@ void URadialMenuLayout::FlipHorizontal(View V) } } -double URadialMenuLayout::WidestSpoke(View V) +double FRadialMenuItem::WidestSpoke(View V) { double Result = 0.0; for (const FRadialMenuItem &Item : V) @@ -55,7 +59,7 @@ double URadialMenuLayout::WidestSpoke(View V) return Result; } -void URadialMenuLayout::CalculateSpread(View V, double Offset) +void FRadialMenuItem::CalculateSpread(View V, double Offset) { for (FRadialMenuItem &Item : V) { @@ -63,7 +67,7 @@ void URadialMenuLayout::CalculateSpread(View V, double Offset) } } -void URadialMenuLayout::CalculateSpokes(View V, float ItemHeight, float InnerRadius, float MinSpoke) +void FRadialMenuItem::CalculateSpokes(View V, int32 TotalItems, float ItemHeight, float InnerRadius, float MinSpoke) { if (V.Num() == 0) return; @@ -72,7 +76,7 @@ void URadialMenuLayout::CalculateSpokes(View V, float ItemHeight, float InnerRad for (int32 I = 0; I < V.Num(); I++) { V[I].RightSide = true; - V[I].Point1 = SpokeVector(I, V.Num(), Items.Num()) * InnerRadius; + V[I].Point1 = SpokeVector(I, V.Num(), TotalItems) * InnerRadius; } // Calculate point2 for all spokes. @@ -86,7 +90,7 @@ void URadialMenuLayout::CalculateSpokes(View V, float ItemHeight, float InnerRad } for (int32 I = Mid; I < V.Num(); I++) { - FVector2D UnitVec = SpokeVector(I, V.Num(), Items.Num()); + FVector2D UnitVec = SpokeVector(I, V.Num(), TotalItems); double Y = (UnitVec.Y * (InnerRadius + MinSpoke)); if (Y < NextLineMin) Y = NextLineMin; NextLineMin = Y + ItemHeight; @@ -114,48 +118,24 @@ public: SLATE_BEGIN_ARGS(SRadialMenu) {} SLATE_END_ARGS() - void Construct(const FArguments& InArgs, URadialMenuLayout* InLayout) + void Construct(const FArguments& InArgs, const FRadialMenuConfig& InConfig) { - Layout = InLayout; + Config = InConfig; } - void Refresh() + void SetConfig(const FRadialMenuConfig& NewConfig) { + Config = NewConfig; + Items = FRadialMenuItem::Calculate(Config); 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 FRadialMenuItem &Item : Items) { for (const FVector2D &P : { Item.Point1, Item.Point2, Item.Point3 }) { @@ -175,120 +155,149 @@ protected: 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()) + 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, LineColor, true, LineThickness); + FSlateDrawElement::MakeLines(OutDrawElements, LayerId, PaintGeom, Points, ESlateDrawEffect::None, Color, true, Thickness); } - if (DotRadius <= 0.0f) return LayerId; - if (!DotTexture.IsValid()) return LayerId; + if (Config.DotRadius <= 0.0f) return LayerId; + if (Config.DotTexture == nullptr) return LayerId; - const FVector2D DotSize(DotRadius * 2.0f); - for (const FRadialMenuItem &Item : Layout->GetItems()) + 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(DotRadius))), + AllottedGeometry.ToPaintGeometry(DotSize, FSlateLayoutTransform(LocalPoint - FVector2D(Config.DotRadius))), &DotBrush, ESlateDrawEffect::None, - LineColor); + Color); } } return LayerId; } private: - TWeakObjectPtr Layout; - float LineThickness = 1.0f; - FLinearColor LineColor = FLinearColor::White; - TWeakObjectPtr DotTexture; - FSlateBrush DotBrush; - float DotRadius = 0.0f; + FRadialMenuConfig Config; + + TArray Items; }; void URadialMenuWidget::SetNumItems(int32 NewNumItems) { - if (NumItems == NewNumItems) return; + if (Config.NumItems == NewNumItems) return; - NumItems = NewNumItems; + Config.NumItems = NewNumItems; SynchronizeProperties(); } void URadialMenuWidget::SetItemHeight(float NewItemHeight) { - if (ItemHeight == NewItemHeight) return; + if (Config.ItemHeight == NewItemHeight) return; - ItemHeight = NewItemHeight; + Config.ItemHeight = NewItemHeight; SynchronizeProperties(); } void URadialMenuWidget::SetInnerRadius(float NewInnerRadius) { - if (InnerRadius == NewInnerRadius) return; + if (Config.InnerRadius == NewInnerRadius) return; - InnerRadius = NewInnerRadius; + Config.InnerRadius = NewInnerRadius; SynchronizeProperties(); } void URadialMenuWidget::SetMinSpoke(float NewMinSpoke) { - if (MinSpoke == NewMinSpoke) return; + if (Config.MinSpoke == NewMinSpoke) return; - MinSpoke = NewMinSpoke; + Config.MinSpoke = NewMinSpoke; SynchronizeProperties(); } void URadialMenuWidget::SetSpread(float NewSpread) { - if (Spread == NewSpread) return; + if (Config.Spread == NewSpread) return; - Spread = NewSpread; + Config.Spread = NewSpread; SynchronizeProperties(); } void URadialMenuWidget::SetLineThickness(float NewLineThickness) { - if (LineThickness == NewLineThickness) return; + if (Config.LineThickness == NewLineThickness) return; - LineThickness = NewLineThickness; + Config.LineThickness = NewLineThickness; SynchronizeProperties(); } void URadialMenuWidget::SetLineColor(FLinearColor NewLineColor) { - if (LineColor == NewLineColor) return; + if (Config.LineColor == NewLineColor) return; - LineColor = NewLineColor; + Config.LineColor = NewLineColor; SynchronizeProperties(); } void URadialMenuWidget::SetDotTexture(UTexture2D* NewDotTexture) { - if (DotTexture == NewDotTexture) return; + if (Config.DotTexture == NewDotTexture) return; - DotTexture = NewDotTexture; + Config.DotTexture = NewDotTexture; SynchronizeProperties(); } void URadialMenuWidget::SetDotRadius(float NewDotRadius) { - if (DotRadius == NewDotRadius) return; + if (Config.DotRadius == NewDotRadius) return; - DotRadius = NewDotRadius; + 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(); } @@ -296,29 +305,64 @@ void URadialMenuWidget::SynchronizeProperties() { Super::SynchronizeProperties(); - if (!Layout) + Items = FRadialMenuItem::Calculate(Config); + if (Items.Num() == 0) { - Layout = NewObject(this); + PointerVector = FVector2D(0,0); } - Layout->Configure(NumItems, ItemHeight, InnerRadius, MinSpoke, Spread); if (MySlateWidget.IsValid()) { - MySlateWidget->SetLineThickness(LineThickness); - MySlateWidget->SetLineColor(LineColor); - MySlateWidget->SetDotTexture(DotTexture); - MySlateWidget->SetDotRadius(DotRadius); - MySlateWidget->Refresh(); + 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() { - if (!Layout) - { - Layout = NewObject(this); - } - MySlateWidget = SNew(SRadialMenu, Layout); - SynchronizeProperties(); + MySlateWidget = SNew(SRadialMenu, Config); return MySlateWidget.ToSharedRef(); } diff --git a/Source/Integration/RadialMenu.h b/Source/Integration/RadialMenu.h index 8da8a1b0..b93c1eff 100644 --- a/Source/Integration/RadialMenu.h +++ b/Source/Integration/RadialMenu.h @@ -13,6 +13,49 @@ class SRadialMenu; class UTexture2D; +USTRUCT(BlueprintType) +struct FRadialMenuConfig +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, Category="RadialMenu") + int32 NumItems = 8; + + UPROPERTY(EditAnywhere, Category="RadialMenu") + float ItemHeight = 20.0f; + + UPROPERTY(EditAnywhere, Category="RadialMenu") + float InnerRadius = 20.0f; + + UPROPERTY(EditAnywhere, Category="RadialMenu") + float MinSpoke = 20.0f; + + UPROPERTY(EditAnywhere, Category="RadialMenu") + float Spread = 20.0f; + + UPROPERTY(EditAnywhere, Category="RadialMenu") + float LineThickness = 4.0f; + + UPROPERTY(EditAnywhere, Category="RadialMenu") + FLinearColor LineColor = FLinearColor::White; + + UPROPERTY(EditAnywhere, Category="RadialMenu") + TObjectPtr DotTexture; + + UPROPERTY(EditAnywhere, Category="RadialMenu") + float DotRadius = 3.0f; + + UPROPERTY(EditAnywhere, Category="RadialMenu") + int32 SelectedItem = -1; + + UPROPERTY(EditAnywhere, Category="RadialMenu") + FLinearColor SelectedColor = FLinearColor::Yellow; + + UPROPERTY(EditAnywhere, Category="RadialMenu") + float SelectedThickness = 2.0f; +}; + + USTRUCT(BlueprintType) struct FRadialMenuItem { @@ -29,26 +72,8 @@ struct FRadialMenuItem UPROPERTY(BlueprintReadOnly) bool RightSide = false; -}; - -UCLASS(BlueprintType) -class URadialMenuLayout : public UObject -{ - GENERATED_BODY() - -public: - UFUNCTION(BlueprintCallable) - void Configure(int32 NItems, float ItemHeight, float InnerRadius, float MinSpoke, float Spread); - - UFUNCTION(BlueprintCallable) - int32 LeftNum() const { return NumLeft; } - - UFUNCTION(BlueprintCallable) - int32 RightNum() const { return NumRight; } - - UFUNCTION(BlueprintCallable) - const TArray &GetItems() const { return Items; } + static TArray Calculate(const FRadialMenuConfig &Config); private: using View = TArrayView; @@ -59,31 +84,24 @@ private: // spokes on both sides. The spokes on a given side are // organized top-to-bottom, and the angle between the // spokes is always equal to (1/NTotal) of the circle. - FVector2D SpokeVector(int32 I, int32 NSide, int32 NTotal); + static FVector2D SpokeVector(int32 I, int32 NSide, int32 NTotal); // Populate Point1 and Point2, these are the // endpoints of the spoke segment. Spokes are // designed to always be long enough to make room // for MinSpoke, but also to make enough room to // keep the menu items from overlapping. - void CalculateSpokes(View V, float ItemHeight, float InnerRadius, float MinSpoke); + static void CalculateSpokes(View V, int32 TotalItems, float ItemHeight, float InnerRadius, float MinSpoke); // Search for the widest spoke, and return its X coordinate. - double WidestSpoke(View V); + static double WidestSpoke(View V); // Populate Point3, this is the endpoint of the spread // line that goes horizontal. - void CalculateSpread(View V, double Offset); + static void CalculateSpread(View V, double Offset); // Flip everything in the specified view horizontally. - void FlipHorizontal(View V); - - // The array of items. - TArray Items; - - // Number of items on the left, and on the right. - int32 NumLeft = 0; - int32 NumRight = 0; + static void FlipHorizontal(View V); }; @@ -121,43 +139,38 @@ public: void SetDotRadius(float NewDotRadius); UFUNCTION(BlueprintCallable) - URadialMenuLayout* GetLayout() const { return Layout; } + bool SetSelectedItem(int32 NewSelectedItem); + UFUNCTION(BlueprintCallable) + void SetSelectedColor(FLinearColor NewSelectedColor); + + UFUNCTION(BlueprintCallable) + void SetSelectedThickness(float NewSelectedThickness); + + UFUNCTION(BlueprintCallable) + const TArray &GetItems() const { return Items; } + + // Returns true if the selected item changed. + UFUNCTION(BlueprintCallable) + bool AddPointer(FVector2D Direction, float Scale); + + UFUNCTION(BlueprintCallable) + FVector2D GetPointer() const { return PointerVector; } + + UFUNCTION(BlueprintCallable) + FLinearColor GetItemColor(int N) const; + protected: + UPROPERTY(EditAnywhere) + FRadialMenuConfig Config; + + TArray Items; + + FVector2D PointerVector = {0,0}; + virtual TSharedRef RebuildWidget() override; virtual void ReleaseSlateResources(bool bReleaseChildren) override; virtual void SynchronizeProperties() override; -private: - UPROPERTY(EditAnywhere, BlueprintReadWrite, Setter, Category="RadialMenu", meta=(ClampMin="0", AllowPrivateAccess="true")) - int32 NumItems = 8; - - UPROPERTY(EditAnywhere, BlueprintReadWrite, Setter, Category="RadialMenu", meta=(AllowPrivateAccess="true")) - float ItemHeight = 20.0f; - - UPROPERTY(EditAnywhere, BlueprintReadWrite, Setter, Category="RadialMenu", meta=(AllowPrivateAccess="true")) - float InnerRadius = 20.0f; - - UPROPERTY(EditAnywhere, BlueprintReadWrite, Setter, Category="RadialMenu", meta=(AllowPrivateAccess="true")) - float MinSpoke = 20.0f; - - UPROPERTY(EditAnywhere, BlueprintReadWrite, Setter, Category="RadialMenu", meta=(AllowPrivateAccess="true")) - float Spread = 20.0f; - - UPROPERTY(EditAnywhere, BlueprintReadWrite, Setter, Category="RadialMenu", meta=(AllowPrivateAccess="true")) - float LineThickness = 2.0f; - - UPROPERTY(EditAnywhere, BlueprintReadWrite, Setter, Category="RadialMenu", meta=(AllowPrivateAccess="true")) - FLinearColor LineColor = FLinearColor::White; - - UPROPERTY(EditAnywhere, BlueprintReadWrite, Setter, Category="RadialMenu", meta=(AllowPrivateAccess="true")) - TObjectPtr DotTexture; - - UPROPERTY(EditAnywhere, BlueprintReadWrite, Setter, Category="RadialMenu", meta=(AllowPrivateAccess="true")) - float DotRadius = 0.0f; - - UPROPERTY() - TObjectPtr Layout; - TSharedPtr MySlateWidget; };