Files
PalUIExtension/Api/TextBlock.hpp

290 lines
13 KiB
C++
Raw Permalink Normal View History

2026-09-07 19:33:58 -04:00
#pragma once
#include <Unreal/FText.hpp>
#include "TextLayoutWidget.hpp"
namespace PalUIExtension::Api
{
class TextBlock : public TextLayoutWidget
{
public:
static auto ClassPath() -> const TCHAR* { return STR("/Script/UMG.TextBlock"); }
static auto DebugTag() -> const TCHAR* { return STR("TextBlock"); }
TextBlock() = default;
explicit TextBlock(UObject* Obj)
: TextLayoutWidget(Obj, ClassOf<TextBlock>(), DebugTag()) {}
// ---- text -----------------------------------------------------------
//
// Note:
// SetText takes FText BY VALUE on the engine side (unlike UWidget::SetToolTipText, which takes const FText&).
auto SetText(const FText& InText) -> bool
{
struct P { FText InText; } p{ InText };
return Call(STR("SetText"), p);
}
auto GetText() -> std::optional<FText>
{
return CallForReturn<FText>(STR("GetText"));
}
// ---- colour ---------------------------------------------------------
auto SetColorAndOpacity(FSlateColor_ InColorAndOpacity) -> bool
{
struct P { FSlateColor_ InColorAndOpacity; } p{ InColorAndOpacity };
return Call(STR("SetColorAndOpacity"), p);
}
// Convenience: the common case is a literal colour, styling mode 0.
auto SetColor(FLinearColor_ Color) -> bool
{
return SetColorAndOpacity(FSlateColor_{ Color, static_cast<uint8>(ESlateColorStylingMode_::UseColor_Specified) });
}
auto SetOpacity(float InOpacity) -> bool
{
struct P { float InOpacity; } p{ InOpacity };
return Call(STR("SetOpacity"), p);
}
auto SetShadowOffset(FVector2D_ InShadowOffset) -> bool
{
struct P { FVector2D_ InShadowOffset; } p{ InShadowOffset };
return Call(STR("SetShadowOffset"), p);
}
auto SetShadowColorAndOpacity(FLinearColor_ InColor) -> bool
{
struct P { FLinearColor_ InColor; } p{ InColor };
return Call(STR("SetShadowColorAndOpacity"), p);
}
// ---- layout / policy -------------------------------------------------
auto SetMinDesiredWidth(float InMinDesiredWidth) -> bool
{
struct P { float InMinDesiredWidth; } p{ InMinDesiredWidth };
return Call(STR("SetMinDesiredWidth"), p);
}
auto SetAutoWrapText(bool InAutoTextWrap) -> bool
{
struct P { bool InAutoTextWrap; } p{ InAutoTextWrap };
return Call(STR("SetAutoWrapText"), p);
}
auto SetTextTransformPolicy(ETextTransformPolicy_ InTransformPolicy) -> bool
{
struct P { ETextTransformPolicy_ InTransformPolicy; } p{ InTransformPolicy };
return Call(STR("SetTextTransformPolicy"), p);
}
auto SetTextOverflowPolicy(ETextOverflowPolicy_ InOverflowPolicy) -> bool
{
struct P { ETextOverflowPolicy_ InOverflowPolicy; } p{ InOverflowPolicy };
return Call(STR("SetTextOverflowPolicy"), p);
}
// ---- materials -------------------------------------------------------
// Plain object pointers. Nothing subtle here.
auto SetFontMaterial(UObject* InMaterial) -> bool
{
struct P { UObject* InMaterial; } p{ InMaterial };
return Call(STR("SetFontMaterial"), p);
}
auto SetFontOutlineMaterial(UObject* InMaterial) -> bool
{
struct P { UObject* InMaterial; } p{ InMaterial };
return Call(STR("SetFontOutlineMaterial"), p);
}
auto GetDynamicFontMaterial() -> std::optional<UObject*> { return CallForReturn<UObject*>(STR("GetDynamicFontMaterial")); }
auto GetDynamicOutlineMaterial() -> std::optional<UObject*> { return CallForReturn<UObject*>(STR("GetDynamicOutlineMaterial")); }
// ---- property reads ---------------------------------------------------
auto GetMinDesiredWidth() const -> std::optional<float> { return PropertyValue<float>(STR("MinDesiredWidth")); }
auto GetShadowOffset() const -> std::optional<FVector2D_> { return PropertyValue<FVector2D_>(STR("ShadowOffset")); }
auto GetShadowColorAndOpacity()const -> std::optional<FLinearColor_> { return PropertyValue<FLinearColor_>(STR("ShadowColorAndOpacity")); }
auto GetColorAndOpacity() const -> std::optional<FSlateColor_> { return PropertyValue<FSlateColor_>(STR("ColorAndOpacity")); }
auto GetTextTransformPolicy() const -> std::optional<ETextTransformPolicy_>{ return PropertyValue<ETextTransformPolicy_>(STR("TextTransformPolicy")); }
auto GetTextOverflowPolicy() const -> std::optional<ETextOverflowPolicy_> { return PropertyValue<ETextOverflowPolicy_>(STR("TextOverflowPolicy")); }
auto GetSimpleTextMode() const -> std::optional<bool> { return PropertyValue<bool>(STR("bSimpleTextMode")); }
auto GetWrapWithInvalidationPanel() const -> std::optional<bool> { return PropertyValue<bool>(STR("bWrapWithInvalidationPanel")); }
// ======================================================================
// FONT
//
// SetFont(FSlateFontInfo) and SetStrikeBrush(FSlateBrush) are NOT
// exposed as value-taking calls, and this is not caution for its
// own sake:
//
// FSlateFontInfo holds TSharedPtr<const FCompositeFont>.
// FSlateBrush holds FSlateResourceHandle, which holds a shared ref.
//
// Any POD mirror you build will copy those pointers without touching
// their refcounts. Then either the engine's copy-assign increments a
// count you never decrement (leak), or a destructor runs on a count
// you never incremented (a free of a live font, surfacing as a crash
// in the renderer somewhere unrelated). There is no struct layout
// that fixes this -- the problem is that these types are not data.
//
// What works instead: mutate the live struct in place through
// reflection. No copy is ever made, so no refcount is ever touched.
// ======================================================================
// Reads the current font size. Whether the engine stores it as float
// or int32 varies by engine version and both are 4 bytes, so the
// size check cannot tell them apart. This logs both readings once --
// exactly one of them will be a sane font size (something like 12 to
// 48); that tells you which typed setter to call. Run it once, then
// delete the call.
auto LogFontSize() const -> void
{
void* SizePtr = FontMemberPtr(STR("Size"), 4);
if (!SizePtr) return;
float AsFloat = *reinterpret_cast<float*>(SizePtr);
int32 AsInt = *reinterpret_cast<int32*>(SizePtr);
Output::send<LogLevel::Warning>(
STR("[TextBlock] Font.Size reads as float={} / int32={} -- ")
STR("whichever looks like a font size is the real type\n"),
AsFloat, AsInt);
}
auto SetFontSizeFloat(float NewSize) -> bool
{
if (void* P = FontMemberPtr(STR("Size"), 4)) { *reinterpret_cast<float*>(P) = NewSize; return PushFontToSlate(); }
return false;
}
auto SetFontSizeInt(int32 NewSize) -> bool
{
if (void* P = FontMemberPtr(STR("Size"), 4)) { *reinterpret_cast<int32*>(P) = NewSize; return PushFontToSlate(); }
return false;
}
auto SetFontLetterSpacing(int32 Value) -> bool
{
if (void* P = FontMemberPtr(STR("LetterSpacing"), 4)) { *reinterpret_cast<int32*>(P) = Value; return PushFontToSlate(); }
return false;
}
auto SetFontSkewAmount(float Value) -> bool
{
if (void* P = FontMemberPtr(STR("SkewAmount"), 4)) { *reinterpret_cast<float*>(P) = Value; return PushFontToSlate(); }
return false;
}
// Sets the named typeface (e.g. "Bold", "Regular", "Italic") on the
// live font. FName is a trivially-copyable index pair, so this one
// is a safe direct write.
auto SetFontTypeface(FName TypefaceName) -> bool
{
if (void* P = FontMemberPtr(STR("TypefaceFontName"), sizeof(FName)))
{
*reinterpret_cast<FName*>(P) = TypefaceName;
return PushFontToSlate();
}
return false;
}
// Raw access, for anything not covered above. Returns null and logs
// if the member is missing or is not the size you expected.
auto FontMemberPtr(const TCHAR* MemberName, size_t ExpectedSize) const -> void*
{
if (!m_obj) return nullptr;
FProperty* FontProp = m_obj->GetPropertyByNameInChain(STR("Font"));
if (!FontProp)
{
Output::send<LogLevel::Warning>(STR("[TextBlock] no 'Font' property\n"));
return nullptr;
}
// FStructProperty::GetStruct() gives the UScriptStruct whose
// property chain describes FSlateFontInfo's members.
auto* AsStruct = static_cast<FStructProperty*>(FontProp);
UScriptStruct* FontStruct = AsStruct->GetStruct();
if (!FontStruct)
{
Output::send<LogLevel::Warning>(STR("[TextBlock] 'Font' is not a struct property\n"));
return nullptr;
}
// The `TFieldRange iter = FontStruct->ForEachProperty();` that
// used to sit here was a leftover from before this was a
// range-for: it built a second, unused range object every call.
FProperty* Member = nullptr;
for (FProperty* Property : FontStruct->ForEachProperty())
{
if (Property && Property->GetName() == MemberName) { Member = Property; break; }
}
if (!Member)
{
Output::send<LogLevel::Warning>(
STR("[TextBlock] FSlateFontInfo has no member '{}' on this build\n"), MemberName);
return nullptr;
}
if (static_cast<size_t>(Member->GetSize()) != ExpectedSize)
{
Output::send<LogLevel::Warning>(
STR("[TextBlock] FSlateFontInfo::{} is {} bytes, expected {}\n"),
MemberName, Member->GetSize(), ExpectedSize);
return nullptr;
}
uint8* FontBase = reinterpret_cast<uint8*>(m_obj) + FontProp->GetOffset_Internal();
return FontBase + Member->GetOffset_Internal();
}
private:
// ------------------------------------------------------------------
// The in-place write above changes UTextBlock::Font but does not
// reach the SSlateTextBlock -- that only happens inside
// SynchronizeProperties, which the setters call and a raw write
// does not. There is no UFUNCTION that forces a resync.
//
// So this calls SetFont with the params block pointing AT THE LIVE
// Font ITSELF. The engine does Font = InFontInfo, a real C++
// self-assignment (safe for TSharedPtr and FName), and then runs
// the resync. Nothing is copied on our side, so nothing can get the
// refcount wrong -- with one open question:
//
// Does UE4SS's ProcessEvent destruct the params buffer after the
// call? If it does, this destructs the widget's live Font.
//
// Evidence it does not: your existing FTextFromString passes an
// FString and reads back an FText through this same path, repeatedly,
// without crashing. That is suggestive, not proof -- a refcount
// underflow frees memory that stays readable until something else
// claims it, so the crash lands far from the cause.
//
// This is the single highest-value thing to nail down in this
// codebase: the answer unlocks FText, FSlateFontInfo, FSlateBrush
// and TArray returns all at once. Test it in isolation: pass an
// FString whose refcount you can watch, call through ProcessEvent,
// and read the count after.
// ------------------------------------------------------------------
auto PushFontToSlate() -> bool
{
if (!m_obj) return false;
FProperty* FontProp = m_obj->GetPropertyByNameInChain(STR("Font"));
if (!FontProp) return false;
void* LiveFont = reinterpret_cast<uint8*>(m_obj) + FontProp->GetOffset_Internal();
UFunction* Fn = m_obj->GetFunctionByNameInChain(STR("SetFont"));
if (!Fn)
{
Output::send<LogLevel::Warning>(STR("[TextBlock] SetFont not found\n"));
return false;
}
m_obj->ProcessEvent(Fn, LiveFont);
return true;
}
};
}