Introduces UiButton: a dedicated dat-widget button that ports UIElement_Button (RegisterElementClass(1,...) @ acclient_2013_pseudo_c.txt:125828). State selection, tiled DrawSprite, and label rendering mirror UiDatElement exactly so the chat Send and Max/Min buttons have zero behavioral change. DatWidgetFactory now maps Type 1 → UiButton (beside Type 7 → UiMeter, Type 11 → UiScrollbar). ChatWindowController's Send and Max/Min bind blocks updated from UiDatElement casts to UiButton casts; ClickThrough=false lines dropped (UiButton is interactive by construction). The old UiPanel.cs UiButton (a plain dev-scaffold rect+text button with no dat sprites) is renamed UiSimpleButton to free the name — no production code instantiated it. Full suite: 402 passed, 2 skipped, 0 failed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
180 lines
8.1 KiB
C#
180 lines
8.1 KiB
C#
using System;
|
|
using System.Linq;
|
|
using AcDream.App.UI;
|
|
|
|
namespace AcDream.App.UI.Layout;
|
|
|
|
/// <summary>
|
|
/// Hybrid factory: behavioral element Types map to dedicated widgets (verbatim
|
|
/// algorithm ports); everything else (and unknown Types) falls back to
|
|
/// <see cref="UiDatElement"/>.
|
|
///
|
|
/// <para>
|
|
/// Type 12 elements that carry NO own state media (pure style prototypes /
|
|
/// BaseElement stores) return null from <see cref="Create"/> and are skipped.
|
|
/// Type 12 elements that DO carry own sprites (e.g. a chat element whose Type-0
|
|
/// derived form inherited Type 12 from its base prototype) are rendered normally.
|
|
/// See docs/research/2026-06-15-layoutdesc-format.md Correction 8.
|
|
/// </para>
|
|
///
|
|
/// <para>
|
|
/// The meter's back/front 3-slice sprite ids live on grandchild image elements,
|
|
/// NOT on the meter element itself (format doc §11). <see cref="BuildMeter"/>
|
|
/// walks two layers down to extract them: the two Type-3 container children
|
|
/// ordered by <see cref="ElementInfo.ReadOrder"/> (back behind = lower, front
|
|
/// on top = higher), then within each container the image children that carry
|
|
/// a DirectState ("" key) sprite, ordered by their X position to obtain
|
|
/// left-cap / center-tile / right-cap.
|
|
/// </para>
|
|
///
|
|
/// <para>
|
|
/// The expand-detail overlay present in the front container carries ONLY named
|
|
/// states ("HideDetail"/"ShowDetail") — no "" DirectState entry — so the
|
|
/// <c>TryGetValue("")</c> filter in <see cref="SliceIds"/> excludes it
|
|
/// automatically.
|
|
/// </para>
|
|
/// </summary>
|
|
public static class DatWidgetFactory
|
|
{
|
|
/// <summary>
|
|
/// Creates the <see cref="UiElement"/> for <paramref name="info"/>, sets its
|
|
/// rect (Left/Top/Width/Height) and Anchors, and returns it.
|
|
/// </summary>
|
|
/// <param name="info">Resolved, merged element snapshot from the LayoutDesc importer.</param>
|
|
/// <param name="resolve">RenderSurface id → (GL tex handle, pixel width, pixel height).
|
|
/// Returns (0,0,0) when the texture is not yet uploaded.</param>
|
|
/// <param name="datFont">Retail UI font for the meter's "cur/max" number overlay.
|
|
/// May be null pre-load — the meter falls back to the debug bitmap font.</param>
|
|
/// <returns>The widget, or <c>null</c> for a pure Type-12 style prototype with no own sprites (caller skips it).</returns>
|
|
public static UiElement? Create(ElementInfo info,
|
|
Func<uint, (uint, int, int)> resolve, UiDatFont? datFont)
|
|
{
|
|
// Type 12 = style prototype / BaseElement store referenced by BaseLayoutId.
|
|
// PURE prototypes (no own state media) are property bags — never rendered; skip them.
|
|
// A Type-12 element that carries its own state media (e.g. a chat Send button whose
|
|
// Type-0 derived element inherited Type 12 from its base prototype) has sprites to
|
|
// show and must render. See format doc §8 and the G1 task note.
|
|
if (info.Type == 12 && info.StateMedia.Count == 0) return null;
|
|
|
|
UiElement e = info.Type switch
|
|
{
|
|
1 => new UiButton(info, resolve), // UIElement_Button (reg :125828)
|
|
7 => BuildMeter(info, resolve, datFont), // UIElement_Meter
|
|
11 => new UiScrollbar(), // UIElement_Scrollbar (reg :124137)
|
|
_ => new UiDatElement(info, resolve), // generic fallback for all other types
|
|
};
|
|
|
|
// Propagate position + size (pixel-exact from the dat).
|
|
e.Left = info.X;
|
|
e.Top = info.Y;
|
|
e.Width = info.Width;
|
|
e.Height = info.Height;
|
|
|
|
// Honor the dat's draw order so overlapping pieces (grip overlay over bevel chrome) layer correctly.
|
|
e.ZOrder = (int)info.ReadOrder;
|
|
|
|
// Map the four raw edge-anchor values to the AnchorEdges bit-flag that the
|
|
// UI layout engine uses for reflow.
|
|
e.Anchors = ElementReader.ToAnchors(info.Left, info.Top, info.Right, info.Bottom);
|
|
|
|
return e;
|
|
}
|
|
|
|
// ── Meter ────────────────────────────────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Builds a <see cref="UiMeter"/> and populates its six 3-slice sprite ids by
|
|
/// reading the meter's grandchild image elements (format doc §11).
|
|
///
|
|
/// <para>
|
|
/// Structure the importer produces for each meter (UIElement_Meter):
|
|
/// <code>
|
|
/// meter (Type 7)
|
|
/// ├── back-layer container (Type 3, lower ReadOrder — drawn first / behind)
|
|
/// │ ├── left-cap image (DirectState "" → File = back-left sprite)
|
|
/// │ ├── center image (DirectState "" → File = back-tile sprite)
|
|
/// │ └── right-cap image (DirectState "" → File = back-right sprite)
|
|
/// ├── front-layer container (Type 3, higher ReadOrder — drawn on top)
|
|
/// │ ├── left-cap image (→ front-left sprite)
|
|
/// │ ├── center image (→ front-tile sprite)
|
|
/// │ ├── right-cap image (→ front-right sprite)
|
|
/// │ └── expand overlay (named "ShowDetail"/"HideDetail" only — NO DirectState — IGNORED)
|
|
/// └── text label (Type 0) (IGNORED — Fill/Label providers bound by VitalsController in Task 6)
|
|
/// </code>
|
|
/// </para>
|
|
///
|
|
/// <para>
|
|
/// <see cref="UiMeter.Fill"/> and <see cref="UiMeter.Label"/> are NOT set here.
|
|
/// They are bound to the live stat providers in Task 6 (VitalsController).
|
|
/// </para>
|
|
/// </summary>
|
|
private static UiMeter BuildMeter(ElementInfo info,
|
|
Func<uint, (uint, int, int)> resolve, UiDatFont? datFont)
|
|
{
|
|
var m = new UiMeter
|
|
{
|
|
SpriteResolve = resolve,
|
|
DatFont = datFont,
|
|
};
|
|
|
|
// The two 3-slice containers are Type-3 children of the meter element.
|
|
// ReadOrder determines draw order: the back track has a LOWER ReadOrder
|
|
// (drawn first, behind the fill), the front has a HIGHER ReadOrder (on top).
|
|
var containers = info.Children
|
|
.Where(c => c.Type == 3)
|
|
.OrderBy(c => c.ReadOrder)
|
|
.ToList();
|
|
|
|
if (containers.Count != 2)
|
|
Console.WriteLine($"[D.2b] meter 0x{info.Id:X8}: {containers.Count} Type-3 slice containers (expected 2) — bars may render as solid-color fallback.");
|
|
|
|
if (containers.Count >= 1)
|
|
{
|
|
var (l, t, r) = SliceIds(containers[0]);
|
|
m.BackLeft = l;
|
|
m.BackTile = t;
|
|
m.BackRight = r;
|
|
}
|
|
|
|
if (containers.Count >= 2)
|
|
{
|
|
var (l, t, r) = SliceIds(containers[1]);
|
|
m.FrontLeft = l;
|
|
m.FrontTile = t;
|
|
m.FrontRight = r;
|
|
}
|
|
|
|
return m;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the (left, tile, right) sprite ids for a 3-slice container,
|
|
/// extracting them from the container's image children that carry a DirectState
|
|
/// ("" key) with a non-zero file id, ordered left-to-right by their X position.
|
|
///
|
|
/// <para>
|
|
/// Children that carry ONLY named states (e.g. the expand-detail overlay with
|
|
/// "ShowDetail"/"HideDetail" entries but no "" key) are excluded automatically
|
|
/// because <see cref="Dictionary{TKey,TValue}.TryGetValue"/> for "" returns
|
|
/// false.
|
|
/// </para>
|
|
/// </summary>
|
|
private static (uint left, uint tile, uint right) SliceIds(ElementInfo container)
|
|
{
|
|
// Only children that have a non-zero DirectState image are slice candidates.
|
|
// The expand-detail overlay has NO DirectState entry, so it's excluded here.
|
|
// Project the File during filtering to avoid a second TryGetValue lookup.
|
|
// Stable sort: on an X tie, original Children insertion order (dat key-sort order) wins.
|
|
var slices = container.Children
|
|
.Where(c => c.StateMedia.TryGetValue("", out var med) && med.File != 0)
|
|
.Select(c => (c.X, File: c.StateMedia[""].File))
|
|
.OrderBy(t => t.X)
|
|
.ToList();
|
|
|
|
uint left = slices.Count > 0 ? slices[0].File : 0u;
|
|
uint tile = slices.Count > 1 ? slices[1].File : 0u;
|
|
uint right = slices.Count > 2 ? slices[2].File : 0u;
|
|
|
|
return (left, tile, right);
|
|
}
|
|
}
|