Files
PalUIExtension/Api/PanelWidget.hpp

174 lines
6.9 KiB
C++
Raw Normal View History

2026-09-07 19:33:58 -04:00
#pragma once
#include <vector>
#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<PanelWidget>(), DebugTag()) {}
auto GetChildrenCount() -> std::optional<int32>
{
return CallForReturn<int32>(STR("GetChildrenCount"));
}
auto HasAnyChildren() -> std::optional<bool>
{
return CallForReturn<bool>(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<int32>
{
struct P { UObject* Content; int32 ReturnValue; } p{ Content, -1 };
return CallForReturn(STR("GetChildIndex"), p, &P::ReturnValue);
}
auto HasChild(UObject* Content) -> std::optional<bool>
{
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<bool>
{
struct P { UObject* Content; bool ReturnValue; } p{ Content, false };
return CallForReturn(STR("RemoveChild"), p, &P::ReturnValue);
}
auto RemoveChildAt(int32 Index) -> std::optional<bool>
{
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<UWidget*> 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<Widget>
{
std::vector<Widget> Children;
auto Count = GetChildrenCount();
if (!Count || *Count <= 0) return Children;
Children.reserve(static_cast<size_t>(*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<UPanelSlot*> 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<UObject*>& Out) const -> bool
{
auto* Slots = TryPropertyPtr<TArrayRead_<UObject*>>(STR("Slots"));
if (!Slots) return false;
if (!Slots->IsPlausible(kMaxPlausibleChildren))
{
Output::send<LogLevel::Warning>(
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<size_t>(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<UObject*>
{
std::vector<UObject*> 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) {}
};
}