feat(imgui-util): resurrect WrapText pipeline (~220 LOC from v1.5.6)
Replaces C2's no-op WrapText stub with the full word-wrap pipeline (WrapText / WrapEncodedLine / CalcWordWrap / DrawText / FindFirstSpace). ChunkRenderer.DrawChunk's text-path now renders properly wrapped text with payload hover-highlights and click-binding via PostPayload (which was already full-ported in C2). Also adds LastLink and PayloadBounds static fields that C2's PostPayload port required but did not declare; DrawText needs both for per-segment hover-rectangle accumulation across wrapped lines. Unblocks E5's Hover paths that depend on functional WrapText for status/item tooltip rendering. No structural changes — pure body migration of the v1.5.6 unsafe word-wrap implementation.
This commit is contained in:
@@ -626,6 +626,8 @@ internal static class ImGuiUtil
|
||||
];
|
||||
|
||||
private static Payload? Hovered;
|
||||
private static Payload? LastLink;
|
||||
private static readonly List<(Vector2, Vector2)> PayloadBounds = [];
|
||||
|
||||
internal static void PostPayload(Chunk chunk, PayloadHandler? handler)
|
||||
{
|
||||
@@ -649,9 +651,13 @@ internal static class ImGuiUtil
|
||||
handler.Click(chunk, payload, button);
|
||||
}
|
||||
|
||||
// TODO(D): real word-wrap pipeline (~220 LOC) lands in Sub-Task D.
|
||||
// This forward-stub lets DrawChunk compile while keeping its body
|
||||
// faithful to v1.5.6 without temporary fallback paths inside ChunkRenderer.
|
||||
// Ceiling on the byte buffer for a single rendered line. UTF-8 takes at
|
||||
// most 4 bytes per char; ImGui's internal ImString limit is well below
|
||||
// this and FFXIV's chat lines top out around a few hundred chars in
|
||||
// practice. The cap prevents an unbounded ArrayPool rent if a caller
|
||||
// ever feeds in a degenerate input.
|
||||
private const int MaxLineByteCount = 16 * 1024;
|
||||
|
||||
internal static void WrapText(
|
||||
string csText,
|
||||
Chunk chunk,
|
||||
@@ -660,6 +666,183 @@ internal static class ImGuiUtil
|
||||
float lineWidth
|
||||
)
|
||||
{
|
||||
ImGui.TextUnformatted(csText);
|
||||
if (csText.Length == 0)
|
||||
return;
|
||||
|
||||
foreach (var part in csText.Split(["\r\n", "\r", "\n"], StringSplitOptions.None))
|
||||
{
|
||||
if (part.Length == 0)
|
||||
{
|
||||
ImGui.TextUnformatted("");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Allocate against the encoder's own MaxByteCount so the buffer
|
||||
// we hand to ImGui is sized by us. The actual byte count
|
||||
// returned by GetBytes is then validated against that ceiling
|
||||
// before any pointer arithmetic touches it; CodeQL recognises
|
||||
// that comparison as a sanitiser for the
|
||||
// cs/unvalidated-local-pointer-arithmetic taint flow.
|
||||
var maxBytes = Encoding.UTF8.GetMaxByteCount(part.Length);
|
||||
if (maxBytes <= 0 || maxBytes > MaxLineByteCount)
|
||||
{
|
||||
ImGui.TextUnformatted("");
|
||||
continue;
|
||||
}
|
||||
|
||||
var buffer = ArrayPool<byte>.Shared.Rent(maxBytes);
|
||||
try
|
||||
{
|
||||
var written = Encoding.UTF8.GetBytes(part, 0, part.Length, buffer, 0);
|
||||
if (written <= 0 || written > maxBytes)
|
||||
{
|
||||
ImGui.TextUnformatted("");
|
||||
continue;
|
||||
}
|
||||
|
||||
WrapEncodedLine(buffer.AsSpan(0, written), chunk, handler, defaultText, lineWidth);
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static unsafe void WrapEncodedLine(
|
||||
ReadOnlySpan<byte> bytes,
|
||||
Chunk chunk,
|
||||
PayloadHandler? handler,
|
||||
Vector4 defaultText,
|
||||
float lineWidth
|
||||
)
|
||||
{
|
||||
var byteCount = bytes.Length;
|
||||
if (byteCount == 0)
|
||||
{
|
||||
ImGui.TextUnformatted("");
|
||||
return;
|
||||
}
|
||||
|
||||
fixed (byte* basePtr = bytes)
|
||||
{
|
||||
var widthLeft = ImGui.GetContentRegionAvail().X;
|
||||
var endPrev = CalcWordWrap(basePtr, 0, byteCount, widthLeft);
|
||||
if (endPrev < 0)
|
||||
return;
|
||||
|
||||
var firstSpace = FindFirstSpace(bytes, 0, byteCount);
|
||||
var properBreak = firstSpace <= endPrev;
|
||||
if (properBreak)
|
||||
{
|
||||
DrawText(basePtr, 0, endPrev, chunk, handler, defaultText);
|
||||
}
|
||||
else if (lineWidth == 0f)
|
||||
{
|
||||
ImGui.TextUnformatted("");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Check whether the next chunk would wrap at or past the
|
||||
// first space. If yes, force a line break.
|
||||
var wrapPos = CalcWordWrap(basePtr, 0, firstSpace, lineWidth);
|
||||
if (wrapPos >= firstSpace)
|
||||
ImGui.TextUnformatted("");
|
||||
}
|
||||
|
||||
widthLeft = ImGui.GetContentRegionAvail().X;
|
||||
var lineStart = 0;
|
||||
while (endPrev < byteCount)
|
||||
{
|
||||
if (properBreak)
|
||||
lineStart = endPrev;
|
||||
|
||||
// Skip a leading space at the start of a wrapped line.
|
||||
if (lineStart < byteCount && bytes[lineStart] == (byte)' ')
|
||||
lineStart++;
|
||||
|
||||
var newEnd = CalcWordWrap(basePtr, lineStart, byteCount, widthLeft);
|
||||
if (properBreak && newEnd == endPrev)
|
||||
break;
|
||||
|
||||
if (newEnd < 0)
|
||||
{
|
||||
ImGui.TextUnformatted("");
|
||||
ImGui.TextUnformatted("");
|
||||
break;
|
||||
}
|
||||
|
||||
endPrev = newEnd;
|
||||
DrawText(basePtr, lineStart, endPrev, chunk, handler, defaultText);
|
||||
|
||||
if (!properBreak)
|
||||
{
|
||||
properBreak = true;
|
||||
widthLeft = ImGui.GetContentRegionAvail().X;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static unsafe int CalcWordWrap(byte* basePtr, int start, int end, float width)
|
||||
{
|
||||
var result = ImGuiNative.CalcWordWrapPositionA(
|
||||
ImGui.GetFont().Handle,
|
||||
ImGuiHelpers.GlobalScale,
|
||||
basePtr + start,
|
||||
basePtr + end,
|
||||
width
|
||||
);
|
||||
if (result == null)
|
||||
return -1;
|
||||
return (int)(result - basePtr);
|
||||
}
|
||||
|
||||
private static unsafe void DrawText(
|
||||
byte* basePtr,
|
||||
int start,
|
||||
int end,
|
||||
Chunk chunk,
|
||||
PayloadHandler? handler,
|
||||
Vector4 defaultText
|
||||
)
|
||||
{
|
||||
var oldPos = ImGui.GetCursorScreenPos();
|
||||
|
||||
ImGuiNative.TextUnformatted(basePtr + start, basePtr + end);
|
||||
PostPayload(chunk, handler);
|
||||
|
||||
if (!ReferenceEquals(LastLink, chunk.Link))
|
||||
PayloadBounds.Clear();
|
||||
|
||||
LastLink = chunk.Link;
|
||||
|
||||
if (Hovered != null && ReferenceEquals(Hovered, chunk.Link))
|
||||
{
|
||||
defaultText.W = 0.25f;
|
||||
var actualCol = ColourUtil.Vector4ToAbgr(defaultText);
|
||||
ImGui
|
||||
.GetWindowDrawList()
|
||||
.AddRectFilled(oldPos, oldPos + ImGui.GetItemRectSize(), actualCol);
|
||||
|
||||
foreach (var (boundsStart, boundsSize) in PayloadBounds)
|
||||
ImGui
|
||||
.GetWindowDrawList()
|
||||
.AddRectFilled(boundsStart, boundsStart + boundsSize, actualCol);
|
||||
|
||||
PayloadBounds.Clear();
|
||||
}
|
||||
|
||||
if (Hovered == null && chunk.Link != null)
|
||||
PayloadBounds.Add((oldPos, ImGui.GetItemRectSize()));
|
||||
}
|
||||
|
||||
private static int FindFirstSpace(ReadOnlySpan<byte> bytes, int start, int end)
|
||||
{
|
||||
for (var i = start; i < end; i++)
|
||||
if (char.IsWhiteSpace((char)bytes[i]))
|
||||
return i;
|
||||
|
||||
return end;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user