290 lines
13 KiB
C++
290 lines
13 KiB
C++
|
|
#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;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
}
|