acdream/src/AcDream.App/UI/Layout/UiDatElement.cs
Erik 621a4ab468 @
fix(D.2b): arrow swap, centered menu text, scrollbar-to-top, Send caption

- scroll arrows: native sprites are opposite (0x06004C6C up / 0x06004C69 down) per live
  visual — swap the assignment, drop the V-flip.
- menu labels centered vertically in each 17px row (was top-aligned, looked corrupt).
- scrollbar pulled up to the panel top so the top arrow meets the window border and the
  max/min button lines up with it (the 6px dat offset left a gap after the resize-bar reclaim).
- Send button: the dat sprite 0x06001915 is a blank gold frame (export-confirmed), so add a
  generic optional Label/LabelFont to UiDatElement and draw "Send" centered on it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@
2026-06-16 11:56:07 +02:00

122 lines
5.5 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>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);
}
}
}