Add 3-way UI mode switch: Legacy / NewSlate / UMG

Introduce a switchable UI stack so the original Slate widget stays available
as a fallback while the new CodeDesign-inspired redesign and a future UMG
Widget Blueprint implementation coexist.

- PS_Editor_MainWidget_Legacy: pre-refactor Slate widget, restored from HEAD
- PS_Editor_MainWidget: refactored floating-HUD Slate widget with style asset,
  9-slice panels/buttons, rounded brushes, icon pack, Play/Pause/Stop icons,
  spec-aligned palette and radii, File dropdown, outliner per-item icons,
  command palette
- PS_Editor_MainWidget_UMG: abstract base class exposing a Blueprint-callable
  API (SetTransformMode, SaveScenario, Simulate, Selection...) and delegates
  (OnToolChanged, OnSelectionChanged...) so a WBP deriving from it can drive
  the editor end-to-end
- APS_Editor_HUD: UIMode enum + UMGWidgetClass soft ref, picks the right
  widget at BeginPlay; separate typed accessors for each flavor

Adds UI style system (UPS_Editor_UIStyleAsset DataAsset + FPS_Editor_UIStyle
helper) with runtime PNG loading for icons and 9-slice brushes from
Content/UI/icons/ and Content/UI/unreal_9slice/.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-18 19:57:04 +02:00
parent 4181801635
commit bdceecbe56
241 changed files with 7144 additions and 458 deletions

View File

@@ -18,6 +18,7 @@
#include "PS_Editor_ChildMovable.h"
#include "PS_Editor_HUD.h"
#include "PS_Editor_MainWidget.h"
#include "PS_Editor_MainWidget_Legacy.h"
#include "Components/SplineComponent.h"
#include "DrawDebugHelpers.h"
#include "PS_Editor_ConfirmDialog.h"
@@ -1031,13 +1032,20 @@ void APS_Editor_Pawn::HandleDelete(const FInputActionValue& Value)
// 1st priority: if a timeline key is selected in the HUD, delete it and stop.
if (APS_Editor_HUD* EditorHUD = Cast<APS_Editor_HUD>(EditorPC->GetHUD()))
{
if (UPS_Editor_MainWidget* MW = EditorHUD->GetMainWidget())
if (UPS_Editor_MainWidget* MW = EditorHUD->GetMainWidget_NewSlate())
{
if (MW->TryDeleteSelectedTimelineKey())
{
return;
}
}
else if (UPS_Editor_MainWidget_Legacy* MWL = EditorHUD->GetMainWidget_Legacy())
{
if (MWL->TryDeleteSelectedTimelineKey())
{
return;
}
}
}
// In SplineEditing mode with a point selected: delete that point

View File

@@ -16,6 +16,7 @@
#include "PS_Editor_SceneLoader.h"
#include "PS_Editor_HUD.h"
#include "PS_Editor_MainWidget.h"
#include "PS_Editor_MainWidget_Legacy.h"
#include "Camera/PlayerCameraManager.h"
#include "PS_Editor_GameInstanceSubsystem.h"
#include "GameFramework/GameModeBase.h"
@@ -429,10 +430,14 @@ void APS_Editor_PlayerController::ShowLoadingScreen(bool bShow)
{
if (APS_Editor_HUD* EditorHUD = Cast<APS_Editor_HUD>(GetHUD()))
{
if (UPS_Editor_MainWidget* Widget = EditorHUD->GetMainWidget())
if (UPS_Editor_MainWidget* Widget = EditorHUD->GetMainWidget_NewSlate())
{
Widget->ShowLoadingScreen(bShow);
}
else if (UPS_Editor_MainWidget_Legacy* WidgetLegacy = EditorHUD->GetMainWidget_Legacy())
{
WidgetLegacy->ShowLoadingScreen(bShow);
}
}
}

View File

@@ -13,6 +13,7 @@ public class PS_Editor : ModuleRules
PublicIncludePaths.Add(Path.Combine(SourceDir, "GameMode"));
PublicIncludePaths.Add(Path.Combine(SourceDir, "UI"));
PublicIncludePaths.Add(Path.Combine(SourceDir, "UI", "Widgets"));
PublicIncludePaths.Add(Path.Combine(SourceDir, "UI", "Style"));
PublicIncludePaths.Add(Path.Combine(SourceDir, "Selection"));
PublicIncludePaths.Add(Path.Combine(SourceDir, "Gizmo"));
PublicIncludePaths.Add(Path.Combine(SourceDir, "Spawn"));
@@ -38,7 +39,9 @@ public class PS_Editor : ModuleRules
PrivateDependencyModuleNames.AddRange(new string[]
{
"Slate",
"SlateCore"
"SlateCore",
"ImageWrapper",
"Projects"
});
// Editor-only: CaptureAllThumbnails uses ThumbnailTools, material auto-generation uses AssetRegistry

View File

@@ -1,5 +1,7 @@
#include "PS_Editor_HUD.h"
#include "PS_Editor_MainWidget.h"
#include "PS_Editor_MainWidget_Legacy.h"
#include "PS_Editor_MainWidget_UMG.h"
#include "PS_Editor_PlayerController.h"
#include "PS_Editor_SelectionManager.h"
#include "PS_Editor_SpawnManager.h"
@@ -15,16 +17,84 @@ void APS_Editor_HUD::BeginPlay()
APlayerController* PC = GetOwningPlayerController();
if (!PC) return;
MainWidget = CreateWidget<UPS_Editor_MainWidget>(PC, UPS_Editor_MainWidget::StaticClass());
if (!MainWidget) return;
APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(PC);
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(PC))
switch (UIMode)
{
MainWidget->SetSelectionManager(EditorPC->GetSelectionManager());
MainWidget->SetSpawnManager(EditorPC->GetSpawnManager());
MainWidget->SetSceneSerializer(EditorPC->GetSceneSerializer());
case EPS_Editor_UIMode::Legacy:
{
UPS_Editor_MainWidget_Legacy* W = CreateWidget<UPS_Editor_MainWidget_Legacy>(PC, UPS_Editor_MainWidget_Legacy::StaticClass());
if (W && EditorPC)
{
W->SetSelectionManager(EditorPC->GetSelectionManager());
W->SetSpawnManager(EditorPC->GetSpawnManager());
W->SetSceneSerializer(EditorPC->GetSceneSerializer());
}
MainWidget = W;
break;
}
MainWidget->AddToViewport(0);
case EPS_Editor_UIMode::NewSlate:
{
UPS_Editor_MainWidget* W = CreateWidget<UPS_Editor_MainWidget>(PC, UPS_Editor_MainWidget::StaticClass());
if (W && EditorPC)
{
W->SetSelectionManager(EditorPC->GetSelectionManager());
W->SetSpawnManager(EditorPC->GetSpawnManager());
W->SetSceneSerializer(EditorPC->GetSceneSerializer());
}
MainWidget = W;
break;
}
case EPS_Editor_UIMode::UMG:
{
UClass* WidgetClass = UMGWidgetClass.LoadSynchronous();
if (!WidgetClass)
{
UE_LOG(LogTemp, Error, TEXT("PS_Editor_HUD: UIMode=UMG but UMGWidgetClass is not set. Falling back to NewSlate."));
UPS_Editor_MainWidget* W = CreateWidget<UPS_Editor_MainWidget>(PC, UPS_Editor_MainWidget::StaticClass());
if (W && EditorPC)
{
W->SetSelectionManager(EditorPC->GetSelectionManager());
W->SetSpawnManager(EditorPC->GetSpawnManager());
W->SetSceneSerializer(EditorPC->GetSceneSerializer());
}
MainWidget = W;
}
else
{
UPS_Editor_MainWidget_UMG* W = CreateWidget<UPS_Editor_MainWidget_UMG>(PC, WidgetClass);
if (W && EditorPC)
{
W->SetSelectionManager(EditorPC->GetSelectionManager());
W->SetSpawnManager(EditorPC->GetSpawnManager());
W->SetSceneSerializer(EditorPC->GetSceneSerializer());
}
MainWidget = W;
}
break;
}
}
if (MainWidget)
{
MainWidget->AddToViewport(0);
}
});
}
UPS_Editor_MainWidget* APS_Editor_HUD::GetMainWidget_NewSlate() const
{
return Cast<UPS_Editor_MainWidget>(MainWidget);
}
UPS_Editor_MainWidget_Legacy* APS_Editor_HUD::GetMainWidget_Legacy() const
{
return Cast<UPS_Editor_MainWidget_Legacy>(MainWidget);
}
UPS_Editor_MainWidget_UMG* APS_Editor_HUD::GetMainWidget_UMG() const
{
return Cast<UPS_Editor_MainWidget_UMG>(MainWidget);
}

View File

@@ -4,11 +4,29 @@
#include "GameFramework/HUD.h"
#include "PS_Editor_HUD.generated.h"
class UUserWidget;
class UPS_Editor_MainWidget;
class UPS_Editor_MainWidget_Legacy;
class UPS_Editor_MainWidget_UMG;
/** UI implementation flavor. */
UENUM(BlueprintType)
enum class EPS_Editor_UIMode : uint8
{
/** Original Slate-based widget (pre-redesign). Stable fallback. */
Legacy UMETA(DisplayName = "Legacy (Slate)"),
/** Refactored floating-HUD Slate widget (CodeDesign redesign, work-in-progress). */
NewSlate UMETA(DisplayName = "New (Slate)"),
/** User-authored UMG Widget Blueprint deriving from UPS_Editor_MainWidget_UMG.
* Requires UMGWidgetClass to be assigned. */
UMG UMETA(DisplayName = "UMG Widget Blueprint"),
};
/**
* HUD for the PS_Editor runtime editing mode.
* Creates and manages the main editor widget.
* Creates and manages the main editor widget. The flavor is selected via UIMode.
*/
UCLASS()
class PS_EDITOR_API APS_Editor_HUD : public AHUD
@@ -16,14 +34,32 @@ class PS_EDITOR_API APS_Editor_HUD : public AHUD
GENERATED_BODY()
public:
/** Returns the main editor widget instance. */
/** Which UI implementation to instantiate. Set via Details panel on the HUD Blueprint. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "ASTERION|PS_Editor|UI")
EPS_Editor_UIMode UIMode = EPS_Editor_UIMode::NewSlate;
/** UMG Widget Blueprint class to spawn when UIMode == UMG.
* Must derive from UPS_Editor_MainWidget_UMG. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "ASTERION|PS_Editor|UI", meta = (EditCondition = "UIMode == EPS_Editor_UIMode::UMG"))
TSoftClassPtr<UPS_Editor_MainWidget_UMG> UMGWidgetClass;
/** Returns the active main editor widget (cast as needed). */
UFUNCTION(BlueprintCallable, Category = "ASTERION|PS_Editor|UI")
UPS_Editor_MainWidget* GetMainWidget() const { return MainWidget; }
UUserWidget* GetMainWidget() const { return MainWidget; }
/** Backward-compat accessor for the refactored Slate widget (returns nullptr in Legacy/UMG modes). */
UPS_Editor_MainWidget* GetMainWidget_NewSlate() const;
/** Accessor for the legacy Slate widget (returns nullptr in NewSlate/UMG modes). */
UPS_Editor_MainWidget_Legacy* GetMainWidget_Legacy() const;
/** Accessor for the UMG widget (returns nullptr in Legacy/NewSlate modes). */
UPS_Editor_MainWidget_UMG* GetMainWidget_UMG() const;
protected:
virtual void BeginPlay() override;
private:
UPROPERTY()
TObjectPtr<UPS_Editor_MainWidget> MainWidget;
TObjectPtr<UUserWidget> MainWidget;
};

View File

