Files
PalUIExtension/Api/WidgetTree.hpp
2026-09-07 19:33:58 -04:00

350 lines
12 KiB
C++

#pragma once
#include <string>
#include <vector>
#include <Unreal/NameTypes.hpp>
#include "UnrealWrapper.hpp"
#include "Widget.hpp"
#include "PanelWidget.hpp"
namespace PalUIExtension::Api
{
class WidgetTree : public UnrealWrapper
{
public:
static auto ClassPath() -> const TCHAR* { return STR("/Script/UMG.WidgetTree"); }
static auto DebugTag() -> const TCHAR* { return STR("WidgetTree"); }
WidgetTree() = default;
explicit WidgetTree(UObject* Obj) : UnrealWrapper(Obj, ClassOf<WidgetTree>(), DebugTag()) {}
static auto FromUserWidget(UObject* UserWidget) -> WidgetTree
{
if (!UserWidget) return WidgetTree{};
UObject* Tree = RawObject{ UserWidget, STR("UserWidget") }.TryObjectProperty(STR("WidgetTree"));
return Tree ? WidgetTree{ Tree } : WidgetTree{};
}
// The tree a widget LIVES IN.
static auto ContainingTree(UObject* AnyWidget) -> WidgetTree
{
if (!AnyWidget) return WidgetTree{};
UClass* TreeClass = ClassOf<WidgetTree>();
if (!TreeClass) return WidgetTree{}; // ClassOf already logged
if (UObject* Tree = AnyWidget->GetTypedOuter(TreeClass))
{
return WidgetTree{ Tree };
}
UObject* Outer = AnyWidget->GetOuterPrivate();
for (int32 Hops = 0; Outer && Hops < kMaxOuterHops; ++Hops)
{
if (Outer->IsA(TreeClass)) return WidgetTree{ Outer };
Outer = Outer->GetOuterPrivate();
}
return WidgetTree{};
}
static auto FromWidget(UObject* AnyWidget) -> WidgetTree
{
if (WidgetTree Owned = FromUserWidget(AnyWidget)) return Owned;
return ContainingTree(AnyWidget);
}
// ---- root ---------------------------------------------------------
auto GetRootWidgetObject() const -> UObject*
{
auto Ptr = PropertyPtr<UObject*>(STR("RootWidget"));
return Ptr ? *Ptr : nullptr;
}
auto Root() const -> Widget
{
UObject* R = GetRootWidgetObject();
return R ? Widget{ R } : Widget{};
}
// ==================================================================
// TRAVERSAL
// ==================================================================
enum class EScope : uint8
{
ThisTreeOnly,
IncludeNestedUserWidgets,
};
// What a visitor wants to happen next.
enum class EVisit : uint8
{
Continue, // descend into this node's children
SkipChildren, // this node is fine, its subtree is not interesting
Stop, // end the walk entirely
};
static constexpr int32 kDefaultNodeBudget = 8192;
static constexpr int32 kMaxDepth = 64;
static constexpr int32 kMaxOuterHops = 32;
template <typename Fn>
auto ForEach(Fn&& Visit, EScope Scope = EScope::ThisTreeOnly, int32 NodeBudget = kDefaultNodeBudget) -> bool
{
WalkState State{ Scope, NodeBudget, false, false };
WalkNode(GetRootWidgetObject(), 0, State, Visit);
if (State.bExhausted)
{
Output::send<LogLevel::Warning>(
STR("[WidgetTree] walk aborted -- exceeded {} nodes or depth {}. ")
STR("Results are incomplete.\n"), NodeBudget, kMaxDepth);
}
return !State.bExhausted;
}
// ==================================================================
// SEARCH
// ==================================================================
auto FindObjectByName(FName Name, EScope Scope = EScope::ThisTreeOnly) -> UObject*
{
if (Name.IsNone()) return nullptr;
UObject* Found = nullptr;
ForEach([&](UObject* Node, int32) -> EVisit {
if (Node->GetNamePrivate() == Name)
{
Found = Node;
return EVisit::Stop;
}
return EVisit::Continue;
}, Scope);
return Found;
}
auto FindObjectByName(const TCHAR* Name, EScope Scope = EScope::ThisTreeOnly) -> UObject*
{
if (!Name) return nullptr;
return FindObjectByName(FName(Name, FNAME_Find), Scope);
}
// Same search, wrapped. Invalid Widget on a miss.
auto Find(const TCHAR* Name, EScope Scope = EScope::ThisTreeOnly) -> Widget
{
UObject* O = FindObjectByName(Name, Scope);
return O ? Widget{ O } : Widget{};
}
template <typename T>
auto FindAs(const TCHAR* Name, EScope Scope = EScope::ThisTreeOnly) -> T
{
UObject* O = FindObjectByName(Name, Scope);
if (!O)
{
Output::send<LogLevel::Warning>(
STR("[WidgetTree] no widget named '{}' in this tree\n"), Name);
return T{};
}
if (!ObjectIsA<T>(O))
{
Output::send<LogLevel::Warning>(
STR("[WidgetTree] '{}' resolved to {}, which is not a {}\n"),
Name, O->GetFullName(), T::DebugTag());
return T{};
}
return T{ O };
}
template <typename T>
auto FindFirstOfType(EScope Scope = EScope::ThisTreeOnly) -> T
{
UClass* Cls = ClassOf<T>();
if (!Cls) return T{};
UObject* Found = nullptr;
ForEach([&](UObject* Node, int32) -> EVisit
{
if (Node->IsA(Cls)) { Found = Node; return EVisit::Stop; }
return EVisit::Continue;
}, Scope);
return Found ? T{ Found } : T{};
}
// Every widget of a given wrapper type, in traversal order.
template <typename T>
auto FindAllOfType(EScope Scope = EScope::ThisTreeOnly) -> std::vector<T>
{
std::vector<T> Out;
UClass* Cls = ClassOf<T>();
if (!Cls) return Out;
ForEach([&](UObject* Node, int32) -> EVisit
{
if (Node->IsA(Cls)) Out.emplace_back(Node);
return EVisit::Continue;
}, Scope);
return Out;
}
// Flat list of every widget object the walk reaches.
auto AllWidgetObjects(EScope Scope = EScope::ThisTreeOnly) -> std::vector<UObject*>
{
std::vector<UObject*> Out;
ForEach([&](UObject* Node, int32) -> EVisit
{
Out.push_back(Node);
return EVisit::Continue;
}, Scope);
return Out;
}
// ==================================================================
// DIAGNOSTICS
// ==================================================================
auto Dump(EScope Scope = EScope::IncludeNestedUserWidgets) -> void
{
if (!m_obj)
{
Output::send<LogLevel::Warning>(STR("[WidgetTree] Dump on an invalid tree\n"));
return;
}
Output::send<LogLevel::Warning>(
STR("[WidgetTree] dump of {}\n"), m_obj->GetFullName());
int32 Count = 0;
ForEach([&](UObject* Node, int32 Depth) -> EVisit
{
++Count;
std::basic_string<TCHAR> Indent;
Indent.reserve(static_cast<size_t>(Depth) * 2);
for (int32 i = 0; i < Depth; ++i) Indent += STR(" ");
UClass* Cls = Node->GetClassPrivate();
std::basic_string<TCHAR> ClassName =
Cls ? Cls->GetName() : std::basic_string<TCHAR>(STR("<no class>"));
if (ObjectIsA<PanelWidget>(Node))
{
Output::send<LogLevel::Warning>(
STR(" {}{} [{}] ({} children)\n"),
Indent.c_str(), Node->GetName().c_str(), ClassName.c_str(),
static_cast<int32>(PanelWidget{ Node }.GetChildObjects().size()));
}
else
{
Output::send<LogLevel::Warning>(
STR(" {}{} [{}]\n"),
Indent.c_str(), Node->GetName().c_str(), ClassName.c_str());
}
return EVisit::Continue;
}, Scope);
Output::send<LogLevel::Warning>(STR("[WidgetTree] {} widgets\n"), Count);
}
// ==================================================================
// CONSTRUCTION
// ==================================================================
auto ConstructWidget(UClass* WidgetClass, const TCHAR* InstanceName) -> UObject*
{
if (!m_obj)
{
Output::send<LogLevel::Warning>(
STR("[WidgetTree] ConstructWidget on an invalid tree\n"));
return nullptr;
}
if (!WidgetClass)
{
Output::send<LogLevel::Warning>(
STR("[WidgetTree] ConstructWidget: null class\n"));
return nullptr;
}
FStaticConstructObjectParameters Params{ WidgetClass };
Params.Outer = m_obj;
if (InstanceName) Params.Name = FName(InstanceName);
UObject* Obj = UObjectGlobals::StaticConstructObject(Params);
if (!Obj)
{
Output::send<LogLevel::Warning>(
STR("[WidgetTree] ConstructWidget: StaticConstructObject returned null for '{}'\n"),
InstanceName ? InstanceName : STR("<unnamed>"));
}
return Obj;
}
template <typename T>
auto ConstructWidget(const TCHAR* InstanceName) -> T
{
UObject* O = ConstructWidget(ClassOf<T>(), InstanceName);
return O ? T{ O } : T{};
}
private:
struct WalkState
{
EScope Scope;
int32 Budget;
bool bStopped;
bool bExhausted;
};
static auto OwnedTreeObject(UObject* Node) -> UObject*
{
return RawObject{ Node, STR("Widget") }.TryObjectProperty(STR("WidgetTree"));
}
template <typename Fn>
static auto WalkNode(UObject* Node, int32 Depth, WalkState& State, Fn& Visit) -> void
{
if (!Node || State.bStopped) return;
if (State.Budget-- <= 0 || Depth >= kMaxDepth)
{
State.bExhausted = true;
State.bStopped = true;
return;
}
const EVisit Result = Visit(Node, Depth);
if (Result == EVisit::Stop) { State.bStopped = true; return; }
if (Result == EVisit::SkipChildren) return;
if (UObject* NestedTree = OwnedTreeObject(Node))
{
if (State.Scope == EScope::IncludeNestedUserWidgets)
{
UObject* NestedRoot =
RawObject{ NestedTree, STR("WidgetTree") }.TryObjectProperty(STR("RootWidget"));
WalkNode(NestedRoot, Depth + 1, State, Visit);
}
return;
}
if (!ObjectIsA<PanelWidget>(Node)) return;
for (UObject* Child : PanelWidget{ Node }.GetChildObjects())
{
if (State.bStopped) return;
WalkNode(Child, Depth + 1, State, Visit);
}
}
};
}