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? Hovered;
|
||||||
|
private static Payload? LastLink;
|
||||||
|
private static readonly List<(Vector2, Vector2)> PayloadBounds = [];
|
||||||
|
|
||||||
internal static void PostPayload(Chunk chunk, PayloadHandler? handler)
|
internal static void PostPayload(Chunk chunk, PayloadHandler? handler)
|
||||||
{
|
{
|
||||||
@@ -649,9 +651,13 @@ internal static class ImGuiUtil
|
|||||||
handler.Click(chunk, payload, button);
|
handler.Click(chunk, payload, button);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(D): real word-wrap pipeline (~220 LOC) lands in Sub-Task D.
|
// Ceiling on the byte buffer for a single rendered line. UTF-8 takes at
|
||||||
// This forward-stub lets DrawChunk compile while keeping its body
|
// most 4 bytes per char; ImGui's internal ImString limit is well below
|
||||||
// faithful to v1.5.6 without temporary fallback paths inside ChunkRenderer.
|
// 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(
|
internal static void WrapText(
|
||||||
string csText,
|
string csText,
|
||||||
Chunk chunk,
|
Chunk chunk,
|
||||||
@@ -660,6 +666,183 @@ internal static class ImGuiUtil
|
|||||||
float lineWidth
|
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