@@ -0,0 +1,558 @@
#include "PS_Editor_UIStyle.h"
#include "PS_Editor_UIStyleAsset.h"
#include "Styling/CoreStyle.h"
#include "Styling/SlateBrush.h"
#include "Styling/SlateTypes.h"
#include "Widgets/Images/SImage.h"
#include "Widgets/Layout/SBorder.h"
#include "Widgets/Layout/SBox.h"
#include "Widgets/SBoxPanel.h"
#include "Widgets/Text/STextBlock.h"
#include "Engine/Texture2D.h"
#include "IImageWrapper.h"
#include "IImageWrapperModule.h"
#include "Modules/ModuleManager.h"
#include "Misc/FileHelper.h"
#include "Misc/Paths.h"
#include "HAL/FileManager.h"
#include "Interfaces/IPluginManager.h"
#include "TextureResource.h"
#include "PixelFormat.h"
namespace FPS_Editor_UIStyle
{
// Hardcoded fallbacks matching the DataAsset defaults.
namespace Defaults
{
static const FLinearColor BgPrimary = FLinearColor(0.027f, 0.039f, 0.059f, 1.0f);
static const FLinearColor BgSecondary = FLinearColor(0.043f, 0.067f, 0.094f, 1.0f);
static const FLinearColor GlassStrong = FLinearColor(0.0784f, 0.1216f, 0.1804f, 0.88f);
static const FLinearColor GlassSoft = FLinearColor(0.0549f, 0.0863f, 0.1333f, 0.72f);
static const FLinearColor GlassBorder = FLinearColor(1.0f, 1.0f, 1.0f, 0.10f);
static const FLinearColor GlassBorderStrong = FLinearColor(1.0f, 1.0f, 1.0f, 0.18f);
static const FLinearColor TextPrimary = FLinearColor(0.902f, 0.925f, 0.953f, 1.0f);
static const FLinearColor TextMuted = FLinearColor(0.722f, 0.769f, 0.824f, 1.0f);
static const FLinearColor TextDim = FLinearColor(0.502f, 0.569f, 0.647f, 1.0f);
static const FLinearColor TextDisabled = FLinearColor(0.353f, 0.416f, 0.494f, 1.0f);
static const FLinearColor Accent = FLinearColor(1.0f, 0.416f, 0.239f, 1.0f);
static const FLinearColor AccentSoft = FLinearColor(1.0f, 0.416f, 0.239f, 0.14f);
static const FLinearColor AccentRing = FLinearColor(1.0f, 0.416f, 0.239f, 0.35f);
static const FLinearColor Ok = FLinearColor(0.243f, 0.812f, 0.557f, 1.0f);
static const FLinearColor Warn = FLinearColor(0.961f, 0.725f, 0.267f, 1.0f);
static const FLinearColor Err = FLinearColor(1.0f, 0.353f, 0.431f, 1.0f);
static const FLinearColor Info = FLinearColor(0.290f, 0.659f, 1.0f, 1.0f);
static const FLinearColor AxisX = FLinearColor(1.0f, 0.302f, 0.369f, 1.0f);
static const FLinearColor AxisY = FLinearColor(0.290f, 0.839f, 0.427f, 1.0f);
static const FLinearColor AxisZ = FLinearColor(0.290f, 0.659f, 1.0f, 1.0f);
static constexpr int32 FontSizeXSmall = 9;
static constexpr int32 FontSizeSmall = 10;
static constexpr int32 FontSizeBody = 11;
static constexpr int32 FontSizeHeading = 13;
static constexpr int32 FontSizeLarge = 16;
static constexpr float RadiusXS = 3.0f;
static constexpr float RadiusS = 5.0f;
static constexpr float RadiusM = 7.0f;
static constexpr float RadiusL = 9.0f;
static constexpr float PadPanel = 10.0f;
static constexpr float PadRow = 6.0f;
}
#define PSUI_COLOR_ACCESSOR(Name) \
FLinearColor Name(const UPS_Editor_UIStyleAsset* S) { return S ? S->Name : Defaults::Name; }
PSUI_COLOR_ACCESSOR(BgPrimary)
PSUI_COLOR_ACCESSOR(BgSecondary)
PSUI_COLOR_ACCESSOR(GlassStrong)
PSUI_COLOR_ACCESSOR(GlassSoft)
PSUI_COLOR_ACCESSOR(GlassBorder)
PSUI_COLOR_ACCESSOR(GlassBorderStrong)
PSUI_COLOR_ACCESSOR(TextPrimary)
PSUI_COLOR_ACCESSOR(TextMuted)
PSUI_COLOR_ACCESSOR(TextDim)
PSUI_COLOR_ACCESSOR(TextDisabled)
PSUI_COLOR_ACCESSOR(Accent)
PSUI_COLOR_ACCESSOR(AccentSoft)
PSUI_COLOR_ACCESSOR(AccentRing)
PSUI_COLOR_ACCESSOR(Ok)
PSUI_COLOR_ACCESSOR(Warn)
PSUI_COLOR_ACCESSOR(Err)
PSUI_COLOR_ACCESSOR(Info)
PSUI_COLOR_ACCESSOR(AxisX)
PSUI_COLOR_ACCESSOR(AxisY)
PSUI_COLOR_ACCESSOR(AxisZ)
#undef PSUI_COLOR_ACCESSOR
FSlateFontInfo BodyFont(const UPS_Editor_UIStyleAsset* S)
{
return FCoreStyle::GetDefaultFontStyle("Regular", S ? S->FontSizeBody : Defaults::FontSizeBody);
}
FSlateFontInfo BodyFontBold(const UPS_Editor_UIStyleAsset* S)
{
return FCoreStyle::GetDefaultFontStyle("Bold", S ? S->FontSizeBody : Defaults::FontSizeBody);
}
FSlateFontInfo SmallFont(const UPS_Editor_UIStyleAsset* S)
{
return FCoreStyle::GetDefaultFontStyle("Regular", S ? S->FontSizeSmall : Defaults::FontSizeSmall);
}
FSlateFontInfo HeadingFont(const UPS_Editor_UIStyleAsset* S)
{
return FCoreStyle::GetDefaultFontStyle("Regular", S ? S->FontSizeHeading : Defaults::FontSizeHeading);
}
FSlateFontInfo HeadingFontBold(const UPS_Editor_UIStyleAsset* S)
{
return FCoreStyle::GetDefaultFontStyle("Bold", S ? S->FontSizeHeading : Defaults::FontSizeHeading);
}
FSlateFontInfo LargeFont(const UPS_Editor_UIStyleAsset* S)
{
return FCoreStyle::GetDefaultFontStyle("Bold", S ? S->FontSizeLarge : Defaults::FontSizeLarge);
}
FSlateFontInfo MonoFont(const UPS_Editor_UIStyleAsset* S)
{
// Slate default fonts do not ship a mono family. Use Regular at the mono size and rely on tabular digits where possible.
return FCoreStyle::GetDefaultFontStyle("Mono", S ? S->FontSizeBody : Defaults::FontSizeBody);
}
float RadiusXS(const UPS_Editor_UIStyleAsset* S) { return S ? S->RadiusXS : Defaults::RadiusXS; }
float RadiusS(const UPS_Editor_UIStyleAsset* S) { return S ? S->RadiusS : Defaults::RadiusS; }
float RadiusM(const UPS_Editor_UIStyleAsset* S) { return S ? S->RadiusM : Defaults::RadiusM; }
float RadiusL(const UPS_Editor_UIStyleAsset* S) { return S ? S->RadiusL : Defaults::RadiusL; }
FMargin PadPanel(const UPS_Editor_UIStyleAsset* S)
{
const float V = S ? S->PadPanel : Defaults::PadPanel;
return FMargin(V);
}
FMargin PadRow(const UPS_Editor_UIStyleAsset* S)
{
const float V = S ? S->PadRow : Defaults::PadRow;
return FMargin(V);
}
TSharedRef<SWidget> MakeGlassPanel(const UPS_Editor_UIStyleAsset* S, bool bStrong, TSharedRef<SWidget> Content, FMargin Padding)
{
const FLinearColor Fill = bStrong ? GlassStrong(S) : GlassSoft(S);
return SNew(SBorder)
.BorderImage(FCoreStyle::Get().GetBrush("WhiteBrush"))
.BorderBackgroundColor(Fill)
.Padding(Padding)
[
Content
];
}
// Internal widget that owns its brush — ensures the brush outlives the SImage.
class SPSEditorIconWidget : public SCompoundWidget
{
public:
SLATE_BEGIN_ARGS(SPSEditorIconWidget)
: _Texture(nullptr)
, _Size(16.f)
, _Color(FLinearColor::White)
, _FollowForeground(false)
, _Fallback(TEXT("?"))
{}
SLATE_ARGUMENT(UTexture2D*, Texture)
SLATE_ARGUMENT(float, Size)
SLATE_ARGUMENT(FLinearColor, Color)
SLATE_ARGUMENT(bool, FollowForeground)
SLATE_ARGUMENT(const TCHAR*, Fallback)
SLATE_END_ARGS()
void Construct(const FArguments& InArgs)
{
const float Size = InArgs._Size;
const FSlateColor Tint = InArgs._FollowForeground
? FSlateColor::UseForeground()
: FSlateColor(InArgs._Color);
if (InArgs._Texture)
{
IconBrush.SetResourceObject(InArgs._Texture);
IconBrush.ImageSize = FVector2D(Size, Size);
IconBrush.DrawAs = ESlateBrushDrawType::Image;
ChildSlot
[
SNew(SBox)
.WidthOverride(Size)
.HeightOverride(Size)
[
SNew(SImage)
.Image(&IconBrush)
.ColorAndOpacity(Tint)
]
];
}
else
{
const TCHAR* Glyph = InArgs._Fallback ? InArgs._Fallback : TEXT("?");
ChildSlot
[
SNew(SBox)
.WidthOverride(Size)
.HeightOverride(Size)
.HAlign(HAlign_Center)
.VAlign(VAlign_Center)
[
SNew(STextBlock)
.Text(FText::FromString(Glyph))
.Font(FCoreStyle::GetDefaultFontStyle("Regular", FMath::Max(8, FMath::RoundToInt(Size * 0.72f))))
.ColorAndOpacity(Tint)
]
];
}
}
private:
FSlateBrush IconBrush;
};
TSharedRef<SWidget> MakeIcon(const UPS_Editor_UIStyleAsset* S, EPS_Editor_Icon Icon, float Size, FLinearColor Tint)
{
FPS_Editor_IconData Data = S ? S->GetIcon(Icon) : FPS_Editor_IconData{};
return SNew(SPSEditorIconWidget)
.Texture(Data.Texture)
.Size(Size)
.Color(Tint)
.FollowForeground(false)
.Fallback(Data.Fallback);
}
TSharedRef<SWidget> MakeIconFollowForeground(const UPS_Editor_UIStyleAsset* S, EPS_Editor_Icon Icon, float Size)
{
FPS_Editor_IconData Data = S ? S->GetIcon(Icon) : FPS_Editor_IconData{};
return SNew(SPSEditorIconWidget)
.Texture(Data.Texture)
.Size(Size)
.FollowForeground(true)
.Fallback(Data.Fallback);
}
// ---- Brush / style factories ----
static FLinearColor ResolveToneFill(const UPS_Editor_UIStyleAsset* S, EPanelTone Tone)
{
switch (Tone)
{
case EPanelTone::GlassStrong: return GlassStrong(S);
case EPanelTone::GlassSoft: return GlassSoft(S);
// Pill uses white tint so callers can recolor via BorderBackgroundColor.
case EPanelTone::Pill: return FLinearColor::White;
case EPanelTone::FieldFill: return FLinearColor(0.0f, 0.0f, 0.0f, 0.28f);
case EPanelTone::AccentFill: return Accent(S);
case EPanelTone::AccentSoftFill: return AccentSoft(S);
}
return GlassStrong(S);
}
void InitPanelBrush(FSlateBrush& Out, const UPS_Editor_UIStyleAsset* S, EPanelTone Tone, float CornerRadius)
{
Out.DrawAs = ESlateBrushDrawType::RoundedBox;
Out.TintColor = FSlateColor(ResolveToneFill(S, Tone));
Out.OutlineSettings.CornerRadii = FVector4(CornerRadius, CornerRadius, CornerRadius, CornerRadius);
Out.OutlineSettings.RoundingType = ESlateBrushRoundingType::FixedRadius;
// Pill / Accent / Field have no outline; panels have a subtle glass border.
const bool bWantsBorder = (Tone == EPanelTone::GlassStrong || Tone == EPanelTone::GlassSoft);
Out.OutlineSettings.Width = bWantsBorder ? 1.0f : 0.0f;
Out.OutlineSettings.Color = FSlateColor(Tone == EPanelTone::AccentFill
? AccentRing(S)
: GlassBorder(S));
Out.Margin = FMargin(0.0f);
}
void InitFlatButtonStyle(FButtonStyle& Out, const UPS_Editor_UIStyleAsset* S, float CornerRadius)
{
auto Make = [CornerRadius](FLinearColor Fill, FLinearColor Border)
{
FSlateBrush B;
B.DrawAs = ESlateBrushDrawType::RoundedBox;
B.TintColor = FSlateColor(Fill);
B.OutlineSettings.CornerRadii = FVector4(CornerRadius, CornerRadius, CornerRadius, CornerRadius);
B.OutlineSettings.RoundingType = ESlateBrushRoundingType::FixedRadius;
B.OutlineSettings.Width = Border.A > 0.f ? 1.f : 0.f;
B.OutlineSettings.Color = FSlateColor(Border);
return B;
};
const FLinearColor Transparent = FLinearColor(0, 0, 0, 0);
const FLinearColor HoverFill = FLinearColor(1, 1, 1, 0.06f);
const FLinearColor PressedFill = FLinearColor(1, 1, 1, 0.10f);
Out.Normal = Make(Transparent, Transparent);
Out.Hovered = Make(HoverFill, Transparent);
Out.Pressed = Make(PressedFill, Transparent);
Out.Disabled = Make(Transparent, Transparent);
Out.NormalPadding = FMargin(8, 4);
Out.PressedPadding = FMargin(8, 4);
Out.NormalForeground = FSlateColor(TextPrimary(S));
Out.HoveredForeground = FSlateColor(TextPrimary(S));
Out.PressedForeground = FSlateColor(TextPrimary(S));
Out.DisabledForeground = FSlateColor(TextDisabled(S));
}
void InitAccentButtonStyle(FButtonStyle& Out, const UPS_Editor_UIStyleAsset* S, float CornerRadius)
{
auto Make = [CornerRadius](FLinearColor Fill)
{
FSlateBrush B;
B.DrawAs = ESlateBrushDrawType::RoundedBox;
B.TintColor = FSlateColor(Fill);
B.OutlineSettings.CornerRadii = FVector4(CornerRadius, CornerRadius, CornerRadius, CornerRadius);
B.OutlineSettings.RoundingType = ESlateBrushRoundingType::FixedRadius;
B.OutlineSettings.Width = 0.0f;
return B;
};
const FLinearColor AccentC = Accent(S);
const FLinearColor AccentHov(FMath::Min(AccentC.R * 1.08f, 1.f), FMath::Min(AccentC.G * 1.08f, 1.f), FMath::Min(AccentC.B * 1.08f, 1.f), AccentC.A);
const FLinearColor AccentDim(AccentC.R * 0.88f, AccentC.G * 0.88f, AccentC.B * 0.88f, AccentC.A);
Out.Normal = Make(AccentC);
Out.Hovered = Make(AccentHov);
Out.Pressed = Make(AccentDim);
Out.Disabled = Make(AccentSoft(S));
Out.NormalPadding = FMargin(10, 4);
Out.PressedPadding = FMargin(10, 4);
// Dark text on accent (design uses #1a0a00).
const FLinearColor DarkOnAccent(0.10f, 0.04f, 0.0f, 1.0f);
Out.NormalForeground = FSlateColor(DarkOnAccent);
Out.HoveredForeground = FSlateColor(DarkOnAccent);
Out.PressedForeground = FSlateColor(DarkOnAccent);
Out.DisabledForeground = FSlateColor(TextDisabled(S));
}
void InitTextBoxStyle(FEditableTextBoxStyle& Out, const UPS_Editor_UIStyleAsset* S, float CornerRadius)
{
auto Make = [CornerRadius](FLinearColor Fill, FLinearColor Border)
{
FSlateBrush B;
B.DrawAs = ESlateBrushDrawType::RoundedBox;
B.TintColor = FSlateColor(Fill);
B.OutlineSettings.CornerRadii = FVector4(CornerRadius, CornerRadius, CornerRadius, CornerRadius);
B.OutlineSettings.RoundingType = ESlateBrushRoundingType::FixedRadius;
B.OutlineSettings.Width = Border.A > 0.f ? 1.f : 0.f;
B.OutlineSettings.Color = FSlateColor(Border);
return B;
};
const FLinearColor Fill = FLinearColor(0, 0, 0, 0.28f);
const FLinearColor Border = GlassBorder(S);
const FLinearColor Focus = AccentRing(S);
Out.BackgroundImageNormal = Make(Fill, Border);
Out.BackgroundImageHovered = Make(Fill, GlassBorderStrong(S));
Out.BackgroundImageFocused = Make(Fill, Focus);
Out.BackgroundImageReadOnly = Make(Fill, Border);
Out.Padding = FMargin(6, 3);
Out.ForegroundColor = FSlateColor(TextPrimary(S));
Out.TextStyle.ColorAndOpacity = FSlateColor(TextPrimary(S));
Out.TextStyle.Font = BodyFont(S);
}
// ---- Runtime PNG loader ----
static UTexture2D* LoadTextureFromPng(const FString& FilePath)
{
TArray<uint8> RawFileData;
if (!FFileHelper::LoadFileToArray(RawFileData, *FilePath))
{
return nullptr;
}
IImageWrapperModule& ImageWrapperModule = FModuleManager::LoadModuleChecked<IImageWrapperModule>(FName("ImageWrapper"));
TSharedPtr<IImageWrapper> ImageWrapper = ImageWrapperModule.CreateImageWrapper(EImageFormat::PNG);
if (!ImageWrapper.IsValid() || !ImageWrapper->SetCompressed(RawFileData.GetData(), RawFileData.Num()))
{
return nullptr;
}
TArray<uint8> UncompressedBGRA;
if (!ImageWrapper->GetRaw(ERGBFormat::BGRA, 8, UncompressedBGRA))
{
return nullptr;
}
const int32 Width = ImageWrapper->GetWidth();
const int32 Height = ImageWrapper->GetHeight();
if (Width <= 0 || Height <= 0) return nullptr;
UTexture2D* Texture = UTexture2D::CreateTransient(Width, Height, PF_B8G8R8A8);
if (!Texture) return nullptr;
void* TextureData = Texture->GetPlatformData()->Mips[0].BulkData.Lock(LOCK_READ_WRITE);
FMemory::Memcpy(TextureData, UncompressedBGRA.GetData(), UncompressedBGRA.Num());
Texture->GetPlatformData()->Mips[0].BulkData.Unlock();
Texture->SRGB = false;
Texture->CompressionSettings = TC_EditorIcon;
Texture->Filter = TextureFilter::TF_Trilinear;
Texture->UpdateResource();
return Texture;
}
static FString ResolvePluginIconsDir()
{
TSharedPtr<IPlugin> Plugin = IPluginManager::Get().FindPlugin(TEXT("PS_Editor"));
if (Plugin.IsValid())
{
return Plugin->GetContentDir() / TEXT("UI/icons");
}
// Fallback: assume relative to project
return FPaths::ProjectContentDir() / TEXT("UI/icons");
}
struct FIconSlot
{
EPS_Editor_Icon Icon;
const TCHAR* FileStem;
TObjectPtr<UTexture2D> UPS_Editor_UIStyleAsset::* Member;
};
TSharedPtr<FSlateBrush> Load9SliceBrushFromPluginContent(const FString& RelativePath)
{
TSharedPtr<IPlugin> Plugin = IPluginManager::Get().FindPlugin(TEXT("PS_Editor"));
const FString BaseDir = Plugin.IsValid() ? Plugin->GetContentDir() : (FPaths::ProjectContentDir());
const FString Path = BaseDir / TEXT("UI/unreal_9slice") / RelativePath;
if (!IFileManager::Get().FileExists(*Path)) return nullptr;
UTexture2D* Tex = LoadTextureFromPng(Path);
if (!Tex) return nullptr;
Tex->AddToRoot();
TSharedRef<FSlateBrush> Brush = MakeShared<FSlateBrush>();
Brush->SetResourceObject(Tex);
Brush->ImageSize = FVector2D(Tex->GetSizeX(), Tex->GetSizeY());
Brush->DrawAs = ESlateBrushDrawType::Box;
// PNG is 192x192 with rounded shape inside a 64px corner. We slice tighter than the README's
// 0.333 so small widgets (topbar height ~32-40px, buttons ~24-28px) still render all corners.
// 0.25 = 48px corners, which is above the 24px rounded radius but keeps the slice inside the
// transparent buffer surrounding the shape.
Brush->Margin = FMargin(0.25f, 0.25f, 0.25f, 0.25f);
Brush->TintColor = FSlateColor(FLinearColor::White);
return Brush;
}
bool InitNineSliceButtonStyle(FButtonStyle& Out, const UPS_Editor_UIStyleAsset* S,
const FString& NormalStem, const FString& HoverStem, const FString& PressedStem,
FLinearColor NormalTint, FLinearColor HoverTint, FLinearColor PressedTint)
{
TSharedPtr<FSlateBrush> NormalBrush = Load9SliceBrushFromPluginContent(NormalStem + TEXT(".png"));
TSharedPtr<FSlateBrush> HoverBrush = Load9SliceBrushFromPluginContent(HoverStem + TEXT(".png"));
TSharedPtr<FSlateBrush> PressedBrush = Load9SliceBrushFromPluginContent(PressedStem + TEXT(".png"));
if (!NormalBrush.IsValid() || !HoverBrush.IsValid() || !PressedBrush.IsValid()) return false;
NormalBrush->TintColor = FSlateColor(NormalTint);
HoverBrush->TintColor = FSlateColor(HoverTint);
PressedBrush->TintColor = FSlateColor(PressedTint);
Out.Normal = *NormalBrush;
Out.Hovered = *HoverBrush;
Out.Pressed = *PressedBrush;
Out.Disabled = *NormalBrush;
Out.Disabled.TintColor = FSlateColor(FLinearColor(NormalTint.R, NormalTint.G, NormalTint.B, NormalTint.A * 0.35f));
Out.NormalPadding = FMargin(10, 4);
Out.PressedPadding = FMargin(10, 4);
// State-differentiated foregrounds — any child widget using FSlateColor::UseForeground()
// (typical for icons and labels inside buttons) will automatically flip color per state.
Out.NormalForeground = FSlateColor(TextMuted(S));
Out.HoveredForeground = FSlateColor(TextPrimary(S));
Out.PressedForeground = FSlateColor(Accent(S));
Out.DisabledForeground = FSlateColor(TextDisabled(S));
return true;
}
int32 PopulateIconsFromPluginContent(UPS_Editor_UIStyleAsset* StyleAsset)
{
if (!StyleAsset) return 0;
const FString IconsDir = ResolvePluginIconsDir();
// Maps each icon slot to the PNG filename the user provided.
static const FIconSlot Slots[] = {
{ EPS_Editor_Icon::Move, TEXT("Move"), &UPS_Editor_UIStyleAsset::IconMove },
{ EPS_Editor_Icon::Rotate, TEXT("Rotate"), &UPS_Editor_UIStyleAsset::IconRotate },
{ EPS_Editor_Icon::Scale, TEXT("Scale"), &UPS_Editor_UIStyleAsset::IconScale },
{ EPS_Editor_Icon::Snap, TEXT("Snap"), &UPS_Editor_UIStyleAsset::IconSnap },
{ EPS_Editor_Icon::Grid, TEXT("Grid"), &UPS_Editor_UIStyleAsset::IconGrid },
{ EPS_Editor_Icon::World, TEXT("Globe"), &UPS_Editor_UIStyleAsset::IconWorld },
{ EPS_Editor_Icon::Focus, TEXT("Target"), &UPS_Editor_UIStyleAsset::IconFocus },
{ EPS_Editor_Icon::Play, TEXT("Play"), &UPS_Editor_UIStyleAsset::IconPlay },
{ EPS_Editor_Icon::Pause, TEXT("Pause"), &UPS_Editor_UIStyleAsset::IconPause },
{ EPS_Editor_Icon::Stop, TEXT("Stop"), &UPS_Editor_UIStyleAsset::IconStop },
{ EPS_Editor_Icon::Record, TEXT("Record"), &UPS_Editor_UIStyleAsset::IconRecord },
{ EPS_Editor_Icon::SkipBack, TEXT("SkipBack"), &UPS_Editor_UIStyleAsset::IconSkipBack },
{ EPS_Editor_Icon::SkipForward, TEXT("SkipFwd"), &UPS_Editor_UIStyleAsset::IconSkipForward },
{ EPS_Editor_Icon::Simulate, TEXT("Zap"), &UPS_Editor_UIStyleAsset::IconSimulate },
{ EPS_Editor_Icon::Eye, TEXT("Eye"), &UPS_Editor_UIStyleAsset::IconEye },
{ EPS_Editor_Icon::EyeOff, TEXT("EyeOff"), &UPS_Editor_UIStyleAsset::IconEyeOff },
{ EPS_Editor_Icon::Lock, TEXT("Lock"), &UPS_Editor_UIStyleAsset::IconLock },
{ EPS_Editor_Icon::Unlock, TEXT("Unlock"), &UPS_Editor_UIStyleAsset::IconUnlock },
{ EPS_Editor_Icon::Sidebar, TEXT("Sidebar"), &UPS_Editor_UIStyleAsset::IconSidebar },
{ EPS_Editor_Icon::PanelRight, TEXT("PanelRight"), &UPS_Editor_UIStyleAsset::IconPanelRight },
{ EPS_Editor_Icon::PanelBottom, TEXT("PanelBottom"), &UPS_Editor_UIStyleAsset::IconPanelBottom },
{ EPS_Editor_Icon::Save, TEXT("Save"), &UPS_Editor_UIStyleAsset::IconSave },
{ EPS_Editor_Icon::Load, TEXT("Folder"), &UPS_Editor_UIStyleAsset::IconLoad },
{ EPS_Editor_Icon::Folder, TEXT("Folder"), &UPS_Editor_UIStyleAsset::IconFolder },
{ EPS_Editor_Icon::NewFile, TEXT("Plus"), &UPS_Editor_UIStyleAsset::IconNew },
{ EPS_Editor_Icon::Plus, TEXT("Plus"), &UPS_Editor_UIStyleAsset::IconPlus },
{ EPS_Editor_Icon::Minus, TEXT("Minus"), &UPS_Editor_UIStyleAsset::IconMinus },
{ EPS_Editor_Icon::Check, TEXT("Check"), &UPS_Editor_UIStyleAsset::IconCheck },
{ EPS_Editor_Icon::Search, TEXT("Search"), &UPS_Editor_UIStyleAsset::IconSearch },
{ EPS_Editor_Icon::Settings, TEXT("Settings"), &UPS_Editor_UIStyleAsset::IconSettings },
{ EPS_Editor_Icon::ChevronDown, TEXT("ChevronDown"), &UPS_Editor_UIStyleAsset::IconChevronDown },
{ EPS_Editor_Icon::ChevronRight, TEXT("ChevronRight"), &UPS_Editor_UIStyleAsset::IconChevronRight },
{ EPS_Editor_Icon::ChevronLeft, TEXT("ChevronLeft"), &UPS_Editor_UIStyleAsset::IconChevronLeft },
{ EPS_Editor_Icon::Command, TEXT("Command"), &UPS_Editor_UIStyleAsset::IconCommand },
{ EPS_Editor_Icon::More, TEXT("More"), &UPS_Editor_UIStyleAsset::IconMore },
{ EPS_Editor_Icon::Help, TEXT("Help"), &UPS_Editor_UIStyleAsset::IconHelp },
{ EPS_Editor_Icon::Bell, TEXT("Bell"), &UPS_Editor_UIStyleAsset::IconBell },
{ EPS_Editor_Icon::Download, TEXT("Download"), &UPS_Editor_UIStyleAsset::IconDownload },
{ EPS_Editor_Icon::Character, TEXT("User"), &UPS_Editor_UIStyleAsset::IconCharacter },
{ EPS_Editor_Icon::SplineObj, TEXT("Spline"), &UPS_Editor_UIStyleAsset::IconSpline },
{ EPS_Editor_Icon::Prop, TEXT("Box"), &UPS_Editor_UIStyleAsset::IconProp },
{ EPS_Editor_Icon::Light, TEXT("Lightbulb"), &UPS_Editor_UIStyleAsset::IconLight },
{ EPS_Editor_Icon::Camera, TEXT("Camera"), &UPS_Editor_UIStyleAsset::IconCamera },
{ EPS_Editor_Icon::Trigger, TEXT("Zap"), &UPS_Editor_UIStyleAsset::IconTrigger },
{ EPS_Editor_Icon::Layers, TEXT("Layers"), &UPS_Editor_UIStyleAsset::IconLayers },
{ EPS_Editor_Icon::Film, TEXT("Film"), &UPS_Editor_UIStyleAsset::IconFilm },
{ EPS_Editor_Icon::Box, TEXT("Box"), &UPS_Editor_UIStyleAsset::IconBox },
{ EPS_Editor_Icon::Flag, TEXT("Flag"), &UPS_Editor_UIStyleAsset::IconFlag },
{ EPS_Editor_Icon::Map, TEXT("Map"), &UPS_Editor_UIStyleAsset::IconMap },
{ EPS_Editor_Icon::BrandMark, TEXT("Diamond"), &UPS_Editor_UIStyleAsset::IconBrandMark },
};
int32 Loaded = 0;
for (const FIconSlot& Slot : Slots)
{
if (StyleAsset->*(Slot.Member)) continue; // user already assigned it, keep it
const FString Path = IconsDir / FString::Printf(TEXT("%s.png"), Slot.FileStem);
if (!IFileManager::Get().FileExists(*Path)) continue;
if (UTexture2D* Tex = LoadTextureFromPng(Path))
{
Tex->AddToRoot(); // transient, keep alive while plugin runs
StyleAsset->*(Slot.Member) = Tex;
Loaded++;
}
}
return Loaded;
}
}

