From 3df33971c323ab37b8b601f6e3bda06b53c7491d Mon Sep 17 00:00:00 2001 From: Emmanuel AYME Date: Wed, 11 Mar 2026 08:12:21 +0100 Subject: [PATCH] Add new lib for Unreal Engine widgets manipulation --- libs/UEngine/UEWidgets.cpp | 354 +++++++++++++++++++++++++++++++++++++ libs/UEngine/UEWidgets.hpp | 118 +++++++++++++ 2 files changed, 472 insertions(+) create mode 100644 libs/UEngine/UEWidgets.cpp create mode 100644 libs/UEngine/UEWidgets.hpp diff --git a/libs/UEngine/UEWidgets.cpp b/libs/UEngine/UEWidgets.cpp new file mode 100644 index 0000000..7da5f3d --- /dev/null +++ b/libs/UEngine/UEWidgets.cpp @@ -0,0 +1,354 @@ +#include "UEWidgets.hpp" +#include "Engine_classes.hpp" + +std::shared_ptr 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(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); +} + +void CenterWidget(SDK::UUserWidget* Widget, float offset, float screenWidth, float screenHeight, float aspectRatio, float targetAspect, float compensation) { + if (!Widget) return; + float targetHeight = (float)screenHeight; // For UW displays + float targetWidth = screenWidth; + + if (aspectRatio > targetAspect) { + targetHeight = 1080.f; // clamp at 1080 for UW + 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(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(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(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& ExcludeObjects, const std::vector& 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(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(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(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(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& ExcludeObjects, const std::vector& 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& 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& 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(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 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(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(Widget); + if (userWidget->WidgetTree && userWidget->WidgetTree->RootWidget) { + DumpWidgetRecursive(userWidget->WidgetTree->RootWidget, Depth + 1, MaxDepth); + } + } + } +} + +void SetLogger(std::shared_ptr logger) { + g_WidgetsLogger = logger; +} \ No newline at end of file diff --git a/libs/UEngine/UEWidgets.hpp b/libs/UEngine/UEWidgets.hpp new file mode 100644 index 0000000..bef95f1 --- /dev/null +++ b/libs/UEngine/UEWidgets.hpp @@ -0,0 +1,118 @@ +#pragma once +#include "UMG_classes.hpp" +#include +#include + +namespace SDK { + class UEngine; + class UObject; + class UWorld; + class UWidget; + class APawn; + class UGameplayStatics; + class UConsole; + class UInputSettings; + class UKismetStringLibrary; + class UGameInstance; + class ULocalPlayer; + class APlayerController; +} + +struct WidgetTrackInfo { + std::string ClassName; + std::string RootWidgetClass; + std::string OuterClass; + bool WasInViewport = false; + int ConstructCount = 0; + int DestructCount = 0; + std::chrono::steady_clock::time_point FirstSeen; +}; +static std::unordered_map g_WidgetTracker; + +/** + * @brief Reposition from left a widget that only displays at left and nothing at right + * @param Widget UUserWdget to offset. + * @param offset offset to apply. + * @param screenWidth current screen width. + * @param screenHeight current screen height. + * @param aspectRatio current screen aspect ratio. + * @param targetAspect target aspect ratio to apply compensation for (default is 16:9 depending on game). + * @param compensation additional compensation after widget resize. + */ +void CenterWidget(SDK::UUserWidget* Widget, float offset, float screenWidth, float screenHeight, float aspectRatio, float targetAspect = 16.f / 9.f, float compensation = 0.f); + +/** + * @brief Reposition from left a widget that only displays at left and nothing at right + * @param Widget UUserWdget to offset. + * @param offset left offset to apply. + * @param Alignment widget aligment (0.5, 0.5) is centered for eg. + */ +void ApplyPositionOffset(SDK::UUserWidget* Widget, float offset, SDK::FVector2D Alignment); + +float ApplyOffsetsSmart(SDK::UWidget* Widget, float Left, float Right, int MaxDepth); + +/** + * @brief Apply offsets recursively to UVCanvasPanelSlot. + * @param widget UUserWidget* pointer. + * @param left offset. + * @param right offset. + * @param ExcludeObjects excluded objects in depth check + * @param ExcludeClass excluded class widgets in depth check + */ +void ApplyOffsetsRecursive(SDK::UWidget* widget, float left, float right = 0, + const std::vector& ExcludeObjects = {}, + const std::vector& ExcludeClass = {}, int MaxDepth = INT_MAX); + +/** + * @brief Apply offsets recursively to Overlay. + * @param widget UUserWidget* pointer. + * @param Offset (left and right offsets). + * @param alignment widget (left, right, center, fill ...) + * @param ExcludeNames excluded class widgets in depth check + * @param MaxDepth Max depth to go through root component + */ +void ApplyOverlayOffsetRecursive(SDK::UWidget* Widget, float left, float right, SDK::EHorizontalAlignment alignment, + const std::vector& ExcludeNames = {}, int MaxDepth = INT_MAX); + +/** + * @brief Apply transform to a widget. + * @param Widget UWidget* pointer. + * @param OffsetX (left offset). + * @param OffsetY (top offset). + */ +void ApplyTransformOffset(SDK::UWidget* Widget, float OffsetX, float OffsetY = 0); + +void FindAndApplyCanvasRecursive(SDK::UWidget* widget, float offset, int MaxDepth = INT_MAX, int currentDepth = 0); + +/** + * @brief Tracks potential widgets candidates for HUD & UI at construction + * @param widget UUserWidget* pointer. + */ +void TrackWidgetConstruct(SDK::UUserWidget* widget); + +/** + * @brief Tracks potential widgets candidates for HUD & UI at destruction + * @param widget UUserWidget* pointer. + */ +void TrackWidgetDestruct(SDK::UUserWidget* widget); + +/** + * @brief Dump and log previously searched UI & HUD wisgets + * @param logger the log object. + */ +void DumpUIAnalysis(std::shared_ptr logger); + +/** + * @brief Clear previously tracked widgets + */ +void ClearWidgetTracking(); + +/** + * @brief Dump all widgets + * @param Widget a UWidget instance to browse + * @param Depth the depth to start from + * @param MaxDepth the maximal depth to reach + */ +void DumpWidgetRecursive(SDK::UWidget* Widget, int Depth = 0, int MaxDepth = 2); + +void SetLogger(std::shared_ptr logger = nullptr);