Lands the codex-worktree D.2b stream plus the extraction the 2026-07-02 UI architecture review mandated before commit: - ItemInteractionController: single owner of double-click use/equip/ container-open, targeted-use mode (health kits), drag-out drop; toolbar shortcut drags don't drop the real item. ItemEquipRules for multi-slot (coat) coverage via equip masks. - Cursor phase: CursorFeedbackController (semantic priority chain: drag > resize > window-move > target-mode > text) + RetailCursorCatalog (enums 0x27/0x28/0x29, hotspot 14,14; ClientUISystem::UpdateCursorState 0x00564630) resolved through the portal EnumIDMap chain by RetailCursorResolver; RetailCursorManager applies dat cursor art to the OS cursor. Register row AP-72 covers the OS standard-cursor fallback. - Character window goes live: CharacterSheetProvider owns sheet assembly, XP-curve/raise-cost math and the raise flow — extracted out of GameWindow per Code Structure Rule 1 instead of committing the ~430-line feature body there. Optimistic XP/credit debits go through eventful store APIs (new ClientObjectTable.UpdateInt64Property + LocalPlayerState.DebitIntProperty/DebitInt64Property) instead of raw property-dictionary writes; register row AP-73 covers the still-missing raise ledger (#163). - RetailWindowFrame: the shared nine-slice window mount recipe; the character window uses it, remaining windows migrate via #164. - Status-bar buttons toggle inventory/character windows; retail row-major backpack ordering; WorldSession.SendUseWithTarget + raise/train sends. GameWindow shrinks 14,214 -> 13,877 lines despite the new features; the sheet/raise logic is unit-tested in CharacterSheetProviderTests instead of trapped in the god object. Build green; full suite 3,286 tests pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
134 lines
5.9 KiB
C#
134 lines
5.9 KiB
C#
using System;
|
|
using System.Numerics;
|
|
using AcDream.App.UI.Layout;
|
|
|
|
namespace AcDream.App.UI;
|
|
|
|
/// <summary>
|
|
/// Generic dat-widget button — the production replacement for any dat element of
|
|
/// Type 1 (UIElement_Button, registered via RegisterElementClass(1, UIElement_Button::Create)
|
|
/// @ acclient_2013_pseudo_c.txt:125828).
|
|
///
|
|
/// <para>
|
|
/// Draws per-state sprite media exactly like <see cref="UiDatElement"/> (same
|
|
/// <c>ActiveState</c> defaulting, same <c>ActiveMedia()</c> fallback chain, same tiled
|
|
/// <c>DrawSprite</c> call with UV-repeat so chrome edges tile correctly) plus an
|
|
/// optional centered text label. The click behavior mirrors <see cref="UiDatElement"/>
|
|
/// one-for-one so the chat Send and Max/Min buttons that previously bound through
|
|
/// <c>UiDatElement.OnClick</c> continue to work without behavioral change.
|
|
/// </para>
|
|
///
|
|
/// <para>
|
|
/// State selection: picks <see cref="ElementInfo.DefaultStateName"/> if set, then
|
|
/// "Normal" if the element has a Normal state sprite, then falls back to the unnamed
|
|
/// DirectState ("" key) — identical to <see cref="UiDatElement"/>.
|
|
/// </para>
|
|
///
|
|
/// <para>
|
|
/// Built by <see cref="DatWidgetFactory"/> for Type-1 elements (chat Send 0x10000019,
|
|
/// Max/Min 0x1000046F). NOT the same as <see cref="UiSimpleButton"/>, which is an
|
|
/// earlier dev-scaffold widget with no dat sprites.
|
|
/// </para>
|
|
/// </summary>
|
|
public sealed class UiButton : UiElement
|
|
{
|
|
private readonly ElementInfo _info;
|
|
private readonly Func<uint, (uint tex, int w, int h)> _resolve;
|
|
|
|
/// <summary>Optional click handler. Wired by the controller (e.g. chat Submit, ToggleMaximize).</summary>
|
|
public Action? OnClick { get; set; }
|
|
|
|
/// <summary>The dat element id from <see cref="ElementInfo.Id"/>.</summary>
|
|
public uint ElementId => _info.Id;
|
|
|
|
/// <summary>Optional centered text label drawn over the sprite (e.g. "Send" on a blank gold frame).</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;
|
|
|
|
/// <summary>Horizontal alignment of <see cref="Label"/>. Center (default) for normal buttons;
|
|
/// Left for the paperdoll "Slots" caption that sits at the left edge, before the slots.</summary>
|
|
public LabelAlignment LabelAlign { get; set; } = LabelAlignment.Center;
|
|
|
|
/// <summary>Label horizontal alignment options.</summary>
|
|
public enum LabelAlignment { Center, Left }
|
|
|
|
/// <summary>
|
|
/// Active state name, runtime-settable (e.g. Max/Min toggling Normal ↔ Minimized).
|
|
/// Matches <see cref="UiDatElement.ActiveState"/>.
|
|
/// </summary>
|
|
public string ActiveState { get; set; } = "";
|
|
|
|
public override string ActiveCursorStateName => ActiveState;
|
|
|
|
/// <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 UiButton(ElementInfo info, Func<uint, (uint tex, int w, int h)> resolve)
|
|
{
|
|
_info = info;
|
|
_resolve = resolve;
|
|
ClickThrough = false; // buttons are interactive — opt OUT of click-through
|
|
|
|
// State defaulting matches UiDatElement exactly:
|
|
// DefaultStateName wins; else "Normal" if that state has a sprite; else DirectState ("").
|
|
if (!string.IsNullOrEmpty(info.DefaultStateName))
|
|
ActiveState = info.DefaultStateName;
|
|
else if (info.StateMedia.ContainsKey("Normal"))
|
|
ActiveState = "Normal";
|
|
// else ActiveState stays "" (DirectState)
|
|
}
|
|
|
|
/// <summary>The button draws its own face + label; any dat label child is reproduced
|
|
/// procedurally, so the importer must not build the button's children as widgets.</summary>
|
|
public override bool ConsumesDatChildren => true;
|
|
|
|
/// <summary>A button is interactive — it must receive its Click even inside a whole-window-Draggable
|
|
/// frame (e.g. the paperdoll "Slots" toggle in the inventory window), so it opts out of the
|
|
/// IA-12 whole-window-drag that would otherwise swallow the press.</summary>
|
|
public override bool HandlesClick => true;
|
|
|
|
/// <summary>
|
|
/// Returns the File id for the current <see cref="ActiveState"/>, falling back to
|
|
/// the DirectState ("" key) if the named state is absent.
|
|
/// Returns 0 if neither exists.
|
|
/// Mirrors <see cref="UiDatElement.ActiveMedia()"/>.
|
|
/// </summary>
|
|
private uint ActiveFile()
|
|
=> _info.StateMedia.TryGetValue(ActiveState, out var m) ? m.File
|
|
: _info.StateMedia.TryGetValue("", out var d) ? d.File : 0u;
|
|
|
|
protected override void OnDraw(UiRenderContext ctx)
|
|
{
|
|
uint file = ActiveFile();
|
|
if (file != 0)
|
|
{
|
|
var (tex, tw, th) = _resolve(file);
|
|
if (tex != 0 && tw != 0 && th != 0)
|
|
{
|
|
// Tiled draw — same call shape as UiDatElement.OnDraw (UV-repeat; GL_REPEAT-wrapped
|
|
// UI texture). Matches ImgTex::TileCSI; no Stretch mode exists.
|
|
ctx.DrawSprite(tex, 0, 0, Width, Height, 0, 0, Width / tw, Height / th, Vector4.One);
|
|
}
|
|
}
|
|
|
|
if (Label is { Length: > 0 } label && LabelFont is { } lf)
|
|
{
|
|
float tx = LabelAlign == LabelAlignment.Left
|
|
? 3f // small left pad (room for a future checkbox box)
|
|
: (Width - lf.MeasureWidth(label)) * 0.5f; // centered (default)
|
|
float ty = (Height - lf.LineHeight) * 0.5f;
|
|
ctx.DrawStringDat(lf, label, tx, ty, LabelColor);
|
|
}
|
|
}
|
|
|
|
public override bool OnEvent(in UiEvent e)
|
|
{
|
|
if (e.Type == UiEventType.Click && OnClick is not null) { OnClick(); return true; }
|
|
return false;
|
|
}
|
|
}
|