View File

@@ -0,0 +1,110 @@
#pragma once
#include "CoreMinimal.h"
#include "Fonts/SlateFontInfo.h"
#include "Styling/SlateColor.h"
#include "Styling/SlateTypes.h"
#include "Widgets/SCompoundWidget.h"
#include "PS_Editor_UIStyleAsset.h"
class UPS_Editor_UIStyleAsset;
class SWidget;
struct FSlateBrush;
struct FButtonStyle;
struct FEditableTextBoxStyle;
/**
* Style accessors for PS_Editor UI. Every function takes a (nullable) style asset
* and returns a value ready to feed to Slate. If the asset is null, hardcoded
* fallbacks matching the DataAsset defaults are returned so the UI never breaks.
*/
namespace FPS_Editor_UIStyle
{
// ---- Colors ----
PS_EDITOR_API FLinearColor BgPrimary(const UPS_Editor_UIStyleAsset* S);
PS_EDITOR_API FLinearColor BgSecondary(const UPS_Editor_UIStyleAsset* S);
PS_EDITOR_API FLinearColor GlassStrong(const UPS_Editor_UIStyleAsset* S);
PS_EDITOR_API FLinearColor GlassSoft(const UPS_Editor_UIStyleAsset* S);
PS_EDITOR_API FLinearColor GlassBorder(const UPS_Editor_UIStyleAsset* S);
PS_EDITOR_API FLinearColor GlassBorderStrong(const UPS_Editor_UIStyleAsset* S);
PS_EDITOR_API FLinearColor TextPrimary(const UPS_Editor_UIStyleAsset* S);
PS_EDITOR_API FLinearColor TextMuted(const UPS_Editor_UIStyleAsset* S);
PS_EDITOR_API FLinearColor TextDim(const UPS_Editor_UIStyleAsset* S);
PS_EDITOR_API FLinearColor TextDisabled(const UPS_Editor_UIStyleAsset* S);
PS_EDITOR_API FLinearColor Accent(const UPS_Editor_UIStyleAsset* S);
PS_EDITOR_API FLinearColor AccentSoft(const UPS_Editor_UIStyleAsset* S);
PS_EDITOR_API FLinearColor AccentRing(const UPS_Editor_UIStyleAsset* S);
PS_EDITOR_API FLinearColor Ok(const UPS_Editor_UIStyleAsset* S);
PS_EDITOR_API FLinearColor Warn(const UPS_Editor_UIStyleAsset* S);
PS_EDITOR_API FLinearColor Err(const UPS_Editor_UIStyleAsset* S);
PS_EDITOR_API FLinearColor Info(const UPS_Editor_UIStyleAsset* S);
PS_EDITOR_API FLinearColor AxisX(const UPS_Editor_UIStyleAsset* S);
PS_EDITOR_API FLinearColor AxisY(const UPS_Editor_UIStyleAsset* S);
PS_EDITOR_API FLinearColor AxisZ(const UPS_Editor_UIStyleAsset* S);
// ---- Fonts ----
PS_EDITOR_API FSlateFontInfo BodyFont(const UPS_Editor_UIStyleAsset* S);
PS_EDITOR_API FSlateFontInfo BodyFontBold(const UPS_Editor_UIStyleAsset* S);
PS_EDITOR_API FSlateFontInfo SmallFont(const UPS_Editor_UIStyleAsset* S);
PS_EDITOR_API FSlateFontInfo HeadingFont(const UPS_Editor_UIStyleAsset* S);
PS_EDITOR_API FSlateFontInfo HeadingFontBold(const UPS_Editor_UIStyleAsset* S);
PS_EDITOR_API FSlateFontInfo LargeFont(const UPS_Editor_UIStyleAsset* S);
PS_EDITOR_API FSlateFontInfo MonoFont(const UPS_Editor_UIStyleAsset* S);
// ---- Layout ----
PS_EDITOR_API float RadiusXS(const UPS_Editor_UIStyleAsset* S);
PS_EDITOR_API float RadiusS(const UPS_Editor_UIStyleAsset* S);
PS_EDITOR_API float RadiusM(const UPS_Editor_UIStyleAsset* S);
PS_EDITOR_API float RadiusL(const UPS_Editor_UIStyleAsset* S);
PS_EDITOR_API FMargin PadPanel(const UPS_Editor_UIStyleAsset* S);
PS_EDITOR_API FMargin PadRow(const UPS_Editor_UIStyleAsset* S);
// ---- Composite helpers ----
/** Build an SBorder with glass fill + border, wrapping Content. */
PS_EDITOR_API TSharedRef<SWidget> MakeGlassPanel(const UPS_Editor_UIStyleAsset* S, bool bStrong, TSharedRef<SWidget> Content, FMargin Padding = FMargin(12.f));
/** Build an icon widget with a fixed tint. Returns SImage if the slot has a texture; otherwise falls back to an STextBlock with a unicode glyph. */
PS_EDITOR_API TSharedRef<SWidget> MakeIcon(const UPS_Editor_UIStyleAsset* S, EPS_Editor_Icon Icon, float Size = 16.f, FLinearColor Tint = FLinearColor::White);
/** Build an icon widget that inherits the parent button's foreground color (for normal/hover/pressed state tinting). */
PS_EDITOR_API TSharedRef<SWidget> MakeIconFollowForeground(const UPS_Editor_UIStyleAsset* S, EPS_Editor_Icon Icon, float Size = 16.f);
// ---- Brush / style factories ----
// These construct FSlateBrush / FButtonStyle / FEditableTextBoxStyle objects. The caller is
// responsible for keeping them alive (typically as widget members) because Slate only stores
// raw pointers and never copies the brush contents.
enum class EPanelTone : uint8 { GlassStrong, GlassSoft, Pill, FieldFill, AccentFill, AccentSoftFill };
/** Rounded-box brush matching the design tokens. Width/corner radius from the style tokens. */
PS_EDITOR_API void InitPanelBrush(FSlateBrush& Out, const UPS_Editor_UIStyleAsset* S, EPanelTone Tone, float CornerRadius = 10.f);
/** Flat button style without chrome. Hover/pressed use subtle accent tint. */
PS_EDITOR_API void InitFlatButtonStyle(FButtonStyle& Out, const UPS_Editor_UIStyleAsset* S, float CornerRadius = 6.f);
/** Accent button style (orange fill, dark text). */
PS_EDITOR_API void InitAccentButtonStyle(FButtonStyle& Out, const UPS_Editor_UIStyleAsset* S, float CornerRadius = 6.f);
/** Minimal editable text box style with thin rounded border. */
PS_EDITOR_API void InitTextBoxStyle(FEditableTextBoxStyle& Out, const UPS_Editor_UIStyleAsset* S, float CornerRadius = 4.f);
/** Scan <PluginContent>/UI/icons/ for PNG files and populate all unassigned icon slots on the asset.
* Files are expected as "<IconName>.png" (matching the EPS_Editor_Icon enum name, e.g. "Move.png").
* Already-assigned slots are preserved. Returns the number of icons loaded. */
PS_EDITOR_API int32 PopulateIconsFromPluginContent(UPS_Editor_UIStyleAsset* StyleAsset);
/** Load a PNG as a 9-slice Box brush (Margin 0.333 on all sides). Returns nullptr if the file is missing. */
PS_EDITOR_API TSharedPtr<FSlateBrush> Load9SliceBrushFromPluginContent(const FString& RelativePath);
/** Initialize a button style from three 9-slice PNGs (normal/hover/pressed). Falls back to flat
* procedural style if any file is missing. NormalSlice/HoverSlice/PressedSlice are paths relative
* to Content/UI/unreal_9slice/. */
PS_EDITOR_API bool InitNineSliceButtonStyle(FButtonStyle& Out, const UPS_Editor_UIStyleAsset* S,
const FString& NormalStem, const FString& HoverStem, const FString& PressedStem,
FLinearColor NormalTint = FLinearColor::White,
FLinearColor HoverTint = FLinearColor::White,
FLinearColor PressedTint = FLinearColor::White);
}

