#pragma once #include #include "Widget.hpp" #include "PanelSlot.hpp" namespace PalUIExtension::Api { class PanelWidget : public Widget { public: static auto ClassPath() -> const TCHAR* { return STR("/Script/UMG.PanelWidget"); } static auto DebugTag() -> const TCHAR* { return STR("PanelWidget"); } PanelWidget() = default; explicit PanelWidget(UObject* Obj) : Widget(Obj, ClassOf(), DebugTag()) {} auto GetChildrenCount() -> std::optional { return CallForReturn(STR("GetChildrenCount")); } auto HasAnyChildren() -> std::optional { return CallForReturn(STR("HasAnyChildren")); } auto GetChildAt(int32 Index) -> Widget { struct P { int32 Index; UObject* ReturnValue; } p{ Index, nullptr }; auto Result = CallForReturn(STR("GetChildAt"), p, &P::ReturnValue); return (Result && *Result) ? Widget{ *Result } : Widget{}; } auto GetChildIndex(UObject* Content) -> std::optional { struct P { UObject* Content; int32 ReturnValue; } p{ Content, -1 }; return CallForReturn(STR("GetChildIndex"), p, &P::ReturnValue); } auto HasChild(UObject* Content) -> std::optional { struct P { UObject* Content; bool ReturnValue; } p{ Content, false }; return CallForReturn(STR("HasChild"), p, &P::ReturnValue); } auto AddChild(UObject* Content) -> PanelSlot { struct P { UObject* Content; UObject* ReturnValue; } p{ Content, nullptr }; auto Result = CallForReturn(STR("AddChild"), p, &P::ReturnValue); return (Result && *Result) ? PanelSlot{ *Result } : PanelSlot{}; } auto RemoveChild(UObject* Content) -> std::optional { struct P { UObject* Content; bool ReturnValue; } p{ Content, false }; return CallForReturn(STR("RemoveChild"), p, &P::ReturnValue); } auto RemoveChildAt(int32 Index) -> std::optional { struct P { int32 Index; bool ReturnValue; } p{ Index, false }; return CallForReturn(STR("RemoveChildAt"), p, &P::ReturnValue); } auto ClearChildren() -> bool { return CallVoid(STR("ClearChildren")); } // ------------------------------------------------------------------ // Deliberately NOT a wrapper around the engine's GetAllChildren. // // That UFUNCTION returns TArray by value. ProcessEvent // constructs that array into the frame with the engine's allocator // and then hands you ownership of the allocation. Getting that // wrong is either a leak on every call or a free across a module // boundary, and neither shows up as a crash near the cause. // // The count + index loop is two extra ProcessEvent calls per child // and zero ownership questions. If you are calling this per-frame // over a large panel, cache the result; if you are calling it once // in a Construct hook, the cost is irrelevant. // ------------------------------------------------------------------ auto GetAllChildren() -> std::vector { std::vector Children; auto Count = GetChildrenCount(); if (!Count || *Count <= 0) return Children; Children.reserve(static_cast(*Count)); for (int32 i = 0; i < *Count; ++i) { Widget Child = GetChildAt(i); if (Child) Children.push_back(Child); } return Children; } // ------------------------------------------------------------------ // Children WITHOUT ProcessEvent. // // GetAllChildren() above costs 1 + N ProcessEvent calls. That is // fine once in a Construct hook and not fine as the inner loop of a // whole-tree walk, where it becomes 1 + N per PANEL. // // UPanelWidget::Slots is a TArray UPROPERTY (confirmed // in the dump: UHTHeaderDump/UMG/Public/PanelWidget.h line 14), and // UPanelSlot::Content is a UPROPERTY too. So the full child list is // reachable as pure memory reads. Slot order is child order -- the // engine's own GetChildAt(i) is Slots[i]->Content. // // Nothing here is copied out of engine memory except the pointers // themselves, which is what makes reading the TArray safe; see the // TArrayRead_ comment in UmgTypes.hpp for where that line is. // // Returns false if Slots could not be read or looked implausible, // WITHOUT touching Out -- so a caller can fall back to // GetAllChildren() and know it isn't double-appending. // ------------------------------------------------------------------ static constexpr int32 kMaxPlausibleChildren = 4096; auto TryGetChildObjects(std::vector& Out) const -> bool { auto* Slots = TryPropertyPtr>(STR("Slots")); if (!Slots) return false; if (!Slots->IsPlausible(kMaxPlausibleChildren)) { Output::send( STR("[{}] 'Slots' does not look like a live TArray ") STR("(Num={}, Max={}, Data={}) -- falling back to ProcessEvent\n"), m_tag, Slots->Num, Slots->Max, Slots->Data ? STR("set") : STR("null")); return false; } Out.reserve(Out.size() + static_cast(Slots->Num)); for (int32 i = 0; i < Slots->Num; ++i) { UObject* Slot = Slots->Data[i]; if (!Slot) continue; // Read Content off the slot directly rather than through the // PanelSlot wrapper: the wrapper's IsA check would log on // anything unexpected, and this runs once per node of a // whole-tree walk. if (UObject* Content = RawObject{ Slot, STR("PanelSlot") }.TryObjectProperty(STR("Content"))) { Out.push_back(Content); } } return true; } // Property read first, ProcessEvent second. Use this anywhere you // just want the children and don't care which route produced them. auto GetChildObjects() -> std::vector { std::vector Out; if (TryGetChildObjects(Out)) return Out; for (Widget& Child : GetAllChildren()) { if (UObject* O = Child.Get()) Out.push_back(O); } return Out; } protected: PanelWidget(UObject* Obj, UClass* Expected, const TCHAR* Tag) : Widget(Obj, Expected, Tag) {} }; }