2026-03-11 08:12:21 +01:00
|
|
|
#include "UEWidgets.hpp"
|
|
|
|
|
#include "Engine_classes.hpp"
|
|
|
|
|
|
|
|
|
|
std::shared_ptr<spdlog::logger> g_WidgetsLogger;
|
|
|
|
|
|
|
|
|
|
void ApplyPositionOffset(SDK::UUserWidget* Widget, float offset, SDK::FVector2D Alignment) {
|
|
|
|
|
if (!Widget) return;
|
|
|
|
|
if (Widget->Slot && Widget->Slot->IsA(SDK::UCanvasPanelSlot::StaticClass())) {
|
|
|
|
|
auto* canvasSlot = static_cast<SDK::UCanvasPanelSlot*>(Widget->Slot);
|
|
|
|
|
|
|
|
|
|
canvasSlot->SetPosition(SDK::FVector2D(offset, 0));
|
|
|
|
|
canvasSlot->SetAlignment(Alignment);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ApplyTransformOffset(SDK::UWidget* Widget, float OffsetX, float OffsetY) {
|
|
|
|
|
if (!Widget) return;
|
|
|
|
|
SDK::FWidgetTransform Transform = Widget->RenderTransform;
|
|
|
|
|
Transform.Translation.X = OffsetX; // Horizontal shifting
|
|
|
|
|
Transform.Translation.Y = OffsetY; // Vertical shifting (optional)
|
|
|
|
|
Widget->SetRenderTransform(Transform);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-13 19:22:41 +01:00
|
|
|
void CenterWidget(SDK::UUserWidget* Widget, float offset, float screenWidth, float screenHeight, float targetWidth, float targetHeight, float compensation) {
|
2026-03-11 08:12:21 +01:00
|
|
|
if (!Widget) return;
|
2026-03-13 19:22:41 +01:00
|
|
|
float aspectRatio = screenWidth / screenHeight;
|
|
|
|
|
float targetAspect = targetWidth / targetHeight;
|
2026-03-11 08:12:21 +01:00
|
|
|
|
|
|
|
|
if (aspectRatio > targetAspect) {
|
|
|
|
|
float scale = targetHeight / screenHeight; // horizontal scale to apply to get the same vertical size as 1080p
|
|
|
|
|
targetWidth = (float)screenWidth * scale;
|
|
|
|
|
}
|
|
|
|
|
targetWidth -= offset * 2;
|
|
|
|
|
|
|
|
|
|
Widget->SetDesiredSizeInViewport(SDK::FVector2D(targetWidth, targetHeight));
|
|
|
|
|
Widget->SetPositionInViewport(SDK::FVector2D(offset, 0.f), true);
|
|
|
|
|
ApplyTransformOffset(Widget, compensation, 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
float ApplyOffsetsSmart_Internal(SDK::UWidget* Widget, float Left, float Right, int Depth = 0, int MaxDepth = 1) {
|
|
|
|
|
if (!Widget) return 0;
|
|
|
|
|
static float leftApplied = 0;
|
|
|
|
|
// Apply if CanvasSlot
|
|
|
|
|
if (Widget->Slot && Widget->Slot->IsA(SDK::UCanvasPanelSlot::StaticClass())) {
|
|
|
|
|
if (Depth <= MaxDepth) {
|
|
|
|
|
auto* Slot = static_cast<SDK::UCanvasPanelSlot*>(Widget->Slot);
|
|
|
|
|
|
|
|
|
|
SDK::FMargin Offsets = Slot->GetOffsets();
|
|
|
|
|
SDK::FAnchors Anchors = Slot->GetAnchors();
|
|
|
|
|
|
|
|
|
|
float MinX = Anchors.Minimum.X;
|
|
|
|
|
float MaxX = Anchors.Maximum.X;
|
|
|
|
|
// Use SetLogger first to initialize logger
|
|
|
|
|
#ifdef MY_VERBOSE_LOGS
|
|
|
|
|
std::string indent(Depth * 2, ' '); // 2 espaces par niveau
|
|
|
|
|
if (g_WidgetsLogger) g_WidgetsLogger->debug("{}CanvasPanelSlot: {} {} - Slot: {} {} - Offets: {} {} {} {} - Anchors: {} {} {} {}", indent,
|
|
|
|
|
Widget->GetName(), Widget->Class->GetName(),
|
|
|
|
|
Slot->GetName(), Slot->Class->GetName(),
|
|
|
|
|
Offsets.Left, Offsets.Top, Offsets.Right, Offsets.Bottom,
|
|
|
|
|
Anchors.Minimum.X, Anchors.Minimum.Y, Anchors.Maximum.X, Anchors.Maximum.Y);
|
|
|
|
|
#endif
|
|
|
|
|
if (MinX == 0.f && MaxX == 1.f) return 0; // Ignore stretch
|
|
|
|
|
if (MinX == 0.5f && MaxX == 0.5f) return 0; // Ignore centered
|
|
|
|
|
|
|
|
|
|
if (MinX == 0.f && MaxX == 0.f) // Modify only pure left
|
|
|
|
|
leftApplied = Offsets.Left = Left;
|
|
|
|
|
else if (MinX == 1.f && MaxX == 1.f) // Modify only pure right
|
|
|
|
|
Offsets.Left = -Right;
|
|
|
|
|
|
|
|
|
|
Slot->SetOffsets(Offsets);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Stop to max depth
|
|
|
|
|
if (Depth >= MaxDepth) return leftApplied;
|
|
|
|
|
|
|
|
|
|
// Browse the children
|
|
|
|
|
if (Widget->IsA(SDK::UPanelWidget::StaticClass())) {
|
|
|
|
|
auto* Panel = static_cast<SDK::UPanelWidget*>(Widget);
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < Panel->GetChildrenCount(); ++i)
|
|
|
|
|
ApplyOffsetsSmart_Internal(Panel->GetChildAt(i), Left, Right, Depth + 1, MaxDepth);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (Widget->IsA(SDK::UUserWidget::StaticClass())) {
|
|
|
|
|
auto* UW = static_cast<SDK::UUserWidget*>(Widget);
|
|
|
|
|
|
|
|
|
|
if (UW->WidgetTree && UW->WidgetTree->RootWidget)
|
|
|
|
|
ApplyOffsetsSmart_Internal(UW->WidgetTree->RootWidget, Left, Right, Depth + 1, MaxDepth);
|
|
|
|
|
}
|
|
|
|
|
return leftApplied;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
float ApplyOffsetsSmart(SDK::UWidget* Widget, float Left, float Right, int MaxDepth) {
|
|
|
|
|
float leftApplied = ApplyOffsetsSmart_Internal(Widget, Left, Right, 0, MaxDepth);
|
|
|
|
|
return leftApplied;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void ApplyOffsetsRecursive_Internal(SDK::UWidget* Widget, float Left, float Right,
|
|
|
|
|
const std::vector<SDK::UObject*>& ExcludeObjects, const std::vector<std::string>& ExcludeClass,
|
|
|
|
|
int CurrentDepth, int MaxDepth) {
|
|
|
|
|
if (!Widget || CurrentDepth > MaxDepth) return;
|
|
|
|
|
|
|
|
|
|
// Apply offsets according to Slot type
|
|
|
|
|
for (const auto& ExcludeObject : ExcludeObjects) {
|
|
|
|
|
if (ExcludeObject == Widget) return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string widgetName;
|
|
|
|
|
std::string className;
|
|
|
|
|
|
|
|
|
|
widgetName = Widget->GetName();
|
|
|
|
|
if (Widget->Class) className = Widget->Class->GetName();
|
|
|
|
|
for (const auto& pattern : ExcludeClass) {
|
|
|
|
|
if (widgetName.find(pattern) != std::string::npos || className.find(pattern) != std::string::npos)
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (Widget->Slot) {
|
|
|
|
|
if (Widget->Slot->IsA(SDK::UCanvasPanelSlot::StaticClass())) {
|
|
|
|
|
auto* Slot = static_cast<SDK::UCanvasPanelSlot*>(Widget->Slot);
|
|
|
|
|
SDK::FMargin Offsets = Slot->GetOffsets();
|
|
|
|
|
|
|
|
|
|
#ifdef MY_VERBOSE_LOGS
|
|
|
|
|
SDK::FAnchors anchors = Slot->GetAnchors();
|
|
|
|
|
if (g_WidgetsLogger) g_WidgetsLogger->debug("CanvasPanelSlot: {} {} - Slot: {} {} - Offets: {} {} {} {} - Anchors: {} {} {} {}", \
|
|
|
|
|
Widget->GetName(), Widget->Class->GetName(), \
|
|
|
|
|
Slot->GetName(), Slot->Class->GetName(), \
|
|
|
|
|
Offsets.Left, Offsets.Top, Offsets.Right, Offsets.Bottom, \
|
|
|
|
|
anchors.Minimum.X, anchors.Minimum.Y, anchors.Maximum.X, anchors.Maximum.Y);
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
Offsets.Left = Left;
|
|
|
|
|
Offsets.Right = Right;
|
|
|
|
|
Slot->SetOffsets(Offsets);
|
|
|
|
|
}
|
|
|
|
|
else if (Widget->Slot->IsA(SDK::UVerticalBoxSlot::StaticClass())) {
|
|
|
|
|
auto* Slot = static_cast<SDK::UVerticalBoxSlot*>(Widget->Slot);
|
|
|
|
|
SDK::FMargin Margin = Slot->Padding;
|
|
|
|
|
#ifdef MY_VERBOSE_LOGS
|
|
|
|
|
if (g_WidgetsLogger) g_WidgetsLogger->debug("VerticalBox Slot: {} {} - Slot: {} {} - Margins: {} {} {} {}", \
|
|
|
|
|
Widget->GetName(), Widget->Class->GetName(), \
|
|
|
|
|
Slot->GetName(), Slot->Class->GetName(), \
|
|
|
|
|
Margin.Left, Margin.Top, Margin.Right, Margin.Bottom);
|
|
|
|
|
#endif
|
|
|
|
|
Margin.Left = Left;
|
|
|
|
|
Margin.Right = Right;
|
|
|
|
|
Slot->SetPadding(Margin);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Go deeper in children if we got a Panel
|
|
|
|
|
if (Widget->IsA(SDK::UPanelWidget::StaticClass()) && CurrentDepth < MaxDepth) {
|
|
|
|
|
auto* Panel = static_cast<SDK::UPanelWidget*>(Widget);
|
|
|
|
|
int childrenCount = Panel->GetChildrenCount();
|
|
|
|
|
for (int i = 0; i < childrenCount; ++i) {
|
|
|
|
|
ApplyOffsetsRecursive_Internal(Panel->GetChildAt(i), Left, Right, ExcludeObjects, ExcludeClass, CurrentDepth + 1, MaxDepth);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Go deeper if the widget is an UserWidget
|
|
|
|
|
if (Widget->IsA(SDK::UUserWidget::StaticClass())) {
|
|
|
|
|
auto* ChildWidget = static_cast<SDK::UUserWidget*>(Widget);
|
|
|
|
|
if (ChildWidget->WidgetTree && ChildWidget->WidgetTree->RootWidget) {
|
|
|
|
|
ApplyOffsetsRecursive_Internal(ChildWidget->WidgetTree->RootWidget, Left, Right, ExcludeObjects, ExcludeClass, CurrentDepth + 1, MaxDepth);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ApplyOffsetsRecursive(SDK::UWidget* Widget, float Left, float Right,
|
|
|
|
|
const std::vector<SDK::UObject*>& ExcludeObjects, const std::vector<std::string>& ExcludeClass, int MaxDepth) {
|
|
|
|
|
ApplyOffsetsRecursive_Internal(Widget, Left, Right, ExcludeObjects, ExcludeClass, 0, MaxDepth);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void ApplyOverlayOffsetRecursive_Internal(SDK::UWidget* Widget, float left, float right, SDK::EHorizontalAlignment alignment,
|
|
|
|
|
const std::vector<std::string>& ExcludeNames, int CurrentDepth, int MaxDepth) {
|
|
|
|
|
if (!Widget || CurrentDepth > MaxDepth) return;
|
|
|
|
|
|
|
|
|
|
auto IsExcluded = [&](SDK::UWidget* widget) -> bool {
|
|
|
|
|
for (const auto& name : ExcludeNames) {
|
|
|
|
|
if (widget->GetName().contains(name))
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
auto AdjustOverlaySlot = [&](SDK::UOverlaySlot* Slot) {
|
|
|
|
|
if (!Slot || IsExcluded(Widget)) return;
|
|
|
|
|
|
|
|
|
|
SDK::FMargin Padding = Slot->Padding;
|
|
|
|
|
|
|
|
|
|
if (Slot->HorizontalAlignment == alignment) {
|
|
|
|
|
Padding.Left = left;
|
|
|
|
|
Padding.Right = right;
|
|
|
|
|
}
|
|
|
|
|
Slot->SetPadding(Padding);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (Widget->Slot && Widget->Slot->IsA(SDK::UOverlaySlot::StaticClass()))
|
|
|
|
|
AdjustOverlaySlot((SDK::UOverlaySlot*)Widget->Slot);
|
|
|
|
|
|
|
|
|
|
if (Widget->IsA(SDK::UPanelWidget::StaticClass()) && CurrentDepth < MaxDepth) {
|
|
|
|
|
SDK::UPanelWidget* Panel = (SDK::UPanelWidget*)Widget;
|
|
|
|
|
int childrenCount = Panel->GetChildrenCount();
|
|
|
|
|
for (int i = 0; i < childrenCount; ++i) {
|
|
|
|
|
ApplyOverlayOffsetRecursive_Internal(Panel->GetChildAt(i), left, right, alignment, ExcludeNames, CurrentDepth + 1, MaxDepth);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ApplyOverlayOffsetRecursive(SDK::UWidget* Widget, float left, float right, SDK::EHorizontalAlignment alignment,
|
|
|
|
|
const std::vector<std::string>& ExcludeNames, int MaxDepth) {
|
|
|
|
|
ApplyOverlayOffsetRecursive_Internal(Widget, left, right, alignment, ExcludeNames, 0, MaxDepth);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// -- Tracking && dumping widgets for debugging --
|
|
|
|
|
void FindAndApplyCanvasRecursive(SDK::UWidget* widget, float offset, int MaxDepth, int currentDepth) {
|
|
|
|
|
if (!widget || currentDepth > MaxDepth) return;
|
|
|
|
|
|
|
|
|
|
// Applying straight if it's a CanvasPanel
|
|
|
|
|
if (widget->IsA(SDK::UCanvasPanel::StaticClass())) {
|
|
|
|
|
ApplyOffsetsRecursive(widget, offset, offset);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// We go deeper if
|
|
|
|
|
if (widget->IsA(SDK::UPanelWidget::StaticClass())) {
|
|
|
|
|
auto* panel = static_cast<SDK::UPanelWidget*>(widget);
|
|
|
|
|
int childrenCount = panel->GetChildrenCount();
|
|
|
|
|
for (int i = 0; i < childrenCount; ++i) {
|
|
|
|
|
FindAndApplyCanvasRecursive(panel->GetChildAt(i), offset, MaxDepth, currentDepth + 1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TrackWidgetConstruct(SDK::UUserWidget* widget) {
|
|
|
|
|
if (!widget || !widget->Class) return;
|
|
|
|
|
|
|
|
|
|
std::string className = widget->Class->GetName();
|
|
|
|
|
auto& info = g_WidgetTracker[className];
|
|
|
|
|
|
|
|
|
|
info.ClassName = className;
|
|
|
|
|
info.ConstructCount++;
|
|
|
|
|
|
|
|
|
|
if (info.ConstructCount == 1)
|
|
|
|
|
info.FirstSeen = std::chrono::steady_clock::now();
|
|
|
|
|
|
|
|
|
|
if (widget->IsInViewport())
|
|
|
|
|
info.WasInViewport = true;
|
|
|
|
|
|
|
|
|
|
if (widget->Outer && widget->Outer->Class)
|
|
|
|
|
info.OuterClass = widget->Outer->Class->GetName();
|
|
|
|
|
|
|
|
|
|
if (widget->WidgetTree && widget->WidgetTree->RootWidget) {
|
|
|
|
|
auto* root = widget->WidgetTree->RootWidget;
|
|
|
|
|
if (root && root->Class)
|
|
|
|
|
info.RootWidgetClass = root->Class->GetName();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TrackWidgetDestruct(SDK::UUserWidget* widget) {
|
|
|
|
|
if (!widget || !widget->Class) return;
|
|
|
|
|
|
|
|
|
|
std::string className = widget->Class->GetName();
|
|
|
|
|
|
|
|
|
|
if (g_WidgetTracker.contains(className))
|
|
|
|
|
g_WidgetTracker[className].DestructCount++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static std::string ClassifyWidget(const WidgetTrackInfo& info) {
|
|
|
|
|
bool persistent = info.ConstructCount > info.DestructCount;
|
|
|
|
|
|
|
|
|
|
if (info.WasInViewport && persistent && info.OuterClass.find("PlayerController") != std::string::npos)
|
|
|
|
|
return "HUD Candidate";
|
|
|
|
|
|
|
|
|
|
if (info.WasInViewport && (info.ClassName.find("Menu") != std::string::npos))
|
|
|
|
|
return "Menu UI";
|
|
|
|
|
|
|
|
|
|
if (info.WasInViewport && (info.ClassName.find("Settings") != std::string::npos))
|
|
|
|
|
return "Settings UI";
|
|
|
|
|
|
|
|
|
|
if (info.WasInViewport && (info.ClassName.find("Save") != std::string::npos ||
|
|
|
|
|
info.ClassName.find("Load") != std::string::npos))
|
|
|
|
|
return "Save/Load UI";
|
|
|
|
|
|
|
|
|
|
if (!persistent)
|
|
|
|
|
return "Temporary Widget";
|
|
|
|
|
|
|
|
|
|
return "Unclassified";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void DumpUIAnalysis(std::shared_ptr<spdlog::logger> logger) {
|
|
|
|
|
if (!logger) return;
|
|
|
|
|
|
|
|
|
|
logger->info("========== UI ANALYSIS ==========");
|
|
|
|
|
|
|
|
|
|
for (auto& [name, info] : g_WidgetTracker) {
|
|
|
|
|
std::string type = ClassifyWidget(info);
|
|
|
|
|
bool persistent = info.ConstructCount > info.DestructCount;
|
|
|
|
|
|
|
|
|
|
logger->info("Class: {}", name);
|
|
|
|
|
logger->info(" Type : {}", type);
|
|
|
|
|
logger->info(" Root Type : {}", info.RootWidgetClass.empty() ? "Unknown" : info.RootWidgetClass);
|
|
|
|
|
logger->info(" Construct : {}", info.ConstructCount);
|
|
|
|
|
logger->info(" Destruct : {}", info.DestructCount);
|
|
|
|
|
logger->info(" Persistent : {}", persistent ? "Yes" : "No");
|
|
|
|
|
logger->info(" InViewport : {}", info.WasInViewport ? "Yes" : "No");
|
|
|
|
|
logger->info(" Outer : {}", info.OuterClass.empty() ? "Unknown" : info.OuterClass);
|
|
|
|
|
logger->info(" ");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logger->info("=================================");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ClearWidgetTracking() {
|
|
|
|
|
g_WidgetTracker.clear();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void DumpWidgetRecursive(SDK::UWidget* Widget, int Depth, int MaxDepth) {
|
|
|
|
|
if (!Widget || Depth > MaxDepth) return;
|
|
|
|
|
|
|
|
|
|
std::string indent(Depth * 2, ' ');
|
|
|
|
|
std::string className = Widget->Class ? Widget->Class->GetName() : "UnknownClass";
|
|
|
|
|
std::string widgetName = Widget->GetName();
|
|
|
|
|
if (g_WidgetsLogger)
|
|
|
|
|
g_WidgetsLogger->debug("{}Widget: {} [{}]", indent, widgetName, className);
|
|
|
|
|
|
|
|
|
|
// Slot info si disponible
|
|
|
|
|
if (Widget->Slot) {
|
|
|
|
|
std::string slotClass = Widget->Slot->Class ? Widget->Slot->Class->GetName() : "UnknownSlot";
|
|
|
|
|
if (g_WidgetsLogger)
|
|
|
|
|
g_WidgetsLogger->debug("{} Slot class: {}", indent, slotClass);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Descendre dans les enfants si c'est un panel et qu'on est encore sous MaxDepth
|
|
|
|
|
if (Depth < MaxDepth) {
|
|
|
|
|
if (Widget->IsA(SDK::UPanelWidget::StaticClass())) {
|
|
|
|
|
auto* panel = static_cast<SDK::UPanelWidget*>(Widget);
|
|
|
|
|
int count = panel->GetChildrenCount();
|
|
|
|
|
for (int i = 0; i < count; ++i) {
|
|
|
|
|
DumpWidgetRecursive(panel->GetChildAt(i), Depth + 1, MaxDepth);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (Widget->IsA(SDK::UUserWidget::StaticClass())) {
|
|
|
|
|
auto* userWidget = static_cast<SDK::UUserWidget*>(Widget);
|
|
|
|
|
if (userWidget->WidgetTree && userWidget->WidgetTree->RootWidget) {
|
|
|
|
|
DumpWidgetRecursive(userWidget->WidgetTree->RootWidget, Depth + 1, MaxDepth);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void SetLogger(std::shared_ptr<spdlog::logger> logger) {
|
|
|
|
|
g_WidgetsLogger = logger;
|
|
|
|
|
}
|