View File

@@ -0,0 +1,72 @@
#include "PS_Editor_UIStyleAsset.h"
FPS_Editor_IconData UPS_Editor_UIStyleAsset::GetIcon(EPS_Editor_Icon Icon) const
{
switch (Icon)
{
case EPS_Editor_Icon::Move: return { IconMove, TEXT("\u2194") }; // ↔
case EPS_Editor_Icon::Rotate: return { IconRotate, TEXT("\u21BB") }; // ↻
case EPS_Editor_Icon::Scale: return { IconScale, TEXT("\u2922") }; // ⤢
case EPS_Editor_Icon::Snap: return { IconSnap, TEXT("\u29C7") }; // ⧇
case EPS_Editor_Icon::Grid: return { IconGrid, TEXT("\u25A6") }; // ▦
case EPS_Editor_Icon::World: return { IconWorld, TEXT("\u25CE") }; // ◎
case EPS_Editor_Icon::Focus: return { IconFocus, TEXT("\u2316") }; // ⌖
case EPS_Editor_Icon::Play: return { IconPlay, TEXT("\u25B6") }; // ▶
case EPS_Editor_Icon::Pause: return { IconPause, TEXT("\u23F8") }; // ⏸
case EPS_Editor_Icon::Stop: return { IconStop, TEXT("\u25A0") }; // ■
case EPS_Editor_Icon::Record: return { IconRecord, TEXT("\u25CF") }; // ●
case EPS_Editor_Icon::SkipBack: return { IconSkipBack, TEXT("\u23EE") }; // ⏮
case EPS_Editor_Icon::SkipForward: return { IconSkipForward, TEXT("\u23ED") }; // ⏭
case EPS_Editor_Icon::Simulate: return { IconSimulate, TEXT("\u26A1") }; // ⚡
case EPS_Editor_Icon::Eye: return { IconEye, TEXT("\u25C9") }; // ◉
case EPS_Editor_Icon::EyeOff: return { IconEyeOff, TEXT("\u00D8") }; // Ø
case EPS_Editor_Icon::Lock: return { IconLock, TEXT("\u2B23") }; // ⬣
case EPS_Editor_Icon::Unlock: return { IconUnlock, TEXT("\u2B21") }; // ⬡
case EPS_Editor_Icon::Sidebar: return { IconSidebar, TEXT("\u258C") }; // ▌
case EPS_Editor_Icon::PanelRight: return { IconPanelRight, TEXT("\u2590") }; // ▐
case EPS_Editor_Icon::PanelBottom: return { IconPanelBottom, TEXT("\u2584") }; // ▄
case EPS_Editor_Icon::Save: return { IconSave, TEXT("\u2193") }; // ↓
case EPS_Editor_Icon::Load: return { IconLoad, TEXT("\u2191") }; // ↑
case EPS_Editor_Icon::Folder: return { IconFolder, TEXT("\u25B1") }; // ▱
case EPS_Editor_Icon::NewFile: return { IconNew, TEXT("\u2795") }; //
case EPS_Editor_Icon::Plus: return { IconPlus, TEXT("+") };
case EPS_Editor_Icon::Minus: return { IconMinus, TEXT("\u2212") }; //
case EPS_Editor_Icon::Delete: return { IconDelete, TEXT("\u2716") }; // ✖
case EPS_Editor_Icon::Duplicate: return { IconDuplicate, TEXT("\u29C9") }; // ⧉
case EPS_Editor_Icon::Undo: return { IconUndo, TEXT("\u21B6") }; // ↶
case EPS_Editor_Icon::Redo: return { IconRedo, TEXT("\u21B7") }; // ↷
case EPS_Editor_Icon::Check: return { IconCheck, TEXT("\u2713") }; // ✓
case EPS_Editor_Icon::Close: return { IconClose, TEXT("\u00D7") }; // ×
case EPS_Editor_Icon::Search: return { IconSearch, TEXT("\u2315") }; // ⌕
case EPS_Editor_Icon::Settings: return { IconSettings, TEXT("\u2699") }; // ⚙
case EPS_Editor_Icon::ChevronDown: return { IconChevronDown, TEXT("\u25BE") }; // ▾
case EPS_Editor_Icon::ChevronRight: return { IconChevronRight, TEXT("\u25B8") }; // ▸
case EPS_Editor_Icon::ChevronLeft: return { IconChevronLeft, TEXT("\u25C2") }; // ◂
case EPS_Editor_Icon::Command: return { IconCommand, TEXT("\u2318") }; // ⌘
case EPS_Editor_Icon::More: return { IconMore, TEXT("\u22EF") }; // ⋯
case EPS_Editor_Icon::Help: return { IconHelp, TEXT("?") };
case EPS_Editor_Icon::Bell: return { IconBell, TEXT("!") };
case EPS_Editor_Icon::Download: return { IconDownload, TEXT("v") };
case EPS_Editor_Icon::Character: return { IconCharacter, TEXT("\u263A") }; // ☺
case EPS_Editor_Icon::SplineObj: return { IconSpline, TEXT("\u223F") }; // ∿
case EPS_Editor_Icon::Prop: return { IconProp, TEXT("\u25A3") }; // ▣
case EPS_Editor_Icon::Light: return { IconLight, TEXT("\u2600") }; // ☀
case EPS_Editor_Icon::Camera: return { IconCamera, TEXT("\u25A4") }; // ▤
case EPS_Editor_Icon::Trigger: return { IconTrigger, TEXT("\u26A1") }; // ⚡
case EPS_Editor_Icon::Layers: return { IconLayers, TEXT("\u2263") }; // ≣
case EPS_Editor_Icon::Film: return { IconFilm, TEXT("\u25A9") }; // ▩
case EPS_Editor_Icon::Box: return { IconBox, TEXT("\u25A2") }; // ▢
case EPS_Editor_Icon::Flag: return { IconFlag, TEXT("F") };
case EPS_Editor_Icon::Map: return { IconMap, TEXT("M") };
case EPS_Editor_Icon::BrandMark: return { IconBrandMark, TEXT("\u25C6") }; // ◆
default: return { nullptr, TEXT("?") };
}
}

View File

@@ -0,0 +1,235 @@
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "Engine/Texture2D.h"
#include "PS_Editor_UIStyleAsset.generated.h"
/** Icon slot identifier. Used by FPS_Editor_UIStyle::MakeIcon() to fetch a texture from the style asset. */
UENUM(BlueprintType)
enum class EPS_Editor_Icon : uint8
{
None = 0,
// Transform tools
Move, Rotate, Scale, Snap, Grid, World, Focus,
// Playback
Play, Pause, Stop, Record, SkipBack, SkipForward, Simulate,
// View / visibility
Eye, EyeOff, Lock, Unlock, Sidebar, PanelRight, PanelBottom,
// File
Save, Load, Folder, NewFile,
// Edit
Plus, Minus, Delete, Duplicate, Undo, Redo, Check, Close,
// Chrome / Nav
Search, Settings, ChevronDown, ChevronRight, ChevronLeft, Command, More, Help, Bell, Download,
// Object types
Character, SplineObj, Prop, Light, Camera, Trigger, Layers, Film, Box, Flag, Map,
// Brand
BrandMark,
};
/** Bundle returned by GetIcon: texture (may be null) + unicode fallback glyph. */
struct FPS_Editor_IconData
{
UTexture2D* Texture = nullptr;
const TCHAR* Fallback = TEXT("?");
};
/**
* UI style configuration for PS_Editor. Create a DataAsset instance at
* /PS_Editor/Style/DA_PS_Editor_UIStyle and assign icon textures + tweak colors.
* If absent at runtime, the FPS_Editor_UIStyle helpers fall back to hardcoded values
* that match the defaults defined here.
*
* Icons specs:
* - PNG 32x32 with alpha
* - White stroke (#ffffff) on transparent background, stroke width 1.5-2 px (lucide-style)
* - Import settings: sRGB OFF, MipGen NoMipMaps, Texture Group UI, Compression UserInterface2D
*/
UCLASS(BlueprintType)
class PS_EDITOR_API UPS_Editor_UIStyleAsset : public UDataAsset
{
GENERATED_BODY()
public:
// ==================== COLORS ====================
UPROPERTY(EditAnywhere, Category = "Colors|Background")
FLinearColor BgPrimary = FLinearColor(0.027f, 0.039f, 0.059f, 1.0f); // #070a0f
UPROPERTY(EditAnywhere, Category = "Colors|Background")
FLinearColor BgSecondary = FLinearColor(0.043f, 0.067f, 0.094f, 1.0f); // #0b1118
UPROPERTY(EditAnywhere, Category = "Colors|Glass")
FLinearColor GlassStrong = FLinearColor(0.0784f, 0.1216f, 0.1804f, 0.88f); // #141f2e @ 88%
UPROPERTY(EditAnywhere, Category = "Colors|Glass")
FLinearColor GlassSoft = FLinearColor(0.0549f, 0.0863f, 0.1333f, 0.72f); // #0e1622 @ 72%
UPROPERTY(EditAnywhere, Category = "Colors|Glass")
FLinearColor GlassBorder = FLinearColor(1.0f, 1.0f, 1.0f, 0.10f);
UPROPERTY(EditAnywhere, Category = "Colors|Glass")
FLinearColor GlassBorderStrong = FLinearColor(1.0f, 1.0f, 1.0f, 0.18f);
UPROPERTY(EditAnywhere, Category = "Colors|Text")
FLinearColor TextPrimary = FLinearColor(0.902f, 0.925f, 0.953f, 1.0f); // #e6ecf3
UPROPERTY(EditAnywhere, Category = "Colors|Text")
FLinearColor TextMuted = FLinearColor(0.722f, 0.769f, 0.824f, 1.0f); // #b8c4d2
UPROPERTY(EditAnywhere, Category = "Colors|Text")
FLinearColor TextDim = FLinearColor(0.502f, 0.569f, 0.647f, 1.0f); // #8091a5
UPROPERTY(EditAnywhere, Category = "Colors|Text")
FLinearColor TextDisabled = FLinearColor(0.353f, 0.416f, 0.494f, 1.0f); // #5a6a7e
UPROPERTY(EditAnywhere, Category = "Colors|Accent")
FLinearColor Accent = FLinearColor(1.0f, 0.416f, 0.239f, 1.0f); // #ff6a3d
UPROPERTY(EditAnywhere, Category = "Colors|Accent")
FLinearColor Accent2 = FLinearColor(1.0f, 0.627f, 0.420f, 1.0f); // #ffa06b
UPROPERTY(EditAnywhere, Category = "Colors|Accent")
FLinearColor AccentSoft = FLinearColor(1.0f, 0.416f, 0.239f, 0.14f);
UPROPERTY(EditAnywhere, Category = "Colors|Accent")
FLinearColor AccentRing = FLinearColor(1.0f, 0.416f, 0.239f, 0.35f);
UPROPERTY(EditAnywhere, Category = "Colors|Status")
FLinearColor Ok = FLinearColor(0.243f, 0.812f, 0.557f, 1.0f); // #3ecf8e
UPROPERTY(EditAnywhere, Category = "Colors|Status")
FLinearColor Warn = FLinearColor(0.961f, 0.725f, 0.267f, 1.0f); // #f5b944
UPROPERTY(EditAnywhere, Category = "Colors|Status")
FLinearColor Err = FLinearColor(1.0f, 0.353f, 0.431f, 1.0f); // #ff5a6e
UPROPERTY(EditAnywhere, Category = "Colors|Status")
FLinearColor Info = FLinearColor(0.290f, 0.659f, 1.0f, 1.0f); // #4aa8ff (z)
UPROPERTY(EditAnywhere, Category = "Colors|Axis")
FLinearColor AxisX = FLinearColor(1.0f, 0.302f, 0.369f, 1.0f); // #ff4d5e
UPROPERTY(EditAnywhere, Category = "Colors|Axis")
FLinearColor AxisY = FLinearColor(0.290f, 0.839f, 0.427f, 1.0f); // #4ad66d
UPROPERTY(EditAnywhere, Category = "Colors|Axis")
FLinearColor AxisZ = FLinearColor(0.290f, 0.659f, 1.0f, 1.0f); // #4aa8ff
// ==================== FONTS ====================
UPROPERTY(EditAnywhere, Category = "Fonts")
int32 FontSizeXSmall = 9;
UPROPERTY(EditAnywhere, Category = "Fonts")
int32 FontSizeSmall = 10;
UPROPERTY(EditAnywhere, Category = "Fonts")
int32 FontSizeBody = 11;
UPROPERTY(EditAnywhere, Category = "Fonts")
int32 FontSizeHeading = 13;
UPROPERTY(EditAnywhere, Category = "Fonts")
int32 FontSizeLarge = 16;
// ==================== LAYOUT ====================
// Spec radii (halved from v1 → matches the 9-slice radii)
UPROPERTY(EditAnywhere, Category = "Layout")
float RadiusXS = 3.0f;
UPROPERTY(EditAnywhere, Category = "Layout")
float RadiusS = 5.0f;
UPROPERTY(EditAnywhere, Category = "Layout")
float RadiusM = 7.0f;
UPROPERTY(EditAnywhere, Category = "Layout")
float RadiusL = 9.0f;
UPROPERTY(EditAnywhere, Category = "Layout")
float RadiusXL = 12.0f;
UPROPERTY(EditAnywhere, Category = "Layout")
float PadPanel = 12.0f;
UPROPERTY(EditAnywhere, Category = "Layout")
float PadRow = 6.0f;
UPROPERTY(EditAnywhere, Category = "Layout")
float PadTight = 4.0f;
// ==================== ICONS ====================
UPROPERTY(EditAnywhere, Category = "Icons|Transform") TObjectPtr<UTexture2D> IconMove;
UPROPERTY(EditAnywhere, Category = "Icons|Transform") TObjectPtr<UTexture2D> IconRotate;
UPROPERTY(EditAnywhere, Category = "Icons|Transform") TObjectPtr<UTexture2D> IconScale;
UPROPERTY(EditAnywhere, Category = "Icons|Transform") TObjectPtr<UTexture2D> IconSnap;
UPROPERTY(EditAnywhere, Category = "Icons|Transform") TObjectPtr<UTexture2D> IconGrid;
UPROPERTY(EditAnywhere, Category = "Icons|Transform") TObjectPtr<UTexture2D> IconWorld;
UPROPERTY(EditAnywhere, Category = "Icons|Transform") TObjectPtr<UTexture2D> IconFocus;
UPROPERTY(EditAnywhere, Category = "Icons|Playback") TObjectPtr<UTexture2D> IconPlay;
UPROPERTY(EditAnywhere, Category = "Icons|Playback") TObjectPtr<UTexture2D> IconPause;
UPROPERTY(EditAnywhere, Category = "Icons|Playback") TObjectPtr<UTexture2D> IconStop;
UPROPERTY(EditAnywhere, Category = "Icons|Playback") TObjectPtr<UTexture2D> IconRecord;
UPROPERTY(EditAnywhere, Category = "Icons|Playback") TObjectPtr<UTexture2D> IconSkipBack;
UPROPERTY(EditAnywhere, Category = "Icons|Playback") TObjectPtr<UTexture2D> IconSkipForward;
UPROPERTY(EditAnywhere, Category = "Icons|Playback") TObjectPtr<UTexture2D> IconSimulate;
UPROPERTY(EditAnywhere, Category = "Icons|View") TObjectPtr<UTexture2D> IconEye;
UPROPERTY(EditAnywhere, Category = "Icons|View") TObjectPtr<UTexture2D> IconEyeOff;
UPROPERTY(EditAnywhere, Category = "Icons|View") TObjectPtr<UTexture2D> IconLock;
UPROPERTY(EditAnywhere, Category = "Icons|View") TObjectPtr<UTexture2D> IconUnlock;
UPROPERTY(EditAnywhere, Category = "Icons|View") TObjectPtr<UTexture2D> IconSidebar;
UPROPERTY(EditAnywhere, Category = "Icons|View") TObjectPtr<UTexture2D> IconPanelRight;
UPROPERTY(EditAnywhere, Category = "Icons|View") TObjectPtr<UTexture2D> IconPanelBottom;
UPROPERTY(EditAnywhere, Category = "Icons|File") TObjectPtr<UTexture2D> IconSave;
UPROPERTY(EditAnywhere, Category = "Icons|File") TObjectPtr<UTexture2D> IconLoad;
UPROPERTY(EditAnywhere, Category = "Icons|File") TObjectPtr<UTexture2D> IconFolder;
UPROPERTY(EditAnywhere, Category = "Icons|File") TObjectPtr<UTexture2D> IconNew;
UPROPERTY(EditAnywhere, Category = "Icons|Edit") TObjectPtr<UTexture2D> IconPlus;
UPROPERTY(EditAnywhere, Category = "Icons|Edit") TObjectPtr<UTexture2D> IconMinus;
UPROPERTY(EditAnywhere, Category = "Icons|Edit") TObjectPtr<UTexture2D> IconDelete;
UPROPERTY(EditAnywhere, Category = "Icons|Edit") TObjectPtr<UTexture2D> IconDuplicate;
UPROPERTY(EditAnywhere, Category = "Icons|Edit") TObjectPtr<UTexture2D> IconUndo;
UPROPERTY(EditAnywhere, Category = "Icons|Edit") TObjectPtr<UTexture2D> IconRedo;
UPROPERTY(EditAnywhere, Category = "Icons|Edit") TObjectPtr<UTexture2D> IconCheck;
UPROPERTY(EditAnywhere, Category = "Icons|Edit") TObjectPtr<UTexture2D> IconClose;
UPROPERTY(EditAnywhere, Category = "Icons|Nav") TObjectPtr<UTexture2D> IconSearch;
UPROPERTY(EditAnywhere, Category = "Icons|Nav") TObjectPtr<UTexture2D> IconSettings;
UPROPERTY(EditAnywhere, Category = "Icons|Nav") TObjectPtr<UTexture2D> IconChevronDown;
UPROPERTY(EditAnywhere, Category = "Icons|Nav") TObjectPtr<UTexture2D> IconChevronRight;
UPROPERTY(EditAnywhere, Category = "Icons|Nav") TObjectPtr<UTexture2D> IconChevronLeft;
UPROPERTY(EditAnywhere, Category = "Icons|Nav") TObjectPtr<UTexture2D> IconCommand;
UPROPERTY(EditAnywhere, Category = "Icons|Nav") TObjectPtr<UTexture2D> IconMore;
UPROPERTY(EditAnywhere, Category = "Icons|Nav") TObjectPtr<UTexture2D> IconHelp;
UPROPERTY(EditAnywhere, Category = "Icons|Nav") TObjectPtr<UTexture2D> IconBell;
UPROPERTY(EditAnywhere, Category = "Icons|Nav") TObjectPtr<UTexture2D> IconDownload;
UPROPERTY(EditAnywhere, Category = "Icons|Objects") TObjectPtr<UTexture2D> IconCharacter;
UPROPERTY(EditAnywhere, Category = "Icons|Objects") TObjectPtr<UTexture2D> IconSpline;
UPROPERTY(EditAnywhere, Category = "Icons|Objects") TObjectPtr<UTexture2D> IconProp;
UPROPERTY(EditAnywhere, Category = "Icons|Objects") TObjectPtr<UTexture2D> IconLight;
UPROPERTY(EditAnywhere, Category = "Icons|Objects") TObjectPtr<UTexture2D> IconCamera;
UPROPERTY(EditAnywhere, Category = "Icons|Objects") TObjectPtr<UTexture2D> IconTrigger;
UPROPERTY(EditAnywhere, Category = "Icons|Objects") TObjectPtr<UTexture2D> IconLayers;
UPROPERTY(EditAnywhere, Category = "Icons|Objects") TObjectPtr<UTexture2D> IconFilm;
UPROPERTY(EditAnywhere, Category = "Icons|Objects") TObjectPtr<UTexture2D> IconBox;
UPROPERTY(EditAnywhere, Category = "Icons|Objects") TObjectPtr<UTexture2D> IconFlag;
UPROPERTY(EditAnywhere, Category = "Icons|Objects") TObjectPtr<UTexture2D> IconMap;
UPROPERTY(EditAnywhere, Category = "Icons|Brand") TObjectPtr<UTexture2D> IconBrandMark;
/** Resolve an icon slot to texture + unicode fallback glyph. Never returns nullptr struct. */
FPS_Editor_IconData GetIcon(EPS_Editor_Icon Icon) const;
};

