feat(chat): retail text style — two-plane glyph outlines, authored SpewBox/chat styles
Campaign CH round 4, user-gate items 1+2. Root cause: retail ships a second (background) glyph atlas per font, dilated 2px on every side, plus two border-pixel scalars (Font.NumHorizontalBorderPixels/ NumVerticalBorderPixels) that acdream's font reader never read — so even the pre-existing outline parameter drew almost nothing once enabled. Landed together (either half alone is a no-op or a regression): - UiDatFont carries BorderX/BorderY from the DAT font resource. - UiRenderContext.DrawStringDat inflates the background blit's source and destination rect by that margin and restructures into retail's exact two-pass whole-string outline-then-fill model (UIElement_Text::DrawSelf), plus the 8-neighbour +-1px fallback for fonts with no background atlas. Corrects the stale "property 0xd" comment to the real ids, 0x21 (Outline) / 0x22 (OutlineColor). - LayoutDesc property 0x21/0x22 import (ElementInfo.Outline/ OutlineColor, LayoutImporter.ReadState, ElementReader.Merge/ ApplyCanonicalLegacyProjection, DatWidgetFactory.BuildText) so every authored-outline element across the DAT set is correct at once. - SpewBox: RetailFontId corrected from a round-3 heuristic (0x40000025) to the actually-authored 0x40000001 (18px bold serif), Outline=true set on the controller's UiText. Fill colour stays the user-gate-round-1-pinned yellow — font atlases are alpha-only (PFID_A8), so there is no baked shading that could explain the screenshot's gold as anything other than the outline itself. - Chat transcript: default fill now seeds from its authored ARGB(255,204,204,204) instead of an unrelated color-table slot (ChatTranscriptRenderer.BuildLines takes the transcript's own DefaultColor as a parameter); the 34-entry LogTextType table is untouched, and every existing CH1 conformance test stays green unmodified. Regenerated the committed chat_2100006f.json fixture from the real installed DAT, confirming end to end (not by missing-field default) that the transcript carries no outline. Tests: font-reader border fields + inflation math pinned against the real DAT font, two-pass draw ordering/tint/inflation via a new TextRenderer.DebugSpriteSegmentVerts test seam, property 0x21/0x22 import at both the ElementReader.Merge and StateDesc-property layers, SpewBox font/outline, and the chat default-shade seed with the color table proven untouched. Full Release suite: 12,610 passed / 4 skipped / 0 failed (AcDream.slnx, complete solution). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
5b54387b8e
commit
bcc34ee301
22 changed files with 1995 additions and 105 deletions
|
|
@ -115,6 +115,31 @@ public sealed class TextRenderer : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test-only: same submission-ordered segmentation as
|
||||
/// <see cref="DebugSpriteSegments"/>, but exposing the FULL per-vertex float
|
||||
/// buffer (8 floats/vertex: x,y,u,v,r,g,b,a — see <see cref="AppendQuad"/>,
|
||||
/// 6 vertices/quad) instead of just texture/count/alpha. Needed by the
|
||||
/// <c>UiRenderContext.DrawStringDat</c> two-pass outline/fill tests — proving
|
||||
/// draw ORDER (outline segment before fill segment), TINT (RGB, not just
|
||||
/// alpha), and the background-plane INFLATION (dest quad size + UV span)
|
||||
/// all requires more than <see cref="DebugSpriteSegments"/> exposes.
|
||||
/// <c>AcDream.App.Tests</c>-only via <c>InternalsVisibleTo</c>.
|
||||
/// </summary>
|
||||
internal IReadOnlyList<(uint Texture, IReadOnlyList<float> Verts)> DebugSpriteSegmentVerts
|
||||
{
|
||||
get
|
||||
{
|
||||
var result = new List<(uint, IReadOnlyList<float>)>(_segUsed);
|
||||
for (int i = 0; i < _segUsed; i++)
|
||||
{
|
||||
SpriteSeg seg = _spriteSegs[i];
|
||||
result.Add((seg.Texture, seg.Verts.ToArray()));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test-only snapshot of the current frame's queued NORMAL-layer BITMAP FONT
|
||||
/// text buffer (<see cref="DrawString"/>/<see cref="DrawStringClipped"/>,
|
||||
|
|
|
|||
|
|
@ -45,22 +45,43 @@ internal static class ChatTranscriptRenderer
|
|||
/// advancing for lines actually appended to THIS window's own scroll
|
||||
/// (<c>AppendStringInfoWithFont</c> only runs for displayed lines).
|
||||
/// </param>
|
||||
/// <param name="defaultColor">
|
||||
/// The transcript element's own base fill color — retail's
|
||||
/// <c>m_curFontColor</c> BEFORE any <c>AppendTextWithFont</c> call ever
|
||||
/// tints it, i.e. the value <c>DoFontReset</c> seeds from the element's
|
||||
/// authored LayoutDesc property <c>0x1B</c> (style <c>0x10000372</c>:
|
||||
/// <c>ARGB(255,204,204,204)</c> for the main chat transcript). Campaign
|
||||
/// CH round 4 (<c>docs/research/2026-08-10-retail-ui-text-style.md</c>
|
||||
/// §5.2 Fix 5): this used to be hardcoded to
|
||||
/// <c>RetailChatColorTable.TryGetColor(0x00u, ...)</c> (colorGreen) —
|
||||
/// the color table's OWN default-slot color, not the ELEMENT's authored
|
||||
/// default. The two are unrelated: the table governs the per-message
|
||||
/// <c>LogTextType</c> tint (untouched by this parameter — every message
|
||||
/// with an in-range type still resolves its own table color exactly as
|
||||
/// before), while <paramref name="defaultColor"/> is only the carried-
|
||||
/// forward seed for a line whose type falls OUTSIDE the table's 34
|
||||
/// entries (matching retail's out-of-range "leave <c>m_curFontColor</c>
|
||||
/// unchanged" rule — see <see cref="RetailChatColorTable"/>'s own doc).
|
||||
/// Callers pass their transcript's <see cref="UiText.DefaultColor"/>.
|
||||
/// </param>
|
||||
public static List<UiText.Line> BuildLines(
|
||||
IReadOnlyList<FormattedLine> detailed,
|
||||
float maxW,
|
||||
Func<string, float> measure,
|
||||
Func<uint, bool>? accept)
|
||||
Func<uint, bool>? accept,
|
||||
Vector4 defaultColor)
|
||||
{
|
||||
var result = new List<UiText.Line>(detailed.Count);
|
||||
if (detailed.Count == 0)
|
||||
return result;
|
||||
|
||||
// Retail's font-color state (m_curFontColor) persists across every
|
||||
// line actually appended to this window — an out-of-range LogTextType
|
||||
// leaves it unchanged rather than reverting to a default (color-table
|
||||
// doc §3.2). Seed the carry with retail's own unfilled-slot default
|
||||
// (colorGreen, index 0x00).
|
||||
RetailChatColorTable.TryGetColor(0x00u, out Vector4 currentColor);
|
||||
// Retail's font-color state (m_curFontColor) persists across every line
|
||||
// actually appended to this window — an out-of-range LogTextType leaves it
|
||||
// unchanged rather than reverting to a color-table default (color-table doc
|
||||
// §3.2). Seed the carry with the ELEMENT's own authored default fill
|
||||
// (defaultColor), matching retail's DoFontReset — not the color table's
|
||||
// unrelated index-0x00 slot.
|
||||
Vector4 currentColor = defaultColor;
|
||||
foreach (FormattedLine d in detailed)
|
||||
{
|
||||
if (accept is not null && !accept(d.LogTextType))
|
||||
|
|
|
|||
|
|
@ -697,7 +697,8 @@ public sealed class ChatWindowController : IRetainedWindowStateController, IReta
|
|||
// no more accept:null "no user filter" placeholder.
|
||||
bool Accept(uint logTextType) => _windowFilters.ShouldDisplay(
|
||||
ChatWindowState.MainWindowId, ChatWindowState.BroadcastTargetWindow, logTextType);
|
||||
var result = ChatTranscriptRenderer.BuildLines(detailed, maxW, measure, Accept);
|
||||
var result = ChatTranscriptRenderer.BuildLines(
|
||||
detailed, maxW, measure, Accept, Transcript.DefaultColor);
|
||||
return StoreTranscriptLayout(result, revision, filter, maxW, datFont, debugFont);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -612,6 +612,10 @@ public static class DatWidgetFactory
|
|||
FontColorPalette = ElementReader.ReadEffectiveColorPalette(
|
||||
info,
|
||||
0x1Bu),
|
||||
// Outline from dat property 0x21 (BoolBaseProperty). Default false — matches
|
||||
// ElementInfo.Outline's own default, so this is a no-op for the ~99% of text
|
||||
// elements that don't author it.
|
||||
Outline = info.Outline,
|
||||
};
|
||||
t.ConfigureDatState(info);
|
||||
|
||||
|
|
@ -622,6 +626,12 @@ public static class DatWidgetFactory
|
|||
if (info.FontColor.HasValue)
|
||||
t.DefaultColor = info.FontColor.Value;
|
||||
|
||||
// Outline color from dat property 0x22 (ColorBaseProperty). Only 9 elements in the
|
||||
// whole DAT set author a non-black value; when absent, UiText's own ctor default
|
||||
// (black, matching retail's m_curOutlineColor default) already applies.
|
||||
if (info.OutlineColor.HasValue)
|
||||
t.OutlineColor = info.OutlineColor.Value;
|
||||
|
||||
if (ResolveAuthoredString(info, stringResolve) is { Length: > 0 } authored)
|
||||
t.LinesProvider = () => [new UiText.Line(authored, t.DefaultColor)];
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using AcDream.App.UI;
|
||||
|
||||
namespace AcDream.App.UI.Layout;
|
||||
|
||||
|
|
@ -108,6 +109,26 @@ public sealed class ElementInfo
|
|||
/// </summary>
|
||||
public Vector4? FontColor;
|
||||
|
||||
/// <summary>
|
||||
/// Outline flag from dat <c>Properties[0x21]</c> (<c>BoolBaseProperty</c>). Retail
|
||||
/// <c>UIElement_Text::SetOutline @0x0046a81c</c> / <c>m_bitField & 0x10</c>.
|
||||
/// Default false (ctor <c>m_bitField=0x300</c> clears the outline bit) — outlining is
|
||||
/// opt-in per element. Propagated in <see cref="ElementReader.Merge"/> with the same
|
||||
/// "derived wins when true" rule used for <see cref="FontDid"/>.
|
||||
/// </summary>
|
||||
public bool Outline;
|
||||
|
||||
/// <summary>
|
||||
/// Outline color from dat <c>Properties[0x22]</c> (<c>ColorBaseProperty</c>). Retail
|
||||
/// <c>m_curOutlineColor</c>, ctor default <c>RGBAColor_Black</c>
|
||||
/// (<c>UIElement_Text::UIElement_Text @0x004686cb</c>). Null means "not authored" —
|
||||
/// the factory then leaves the widget at its own black default
|
||||
/// (<see cref="UiRenderContext.DefaultOutlineColor"/>). Propagated in
|
||||
/// <see cref="ElementReader.Merge"/> with the same "non-null derived wins" rule used
|
||||
/// for <see cref="FontColor"/>.
|
||||
/// </summary>
|
||||
public Vector4? OutlineColor;
|
||||
|
||||
/// <summary>
|
||||
/// Sprite per state: state name → (RenderSurface file id, DrawMode int).
|
||||
/// The <c>""</c> key represents the unnamed DirectState (<c>ElementDesc.StateDesc</c>).
|
||||
|
|
@ -307,6 +328,12 @@ public static class ElementReader
|
|||
// FontColor: derived wins when it has an explicit (non-null) color; otherwise inherit the base.
|
||||
// Null means "dat carried no 0x1B property" — so null-derived does NOT override a non-null base.
|
||||
FontColor = derived.FontColor ?? base_.FontColor,
|
||||
// Outline: derived wins when true (the dat property 0x21 was present and read as
|
||||
// true); otherwise inherit the base. False-derived never overrides a true base —
|
||||
// matching the FontDid/HJustify "non-default wins" convention.
|
||||
Outline = derived.Outline || base_.Outline,
|
||||
// OutlineColor: same "non-null derived wins" rule as FontColor.
|
||||
OutlineColor = derived.OutlineColor ?? base_.OutlineColor,
|
||||
// DefaultStateName: derived wins if set; otherwise inherit the base's default.
|
||||
DefaultStateName = !string.IsNullOrEmpty(derived.DefaultStateName) ? derived.DefaultStateName : base_.DefaultStateName,
|
||||
// This helper merges one element snapshot only. LayoutImporter separately
|
||||
|
|
@ -384,6 +411,33 @@ public static class ElementReader
|
|||
info.FontColor = new Vector4(c.Red / 255f, c.Green / 255f, c.Blue / 255f, alpha);
|
||||
}
|
||||
}
|
||||
|
||||
// Outline (0x21): BoolBaseProperty. Retail SetOutline @0x0046a81c / m_bitField & 0x10.
|
||||
if (info.TryGetEffectiveProperty(0x21u, out var outline)
|
||||
&& outline.Kind == UiPropertyKind.Bool)
|
||||
{
|
||||
info.Outline = outline.BoolValue;
|
||||
}
|
||||
|
||||
// OutlineColor (0x22): ColorBaseProperty (matches FontColor's Array-tolerant read —
|
||||
// no authored 0x22 is currently array-wrapped, but the fallback costs nothing).
|
||||
// Retail m_curOutlineColor, ctor default RGBAColor_Black.
|
||||
if (info.TryGetEffectiveProperty(0x22u, out var outlineColor))
|
||||
{
|
||||
UiPropertyValue? outlineColorValue = outlineColor.Kind == UiPropertyKind.Color
|
||||
? outlineColor
|
||||
: outlineColor.Kind == UiPropertyKind.Array
|
||||
&& outlineColor.ArrayValue.Count > 0
|
||||
&& outlineColor.ArrayValue[0].Kind == UiPropertyKind.Color
|
||||
? outlineColor.ArrayValue[0]
|
||||
: null;
|
||||
if (outlineColorValue is not null)
|
||||
{
|
||||
var c = outlineColorValue.ColorValue;
|
||||
float alpha = c.Alpha == 0 ? 1f : c.Alpha / 255f;
|
||||
info.OutlineColor = new Vector4(c.Red / 255f, c.Green / 255f, c.Blue / 255f, alpha);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -277,7 +277,8 @@ public sealed class FloatingChatWindowController : IRetainedPanelController
|
|||
|
||||
bool Accept(uint logTextType) => windowFilters.ShouldDisplay(
|
||||
WindowId, ChatWindowState.BroadcastTargetWindow, logTextType);
|
||||
var result = ChatTranscriptRenderer.BuildLines(detailed, maxW, measure, Accept);
|
||||
var result = ChatTranscriptRenderer.BuildLines(
|
||||
detailed, maxW, measure, Accept, Transcript.DefaultColor);
|
||||
|
||||
_cachedTranscriptRevision = revision;
|
||||
_cachedFilter = filter;
|
||||
|
|
|
|||
|
|
@ -559,6 +559,27 @@ public static class LayoutImporter
|
|||
float a = c.Alpha == 0 ? 1f : c.Alpha / 255f;
|
||||
info.FontColor = new System.Numerics.Vector4(c.Red / 255f, c.Green / 255f, c.Blue / 255f, a);
|
||||
}
|
||||
|
||||
// Outline (0x21): BoolBaseProperty. Retail SetOutline @0x0046a81c / m_bitField &
|
||||
// 0x10. Only update while still at the default (false); derived-wins handled in
|
||||
// ElementReader.Merge — same "only update if still at default" pattern as HJustify.
|
||||
if (!info.Outline
|
||||
&& sd.Properties.TryGetValue(0x21u, out var outlineRaw)
|
||||
&& outlineRaw is BoolBaseProperty outlineBool)
|
||||
{
|
||||
info.Outline = outlineBool.Value;
|
||||
}
|
||||
|
||||
// OutlineColor (0x22): ColorBaseProperty. Retail m_curOutlineColor, ctor default
|
||||
// RGBAColor_Black. Only read when not already set — same pattern as FontColor.
|
||||
if (info.OutlineColor is null
|
||||
&& sd.Properties.TryGetValue(0x22u, out var outlineColorRaw)
|
||||
&& outlineColorRaw is ColorBaseProperty outlineColorProp)
|
||||
{
|
||||
var oc = outlineColorProp.Value;
|
||||
float oa = oc.Alpha == 0 ? 1f : oc.Alpha / 255f;
|
||||
info.OutlineColor = new System.Numerics.Vector4(oc.Red / 255f, oc.Green / 255f, oc.Blue / 255f, oa);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -107,29 +107,74 @@ namespace AcDream.App.UI;
|
|||
/// </list>
|
||||
/// Register row AP-178 updated to record both dispositions.
|
||||
/// </remarks>
|
||||
/// <remarks>
|
||||
/// <b>Campaign CH user-gate round 4 (2026-08-10),
|
||||
/// <c>docs/research/2026-08-10-retail-ui-text-style.md</c>:</b> the earlier
|
||||
/// "absent from both dats" finding for the SpewBox's own line template
|
||||
/// (element <c>0x1000004A</c>, base style <c>0x10000377</c> in layout
|
||||
/// <c>0x2100003F</c>) was WRONG — it was missed because the element is a
|
||||
/// ROOT of its layout (a children-only walk skips it) and its font/color
|
||||
/// live in a BaseElement in a DIFFERENT LayoutDesc plus a NAMED state, not
|
||||
/// its own DirectState. Extending the sweep to walk roots, BaseElement/
|
||||
/// BaseLayoutId inheritance, and named-state properties found it, resolving
|
||||
/// three of AP-178's four remaining open sub-claims:
|
||||
/// <list type="bullet">
|
||||
/// <item><b>Font:</b> <see cref="RetailFontId"/> is now
|
||||
/// <b>0x40000001</b> (18px bold serif), not the round-3 smallest-font
|
||||
/// heuristic <c>0x40000025</c>. Three independent cross-checks: (1) base
|
||||
/// style <c>0x10000377</c> authors FontDID <c>[0x40000001]</c> directly;
|
||||
/// (2) the line template's authored height is 18px, exactly
|
||||
/// <c>0x40000001</c>'s <c>MaxCharHeight</c>; (3) 4 items × 18px = 72px,
|
||||
/// the authored box height (<see cref="SpewBoxHeight"/>).</item>
|
||||
/// <item><b>Outline:</b> the line template's state <c>0x10000002</c>
|
||||
/// authors property <c>0x21</c> (Outline) = <c>true</c>, outline color
|
||||
/// NOT authored → ctor default black
|
||||
/// (<see cref="UiRenderContext.DefaultOutlineColor"/>). This is the
|
||||
/// "heavy black border" the user's screenshot showed and the earlier
|
||||
/// rounds never reproduced — <see cref="UiText.Outline"/> is now set on
|
||||
/// construction.</item>
|
||||
/// <item><b>Position/extent:</b> confirmed AUTHORED, not merely
|
||||
/// user-matched by luck. <c>0x10000048</c> (the gmSpewBoxUI root) is a
|
||||
/// ROOT element of its own layout, so its parent is the viewport:
|
||||
/// <c>pos(0,0)</c>, edge codes <c>L3/R3</c> ("centered" per
|
||||
/// <c>ElementReader.ToAnchors</c>'s doc comment) and <c>T1</c>
|
||||
/// (top-anchored) resolve to a 450×72 block horizontally centered, flush
|
||||
/// to the viewport top — exactly what <see cref="TopOffset"/>=0 and the
|
||||
/// per-frame centered <c>Left</c> recompute already do. AP-178's position
|
||||
/// sub-claim retires.</item>
|
||||
/// </list>
|
||||
/// Only fill COLOR (see <see cref="SpewBoxColor"/> — the atlas is PFID_A8,
|
||||
/// alpha-only, so it cannot carry baked shading; the user-pinned yellow
|
||||
/// stands pending an exact retail measurement) and the AP-177 line-lifetime
|
||||
/// timeout remain open.
|
||||
/// </remarks>
|
||||
internal sealed class SpewBoxController : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Retail dat Font id this controller resolves for its text
|
||||
/// (Campaign CH user-gate round 3 — see the class remarks). Not
|
||||
/// retail's own measured SpewBox font (unmeasurable — no FontDid
|
||||
/// property was found on the authored element); the smallest DAT font
|
||||
/// confirmed in use by any currently-imported retail LayoutDesc,
|
||||
/// chosen so the rendered text is visibly smaller than the prior debug
|
||||
/// fallback, per the user's report.
|
||||
/// Retail dat Font id this controller resolves for its text. Campaign CH
|
||||
/// user-gate round 4 (2026-08-10): AUTHORED, not a heuristic — base
|
||||
/// style <c>0x10000377</c> (layout <c>0x2100003F</c>), which the
|
||||
/// SpewBox's own line template (<c>0x1000004A</c> in layout
|
||||
/// <c>0x21000011</c>) inherits, authors FontDID <c>[0x40000001]</c>
|
||||
/// directly. <c>0x40000001</c> is the classic AC bold-serif UI face at
|
||||
/// 18px (<c>MaxCharHeight=18</c>, matching the line template's own
|
||||
/// authored 18px height and the 4×18=72px authored box height). See the
|
||||
/// class remarks for the three-way cross-check and
|
||||
/// <c>docs/research/2026-08-10-retail-ui-text-style.md</c> §4.2.
|
||||
/// </summary>
|
||||
internal const uint RetailFontId = 0x40000025u;
|
||||
internal const uint RetailFontId = 0x40000001u;
|
||||
|
||||
/// <summary>
|
||||
/// Register row AP-178 (screen position): retail's authored ABSOLUTE
|
||||
/// screen position is still unknown — the LayoutDesc dump (see class
|
||||
/// remarks) recovered the element's position as <c>(0,0)</c> relative
|
||||
/// to a PARENT this sweep could not identify. Campaign CH user-gate
|
||||
/// round 3 (2026-08-10): the user reported the box was not flush to
|
||||
/// the very top of the screen; mounted at <c>0</c> now, per explicit
|
||||
/// user direction — still acdream's own placement choice pending the
|
||||
/// true retail parent/offset, but now matching the user's live report
|
||||
/// instead of an arbitrary 60px placeholder. (The SIBLING row AP-177 —
|
||||
/// Register row AP-178 (screen position): CONFIRMED authored, not a
|
||||
/// placeholder that happens to match. Campaign CH user-gate round 4
|
||||
/// (2026-08-10): the SpewBox root (<c>0x10000048</c>) is a ROOT element
|
||||
/// of its own layout (<c>0x21000011</c>), so its parent is the
|
||||
/// viewport — <c>pos(0,0)</c> plus edge codes <c>L3/R3</c> (centered)
|
||||
/// and <c>T1</c> (top-anchored) resolve to exactly this: a fixed-width
|
||||
/// block horizontally centered and flush to the viewport top. The
|
||||
/// round-3 "user-directed approximation pending the true retail
|
||||
/// parent/offset" framing is retired — the parent (the viewport root)
|
||||
/// and the offset (0) are now both resolved. (The SIBLING row AP-177 —
|
||||
/// the invented line-lifetime timeout — lives in
|
||||
/// <see cref="SpewBoxState.DefaultLifetime"/>'s own doc comment, not
|
||||
/// here; this controller does not own that concern.)
|
||||
|
|
@ -173,13 +218,22 @@ internal sealed class SpewBoxController : IDisposable
|
|||
/// (<c>colorBrightRed</c>) is still explicitly NOT this — retail's own
|
||||
/// <c>BuildChatColorLookupTable</c> writes to <c>ChatInterface::m_chatLog</c>,
|
||||
/// a completely different element tree the SpewBox never touches
|
||||
/// (research doc §3.2.3); the LayoutDesc dump (see class remarks) also
|
||||
/// never surfaced a colour property for this element. The exact retail
|
||||
/// value simply happens to coincide with the Tell colour, per the user's
|
||||
/// live observation. POSITION and FONT were re-addressed at Campaign CH
|
||||
/// user-gate round 3 (2026-08-10) — see the class remarks and the
|
||||
/// <see cref="TopOffset"/>/<see cref="RetailFontId"/> comments; both
|
||||
/// remain acdream-directed approximations, not resolved retail values.
|
||||
/// (research doc §3.2.3). POSITION and FONT were resolved as AUTHORED at
|
||||
/// Campaign CH user-gate round 4 (2026-08-10) — see the class remarks and
|
||||
/// the <see cref="TopOffset"/>/<see cref="RetailFontId"/> comments. The
|
||||
/// AUTHORED value for THIS element's state <c>0x10000002</c> is actually
|
||||
/// pure red <c>ARGB(255,255,0,0)</c> — but the user's live retail
|
||||
/// screenshot shows gold/amber, not red, so the user's own eye remains
|
||||
/// the axiom here (per <c>feedback_retail_oracle_no_whack_a_mole</c>).
|
||||
/// Round 4 checked whether font <c>0x40000001</c>'s FILL-plane atlas
|
||||
/// could explain the gold as baked shading over the pinned yellow: it
|
||||
/// cannot — every dat font atlas (this one included) is <c>PFID_A8</c>,
|
||||
/// alpha-only coverage with no per-pixel colour channel, so there is no
|
||||
/// baked tint to compose with. The gold reading is therefore the OUTLINE
|
||||
/// itself (a black border around a bright yellow glyph reads warmer/
|
||||
/// richer than the same colour drawn flat) rather than atlas shading.
|
||||
/// The user-pinned yellow stands; only an exact cdb capture of live
|
||||
/// retail's <c>m_curFontColor</c> would resolve the remaining gap.
|
||||
/// </summary>
|
||||
private static readonly Vector4 SpewBoxColor = new(1f, 1f, 0.247f, 1f);
|
||||
|
||||
|
|
@ -245,6 +299,12 @@ internal sealed class SpewBoxController : IDisposable
|
|||
ClickThrough = true,
|
||||
ZOrder = int.MaxValue,
|
||||
DefaultColor = SpewBoxColor,
|
||||
// Campaign CH user-gate round 4: the line template's authored state
|
||||
// 0x10000002 sets property 0x21 (Outline) = true with no authored
|
||||
// outline colour, i.e. the ctor black default
|
||||
// (UiRenderContext.DefaultOutlineColor) — the heavy black border in
|
||||
// the user's screenshot. See the class remarks.
|
||||
Outline = true,
|
||||
Visible = false,
|
||||
};
|
||||
_text.LinesProvider = () => _lines;
|
||||
|
|
|
|||
|
|
@ -12,16 +12,22 @@ namespace AcDream.App.UI;
|
|||
/// <summary>
|
||||
/// A retail dat-font (DB_TYPE_FONT, id range 0x40000000-0x40000FFF) ready for
|
||||
/// 2D drawing. Holds the two GL atlas textures (foreground glyph pixels +
|
||||
/// background outline/shadow), the per-glyph descriptor table, and the line
|
||||
/// background outline/shadow), the per-glyph descriptor table, the border-pixel
|
||||
/// inflation margin (<see cref="BorderX"/>/<see cref="BorderY"/>), and the line
|
||||
/// metrics, so <see cref="UiRenderContext.DrawStringDat"/> can blit each glyph
|
||||
/// as two textured quads exactly the way the retail client does.
|
||||
///
|
||||
/// <para>
|
||||
/// Retail render model — <c>SurfaceWindow::DrawCharacter</c>
|
||||
/// (acclient 0x00442bd0, Font::GetCharDesc + the two SurfaceWindow blits): for
|
||||
/// each glyph it copies the BACKGROUND atlas sub-rect first, tinted with the
|
||||
/// outline color (black), then the FOREGROUND atlas sub-rect, tinted with the
|
||||
/// requested text color. The pen advances by
|
||||
/// Retail render model — <c>UIElement_Text::DrawSelf</c> (acclient 0x00467aa0)
|
||||
/// runs the WHOLE glyph run TWICE when the element's outline flag (LayoutDesc
|
||||
/// property 0x21, <c>m_bitField & 0x10</c>) is set: pass 0 draws every glyph's
|
||||
/// outline (the BACKGROUND atlas sub-rect, inflated by <see cref="BorderX"/>/
|
||||
/// <see cref="BorderY"/> on every side, tinted with the element's outline color —
|
||||
/// ctor default black, property 0x22), then pass 1 draws every glyph's fill (the
|
||||
/// FOREGROUND atlas sub-rect, tinted with the requested text color) on top. With
|
||||
/// the outline flag clear, only pass 1 runs. <c>SurfaceWindow::DrawCharacter</c>
|
||||
/// (acclient 0x00442bd0, Font::GetCharDesc + the two SurfaceWindow blits) is the
|
||||
/// per-glyph blit each pass calls. The pen advances by
|
||||
/// <c>HorizontalOffsetBefore + Width + HorizontalOffsetAfter</c> (the function's
|
||||
/// return value, accumulated by the string loop at 0x00467ed4
|
||||
/// <c>edi_3 += var_98</c>), and each glyph is drawn starting at
|
||||
|
|
@ -34,8 +40,8 @@ namespace AcDream.App.UI;
|
|||
/// (255,255,255, alpha). The UI sprite shader path (ui_text.frag,
|
||||
/// <c>uUseTexture==2</c>) MULTIPLIES the sampled texel by the per-vertex tint
|
||||
/// (<c>texture(uTex,vUv) * vColor</c>), so tinting a white+alpha glyph by a
|
||||
/// color gives that color with the glyph's alpha — black for the outline pass,
|
||||
/// text color for the fill pass. No shader change was needed.
|
||||
/// color gives that color with the glyph's alpha — the outline color for the
|
||||
/// outline pass, text color for the fill pass. No shader change was needed.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class UiDatFont
|
||||
|
|
@ -60,19 +66,38 @@ public sealed class UiDatFont
|
|||
/// <summary>Distance from a line's top to its baseline (retail BaselineOffset).</summary>
|
||||
public float BaselineOffset { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Retail <c>Font::m_NumHorizontalBorderPixels</c> / <c>m_NumVerticalBorderPixels</c>
|
||||
/// (<c>Font::Serialize @0x00443650</c>; verified struct offsets <c>Font+0x40</c> /
|
||||
/// <c>Font+0x44</c>). The background (outline) atlas glyph is the foreground glyph
|
||||
/// dilated 2px on every side, sitting inside a margin this wide — measured: the FG
|
||||
/// glyph always sits at exactly <c>(BorderX, BorderY)</c> inside its inflated window,
|
||||
/// and the BG glyph's alpha bbox begins at <c>(BorderX-2, BorderY-2)</c>. Zero for
|
||||
/// fonts with no background atlas (e.g. the CJK/unicode family, <see cref="HasBackground"/>
|
||||
/// is false). <see cref="UiRenderContext.DrawStringDat"/> inflates the background blit's
|
||||
/// source AND destination rect by exactly this much on every side
|
||||
/// (<c>SurfaceWindow::DrawCharacter @0x00442d3a</c> + <c>CreateCharRectPair @0x00441480</c>)
|
||||
/// so the dilation is actually captured instead of cropped away.
|
||||
/// </summary>
|
||||
public int BorderX { get; }
|
||||
public int BorderY { get; }
|
||||
|
||||
private readonly Dictionary<char, FontCharDesc> _glyphs;
|
||||
|
||||
internal UiDatFont(
|
||||
uint fgTex, int fgW, int fgH,
|
||||
uint bgTex, int bgW, int bgH,
|
||||
float lineHeight, float baselineOffset,
|
||||
Dictionary<char, FontCharDesc> glyphs)
|
||||
Dictionary<char, FontCharDesc> glyphs,
|
||||
int borderX = 0, int borderY = 0)
|
||||
{
|
||||
ForegroundTexture = fgTex; ForegroundWidth = fgW; ForegroundHeight = fgH;
|
||||
BackgroundTexture = bgTex; BackgroundWidth = bgW; BackgroundHeight = bgH;
|
||||
LineHeight = lineHeight;
|
||||
BaselineOffset = baselineOffset;
|
||||
_glyphs = glyphs;
|
||||
BorderX = borderX;
|
||||
BorderY = borderY;
|
||||
}
|
||||
|
||||
/// <summary>True if this font carries a separate outline/shadow atlas
|
||||
|
|
@ -123,7 +148,9 @@ public sealed class UiDatFont
|
|||
bgTex, bgW, bgH,
|
||||
lineHeight: font.MaxCharHeight,
|
||||
baselineOffset: font.BaselineOffset,
|
||||
glyphs);
|
||||
glyphs,
|
||||
borderX: (int)font.NumHorizontalBorderPixels,
|
||||
borderY: (int)font.NumVerticalBorderPixels);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -202,28 +202,64 @@ public sealed class UiRenderContext
|
|||
TextRenderer.DrawString(f, text, screenX, screenY, alphaColor);
|
||||
}
|
||||
|
||||
/// <summary>Retail <c>UIElement_Text</c>'s constructor outline color default
|
||||
/// (<c>RGBAColor_Black</c>, <c>UIElement_Text::UIElement_Text @0x004686cb</c>).
|
||||
/// Used by <see cref="DrawStringDat"/> when the caller doesn't supply an
|
||||
/// explicit <c>outlineColor</c> (LayoutDesc property 0x22 is authored on only
|
||||
/// 9 elements in the whole DAT set — every other outlined element uses this).</summary>
|
||||
public static readonly Vector4 DefaultOutlineColor = new(0f, 0f, 0f, 1f);
|
||||
|
||||
/// <summary>
|
||||
/// Draw a single line of text with a retail dat font (<see cref="UiDatFont"/>),
|
||||
/// at <paramref name="x"/>,<paramref name="y"/> = the top-left of the
|
||||
/// typographic block (in this element's local space). Mirrors retail's
|
||||
/// <c>SurfaceWindow::DrawCharacter</c> (acclient 0x00442bd0): for each glyph
|
||||
/// the BACKGROUND atlas sub-rect is blitted first tinted black (the outline),
|
||||
/// then the FOREGROUND atlas sub-rect tinted <paramref name="color"/> (the
|
||||
/// fill). The pen advances by
|
||||
/// typographic block (in this element's local space). The pen advances by
|
||||
/// <c>HorizontalOffsetBefore + Width + HorizontalOffsetAfter</c> and each
|
||||
/// glyph is positioned at <c>pen + HorizontalOffsetBefore</c> on the X axis
|
||||
/// and at <c>baseline + VerticalOffsetBefore - (BaselineOffset)</c> via the
|
||||
/// glyph's OffsetY into the atlas.
|
||||
/// and at <c>baseline + VerticalOffsetBefore</c> via the glyph's OffsetY into
|
||||
/// the atlas.
|
||||
///
|
||||
/// <para><paramref name="outline"/> gates the black outline pass. Retail decides
|
||||
/// this PER text element: <c>UIElement_Text::DrawSelf</c> (acclient 0x00467aa0)
|
||||
/// runs the outline pass only when <c>m_bitField & 0x10</c> is set — i.e. the
|
||||
/// element called <c>SetOutline(true)</c> (LayoutDesc property 0xd). The DEFAULT
|
||||
/// is OFF (one fill-only pass): the talk-focus menu items set no outline, so an
|
||||
/// always-on outline shows as a grey halo over the solid menu panel. Pass
|
||||
/// <c>outline:true</c> only for elements retail outlines.</para>
|
||||
/// <para>
|
||||
/// <b>Two-pass outline model</b> — <c>UIElement_Text::DrawSelf</c>
|
||||
/// (acclient 0x00467aa0): <c>var_b0 = (m_bitField & 0x10) ? 0 : 1;</c> — when
|
||||
/// the outline bit is CLEAR the loop runs once (fill only, <c>var_b0==1</c>);
|
||||
/// when SET it runs twice, pass 0 (outline) for the WHOLE STRING first, then
|
||||
/// pass 1 (fill) for the whole string. Fills therefore always paint over every
|
||||
/// neighbour's outline — this method reproduces that by iterating the string
|
||||
/// twice rather than interleaving outline+fill per glyph, so on tight kerning
|
||||
/// glyph N+1's outline never covers glyph N's fill (nor vice versa).
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// <paramref name="outline"/> gates the outline passes. Retail decides this PER
|
||||
/// text element: the bit is <c>m_bitField & 0x10</c>, set by LayoutDesc
|
||||
/// property <b>0x21</b> (<c>SetOutline @0x0046a81c</c>) — NOT property 0xd (an
|
||||
/// earlier comment here named the wrong id: 0xd is the *switch-case index* inside
|
||||
/// <c>UIElement_Text::OnSetAttribute</c>, not the authored property). The DEFAULT
|
||||
/// is OFF (one fill-only pass, ctor <c>m_bitField=0x300</c> clears bit 0x10):
|
||||
/// outlining is opt-in per element (~100 authored rows across 15 layouts).
|
||||
/// <paramref name="outlineColor"/> is the element's <c>m_curOutlineColor</c>
|
||||
/// (LayoutDesc property 0x22, ctor default <see cref="DefaultOutlineColor"/> —
|
||||
/// black; only 9 elements in the whole DAT set author a non-black value).
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// <b>Outline mechanism</b> — retail picks by data, per font
|
||||
/// (<c>feedback_retail_dispatch_is_data_driven</c> shape): when
|
||||
/// <see cref="UiDatFont.HasBackground"/> is true, the outline pass blits the
|
||||
/// BACKGROUND (dilated) atlas sub-rect, inflated by
|
||||
/// <see cref="UiDatFont.BorderX"/>/<see cref="UiDatFont.BorderY"/> on every
|
||||
/// side of both the source AND destination rect
|
||||
/// (<c>SurfaceWindow::DrawCharacter @0x00442d3a</c> +
|
||||
/// <c>CreateCharRectPair @0x00441480</c>) — the un-inflated rect crops the
|
||||
/// dilation away, making the outline invisible even with the flag on. When the
|
||||
/// font has NO background atlas (the CJK/unicode family), retail falls back to
|
||||
/// 8 neighbour blits of the FOREGROUND glyph at ±1px
|
||||
/// (<c>UIElement_Text::DrawSelf</c> 0x00467d7e-0x00467e14).
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public void DrawStringDat(UiDatFont font, string text, float x, float y, Vector4 color, bool outline = false)
|
||||
public void DrawStringDat(
|
||||
UiDatFont font, string text, float x, float y, Vector4 color,
|
||||
bool outline = false, Vector4? outlineColor = null)
|
||||
{
|
||||
if (font is null || string.IsNullOrEmpty(text)) return;
|
||||
|
||||
|
|
@ -232,7 +268,6 @@ public sealed class UiRenderContext
|
|||
// anchor each glyph's quad at the line top (y) plus its VerticalOffsetBefore.
|
||||
float originX = _current.X + x;
|
||||
float originY = _current.Y + y;
|
||||
float pen = originX;
|
||||
|
||||
// Snap the LINE baseline to a whole pixel ONCE. Retail's
|
||||
// SurfaceWindow::DrawCharacter (acclient 0x00442bd0) takes an int32 pen Y
|
||||
|
|
@ -246,8 +281,27 @@ public sealed class UiRenderContext
|
|||
// line on one row and pixel-aligned.
|
||||
float baseY = System.MathF.Round(originY);
|
||||
|
||||
var outlineTint = new Vector4(0f, 0f, 0f, color.W);
|
||||
if (outline)
|
||||
{
|
||||
Vector4 oc = outlineColor ?? DefaultOutlineColor;
|
||||
var outlineTint = new Vector4(oc.X, oc.Y, oc.Z, color.W);
|
||||
// PASS 0 — outline, whole string (acclient 0x00467aa0, var_b0 starts at 0).
|
||||
DrawStringDatPass(font, text, originX, baseY, outlineTint, isOutlinePass: true);
|
||||
}
|
||||
|
||||
// PASS 1 (or the only pass, when outline is off) — fill, whole string.
|
||||
DrawStringDatPass(font, text, originX, baseY, color, isOutlinePass: false);
|
||||
}
|
||||
|
||||
/// <summary>One whole-string pass of <see cref="DrawStringDat"/>'s two-pass
|
||||
/// model: either every glyph's outline (<paramref name="isOutlinePass"/> true)
|
||||
/// or every glyph's fill. Recomputes the pen from scratch — a pure function of
|
||||
/// <paramref name="font"/>/<paramref name="text"/>, so both passes advance
|
||||
/// identically and stay in lock-step without sharing mutable state.</summary>
|
||||
private void DrawStringDatPass(
|
||||
UiDatFont font, string text, float originX, float baseY, Vector4 tint, bool isOutlinePass)
|
||||
{
|
||||
float pen = originX;
|
||||
for (int i = 0; i < text.Length; i++)
|
||||
{
|
||||
if (!font.TryGetGlyph(text[i], out var g))
|
||||
|
|
@ -255,7 +309,7 @@ public sealed class UiRenderContext
|
|||
|
||||
// Horizontal: snap each glyph's dest X to a whole pixel (the pen keeps its
|
||||
// true fractional advance). Vertical: integer baseline + integer per-glyph
|
||||
// offset — never an independent per-glyph round (see baseY note above).
|
||||
// offset — never an independent per-glyph round (see DrawStringDat's baseY note).
|
||||
float gx = System.MathF.Round(pen + g.HorizontalOffsetBefore);
|
||||
float gy = baseY + g.VerticalOffsetBefore;
|
||||
float gw = g.Width;
|
||||
|
|
@ -263,35 +317,76 @@ public sealed class UiRenderContext
|
|||
|
||||
if (gw > 0f && gh > 0f)
|
||||
{
|
||||
// Background (outline) atlas pass, tinted black — drawn behind. Gated by
|
||||
// `outline` (retail's per-element m_bitField & 0x10); off by default so UI
|
||||
// text is crisp fill-only and free of the grey halo over solid panels.
|
||||
// Both passes route through ApplyAlpha (applyAlpha: true) so a window's
|
||||
// opacity fades glyphs exactly like its chrome/background sprites — retail's
|
||||
// ChatInterface::SetOpacity (0x004F3120) fades the whole composited surface.
|
||||
if (outline && font.BackgroundTexture != 0)
|
||||
{
|
||||
var (bu0, bv0, bu1, bv1) = AtlasUv(
|
||||
g.OffsetX, g.OffsetY, g.Width, g.Height,
|
||||
font.BackgroundWidth, font.BackgroundHeight);
|
||||
DrawSpriteAbsolute(
|
||||
font.BackgroundTexture, gx, gy, gw, gh,
|
||||
bu0, bv0, bu1, bv1, outlineTint, applyAlpha: true);
|
||||
}
|
||||
|
||||
// Foreground (fill) atlas pass, tinted with the requested color.
|
||||
var (fu0, fv0, fu1, fv1) = AtlasUv(
|
||||
g.OffsetX, g.OffsetY, g.Width, g.Height,
|
||||
font.ForegroundWidth, font.ForegroundHeight);
|
||||
DrawSpriteAbsolute(
|
||||
font.ForegroundTexture, gx, gy, gw, gh,
|
||||
fu0, fv0, fu1, fv1, color, applyAlpha: true);
|
||||
if (isOutlinePass)
|
||||
DrawOutlineGlyph(font, g, gx, gy, gw, gh, tint);
|
||||
else
|
||||
DrawFillGlyph(font, g, gx, gy, gw, gh, tint);
|
||||
}
|
||||
|
||||
pen += UiDatFont.GlyphAdvance(g);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Foreground (fill) atlas blit, tinted with the requested text color.
|
||||
/// Both passes route through <c>DrawSpriteAbsolute(..., applyAlpha: true)</c> so a
|
||||
/// window's opacity fades glyphs exactly like its chrome/background sprites —
|
||||
/// retail's <c>ChatInterface::SetOpacity</c> (0x004F3120) fades the whole
|
||||
/// composited surface.</summary>
|
||||
private void DrawFillGlyph(
|
||||
UiDatFont font, DatReaderWriter.Types.FontCharDesc g,
|
||||
float gx, float gy, float gw, float gh, Vector4 tint)
|
||||
{
|
||||
var (fu0, fv0, fu1, fv1) = AtlasUv(
|
||||
g.OffsetX, g.OffsetY, g.Width, g.Height,
|
||||
font.ForegroundWidth, font.ForegroundHeight);
|
||||
DrawSpriteAbsolute(font.ForegroundTexture, gx, gy, gw, gh, fu0, fv0, fu1, fv1, tint, applyAlpha: true);
|
||||
}
|
||||
|
||||
/// <summary>Outline-pass blit for one glyph: the background (dilated) atlas
|
||||
/// sub-rect inflated by the font's border-pixel margin when the font carries
|
||||
/// one, else retail's 8-neighbour ±1px foreground-glyph fallback.</summary>
|
||||
private void DrawOutlineGlyph(
|
||||
UiDatFont font, DatReaderWriter.Types.FontCharDesc g,
|
||||
float gx, float gy, float gw, float gh, Vector4 tint)
|
||||
{
|
||||
if (font.BackgroundTexture != 0)
|
||||
{
|
||||
// Background (dilated) plane, inflated by (BorderX, BorderY) on every side of
|
||||
// BOTH the source sub-rect and the destination rect — CreateCharRectPair's
|
||||
// background call (0x00442d3a) inflates both symmetrically before the atlas
|
||||
// blit. The un-inflated rect (acdream's prior behavior) crops the dilation
|
||||
// away entirely, leaving only a single stray pixel where a descender pokes out.
|
||||
int bx = font.BorderX, by = font.BorderY;
|
||||
float ix = gx - bx;
|
||||
float iy = gy - by;
|
||||
float iw = gw + 2f * bx;
|
||||
float ih = gh + 2f * by;
|
||||
var (bu0, bv0, bu1, bv1) = AtlasUv(
|
||||
g.OffsetX - bx, g.OffsetY - by, (int)iw, (int)ih,
|
||||
font.BackgroundWidth, font.BackgroundHeight);
|
||||
DrawSpriteAbsolute(font.BackgroundTexture, ix, iy, iw, ih, bu0, bv0, bu1, bv1, tint, applyAlpha: true);
|
||||
}
|
||||
else
|
||||
{
|
||||
// No background plane (retail's 0-border fonts — the CJK/unicode family):
|
||||
// fall back to retail's 8-neighbour ±1px foreground-glyph blit
|
||||
// (UIElement_Text::DrawSelf 0x00467d7e-0x00467e14).
|
||||
var (fu0, fv0, fu1, fv1) = AtlasUv(
|
||||
g.OffsetX, g.OffsetY, g.Width, g.Height,
|
||||
font.ForegroundWidth, font.ForegroundHeight);
|
||||
for (int dy = -1; dy <= 1; dy++)
|
||||
{
|
||||
for (int dx = -1; dx <= 1; dx++)
|
||||
{
|
||||
if (dx == 0 && dy == 0) continue;
|
||||
DrawSpriteAbsolute(
|
||||
font.ForegroundTexture, gx + dx, gy + dy, gw, gh,
|
||||
fu0, fv0, fu1, fv1, tint, applyAlpha: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Convert an (OffsetX,OffsetY,Width,Height) atlas pixel sub-rect to
|
||||
/// normalized UVs for an atlas of <paramref name="atlasW"/> x
|
||||
/// <paramref name="atlasH"/>. Guards against a zero-sized atlas.</summary>
|
||||
|
|
|
|||
|
|
@ -90,6 +90,27 @@ public sealed class UiText : UiElement, IUiDatStateful
|
|||
/// the controller (e.g. <c>ChatWindowController</c>).</summary>
|
||||
public Vector4 BackgroundColor { get; set; } = new(0f, 0f, 0f, 0f);
|
||||
|
||||
/// <summary>
|
||||
/// Retail LayoutDesc property <c>0x21</c> (<c>UIElement_Text::SetOutline
|
||||
/// @0x0046a81c</c>, <c>m_bitField & 0x10</c>). When true, every dat-font draw
|
||||
/// on this element runs retail's two-pass outline+fill model
|
||||
/// (<see cref="UiRenderContext.DrawStringDat"/>). Default false, matching the
|
||||
/// ctor bitfield (<c>0x300</c>) which clears the outline bit — outlining is
|
||||
/// opt-in per element. Set by <see cref="AcDream.App.UI.Layout.DatWidgetFactory"/>
|
||||
/// from <see cref="AcDream.App.UI.Layout.ElementInfo.Outline"/> for DAT-imported
|
||||
/// text, or directly by a synthesized controller (e.g. the SpewBox).
|
||||
/// </summary>
|
||||
public bool Outline { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Retail LayoutDesc property <c>0x22</c> (<c>m_curOutlineColor</c>). Only
|
||||
/// meaningful when <see cref="Outline"/> is true. Default black, matching the
|
||||
/// ctor default (<c>RGBAColor_Black</c>,
|
||||
/// <c>UIElement_Text::UIElement_Text @0x004686cb</c>) — only 9 elements in the
|
||||
/// whole DAT set author a non-black outline color.
|
||||
/// </summary>
|
||||
public Vector4 OutlineColor { get; set; } = UiRenderContext.DefaultOutlineColor;
|
||||
|
||||
/// <summary>Optional dat state-sprite background (the element's own media), drawn
|
||||
/// UNDER the text. Set by DatWidgetFactory.BuildText from the ElementInfo. 0 = none.</summary>
|
||||
public uint BackgroundSprite { get; set; }
|
||||
|
|
@ -425,7 +446,7 @@ public sealed class UiText : UiElement, IUiDatStateful
|
|||
{
|
||||
float cx = (Width - cdf.MeasureWidth(line0.Text)) * 0.5f;
|
||||
float cy = VOffset(Height, cdf.LineHeight, Padding, VerticalJustify);
|
||||
ctx.DrawStringDat(cdf, line0.Text, cx, cy, line0.Color);
|
||||
ctx.DrawStringDat(cdf, line0.Text, cx, cy, line0.Color, Outline, OutlineColor);
|
||||
}
|
||||
else if ((Font ?? ctx.DefaultFont) is { } cbf)
|
||||
{
|
||||
|
|
@ -447,7 +468,7 @@ public sealed class UiText : UiElement, IUiDatStateful
|
|||
{
|
||||
float rx = Width - rdf.MeasureWidth(line0.Text) - Padding;
|
||||
float ry = VOffset(Height, rdf.LineHeight, Padding, VerticalJustify);
|
||||
ctx.DrawStringDat(rdf, line0.Text, rx, ry, line0.Color);
|
||||
ctx.DrawStringDat(rdf, line0.Text, rx, ry, line0.Color, Outline, OutlineColor);
|
||||
}
|
||||
else if ((Font ?? ctx.DefaultFont) is { } rbf)
|
||||
{
|
||||
|
|
@ -468,7 +489,7 @@ public sealed class UiText : UiElement, IUiDatStateful
|
|||
if (DatFont is { } datSingle)
|
||||
{
|
||||
float y = VOffset(Height, datSingle.LineHeight, Padding, VerticalJustify);
|
||||
ctx.DrawStringDat(datSingle, line0.Text, Padding, y, line0.Color);
|
||||
ctx.DrawStringDat(datSingle, line0.Text, Padding, y, line0.Color, Outline, OutlineColor);
|
||||
}
|
||||
else if ((Font ?? ctx.DefaultFont) is { } bitmapSingle)
|
||||
{
|
||||
|
|
@ -559,7 +580,7 @@ public sealed class UiText : UiElement, IUiDatStateful
|
|||
}
|
||||
|
||||
if (datFont is not null)
|
||||
ctx.DrawStringDat(datFont, text, lineX, y, lines[i].Color);
|
||||
ctx.DrawStringDat(datFont, text, lineX, y, lines[i].Color, Outline, OutlineColor);
|
||||
else
|
||||
ctx.DrawString(text, lineX, y, lines[i].Color, bitmapFont);
|
||||
}
|
||||
|
|
@ -602,7 +623,7 @@ public sealed class UiText : UiElement, IUiDatStateful
|
|||
if (run.Text.Length == 0) continue;
|
||||
if (datFont is not null)
|
||||
{
|
||||
ctx.DrawStringDat(datFont, run.Text, x, y, run.Color);
|
||||
ctx.DrawStringDat(datFont, run.Text, x, y, run.Color, Outline, OutlineColor);
|
||||
x += datFont.MeasureWidth(run.Text);
|
||||
}
|
||||
else
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue