fix(ui): round-5 review polish — S1 block outline pass, S2 non-UiText outline paths, S3 citation fix

Collects the post-gate polish left uncommitted by the killed round-5 agent
(S1/S3 + review fixes N1/N3/N4) and completes the missing S2 half:

- S1: UiText multi-line transcript + colored-run label now submit EVERY
  line/run's outline pass before ANY fill pass, matching retail's
  UIElement_Text::DrawSelf @0x00467aa0 whole-block walk. DrawStringDatPass
  is exposed for block-level batching; single lines keep DrawStringDat.
- S2 (completed this commit): authored outline 0x21/0x22 now reaches every
  text-bearing widget — UiButton, UiDatElement, UiField, UiMeter, UiMenu,
  UiCatalogSlot — seeded from the element's effective-default state exactly
  like UiText (BuildButton lifts the label-bearing Text child's authored
  value first, same chain as the label color). Per-STATE outline switching
  (dialog/character/combat buttons author 0x21 in state 0x3 only) is NOT
  ported — filed as register row AP-192 in this commit.
- S3: ChatWindowController reconciliation comment corrects the misread
  indicator action ids 0x10000514-17 -> 0x10000114-17 and re-attributes the
  id-coincidence to the pagination widget's m_prevButton/m_nextButton, not
  gmFriendsUI; register + window-shell research doc corrected to match.
- N1: LayoutImporter's duplicate per-state any-state-first-wins 0x21 read is
  deleted — ElementReader.ApplyCanonicalLegacyProjection's DirectState-then-
  effective-default resolution is the single source (the duplicate would have
  lit state-0x3-only outlines permanently once S2 widened consumption).
- N3: the outline pass tints with the outline color's OWN alpha, not the
  fill's (retail tints m_curOutlineColor and m_curTextColor independently).
- N4: the outline-inflated glyph SOURCE rect is clamped to the atlas bounds
  with matching dest shrink, porting CreateCharRectPair @0x00441480's edge
  behavior — edge glyphs crop instead of sampling a neighbour's texels.

Full Release suite: 12,610 passed / 4 skipped / 0 failed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-10 22:15:20 +02:00
parent ed0dbff90a
commit aa6635aebf
14 changed files with 258 additions and 66 deletions

View file

@ -545,6 +545,16 @@ public sealed class UiText : UiElement, IUiDatStateful
// Normalised selection span (start <= end), if any.
bool hasSel = TryGetOrderedSelection(out Pos selStart, out Pos selEnd);
// Gather each visible line's draw geometry first, rather than drawing text
// inline in this loop. Retail's UIElement_Text::DrawSelf @0x00467aa0 walks
// EVERY glyph of the whole BLOCK in the outline pass before any fill; calling
// DrawStringDat once per line here (outline+fill, outline+fill, ...) would let
// line N+1's outline draw AFTER line N's fill and notch a descender that pokes
// up into the line above (round-5 review S1). The DAT-font branch below instead
// submits every visible line's outline pass, THEN every visible line's fill
// pass. The bitmap-font branch has no outline concept and draws inline as before.
List<(string Text, float X, float Y, Vector4 Color)>? datLines = null;
for (int i = 0; i < lines.Count; i++)
{
float y = baseY + i * lh;
@ -574,15 +584,31 @@ public sealed class UiText : UiElement, IUiDatStateful
hw = bitmapFont.MeasureWidth(text.Substring(c0, c1 - c0));
}
// Highlight sits BEHIND the line's text → sprite bucket, submitted
// before this line's DrawStringDat.
// before this line's text (still true: this happens before either
// pass below runs for ANY line).
ctx.DrawFill(hx, y, hw, lh, SelectionColor);
}
}
if (datFont is not null)
ctx.DrawStringDat(datFont, text, lineX, y, lines[i].Color, Outline, OutlineColor);
{
(datLines ??= new()).Add((text, lineX, y, lines[i].Color));
}
else
{
ctx.DrawString(text, lineX, y, lines[i].Color, bitmapFont);
}
}
if (datLines is not null)
{
// Outline-OFF stays a single fill-only pass per line — byte-identical to
// the pre-S1 per-line DrawStringDat(outline:false) submission order.
if (Outline)
foreach (var line in datLines)
ctx.DrawStringDatPass(datFont!, line.Text, line.X, line.Y, OutlineColor, isOutlinePass: true);
foreach (var line in datLines)
ctx.DrawStringDatPass(datFont!, line.Text, line.X, line.Y, line.Color, isOutlinePass: false);
}
}
@ -618,16 +644,31 @@ public sealed class UiText : UiElement, IUiDatStateful
Padding,
VerticalJustify);
foreach (TextRun run in runs)
if (datFont is not null)
{
if (run.Text.Length == 0) continue;
if (datFont is not null)
// Same BLOCK-level outline-then-fill batching as the multi-line path above
// (round-5 review S1) — several colored runs on ONE line share one Outline
// flag, and retail's outline pass covers the whole block before any fill.
var runGeometry = new List<(string Text, float X, Vector4 Color)>();
float penX = x;
foreach (TextRun run in runs)
{
ctx.DrawStringDat(datFont, run.Text, x, y, run.Color, Outline, OutlineColor);
x += datFont.MeasureWidth(run.Text);
if (run.Text.Length == 0) continue;
runGeometry.Add((run.Text, penX, run.Color));
penX += datFont.MeasureWidth(run.Text);
}
else
if (Outline)
foreach (var run in runGeometry)
ctx.DrawStringDatPass(datFont, run.Text, run.X, y, OutlineColor, isOutlinePass: true);
foreach (var run in runGeometry)
ctx.DrawStringDatPass(datFont, run.Text, run.X, y, run.Color, isOutlinePass: false);
}
else
{
foreach (TextRun run in runs)
{
if (run.Text.Length == 0) continue;
ctx.DrawString(run.Text, x, y, run.Color, bitmapFont);
x += bitmapFont!.MeasureWidth(run.Text);
}