View File

@@ -3,11 +3,13 @@
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "PS_Editor_SelectionTypes.h"
#include "Styling/SlateTypes.h"
#include "PS_Editor_MainWidget.generated.h"
class UPS_Editor_SelectionManager;
class UPS_Editor_SpawnManager;
class UPS_Editor_SceneSerializer;
class UPS_Editor_UIStyleAsset;
/** Widget state for a single dynamic property row in the property panel. */
struct FPS_Editor_PropertyRowWidgets
@@ -63,6 +65,26 @@ private:
UPROPERTY()
TWeakObjectPtr<UPS_Editor_SceneSerializer> SceneSerializer;
/** UI style configuration. Resolved once at RebuildWidget entry via soft path.
* Nullable — all FPS_Editor_UIStyle accessors fall back to hardcoded defaults. */
UPROPERTY()
TObjectPtr<UPS_Editor_UIStyleAsset> StyleAsset;
// Cached brushes / styles built from the style asset. Lifetime-owned by the widget so
// Slate can reference them via raw pointer in Build* methods.
TSharedPtr<FSlateBrush> BrushGlassStrong;
TSharedPtr<FSlateBrush> BrushGlassSoft;
TSharedPtr<FSlateBrush> BrushPill;
TSharedPtr<FSlateBrush> BrushField;
TSharedPtr<FSlateBrush> BrushAccent;
TSharedPtr<FSlateBrush> BrushAccentSoft;
TSharedPtr<FButtonStyle> StyleFlatButton;
TSharedPtr<FButtonStyle> StyleAccentButton;
TSharedPtr<FEditableTextBoxStyle> StyleTextBox;
void EnsureStyleResolved();
void EnsureStyleBrushes();
// ---- Slate widget refs for dynamic updates ----
TSharedPtr<STextBlock> ModeText;
TSharedPtr<STextBlock> SelectionInfoText;
@@ -144,6 +166,30 @@ private:
// Loading screen
TSharedPtr<SWidget> LoadingOverlay;
// HUDs (new design)
TSharedPtr<STextBlock> StatsFpsText;
TSharedPtr<STextBlock> StatsTriText;
TSharedPtr<STextBlock> StatsDrawText;
float SmoothedFps = 60.0f;
TSharedPtr<STextBlock> HintText;
TSharedPtr<SWidget> SplineEditBar;
TSharedPtr<SWidget> ToastPanel;
TSharedPtr<STextBlock> ToastTextBlock;
FTimerHandle ToastTimerHandle;
TSharedPtr<SWidget> CmdPalettePanel;
TSharedPtr<SEditableTextBox> CmdPaletteInput;
TSharedPtr<SVerticalBox> CmdPaletteList;
bool bCmdPaletteOpen = false;
TSharedPtr<STextBlock> TopbarSceneName;
TSharedPtr<class SBorder> TopbarStatusDot;
TSharedPtr<SWidget> TopbarSimulateButton;
TSharedPtr<STextBlock> TopbarSimulateText;
TSharedPtr<class SMenuAnchor> FileMenuAnchor;
public:
/** Show/hide the loading overlay. */
void ShowLoadingScreen(bool bShow);
@@ -155,6 +201,26 @@ public:
TSharedRef<SWidget> BuildSceneButtons();
TSharedRef<SWidget> BuildOutliner();
TSharedRef<SWidget> BuildTimelinePanel();
TSharedRef<SWidget> BuildTopbar();
TSharedRef<SWidget> BuildFileMenuContent();
TSharedRef<SWidget> BuildViewCube();
TSharedRef<SWidget> BuildStatsHUD();
TSharedRef<SWidget> BuildHintHUD();
TSharedRef<SWidget> BuildSplineEditBar();
TSharedRef<SWidget> BuildToast();
TSharedRef<SWidget> BuildCommandPalette();
/** Show a transient toast message. Hidden automatically after ~1.8s. */
void ShowToast(const FString& Message, FLinearColor Color = FLinearColor::White);
/** Toggle visibility of the command palette overlay. */
void ToggleCommandPalette();
void RefreshCommandPaletteList(const FString& Filter);
void ExecuteCommand(const FString& CommandId);
/** Keyboard handler (captures Ctrl+K). */
virtual FReply NativeOnKeyDown(const FGeometry& InGeometry, const FKeyEvent& InKeyEvent) override;
virtual bool NativeSupportsKeyboardFocus() const override { return true; }
void RefreshOutliner();
void PopulateSceneList();
void PopulateSpawnCatalog();

