BUG 1 (tab bar): Tab group elements (0x10000228/229/538) are UiText with
ConsumesDatChildren=true so their 3 button children are consumed at import.
Fix: inject 3 sprite UiTexts per tab as CHILDREN OF LAYOUT ROOT at absolute
tab rects, ZOrder=8/9 so they draw over dat-imported UiTexts (ZOrder=1-3).
Original tab groups hidden. Active tab (Attributes) gold; inactive parchment.
BUG 2 (footer): Three root causes, all fixed.
(a) _byId stores LAST registered copy per id: stateA (0x10000240) was
the Titles-page copy, hidden by the page-visibility pass. Fixed by
walking root.Children to find the Attributes page (contains NameId)
then FindInSubtree for stateA within that subtree.
(b) Attributes-page stateB/stateC siblings (stacked at y=545) were still
Visible=True, drawing over stateA line-1/line-2. Fixed with
HideAllById walking the Attributes page subtree for ids 241/247.
(c) Footer label elements (H=17-18px, Padding=4f) were routed through
UiTexts scroll path: bottom-pinned baseY ended above the top clip
boundary, silently blanking all text. Fixed: LabelProvider sets
Padding=0f for directly-bound footer single-line labels.
UiDatElement.ElementId exposes _info.Id for subtree id-based walks.
676 tests pass, vitals panel unaffected (regression screenshot clean).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
127 lines
5.8 KiB
C#
127 lines
5.8 KiB
C#
using System;
|
||
using System.Numerics;
|
||
|
||
namespace AcDream.App.UI.Layout;
|
||
|
||
/// <summary>
|
||
/// Generic dat element: draws its active state's media by DrawMode (Normal=tile,
|
||
/// Alphablend/Overlay=blended overlay). The fallback renderer for every element type
|
||
/// without a dedicated behavioral widget (chrome corners/edges, drag bars, resize grips);
|
||
/// faithful because retail's base element render is exactly "stamp the media per draw-mode".
|
||
///
|
||
/// <para>
|
||
/// For Plan 1, all observed draw modes produce the same alpha-blended tiled quad — the
|
||
/// sprite shader already alpha-blends, so no per-mode branch is needed here. The named
|
||
/// constants document the real enum for Plan 2.
|
||
/// </para>
|
||
///
|
||
/// <para>
|
||
/// DrawModeType (DatReaderWriter.Enums), stored as int in <see cref="ElementInfo"/> to
|
||
/// keep this dat-free. See docs/research/2026-06-15-layoutdesc-format.md §6:
|
||
/// <c>Undefined=0, Normal=1, Overlay=2, Alphablend=3</c>. There is no Stretch mode.
|
||
/// </para>
|
||
///
|
||
/// <para>
|
||
/// Tiling uses UV-repeat on BOTH axes (<c>Width/tw</c>, <c>Height/th</c>) so vertical
|
||
/// chrome edges (e.g. a 5×10 sprite drawn over a 5×48 rect) tile vertically too.
|
||
/// <see cref="AcDream.App.Rendering.TextureCache.UploadRgba8"/> sets
|
||
/// <c>GL_REPEAT</c> on both S and T, so vertical tiling is always active.
|
||
/// </para>
|
||
/// </summary>
|
||
public sealed class UiDatElement : UiElement
|
||
{
|
||
// DrawModeType enum values from DatReaderWriter.Enums.
|
||
// See docs/research/2026-06-15-layoutdesc-format.md §6.
|
||
#pragma warning disable IDE0051 // private constants kept for documentation / Plan 2
|
||
private const int DrawUndefined = 0;
|
||
private const int DrawNormal = 1;
|
||
private const int DrawOverlay = 2;
|
||
private const int DrawAlphablend = 3;
|
||
#pragma warning restore IDE0051
|
||
|
||
private readonly ElementInfo _info;
|
||
private readonly Func<uint, (uint tex, int w, int h)> _resolve;
|
||
|
||
/// <summary>The dat element id from <see cref="ElementInfo.Id"/>. Exposed so controllers
|
||
/// can identify which logical element a UiDatElement represents when walking subtrees
|
||
/// (e.g. footer state groups that appear once per tab page but share the same dat id).</summary>
|
||
public uint ElementId => _info.Id;
|
||
|
||
/// <summary>Which state name to render. <c>""</c> = the unnamed DirectState.
|
||
/// Falls back to DirectState if the named state is absent.</summary>
|
||
public string ActiveState { get; set; } = "";
|
||
|
||
/// <param name="info">Merged <see cref="ElementInfo"/> for this element.</param>
|
||
/// <param name="resolve">Dat file-id → (GL texture handle, native px width, native px height).
|
||
/// Returns (0,0,0) when the texture is not yet uploaded.</param>
|
||
public UiDatElement(ElementInfo info, Func<uint, (uint tex, int w, int h)> resolve)
|
||
{
|
||
_info = info;
|
||
_resolve = resolve;
|
||
ClickThrough = true; // generic decoration; behavioral widgets opt back in
|
||
|
||
// Pick the initial active state: retail applies DefaultState when set; falls back
|
||
// to "Normal" when the element has a Normal-state sprite (retail's implicit default
|
||
// for stateful elements like tabs and buttons); else the unnamed DirectState ("").
|
||
if (!string.IsNullOrEmpty(info.DefaultStateName))
|
||
ActiveState = info.DefaultStateName;
|
||
else if (info.StateMedia.ContainsKey("Normal"))
|
||
ActiveState = "Normal";
|
||
// else ActiveState stays "" (DirectState)
|
||
}
|
||
|
||
/// <summary>
|
||
/// Returns the (File, DrawMode) for the current <see cref="ActiveState"/>,
|
||
/// falling back to the DirectState (<c>""</c> key) if the named state is absent.
|
||
/// Returns (0, 0) if neither exists.
|
||
/// </summary>
|
||
// exposed for unit testing
|
||
public (uint File, int DrawMode) ActiveMedia()
|
||
=> _info.StateMedia.TryGetValue(ActiveState, out var m) ? m
|
||
: _info.StateMedia.TryGetValue("", out var d) ? d
|
||
: (0u, 0);
|
||
|
||
/// <summary>Optional click handler. Set by a controller for interactive dat
|
||
/// elements (e.g. the chat Send / max-min buttons). Requires
|
||
/// <see cref="UiElement.ClickThrough"/> = false to receive click events.</summary>
|
||
public Action? OnClick { get; set; }
|
||
|
||
public override bool OnEvent(in UiEvent e)
|
||
{
|
||
if (e.Type == UiEventType.Click && OnClick is not null) { OnClick(); return true; }
|
||
return false;
|
||
}
|
||
|
||
/// <summary>Optional centered text label drawn over the sprite (e.g. the "Send"
|
||
/// button face whose dat sprite is a blank frame). Null = sprite only.</summary>
|
||
public string? Label { get; set; }
|
||
/// <summary>Dat font for <see cref="Label"/>. Required for the label to draw.</summary>
|
||
public UiDatFont? LabelFont { get; set; }
|
||
/// <summary>Label color (default white).</summary>
|
||
public Vector4 LabelColor { get; set; } = Vector4.One;
|
||
|
||
protected override void OnDraw(UiRenderContext ctx)
|
||
{
|
||
var (file, _) = ActiveMedia();
|
||
if (file != 0)
|
||
{
|
||
var (tex, tw, th) = _resolve(file);
|
||
if (tex != 0 && tw != 0 && th != 0)
|
||
{
|
||
// Normal → TILE at native size on both axes (UV-repeat; GL_REPEAT-wrapped UI
|
||
// texture), matching ImgTex::TileCSI. Overlay/Alphablend use the same blit (the
|
||
// sprite shader already alpha-blends). No Stretch mode exists in DrawModeType.
|
||
ctx.DrawSprite(tex, 0, 0, Width, Height, 0, 0, Width / tw, Height / th, Vector4.One);
|
||
}
|
||
}
|
||
|
||
// Centered text label over the sprite (retail draws button captions as text;
|
||
// their dat sprites are blank frames).
|
||
if (Label is { Length: > 0 } label && LabelFont is { } lf)
|
||
{
|
||
float tx = (Width - lf.MeasureWidth(label)) * 0.5f;
|
||
float ty = (Height - lf.LineHeight) * 0.5f;
|
||
ctx.DrawStringDat(lf, label, tx, ty, LabelColor);
|
||
}
|
||
}
|
||
}
|