From cd3266a8215c1cd8c587b896fe58c384a9244f6f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Feb 2026 19:00:52 +0000 Subject: [PATCH 1/5] Initial plan From ab23d91d4ec1725c4faf0325c1bf41ef6778b731 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Feb 2026 19:14:01 +0000 Subject: [PATCH 2/5] Implement SNaBorderedWindow widget and UNaBorderedWindow UMG wrapper in Widgets/WindowWidgets Co-authored-by: SodiumZH <46860829+SodiumZH@users.noreply.github.com> --- .../Source/NaWidgets/NaWidgets.Build.cs | 3 +- .../WindowWidgets/NaBorderedWindow.cpp | 62 +++++ .../WindowWidgets/SNaBorderedWindow.cpp | 251 ++++++++++++++++++ .../Widgets/WindowWidgets/NaBorderedWindow.h | 50 ++++ .../Widgets/WindowWidgets/SNaBorderedWindow.h | 152 +++++++++++ 5 files changed, 517 insertions(+), 1 deletion(-) create mode 100644 Plugins/NaWidgets/Source/NaWidgets/Private/Widgets/WindowWidgets/NaBorderedWindow.cpp create mode 100644 Plugins/NaWidgets/Source/NaWidgets/Public/Widgets/WindowWidgets/NaBorderedWindow.h diff --git a/Plugins/NaWidgets/Source/NaWidgets/NaWidgets.Build.cs b/Plugins/NaWidgets/Source/NaWidgets/NaWidgets.Build.cs index 81124cc..5e13e06 100644 --- a/Plugins/NaWidgets/Source/NaWidgets/NaWidgets.Build.cs +++ b/Plugins/NaWidgets/Source/NaWidgets/NaWidgets.Build.cs @@ -38,7 +38,8 @@ public NaWidgets(ReadOnlyTargetRules Target) : base(Target) "CoreUObject", "Engine", "Slate", - "SlateCore" + "SlateCore", + "UMG" // ... add private dependencies that you statically link with here ... } ); diff --git a/Plugins/NaWidgets/Source/NaWidgets/Private/Widgets/WindowWidgets/NaBorderedWindow.cpp b/Plugins/NaWidgets/Source/NaWidgets/Private/Widgets/WindowWidgets/NaBorderedWindow.cpp new file mode 100644 index 0000000..7467cad --- /dev/null +++ b/Plugins/NaWidgets/Source/NaWidgets/Private/Widgets/WindowWidgets/NaBorderedWindow.cpp @@ -0,0 +1,62 @@ +// By Sodium + +#include "Widgets/WindowWidgets/NaBorderedWindow.h" + +TSharedRef UNaBorderedWindow::RebuildWidget() +{ + TSharedPtr ContentSlate; + if (Content) + ContentSlate = Content->TakeWidget(); + + SAssignNew(BorderedWindow, SNaBorderedWindow) + .Params(&Params) + .MinBodySize(MinBodySize) + .MaxBodySize(MaxBodySize) + .Content(ContentSlate); + + return BorderedWindow.ToSharedRef(); +} + +void UNaBorderedWindow::ReleaseSlateResources(bool bReleaseChildren) +{ + Super::ReleaseSlateResources(bReleaseChildren); + BorderedWindow.Reset(); +} + +void UNaBorderedWindow::SynchronizeProperties() +{ + Super::SynchronizeProperties(); + if (BorderedWindow.IsValid()) + { + BorderedWindow->UpdateImages(Params); + BorderedWindow->SetBorderWidths(Params.BorderTop, Params.BorderBottom, Params.BorderLeft, Params.BorderRight); + BorderedWindow->SetBodySize(Params.BodySize); + } +} + +const FText UNaBorderedWindow::GetPaletteCategory() +{ + return FText::FromString(TEXT("NaWidgets")); +} + +void UNaBorderedWindow::SetBodySize(FVector2D NewSize) +{ + Params.BodySize = NewSize; + if (BorderedWindow.IsValid()) + BorderedWindow->SetBodySize(NewSize); +} + +void UNaBorderedWindow::SetBorderWidths(float Top, float Bottom, float Left, float Right) +{ + Params.BorderTop = Top; + Params.BorderBottom = Bottom; + Params.BorderLeft = Left; + Params.BorderRight = Right; + if (BorderedWindow.IsValid()) + BorderedWindow->SetBorderWidths(Top, Bottom, Left, Right); +} + +FVector2D UNaBorderedWindow::GetBodySize() const +{ + return Params.BodySize; +} diff --git a/Plugins/NaWidgets/Source/NaWidgets/Private/Widgets/WindowWidgets/SNaBorderedWindow.cpp b/Plugins/NaWidgets/Source/NaWidgets/Private/Widgets/WindowWidgets/SNaBorderedWindow.cpp index e69de29..2ebb891 100644 --- a/Plugins/NaWidgets/Source/NaWidgets/Private/Widgets/WindowWidgets/SNaBorderedWindow.cpp +++ b/Plugins/NaWidgets/Source/NaWidgets/Private/Widgets/WindowWidgets/SNaBorderedWindow.cpp @@ -0,0 +1,251 @@ +// By Sodium + +#include "Widgets/WindowWidgets/SNaBorderedWindow.h" +#include "SlateOptMacros.h" +#include "Widgets/Images/SImage.h" +#include "Widgets/Layout/SCanvas.h" +#include "Widgets/Layout/SBox.h" +#include "Widgets/SNullWidget.h" +#include "Styling/CoreStyle.h" + +static FSlateBrush MakeBorderedWindowBrush(UObject* ResourceObj, FVector2D Size) +{ + FSlateBrush Brush = *FCoreStyle::Get().GetDefaultBrush(); + Brush.SetResourceObject(ResourceObj); + Brush.SetImageSize(Size); + Brush.Tiling = ESlateBrushTileType::NoTile; + return Brush; +} + +BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION +void SNaBorderedWindow::Construct(const FArguments& InArgs) +{ + if (InArgs._Params.Get()) + Params = *InArgs._Params.Get(); + MinBodySize = InArgs._MinBodySize.Get(); + MaxBodySize = InArgs._MaxBodySize.Get(); + ContentWidget = InArgs._Content.Get(); + + // Initialize brushes from params + BrushTopLeft = MakeBorderedWindowBrush(Params.ImageTopLeft, FVector2D(Params.BorderLeft, Params.BorderTop)); + BrushTop = MakeBorderedWindowBrush(Params.ImageTop, FVector2D(Params.BodySize.X, Params.BorderTop)); + BrushTopRight = MakeBorderedWindowBrush(Params.ImageTopRight, FVector2D(Params.BorderRight, Params.BorderTop)); + BrushLeft = MakeBorderedWindowBrush(Params.ImageLeft, FVector2D(Params.BorderLeft, Params.BodySize.Y)); + BrushCenter = MakeBorderedWindowBrush(Params.ImageCenter, Params.BodySize); + BrushRight = MakeBorderedWindowBrush(Params.ImageRight, FVector2D(Params.BorderRight, Params.BodySize.Y)); + BrushBottomLeft = MakeBorderedWindowBrush(Params.ImageBottomLeft, FVector2D(Params.BorderLeft, Params.BorderBottom)); + BrushBottom = MakeBorderedWindowBrush(Params.ImageBottom, FVector2D(Params.BodySize.X, Params.BorderBottom)); + BrushBottomRight = MakeBorderedWindowBrush(Params.ImageBottomRight, FVector2D(Params.BorderRight, Params.BorderBottom)); + + // Create image sub-widgets + SAssignNew(ImgTopLeft, SImage).Image(&BrushTopLeft); + SAssignNew(ImgTop, SImage).Image(&BrushTop); + SAssignNew(ImgTopRight, SImage).Image(&BrushTopRight); + SAssignNew(ImgLeft, SImage).Image(&BrushLeft); + SAssignNew(ImgCenter, SImage).Image(&BrushCenter); + SAssignNew(ImgRight, SImage).Image(&BrushRight); + SAssignNew(ImgBottomLeft, SImage).Image(&BrushBottomLeft); + SAssignNew(ImgBottom, SImage).Image(&BrushBottom); + SAssignNew(ImgBottomRight, SImage).Image(&BrushBottomRight); + + RebuildLayout(); +} +END_SLATE_FUNCTION_BUILD_OPTIMIZATION + +void SNaBorderedWindow::RebuildLayout() +{ + // Update brush sizes to match current params + BrushTopLeft.SetImageSize(FVector2D(Params.BorderLeft, Params.BorderTop)); + BrushTop.SetImageSize(FVector2D(Params.BodySize.X, Params.BorderTop)); + BrushTopRight.SetImageSize(FVector2D(Params.BorderRight, Params.BorderTop)); + BrushLeft.SetImageSize(FVector2D(Params.BorderLeft, Params.BodySize.Y)); + BrushCenter.SetImageSize(Params.BodySize); + BrushRight.SetImageSize(FVector2D(Params.BorderRight, Params.BodySize.Y)); + BrushBottomLeft.SetImageSize(FVector2D(Params.BorderLeft, Params.BorderBottom)); + BrushBottom.SetImageSize(FVector2D(Params.BodySize.X, Params.BorderBottom)); + BrushBottomRight.SetImageSize(FVector2D(Params.BorderRight, Params.BorderBottom)); + + const FVector2D TotalSize( + Params.BorderLeft + Params.BodySize.X + Params.BorderRight, + Params.BorderTop + Params.BodySize.Y + Params.BorderBottom + ); + + // Slot positions + const FVector2D PosTopLeft (0.f, 0.f); + const FVector2D PosTop (Params.BorderLeft, 0.f); + const FVector2D PosTopRight (Params.BorderLeft + Params.BodySize.X, 0.f); + const FVector2D PosLeft (0.f, Params.BorderTop); + const FVector2D PosCenter (Params.BorderLeft, Params.BorderTop); + const FVector2D PosRight (Params.BorderLeft + Params.BodySize.X, Params.BorderTop); + const FVector2D PosBottomLeft (0.f, Params.BorderTop + Params.BodySize.Y); + const FVector2D PosBottom (Params.BorderLeft, Params.BorderTop + Params.BodySize.Y); + const FVector2D PosBottomRight(Params.BorderLeft + Params.BodySize.X, Params.BorderTop + Params.BodySize.Y); + + // Slot sizes + const FVector2D SzCornerTL(Params.BorderLeft, Params.BorderTop); + const FVector2D SzTop (Params.BodySize.X, Params.BorderTop); + const FVector2D SzCornerTR(Params.BorderRight, Params.BorderTop); + const FVector2D SzLeft (Params.BorderLeft, Params.BodySize.Y); + const FVector2D SzCenter (Params.BodySize); + const FVector2D SzRight (Params.BorderRight, Params.BodySize.Y); + const FVector2D SzCornerBL(Params.BorderLeft, Params.BorderBottom); + const FVector2D SzBottom (Params.BodySize.X, Params.BorderBottom); + const FVector2D SzCornerBR(Params.BorderRight, Params.BorderBottom); + + // Optional content widget placed over the center area + const TSharedRef CenterContent = ContentWidget.IsValid() + ? ContentWidget.ToSharedRef() + : SNullWidget::NullWidget; + + // Rebuild canvas with updated absolute positions/sizes + SAssignNew(Canvas, SCanvas) + + SCanvas::Slot().Position(PosTopLeft).Size(SzCornerTL) [ImgTopLeft.ToSharedRef()] + + SCanvas::Slot().Position(PosTop).Size(SzTop) [ImgTop.ToSharedRef()] + + SCanvas::Slot().Position(PosTopRight).Size(SzCornerTR) [ImgTopRight.ToSharedRef()] + + SCanvas::Slot().Position(PosLeft).Size(SzLeft) [ImgLeft.ToSharedRef()] + + SCanvas::Slot().Position(PosCenter).Size(SzCenter) [ImgCenter.ToSharedRef()] + + SCanvas::Slot().Position(PosRight).Size(SzRight) [ImgRight.ToSharedRef()] + + SCanvas::Slot().Position(PosBottomLeft).Size(SzCornerBL) [ImgBottomLeft.ToSharedRef()] + + SCanvas::Slot().Position(PosBottom).Size(SzBottom) [ImgBottom.ToSharedRef()] + + SCanvas::Slot().Position(PosBottomRight).Size(SzCornerBR)[ImgBottomRight.ToSharedRef()] + + SCanvas::Slot().Position(PosCenter).Size(SzCenter) [CenterContent]; + + ChildSlot + [ + SNew(SBox) + .WidthOverride(TotalSize.X) + .HeightOverride(TotalSize.Y) + [ + Canvas.ToSharedRef() + ] + ]; +} + +FVector2D SNaBorderedWindow::ClampBodySize(FVector2D Size) const +{ + return FVector2D( + FMath::Clamp(Size.X, MinBodySize.X, MaxBodySize.X), + FMath::Clamp(Size.Y, MinBodySize.Y, MaxBodySize.Y) + ); +} + +void SNaBorderedWindow::SetBodySize(FVector2D NewSize) +{ + Params.BodySize = ClampBodySize(NewSize); + RebuildLayout(); +} + +void SNaBorderedWindow::SetBorderWidths(float Top, float Bottom, float Left, float Right) +{ + Params.BorderTop = Top; + Params.BorderBottom = Bottom; + Params.BorderLeft = Left; + Params.BorderRight = Right; + RebuildLayout(); +} + +void SNaBorderedWindow::UpdateImages(const FNaBorderedWindowParams& NewParams) +{ + Params = NewParams; + BrushTopLeft.SetResourceObject(Params.ImageTopLeft); + BrushTop.SetResourceObject(Params.ImageTop); + BrushTopRight.SetResourceObject(Params.ImageTopRight); + BrushLeft.SetResourceObject(Params.ImageLeft); + BrushCenter.SetResourceObject(Params.ImageCenter); + BrushRight.SetResourceObject(Params.ImageRight); + BrushBottomLeft.SetResourceObject(Params.ImageBottomLeft); + BrushBottom.SetResourceObject(Params.ImageBottom); + BrushBottomRight.SetResourceObject(Params.ImageBottomRight); + RebuildLayout(); +} + +FReply SNaBorderedWindow::OnMouseButtonDown(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) +{ + if (MouseEvent.GetEffectingButton() == EKeys::LeftMouseButton) + { + const FVector2D LocalPos = MyGeometry.AbsoluteToLocal(MouseEvent.GetScreenSpacePosition()); + const EWindowRegion Region = GetRegionAtPosition(MyGeometry, LocalPos); + + if (Region == EWindowRegion::TopBorder) + { + bIsDragging = true; + DragStartPosition = MouseEvent.GetScreenSpacePosition(); + return FReply::Handled().CaptureMouse(SharedThis(this)); + } + else if (Region == EWindowRegion::BottomRightCorner) + { + bIsResizing = true; + DragStartPosition = LocalPos; + DragStartBodySize = Params.BodySize; + return FReply::Handled().CaptureMouse(SharedThis(this)); + } + } + + return FReply::Unhandled(); +} + +FReply SNaBorderedWindow::OnMouseButtonUp(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) +{ + if (MouseEvent.GetEffectingButton() == EKeys::LeftMouseButton) + { + if (bIsDragging || bIsResizing) + { + bIsDragging = false; + bIsResizing = false; + return FReply::Handled().ReleaseMouseCapture(); + } + } + + return FReply::Unhandled(); +} + +FReply SNaBorderedWindow::OnMouseMove(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) +{ + if (bIsResizing) + { + const FVector2D CurrentPos = MyGeometry.AbsoluteToLocal(MouseEvent.GetScreenSpacePosition()); + const FVector2D Delta = CurrentPos - DragStartPosition; + + FVector2D NewBodySize = DragStartBodySize + Delta; + Params.BodySize = ClampBodySize(NewBodySize); + RebuildLayout(); + return FReply::Handled(); + } + else if (bIsDragging) + { + // Window movement is handled by the parent container via position change + return FReply::Handled(); + } + + return FReply::Unhandled(); +} + +SNaBorderedWindow::EWindowRegion SNaBorderedWindow::GetRegionAtPosition( + const FGeometry& MyGeometry, const FVector2D& LocalPosition) const +{ + // Bottom-right corner + if (LocalPosition.X >= Params.BorderLeft + Params.BodySize.X && + LocalPosition.Y >= Params.BorderTop + Params.BodySize.Y) + { + return EWindowRegion::BottomRightCorner; + } + + // Top border (excluding corners) + if (LocalPosition.Y < Params.BorderTop && + LocalPosition.X >= Params.BorderLeft && + LocalPosition.X < Params.BorderLeft + Params.BodySize.X) + { + return EWindowRegion::TopBorder; + } + + // Center + if (LocalPosition.X >= Params.BorderLeft && + LocalPosition.X < Params.BorderLeft + Params.BodySize.X && + LocalPosition.Y >= Params.BorderTop && + LocalPosition.Y < Params.BorderTop + Params.BodySize.Y) + { + return EWindowRegion::Center; + } + + return EWindowRegion::None; +} diff --git a/Plugins/NaWidgets/Source/NaWidgets/Public/Widgets/WindowWidgets/NaBorderedWindow.h b/Plugins/NaWidgets/Source/NaWidgets/Public/Widgets/WindowWidgets/NaBorderedWindow.h new file mode 100644 index 0000000..66385a4 --- /dev/null +++ b/Plugins/NaWidgets/Source/NaWidgets/Public/Widgets/WindowWidgets/NaBorderedWindow.h @@ -0,0 +1,50 @@ +// By Sodium + +#pragma once + +#include "CoreMinimal.h" +#include "Components/Widget.h" +#include "Widgets/WindowWidgets/SNaBorderedWindow.h" +#include "NaBorderedWindow.generated.h" + +/** + * UMG wrapper for SNaBorderedWindow. + * Exposes the 9-part bordered window widget for use in UMG and Blueprints. + */ +UCLASS() +class NAWIDGETS_API UNaBorderedWindow : public UWidget +{ + GENERATED_BODY() + +public: + /** Image and size configuration for all 9 parts. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Appearance") + FNaBorderedWindowParams Params; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Sizing") + FVector2D MinBodySize = FVector2D(50.f, 50.f); + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Sizing") + FVector2D MaxBodySize = FVector2D(1000.f, 1000.f); + + /** Optional widget placed over the center body area. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Content") + UWidget* Content = nullptr; + + UFUNCTION(BlueprintCallable, Category = "Bordered Window") + void SetBodySize(FVector2D NewSize); + + UFUNCTION(BlueprintCallable, Category = "Bordered Window") + void SetBorderWidths(float Top, float Bottom, float Left, float Right); + + UFUNCTION(BlueprintCallable, Category = "Bordered Window") + FVector2D GetBodySize() const; + + TSharedPtr BorderedWindow; + +protected: + virtual TSharedRef RebuildWidget() override; + virtual void ReleaseSlateResources(bool bReleaseChildren) override; + virtual void SynchronizeProperties() override; + virtual const FText GetPaletteCategory() override; +}; diff --git a/Plugins/NaWidgets/Source/NaWidgets/Public/Widgets/WindowWidgets/SNaBorderedWindow.h b/Plugins/NaWidgets/Source/NaWidgets/Public/Widgets/WindowWidgets/SNaBorderedWindow.h index e69de29..ded8c2d 100644 --- a/Plugins/NaWidgets/Source/NaWidgets/Public/Widgets/WindowWidgets/SNaBorderedWindow.h +++ b/Plugins/NaWidgets/Source/NaWidgets/Public/Widgets/WindowWidgets/SNaBorderedWindow.h @@ -0,0 +1,152 @@ +// By Sodium + +#pragma once + +#include "CoreMinimal.h" +#include "Widgets/SCompoundWidget.h" +#include "Widgets/Images/SImage.h" +#include "Widgets/Layout/SCanvas.h" +#include "Widgets/Layout/SBox.h" +#include "Styling/SlateBrush.h" +#include "SNaBorderedWindow.generated.h" + +/** Configuration struct for all 9 image parts and size parameters of SNaBorderedWindow. */ +USTRUCT(BlueprintType) +struct NAWIDGETS_API FNaBorderedWindowParams +{ + GENERATED_BODY() + + /** Main body size (center rectangle). */ + UPROPERTY(EditAnywhere, BlueprintReadWrite) + FVector2D BodySize = FVector2D(200.f, 150.f); + + UPROPERTY(EditAnywhere, BlueprintReadWrite) + float BorderTop = 8.f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite) + float BorderBottom = 8.f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite) + float BorderLeft = 8.f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite) + float BorderRight = 8.f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite) + UObject* ImageCenter = nullptr; + + UPROPERTY(EditAnywhere, BlueprintReadWrite) + UObject* ImageTop = nullptr; + + UPROPERTY(EditAnywhere, BlueprintReadWrite) + UObject* ImageBottom = nullptr; + + UPROPERTY(EditAnywhere, BlueprintReadWrite) + UObject* ImageLeft = nullptr; + + UPROPERTY(EditAnywhere, BlueprintReadWrite) + UObject* ImageRight = nullptr; + + UPROPERTY(EditAnywhere, BlueprintReadWrite) + UObject* ImageTopLeft = nullptr; + + UPROPERTY(EditAnywhere, BlueprintReadWrite) + UObject* ImageTopRight = nullptr; + + UPROPERTY(EditAnywhere, BlueprintReadWrite) + UObject* ImageBottomLeft = nullptr; + + UPROPERTY(EditAnywhere, BlueprintReadWrite) + UObject* ImageBottomRight = nullptr; +}; + +/** + * SNaBorderedWindow is a bordered window widget composed of 9 image parts. + * Supports dragging (top border) and resizing (bottom-right corner). + */ +class NAWIDGETS_API SNaBorderedWindow : public SCompoundWidget +{ +public: + SLATE_BEGIN_ARGS(SNaBorderedWindow) + { + _Params = nullptr; + _MinBodySize = FVector2D(50.f, 50.f); + _MaxBodySize = FVector2D(1000.f, 1000.f); + } + + /** Parameters for images and initial sizes. Only used during construction. */ + SLATE_ATTRIBUTE(const FNaBorderedWindowParams*, Params) + SLATE_ATTRIBUTE(FVector2D, MinBodySize) + SLATE_ATTRIBUTE(FVector2D, MaxBodySize) + /** Optional content widget placed over the center body area. */ + SLATE_ATTRIBUTE(TSharedPtr, Content) + + SLATE_END_ARGS() + + void Construct(const FArguments& InArgs); + + /** Resize the body area, clamped to MinBodySize/MaxBodySize. */ + void SetBodySize(FVector2D NewSize); + + /** Update all four border widths and rebuild layout. */ + void SetBorderWidths(float Top, float Bottom, float Left, float Right); + + /** Replace all image resources and rebuild layout. */ + void UpdateImages(const FNaBorderedWindowParams& NewParams); + +protected: + TSharedPtr Canvas; + + /* The 9 image sub-widgets. */ + TSharedPtr ImgCenter; + TSharedPtr ImgTop; + TSharedPtr ImgBottom; + TSharedPtr ImgLeft; + TSharedPtr ImgRight; + TSharedPtr ImgTopLeft; + TSharedPtr ImgTopRight; + TSharedPtr ImgBottomLeft; + TSharedPtr ImgBottomRight; + + /* Brushes for the 9 image parts. */ + FSlateBrush BrushCenter; + FSlateBrush BrushTop; + FSlateBrush BrushBottom; + FSlateBrush BrushLeft; + FSlateBrush BrushRight; + FSlateBrush BrushTopLeft; + FSlateBrush BrushTopRight; + FSlateBrush BrushBottomLeft; + FSlateBrush BrushBottomRight; + + FNaBorderedWindowParams Params; + FVector2D MinBodySize; + FVector2D MaxBodySize; + + TSharedPtr ContentWidget; + + bool bIsDragging = false; + bool bIsResizing = false; + FVector2D DragStartPosition; + FVector2D DragStartBodySize; + + /** Rebuild the canvas layout after any size or image change. */ + void RebuildLayout(); + + /** Clamp a candidate body size to [MinBodySize, MaxBodySize]. */ + FVector2D ClampBodySize(FVector2D Size) const; + + virtual FReply OnMouseButtonDown(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override; + virtual FReply OnMouseButtonUp(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override; + virtual FReply OnMouseMove(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override; + + enum class EWindowRegion + { + None, + TopBorder, + BottomRightCorner, + Center + }; + + EWindowRegion GetRegionAtPosition(const FGeometry& MyGeometry, const FVector2D& LocalPosition) const; +}; From a387b75f84d266fbaf7e30e0fa13af63081133c1 Mon Sep 17 00:00:00 2001 From: SodiumZH <46860829+SodiumZH@users.noreply.github.com> Date: Tue, 24 Feb 2026 15:51:00 +0800 Subject: [PATCH 3/5] Fix wrong include --- .../Private/Widgets/WindowWidgets/SNaBorderedWindow.cpp | 2 +- .../NaWidgets/Public/Widgets/WindowWidgets/SNaBorderedWindow.h | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Plugins/NaWidgets/Source/NaWidgets/Private/Widgets/WindowWidgets/SNaBorderedWindow.cpp b/Plugins/NaWidgets/Source/NaWidgets/Private/Widgets/WindowWidgets/SNaBorderedWindow.cpp index 2ebb891..9eac0dd 100644 --- a/Plugins/NaWidgets/Source/NaWidgets/Private/Widgets/WindowWidgets/SNaBorderedWindow.cpp +++ b/Plugins/NaWidgets/Source/NaWidgets/Private/Widgets/WindowWidgets/SNaBorderedWindow.cpp @@ -3,7 +3,7 @@ #include "Widgets/WindowWidgets/SNaBorderedWindow.h" #include "SlateOptMacros.h" #include "Widgets/Images/SImage.h" -#include "Widgets/Layout/SCanvas.h" +#include "Widgets/SCanvas.h" #include "Widgets/Layout/SBox.h" #include "Widgets/SNullWidget.h" #include "Styling/CoreStyle.h" diff --git a/Plugins/NaWidgets/Source/NaWidgets/Public/Widgets/WindowWidgets/SNaBorderedWindow.h b/Plugins/NaWidgets/Source/NaWidgets/Public/Widgets/WindowWidgets/SNaBorderedWindow.h index ded8c2d..5d4ba1b 100644 --- a/Plugins/NaWidgets/Source/NaWidgets/Public/Widgets/WindowWidgets/SNaBorderedWindow.h +++ b/Plugins/NaWidgets/Source/NaWidgets/Public/Widgets/WindowWidgets/SNaBorderedWindow.h @@ -5,8 +5,7 @@ #include "CoreMinimal.h" #include "Widgets/SCompoundWidget.h" #include "Widgets/Images/SImage.h" -#include "Widgets/Layout/SCanvas.h" -#include "Widgets/Layout/SBox.h" +#include "Widgets/SCanvas.h" #include "Styling/SlateBrush.h" #include "SNaBorderedWindow.generated.h" From b86adddbb4480ec84197b88a0becf4faef02eca3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Feb 2026 11:40:49 +0000 Subject: [PATCH 4/5] Handle top-border drag directly in SNaBorderedWindow via render transform Co-authored-by: SodiumZH <46860829+SodiumZH@users.noreply.github.com> --- .../Private/Widgets/WindowWidgets/SNaBorderedWindow.cpp | 5 ++++- .../Public/Widgets/WindowWidgets/SNaBorderedWindow.h | 5 +++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Plugins/NaWidgets/Source/NaWidgets/Private/Widgets/WindowWidgets/SNaBorderedWindow.cpp b/Plugins/NaWidgets/Source/NaWidgets/Private/Widgets/WindowWidgets/SNaBorderedWindow.cpp index 9eac0dd..78ed94c 100644 --- a/Plugins/NaWidgets/Source/NaWidgets/Private/Widgets/WindowWidgets/SNaBorderedWindow.cpp +++ b/Plugins/NaWidgets/Source/NaWidgets/Private/Widgets/WindowWidgets/SNaBorderedWindow.cpp @@ -170,6 +170,7 @@ FReply SNaBorderedWindow::OnMouseButtonDown(const FGeometry& MyGeometry, const F { bIsDragging = true; DragStartPosition = MouseEvent.GetScreenSpacePosition(); + DragStartRenderOffset = CurrentRenderOffset; return FReply::Handled().CaptureMouse(SharedThis(this)); } else if (Region == EWindowRegion::BottomRightCorner) @@ -213,7 +214,9 @@ FReply SNaBorderedWindow::OnMouseMove(const FGeometry& MyGeometry, const FPointe } else if (bIsDragging) { - // Window movement is handled by the parent container via position change + const FVector2D Delta = MouseEvent.GetScreenSpacePosition() - DragStartPosition; + CurrentRenderOffset = DragStartRenderOffset + Delta; + SetRenderTransform(FSlateRenderTransform(CurrentRenderOffset)); return FReply::Handled(); } diff --git a/Plugins/NaWidgets/Source/NaWidgets/Public/Widgets/WindowWidgets/SNaBorderedWindow.h b/Plugins/NaWidgets/Source/NaWidgets/Public/Widgets/WindowWidgets/SNaBorderedWindow.h index 5d4ba1b..5669634 100644 --- a/Plugins/NaWidgets/Source/NaWidgets/Public/Widgets/WindowWidgets/SNaBorderedWindow.h +++ b/Plugins/NaWidgets/Source/NaWidgets/Public/Widgets/WindowWidgets/SNaBorderedWindow.h @@ -129,6 +129,11 @@ class NAWIDGETS_API SNaBorderedWindow : public SCompoundWidget FVector2D DragStartPosition; FVector2D DragStartBodySize; + /** Accumulated render-transform translation applied while dragging. */ + FVector2D CurrentRenderOffset = FVector2D::ZeroVector; + /** Snapshot of CurrentRenderOffset taken when a drag begins. */ + FVector2D DragStartRenderOffset = FVector2D::ZeroVector; + /** Rebuild the canvas layout after any size or image change. */ void RebuildLayout(); From 984f7a7073fd410453c01190e08f353ba564b522 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Feb 2026 11:48:44 +0000 Subject: [PATCH 5/5] Replace SetRenderTransform drag with real canvas-slot position update via TAttribute Co-authored-by: SodiumZH <46860829+SodiumZH@users.noreply.github.com> --- .../WindowWidgets/SNaBorderedWindow.cpp | 28 +++++++++++++------ .../Widgets/WindowWidgets/SNaBorderedWindow.h | 12 +++++--- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/Plugins/NaWidgets/Source/NaWidgets/Private/Widgets/WindowWidgets/SNaBorderedWindow.cpp b/Plugins/NaWidgets/Source/NaWidgets/Private/Widgets/WindowWidgets/SNaBorderedWindow.cpp index 78ed94c..cb41564 100644 --- a/Plugins/NaWidgets/Source/NaWidgets/Private/Widgets/WindowWidgets/SNaBorderedWindow.cpp +++ b/Plugins/NaWidgets/Source/NaWidgets/Private/Widgets/WindowWidgets/SNaBorderedWindow.cpp @@ -110,14 +110,24 @@ void SNaBorderedWindow::RebuildLayout() + SCanvas::Slot().Position(PosBottomRight).Size(SzCornerBR)[ImgBottomRight.ToSharedRef()] + SCanvas::Slot().Position(PosCenter).Size(SzCenter) [CenterContent]; + // Outer canvas: its single slot's position is driven by WindowPosition so that + // dragging changes the real layout position, moving all children with it. + SAssignNew(OuterCanvas, SCanvas) + + SCanvas::Slot() + .Position(TAttribute::CreateSP(this, &SNaBorderedWindow::GetWindowPosition)) + .Size(TotalSize) + [ + SNew(SBox) + .WidthOverride(TotalSize.X) + .HeightOverride(TotalSize.Y) + [ + Canvas.ToSharedRef() + ] + ]; + ChildSlot [ - SNew(SBox) - .WidthOverride(TotalSize.X) - .HeightOverride(TotalSize.Y) - [ - Canvas.ToSharedRef() - ] + OuterCanvas.ToSharedRef() ]; } @@ -170,7 +180,7 @@ FReply SNaBorderedWindow::OnMouseButtonDown(const FGeometry& MyGeometry, const F { bIsDragging = true; DragStartPosition = MouseEvent.GetScreenSpacePosition(); - DragStartRenderOffset = CurrentRenderOffset; + DragStartWindowPosition = WindowPosition; return FReply::Handled().CaptureMouse(SharedThis(this)); } else if (Region == EWindowRegion::BottomRightCorner) @@ -215,8 +225,8 @@ FReply SNaBorderedWindow::OnMouseMove(const FGeometry& MyGeometry, const FPointe else if (bIsDragging) { const FVector2D Delta = MouseEvent.GetScreenSpacePosition() - DragStartPosition; - CurrentRenderOffset = DragStartRenderOffset + Delta; - SetRenderTransform(FSlateRenderTransform(CurrentRenderOffset)); + WindowPosition = DragStartWindowPosition + Delta; + OuterCanvas->Invalidate(EInvalidateWidgetReason::Layout); return FReply::Handled(); } diff --git a/Plugins/NaWidgets/Source/NaWidgets/Public/Widgets/WindowWidgets/SNaBorderedWindow.h b/Plugins/NaWidgets/Source/NaWidgets/Public/Widgets/WindowWidgets/SNaBorderedWindow.h index 5669634..c692bf8 100644 --- a/Plugins/NaWidgets/Source/NaWidgets/Public/Widgets/WindowWidgets/SNaBorderedWindow.h +++ b/Plugins/NaWidgets/Source/NaWidgets/Public/Widgets/WindowWidgets/SNaBorderedWindow.h @@ -93,7 +93,11 @@ class NAWIDGETS_API SNaBorderedWindow : public SCompoundWidget /** Replace all image resources and rebuild layout. */ void UpdateImages(const FNaBorderedWindowParams& NewParams); + /** Returns the current screen-space position of the window (updated while dragging). */ + FVector2D GetWindowPosition() const { return WindowPosition; } + protected: + TSharedPtr OuterCanvas; TSharedPtr Canvas; /* The 9 image sub-widgets. */ @@ -129,10 +133,10 @@ class NAWIDGETS_API SNaBorderedWindow : public SCompoundWidget FVector2D DragStartPosition; FVector2D DragStartBodySize; - /** Accumulated render-transform translation applied while dragging. */ - FVector2D CurrentRenderOffset = FVector2D::ZeroVector; - /** Snapshot of CurrentRenderOffset taken when a drag begins. */ - FVector2D DragStartRenderOffset = FVector2D::ZeroVector; + /** Current layout position of the window within the outer canvas. */ + FVector2D WindowPosition = FVector2D::ZeroVector; + /** Snapshot of WindowPosition taken when a drag begins. */ + FVector2D DragStartWindowPosition = FVector2D::ZeroVector; /** Rebuild the canvas layout after any size or image change. */ void RebuildLayout();