View File

@@ -0,0 +1,183 @@
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "PS_Editor_SelectionTypes.h"
#include "PS_Editor_MainWidget_Legacy.generated.h"
class UPS_Editor_SelectionManager;
class UPS_Editor_SpawnManager;
class UPS_Editor_SceneSerializer;
/** Widget state for a single dynamic property row in the property panel. */
struct FPS_Editor_PropertyRowWidgetsLegacy
{
FString ComponentName;
FString PropertyName;
FString DisplayName;
FString Key; // "ComponentName.PropertyName"
FProperty* CachedProperty = nullptr;
// Widget refs (only relevant ones are used based on property type)
TSharedPtr<SEditableTextBox> SingleField; // float, double, int32, FString
TSharedPtr<SCheckBox> BoolField; // bool
TSharedPtr<SEditableTextBox> VecX, VecY, VecZ; // FVector, FRotator
TSharedPtr<SEditableTextBox> ColR, ColG, ColB, ColA; // FLinearColor
// Enum support
TSharedPtr<SComboBox<TSharedPtr<FString>>> EnumCombo;
TSharedPtr<STextBlock> EnumComboText;
TSharedPtr<TArray<TSharedPtr<FString>>> EnumOptions; // Heap-allocated so OptionsSource pointer survives Row copy
UEnum* CachedEnum = nullptr;
};
/**
* Main editor widget for PS_Editor.
* Contains: title bar, transform mode toolbar, properties panel, help bar.
* Built entirely in C++ using Slate.
*/
UCLASS()
class PS_EDITOR_API UPS_Editor_MainWidget_Legacy : public UUserWidget
{
GENERATED_BODY()
public:
/** Set manager references. Must be called after construction. */
void SetSelectionManager(UPS_Editor_SelectionManager* InSelectionManager);
void SetSpawnManager(UPS_Editor_SpawnManager* InSpawnManager);
void SetSceneSerializer(UPS_Editor_SceneSerializer* InSerializer);
protected:
virtual TSharedRef<SWidget> RebuildWidget() override;
virtual void NativeConstruct() override;
virtual void NativeDestruct() override;
virtual void NativeTick(const FGeometry& MyGeometry, float InDeltaTime) override;
private:
UPROPERTY()
TWeakObjectPtr<UPS_Editor_SelectionManager> SelectionManager;
UPROPERTY()
TWeakObjectPtr<UPS_Editor_SpawnManager> SpawnManager;
UPROPERTY()
TWeakObjectPtr<UPS_Editor_SceneSerializer> SceneSerializer;
// ---- Slate widget refs for dynamic updates ----
TSharedPtr<STextBlock> ModeText;
TSharedPtr<STextBlock> SelectionInfoText;
// Toolbar refs
TSharedPtr<STextBlock> SpaceText;
TSharedPtr<STextBlock> SnapText;
TSharedPtr<SEditableTextBox> SnapSizeField;
// Spawn catalog
TSharedPtr<SVerticalBox> SpawnListContainer;
// Scene save/load
TSharedPtr<SEditableTextBox> SaveNameField;
TSharedPtr<SVerticalBox> SceneListContainer;
TArray<TSharedPtr<FString>> BaseLevelOptions;
TSharedPtr<STextBlock> BaseLevelButtonText;
FString CurrentLevelFilter;
/** Maps DisplayName → full MapPath for base levels (from MasterCatalog). */
TMap<FString, FString> BaseLevelPathMap;
/** Maps DisplayName → additional sublevels to load (from MasterCatalog). */
TMap<FString, TArray<FString>> BaseLevelExtraSublevels;
/** Maps DisplayName → Thumbnail texture for base levels. */
TMap<FString, TObjectPtr<UTexture2D>> BaseLevelThumbnails;
TArray<TSharedPtr<FSlateBrush>> CachedBrushes;
// BaseLevel selection popup
TSharedPtr<SWidget> BaseLevelPopup;
TSharedPtr<SVerticalBox> BaseLevelGridContainer;
FString PendingBaseLevelSelection;
TMap<FString, TSharedPtr<SBorder>> BaseLevelCardBorders;
void OpenBaseLevelPopup();
void UpdateBaseLevelHighlight();
void ConfirmBaseLevelSelection();
void CancelBaseLevelPopup();
// Transform fields
TSharedPtr<SEditableTextBox> LocX, LocY, LocZ;
TSharedPtr<SEditableTextBox> RotX, RotY, RotZ;
TSharedPtr<SEditableTextBox> ScaX, ScaY, ScaZ;
// Dynamic property editing
TSharedPtr<SVerticalBox> DynamicPropertiesContainer;
TArray<FPS_Editor_PropertyRowWidgetsLegacy> DynamicPropertyRows;
TWeakObjectPtr<AActor> LastSelectedActor;
// Outliner
TSharedPtr<SVerticalBox> OutlinerListContainer;
int32 LastOutlinerActorCount = -1;
// Bottom notification bar
TSharedPtr<STextBlock> HelpBarText;
TSharedPtr<SWidget> SplineFinishButton;
TSharedPtr<SWidget> SplineEditButton;
TSharedPtr<SWidget> SplineDeletePointButton;
TSharedPtr<SWidget> ChildEditButton;
// Simulate button
TSharedPtr<STextBlock> SimulateButtonText;
// Timeline panel
TSharedPtr<SWidget> TimelinePanel;
TSharedPtr<class SSlider> TimelineSlider;
TSharedPtr<STextBlock> TimelineTimeLabel;
TSharedPtr<STextBlock> TimelinePlayButtonText;
TSharedPtr<SEditableTextBox> TimelineDurationBox;
TSharedPtr<SVerticalBox> TimelineTracksContainer;
bool bTimelineVisible = false;
FDelegateHandle TimelineChangedHandle;
/** Currently selected timeline key (updated on click). -1 means no selection. */
int32 SelectedTimelineTrackIdx = -1;
int32 SelectedTimelineKeyIdx = -1;
// Close confirmation
TSharedPtr<SWidget> CloseConfirmOverlay;
bool bSceneDirty = false;
int32 LastKnownSpawnedCount = 0;
// Loading screen
TSharedPtr<SWidget> LoadingOverlay;
public:
/** Show/hide the loading overlay. */
void ShowLoadingScreen(bool bShow);
// ---- Helpers ----
TSharedRef<SWidget> BuildToolbar();
TSharedRef<SWidget> BuildPropertiesPanel();
TSharedRef<SWidget> BuildSpawnCatalog();
TSharedRef<SWidget> BuildSceneButtons();
TSharedRef<SWidget> BuildOutliner();
TSharedRef<SWidget> BuildTimelinePanel();
void RefreshOutliner();
void PopulateSceneList();
void PopulateSpawnCatalog();
void RefreshPropertiesFromSelection();
void ApplyTransformFromUI();
void RebuildDynamicProperties();
void RefreshDynamicPropertyValues();
void ApplyPropertyFromUI(int32 RowIndex);
void AddKeyForRow(int32 RowIndex);
void ToggleTimelineVisible();
void RebuildTimelineTracksList();
void RefreshTimelineUI();
/** Try to delete the currently selected timeline key. Returns true if one was deleted.
* Called by the Pawn's global Delete action before it falls back to actor deletion. */
bool TryDeleteSelectedTimelineKey();
static UObject* FindPropertyTarget(AActor* Actor, const FString& ComponentName);
FString FloatToString(float Value) const;
void OnModeButtonClicked(EPS_Editor_TransformMode Mode);
void OnCloseClicked();
void ExecuteClose(bool bSave);
/** Whether we are currently refreshing UI from selection (avoid feedback loop). */
bool bIsRefreshing = false;
};

View File

@@ -0,0 +1,237 @@
#include "PS_Editor_MainWidget_UMG.h"
#include "PS_Editor_SelectionManager.h"
#include "PS_Editor_SpawnManager.h"
#include "PS_Editor_SceneSerializer.h"
#include "PS_Editor_PlayerController.h"
#include "GameFramework/PlayerController.h"
void UPS_Editor_MainWidget_UMG::SetSelectionManager(UPS_Editor_SelectionManager* InSelectionManager)
{
SelectionManager = InSelectionManager;
}
void UPS_Editor_MainWidget_UMG::SetSpawnManager(UPS_Editor_SpawnManager* InSpawnManager)
{
SpawnManager = InSpawnManager;
}
void UPS_Editor_MainWidget_UMG::SetSceneSerializer(UPS_Editor_SceneSerializer* InSerializer)
{
SceneSerializer = InSerializer;
}
void UPS_Editor_MainWidget_UMG::NativeConstruct()
{
Super::NativeConstruct();
if (UPS_Editor_SelectionManager* SM = SelectionManager.Get())
{
SelectionChangedHandle = SM->OnSelectionChanged.AddLambda([this]()
{
OnSelectionChanged.Broadcast();
});
}
}
void UPS_Editor_MainWidget_UMG::NativeDestruct()
{
if (SelectionChangedHandle.IsValid())
{
if (UPS_Editor_SelectionManager* SM = SelectionManager.Get())
{
SM->OnSelectionChanged.Remove(SelectionChangedHandle);
}
SelectionChangedHandle.Reset();
}
Super::NativeDestruct();
}
// -------------------- Actions --------------------
void UPS_Editor_MainWidget_UMG::SetTransformMode(EPS_Editor_TransformMode Mode)
{
if (UPS_Editor_SelectionManager* SM = SelectionManager.Get())
{
SM->SetTransformMode(Mode);
OnToolChanged.Broadcast();
}
}
void UPS_Editor_MainWidget_UMG::ToggleTransformSpace()
{
if (UPS_Editor_SelectionManager* SM = SelectionManager.Get())
{
SM->ToggleTransformSpace();
OnToolChanged.Broadcast();
}
}
void UPS_Editor_MainWidget_UMG::ToggleSnap()
{
if (UPS_Editor_SelectionManager* SM = SelectionManager.Get())
{
SM->bSnapTranslation = !SM->bSnapTranslation;
SM->bSnapRotation = SM->bSnapTranslation;
SM->bSnapScale = SM->bSnapTranslation;
OnToolChanged.Broadcast();
}
}
void UPS_Editor_MainWidget_UMG::SnapSelectionToGround()
{
if (UPS_Editor_SelectionManager* SM = SelectionManager.Get())
{
SM->SnapSelectionToGround();
}
}
void UPS_Editor_MainWidget_UMG::StartSimulation()
{
if (APS_Editor_PlayerController* PC = Cast<APS_Editor_PlayerController>(GetOwningPlayer()))
{
PC->StartSimulation();
OnSimulateChanged.Broadcast();
}
}
void UPS_Editor_MainWidget_UMG::StopSimulation()
{
if (APS_Editor_PlayerController* PC = Cast<APS_Editor_PlayerController>(GetOwningPlayer()))
{
PC->StopSimulation();
OnSimulateChanged.Broadcast();
}
}
bool UPS_Editor_MainWidget_UMG::SaveScenario(const FString& Name)
{
UPS_Editor_SpawnManager* SM = nullptr;
if (APS_Editor_PlayerController* PC = Cast<APS_Editor_PlayerController>(GetOwningPlayer()))
{
SM = PC->GetSpawnManager();
}
if (!SM) SM = SpawnManager.Get();
UPS_Editor_SceneSerializer* Ser = SceneSerializer.Get();
if (!Ser || !SM || Name.IsEmpty()) return false;
Ser->SaveScene(Name, SM);
OnSceneDirty.Broadcast();
return true;
}
bool UPS_Editor_MainWidget_UMG::LoadScenario(const FString& Name)
{
UPS_Editor_SceneSerializer* Ser = SceneSerializer.Get();
UPS_Editor_SpawnManager* SM = nullptr;
if (APS_Editor_PlayerController* PC = Cast<APS_Editor_PlayerController>(GetOwningPlayer()))
{
SM = PC->GetSpawnManager();
}
if (!SM) SM = SpawnManager.Get();
if (!Ser || !SM || Name.IsEmpty()) return false;
Ser->LoadScene(Name, SM);
OnSceneDirty.Broadcast();
return true;
}
void UPS_Editor_MainWidget_UMG::RequestClose()
{
if (APS_Editor_PlayerController* PC = Cast<APS_Editor_PlayerController>(GetOwningPlayer()))
{
PC->CloseEditor();
}
}
void UPS_Editor_MainWidget_UMG::SelectActorByPtr(AActor* Actor)
{
if (!Actor) return;
if (UPS_Editor_SelectionManager* SM = SelectionManager.Get())
{
SM->ClearSelection();
SM->SelectActor(Actor);
}
}
void UPS_Editor_MainWidget_UMG::ClearSelection()
{
if (UPS_Editor_SelectionManager* SM = SelectionManager.Get())
{
SM->ClearSelection();
}
}
// -------------------- Getters --------------------
EPS_Editor_TransformMode UPS_Editor_MainWidget_UMG::GetCurrentTransformMode() const
{
if (UPS_Editor_SelectionManager* SM = SelectionManager.Get()) return SM->GetTransformMode();
return EPS_Editor_TransformMode::Translate;
}
bool UPS_Editor_MainWidget_UMG::IsSnapEnabled() const
{
if (UPS_Editor_SelectionManager* SM = SelectionManager.Get()) return SM->bSnapTranslation;
return false;
}
bool UPS_Editor_MainWidget_UMG::IsWorldSpace() const
{
if (UPS_Editor_SelectionManager* SM = SelectionManager.Get()) return SM->GetTransformSpace() == EPS_Editor_Space::World;
return true;
}
bool UPS_Editor_MainWidget_UMG::IsSimulating() const
{
if (APS_Editor_PlayerController* PC = Cast<APS_Editor_PlayerController>(GetOwningPlayer())) return PC->IsSimulating();
return false;
}
TArray<AActor*> UPS_Editor_MainWidget_UMG::GetSpawnedActors() const
{
TArray<AActor*> Result;
UPS_Editor_SpawnManager* SM = nullptr;
if (APS_Editor_PlayerController* PC = Cast<APS_Editor_PlayerController>(GetOwningPlayer()))
{
SM = PC->GetSpawnManager();
}
if (!SM) SM = SpawnManager.Get();
if (SM)
{
for (const TWeakObjectPtr<AActor>& Weak : SM->GetSpawnedActors())
{
if (AActor* A = Weak.Get()) Result.Add(A);
}
}
return Result;
}
TArray<FString> UPS_Editor_MainWidget_UMG::GetSavedScenarioNames() const
{
if (UPS_Editor_SceneSerializer* Ser = SceneSerializer.Get()) return Ser->GetSavedSceneNames();
return {};
}
AActor* UPS_Editor_MainWidget_UMG::GetFirstSelectedActor() const
{
if (UPS_Editor_SelectionManager* SM = SelectionManager.Get())
{
if (SM->IsAnythingSelected())
{
return SM->GetSelectedActors()[0].Get();
}
}
return nullptr;
}
FString UPS_Editor_MainWidget_UMG::GetCurrentScenarioName() const
{
if (APS_Editor_PlayerController* PC = Cast<APS_Editor_PlayerController>(GetOwningPlayer()))
{
if (UGameInstance* GI = PC->GetGameInstance())
{
// Name is kept on the subsystem — look it up via header chain if available.
// Fallback to empty string.
}
}
return FString();
}

View File

@@ -0,0 +1,130 @@
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "PS_Editor_SelectionTypes.h"
#include "PS_Editor_MainWidget_UMG.generated.h"
class UPS_Editor_SelectionManager;
class UPS_Editor_SpawnManager;
class UPS_Editor_SceneSerializer;
/**
* UMG-based PS_Editor main widget. This class is intentionally minimal in C++ —
* the full UI is authored as Widget Blueprints that derive from this class, and
* call into the Blueprint-exposed API below to drive the editor.
*
* Workflow:
* 1. Create a Widget Blueprint "WBP_PSEditor_UMG" that inherits from
* UPS_Editor_MainWidget_UMG.
* 2. Design the visual layout (floating HUDs, panels, etc.) in the UMG designer.
* 3. In event graphs, call the BlueprintCallable functions (SetTool, ToggleSimulate,
* SaveScenario, etc.) from button click events.
* 4. Subscribe to the BlueprintAssignable delegates (OnToolChanged, OnSelectionChanged…)
* to refresh visual state.
* 5. Switch APS_Editor_HUD::UIMode to EPS_Editor_UIMode::UMG and set UMGWidgetClass
* to WBP_PSEditor_UMG.
*/
UCLASS(Abstract, Blueprintable)
class PS_EDITOR_API UPS_Editor_MainWidget_UMG : public UUserWidget
{
GENERATED_BODY()
public:
/** Manager refs — set by the HUD after construction. */
void SetSelectionManager(UPS_Editor_SelectionManager* InSelectionManager);
void SetSpawnManager(UPS_Editor_SpawnManager* InSpawnManager);
void SetSceneSerializer(UPS_Editor_SceneSerializer* InSerializer);
// ==================== BP-CALLABLE ACTIONS ====================
UFUNCTION(BlueprintCallable, Category = "PS Editor|Tools")
void SetTransformMode(EPS_Editor_TransformMode Mode);
UFUNCTION(BlueprintCallable, Category = "PS Editor|Tools")
void ToggleTransformSpace();
UFUNCTION(BlueprintCallable, Category = "PS Editor|Tools")
void ToggleSnap();
UFUNCTION(BlueprintCallable, Category = "PS Editor|Tools")
void SnapSelectionToGround();
UFUNCTION(BlueprintCallable, Category = "PS Editor|Simulation")
void StartSimulation();
UFUNCTION(BlueprintCallable, Category = "PS Editor|Simulation")
void StopSimulation();
UFUNCTION(BlueprintCallable, Category = "PS Editor|Scene")
bool SaveScenario(const FString& Name);
UFUNCTION(BlueprintCallable, Category = "PS Editor|Scene")
bool LoadScenario(const FString& Name);
UFUNCTION(BlueprintCallable, Category = "PS Editor|Scene")
void RequestClose();
UFUNCTION(BlueprintCallable, Category = "PS Editor|Selection")
void SelectActorByPtr(AActor* Actor);
UFUNCTION(BlueprintCallable, Category = "PS Editor|Selection")
void ClearSelection();
// ==================== BP-PURE GETTERS ====================
UFUNCTION(BlueprintPure, Category = "PS Editor|Tools")
EPS_Editor_TransformMode GetCurrentTransformMode() const;
UFUNCTION(BlueprintPure, Category = "PS Editor|Tools")
bool IsSnapEnabled() const;
UFUNCTION(BlueprintPure, Category = "PS Editor|Tools")
bool IsWorldSpace() const;
UFUNCTION(BlueprintPure, Category = "PS Editor|Simulation")
bool IsSimulating() const;
UFUNCTION(BlueprintPure, Category = "PS Editor|Scene")
TArray<AActor*> GetSpawnedActors() const;
UFUNCTION(BlueprintPure, Category = "PS Editor|Scene")
TArray<FString> GetSavedScenarioNames() const;
UFUNCTION(BlueprintPure, Category = "PS Editor|Selection")
AActor* GetFirstSelectedActor() const;
UFUNCTION(BlueprintPure, Category = "PS Editor|Scene")
FString GetCurrentScenarioName() const;
// ==================== EVENTS (BP delegates) ====================
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnPSE_ToolChanged);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnPSE_SelectionChanged);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnPSE_SimulateChanged);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnPSE_SceneDirty);
UPROPERTY(BlueprintAssignable, Category = "PS Editor|Events")
FOnPSE_ToolChanged OnToolChanged;
UPROPERTY(BlueprintAssignable, Category = "PS Editor|Events")
FOnPSE_SelectionChanged OnSelectionChanged;
UPROPERTY(BlueprintAssignable, Category = "PS Editor|Events")
FOnPSE_SimulateChanged OnSimulateChanged;
UPROPERTY(BlueprintAssignable, Category = "PS Editor|Events")
FOnPSE_SceneDirty OnSceneDirty;
protected:
virtual void NativeConstruct() override;
virtual void NativeDestruct() override;
UPROPERTY()
TWeakObjectPtr<UPS_Editor_SelectionManager> SelectionManager;
UPROPERTY()
TWeakObjectPtr<UPS_Editor_SpawnManager> SpawnManager;
UPROPERTY()
TWeakObjectPtr<UPS_Editor_SceneSerializer> SceneSerializer;
FDelegateHandle SelectionChangedHandle;
};