using System.Numerics;
namespace AcDream.App.UI;
/// Justification of a meter's absorbed state caption (dat property
/// 0x14 on the caption child's own state: 0/2 = Left, 3/5 = Right, 1 = Center
/// — the same mapping ElementReader applies at element level).
public enum UiMeterLabelAlign : byte { Left = 0, Center = 1, Right = 2 }
///
/// One absorbed vitals detail-icon overlay (the 0x100004A9 child of a
/// meter's back/front slice container): its ShowDetail sprite, authored rect
/// (local to the container, which spans the meter at 0,0), the authored raw
/// edge-anchor modes (ElementDesc Left/Top/Right/Bottom, values 0–4),
/// and the container's authored size. The overlays author L3/R3 —
/// retail's CENTER anchors — so when the meter is resized away from its
/// authored width the icon re-centers through the exact
/// port of
/// UIElement::UpdateForParentSizeChange @0x00462640 instead of staying
/// pinned at its authored X (the gate-observed left-drift).
///
internal readonly record struct UiMeterDetailOverlaySpec(
uint Sprite,
float X, float Y, float W, float H,
uint LeftMode, uint TopMode, uint RightMode, uint BottomMode,
float ParentW, float ParentH);
///
/// A horizontal vital bar (retail HP/Stamina/Mana style): a background rect, a
/// partial-width solid fill, and an optional centered "current/max" numeric
/// overlay. returns 0..1 (null = no data → empty bar);
/// returns the overlay text (null = no number).
///
///
/// Solid-color fill + debug font for Spec 1. The retail gradient bar sprite
/// (glassy center highlight) and the retail dat font are a later polish pass —
/// retail's vitals are bars exactly like this, just sprited.
///
///
public sealed class UiMeter : UiElement, IUiDatStateful
{
private readonly Dictionary _stateFillSprites = new();
private readonly Dictionary _stateLabels = new();
private (string Text, UiMeterLabelAlign Align)? _activeStateLabel;
// Vitals ShowDetail icon overlays (see ConfigureDetailOverlay).
private bool _detailConfigured;
private bool _detailPassToChildren;
private UiMeterDetailOverlaySpec _detailBack;
private UiMeterDetailOverlaySpec _detailFront;
/// True when this meter absorbed the vitals detail-icon overlays. Exposed for tests.
internal bool HasDetailOverlay => _detailConfigured;
/// The dim back-container detail icon (ShowDetail media). Exposed for tests.
internal uint DetailBackSprite => _detailBack.Sprite;
/// The bright fill-clipped front-container detail icon. Exposed for tests.
internal uint DetailFrontSprite => _detailFront.Sprite;
/// The back overlay's authored meter-local rect. Exposed for tests.
internal (float X, float Y, float W, float H) DetailBackRect =>
(_detailBack.X, _detailBack.Y, _detailBack.W, _detailBack.H);
/// The complete absorbed back overlay. Exposed for tests.
internal UiMeterDetailOverlaySpec DetailBack => _detailBack;
/// The complete absorbed front overlay. Exposed for tests.
internal UiMeterDetailOverlaySpec DetailFront => _detailFront;
/// Dat element id, set by the layout importer so duplicated page copies can be scoped.
public uint ElementId { get; set; }
/// Fill fraction provider; a null result draws an empty bar.
public Func Fill { get; set; } = () => 0f;
/// Centered overlay text provider (e.g. "291/291"); null = none.
public Func Label { get; set; } = () => null;
public Vector4 BarColor { get; set; } = new(1f, 0f, 0f, 1f);
public Vector4 BgColor { get; set; } = new(0f, 0f, 0f, 0.5f);
public Vector4 LabelColor { get; set; } = new(1f, 1f, 1f, 1f);
/// Retail LayoutDesc property 0x21 (two-pass glyph outline,
/// UIElement_Text::SetOutline @0x0046a81c). Seeded by DatWidgetFactory
/// from the element's effective-default state, same as
/// (round-5 review S2 — per-STATE switching is AP-192).
public bool Outline { get; set; }
/// Retail LayoutDesc property 0x22 (m_curOutlineColor,
/// ctor default black). Only meaningful when is true.
public Vector4 OutlineColor { get; set; } = UiRenderContext.DefaultOutlineColor;
/// Retail dat font (Font 0x40000000) for the "cur/max" overlay. When
/// set, the label renders through the dat-font two-pass blit (outline + fill);
/// when null, the debug bitmap font
/// is used instead. Set by the host when the retail UI is active.
public UiDatFont? DatFont { get; set; }
/// Resolver from a RenderSurface DataId to (GL handle, w, h). When set
/// with the 9-slice ids below, the bar draws the retail sprites instead of solid color.
public Func? SpriteResolve { get; set; }
// Retail vital bars are a horizontal 3-slice: a fixed-width bevelled left-cap,
// a TILED gradient middle (the "fill-tile" repeats at native width — it does not
// stretch), and a fixed-width right-cap. The "back" slice is the empty track
// (drawn full width); the "front" slice is the coloured fill (drawn full-geometry
// but CLIPPED to the fill fraction — its own right-cap shows at 100%, the back's
// shows through when partial). Ids come from the stacked vitals LayoutDesc
// (0x2100006C) via the dump-vitals-layout CLI; 0 = none.
/// Empty-track left-cap RenderSurface id.
public uint BackLeft { get; set; }
/// Empty-track middle (tiled gradient) RenderSurface id.
public uint BackTile { get; set; }
/// Empty-track right-cap RenderSurface id.
public uint BackRight { get; set; }
/// Coloured-fill left-cap RenderSurface id.
public uint FrontLeft { get; set; }
/// Coloured-fill middle (tiled gradient) RenderSurface id.
public uint FrontTile { get; set; }
/// Coloured-fill right-cap RenderSurface id.
public uint FrontRight { get; set; }
/// The active numeric DAT state for a stateful fill meter.
public uint ActiveRetailStateId { get; private set; }
/// The caption latched by the active retail state (null when the
/// state authors none). Exposed for tests.
internal string? ActiveStateLabel => _activeStateLabel?.Text;
/// The latched caption's authored justification. Exposed for tests.
internal UiMeterLabelAlign? ActiveStateLabelAlign => _activeStateLabel?.Align;
///
/// Registers a fill sprite carried by a child state of the imported meter.
/// Used by the powerbar shape in LayoutDesc 0x21000072, whose track is on
/// the meter and whose Jump/Melee/Missile/DDD fills are states of one child.
///
internal void ConfigureStateFill(uint stateId, uint spriteId)
{
if (stateId != 0 && spriteId != 0)
_stateFillSprites[stateId] = spriteId;
}
///
/// Registers a caption carried by a state of the meter's absorbed Type-12
/// child (gmPowerbarUI's 0x10000035: JumpMode 'Height' / MeleeMode 'Power'
/// / MissileMode 'Accuracy'). Retail shows it via the meter's
/// PassToChildren state cascade; the absorbed equivalent latches it in
/// and draws it when no live
/// provider supplies text. Each mode STATE authors its
/// own justification (dat property 0x14 — the powerbar modes author 0x3 =
/// Right; only the element default is centered), so alignment rides the
/// state entry.
///
internal void ConfigureStateLabel(uint stateId, string text, UiMeterLabelAlign align)
{
if (stateId != 0 && !string.IsNullOrEmpty(text))
_stateLabels[stateId] = (text, align);
}
///
/// Registers the vitals "detail" icon overlays absorbed from the meter's
/// two slice containers (the 0x100004A9 children — health heart
/// 0x06007490/91, stamina sword 0x06007492/93, mana scepter 0x06007494/95).
/// Retail authors each icon TWICE: a dim version in the BACK container
/// (drawn unclipped over the empty track) and a bright version in the
/// FRONT container (fill-clipped with the rest of the front layer —
/// UIElement_Meter::DrawChildren @0x0046FBD0 clips the whole
/// m_pcChildImage child, element id 2, to the 0x69 fraction), so
/// the icon itself fills up with the vital. Both overlays author media
/// ONLY for ShowDetail (HideDetail authors File=0), so they
/// draw solely in that state. Each spec carries the overlay's authored
/// rect (local to its container, which spans the meter at 0,0), its raw
/// edge-anchor modes, and the container's authored size — see
/// for how those position the icon
/// on a resized meter.
///
internal void ConfigureDetailOverlay(
in UiMeterDetailOverlaySpec back,
in UiMeterDetailOverlaySpec front,
bool passToChildren)
{
_detailBack = back;
_detailFront = front;
_detailConfigured = back.Sprite != 0 || front.Sprite != 0;
_detailPassToChildren = passToChildren;
}
///
/// The overlay's effective meter-local rect at the meter's CURRENT size.
/// At the authored container size the authored rect stands verbatim —
/// retail's UIElement::UpdateForParentSizeChange @0x00462640 runs
/// only when a parent actually resizes, and the designers hand-placed
/// rects that are not always the exact center formula (the stamina sword
/// authors X=32 where the mode-3 formula yields 33). On any other size
/// the authored edge modes are applied from the ORIGINAL child/parent
/// rects through , the exact port of
/// that retail routine — the overlays' authored L3/R3 center
/// anchors are what keep the heart/sword/scepter centered when the
/// vitals window is resized (retail near mode 3 =
/// curParentW/2 - origChildW/2 @0x004627ca; far mode 3 =
/// curParentW/2 + origChildW/2 - 1 @0x00462827, integer
/// arithmetic, so odd authored widths lose one pixel exactly as retail
/// does).
///
internal static (float X, float Y, float W, float H) ComputeDetailOverlayRect(
in UiMeterDetailOverlaySpec overlay, float meterW, float meterH)
{
int parentW = (int)meterW, parentH = (int)meterH;
int origParentW = (int)overlay.ParentW, origParentH = (int)overlay.ParentH;
if (origParentW <= 0 || origParentH <= 0
|| (parentW == origParentW && parentH == origParentH))
{
return (overlay.X, overlay.Y, overlay.W, overlay.H);
}
var originalChild = UiPixelRect.FromPositionAndSize(
(int)overlay.X, (int)overlay.Y, (int)overlay.W, (int)overlay.H);
var originalParent = UiPixelRect.FromPositionAndSize(
0, 0, origParentW, origParentH);
var currentParent = UiPixelRect.FromPositionAndSize(
0, 0, parentW, parentH);
UiPixelRect effective = UiLayoutPolicy.Apply(
overlay.LeftMode,
overlay.TopMode,
overlay.RightMode,
overlay.BottomMode,
originalChild,
originalParent,
originalChild,
currentParent);
return (effective.X0, effective.Y0, effective.Width, effective.Height);
}
public bool TrySetRetailState(uint stateId)
{
// Vitals detail toggle (gmVitalsUI::ListenToElementMessage @0x004BFC00
// flips the window root between HideDetail 0x10000006 and ShowDetail
// 0x10000007; the meter receives the state through the authored
// PassToChildren cascade — UIElement::SetState @0x00464E70). The
// meter's own HideDetail/ShowDetail StateDescs are media-less and
// exist purely to keep propagating (PassToChildren=true), so forward
// to the absorbed text child (whose per-state 0x3B hides the numbers
// in ShowDetail) and let OnDraw key the icon overlays off the state.
if (_detailConfigured
&& stateId is RetailUiStateIds.HideDetail or RetailUiStateIds.ShowDetail)
{
ActiveRetailStateId = stateId;
if (_detailPassToChildren)
foreach (UiElement child in Children)
if (child is IUiDatStateful stateful)
stateful.TrySetRetailState(stateId);
return true;
}
bool hasFill = _stateFillSprites.TryGetValue(stateId, out uint spriteId);
bool hasLabel = _stateLabels.TryGetValue(stateId, out var caption);
if (!hasFill && !hasLabel)
return false;
ActiveRetailStateId = stateId;
if (hasFill)
{
FrontLeft = 0;
FrontTile = spriteId;
FrontRight = 0;
}
_activeStateLabel = hasLabel ? caption : null;
return true;
}
/// Vertical orientation (retail m_eDirection 2/4). Default false =
/// horizontal (direction 1). The burden bar is the only vertical meter today.
public bool Vertical { get; set; }
/// For a vertical meter, fill grows from the bottom up (retail direction 4)
/// when true, top-down (direction 2) when false. Default bottom-up. Visual-confirmed.
public bool FillFromBottom { get; set; } = true;
public UiMeter() { ClickThrough = true; }
/// The meter draws its own 3-slice bars; the importer must not build its
/// grandchild slice/text elements as separate widgets.
public override bool ConsumesDatChildren => true;
private static float AlignX(UiMeterLabelAlign align, float width, float textWidth)
=> align switch
{
UiMeterLabelAlign.Left => 0f,
UiMeterLabelAlign.Right => width - textWidth,
_ => (width - textWidth) * 0.5f,
};
/// Clamp to [0,1] and return the fill rect
/// (local px) for a bar of x .
public static (float x, float y, float w, float h) ComputeFillRect(
float pct, float w, float h)
{
if (pct < 0f) pct = 0f;
if (pct > 1f) pct = 1f;
return (0f, 0f, w * pct, h);
}
/// Clamp to [0,1] and return the vertical fill rect
/// (local px). true → the fill occupies the bottom
/// h*pct px (retail direction 4); false → the top (direction 2).
public static (float x, float y, float w, float h) ComputeVFillRect(
float pct, float w, float h, bool fromBottom)
{
if (pct < 0f) pct = 0f;
if (pct > 1f) pct = 1f;
float fh = h * pct;
return (0f, fromBottom ? h - fh : 0f, w, fh);
}
protected override void OnDraw(UiRenderContext ctx)
{
float? pct = Fill();
float p = pct is float pf ? (pf < 0f ? 0f : pf > 1f ? 1f : pf) : 0f;
if (SpriteResolve is { } resolve && (BackLeft != 0 || BackTile != 0 || FrontTile != 0))
{
if (Vertical)
{
// Track full height, fill clipped to the fraction along the fill direction.
// The burden meter is a single-tile bar (BackTile/FrontTile, no caps).
DrawVBar(ctx, resolve, BackTile, Height, fromBottom: FillFromBottom, isFill: false);
if (pct is not null && p > 0f)
DrawVBar(ctx, resolve, FrontTile, Height * p, fromBottom: FillFromBottom, isFill: true);
}
else
{
// Retail meter (UIElement_Meter::DrawChildren): the BACK 3-slice is the
// empty track, drawn full width; the FRONT 3-slice is the coloured fill,
// drawn at FULL width too but horizontally CLIPPED to the fill fraction.
// The front carries its own right-cap (shown at 100%); clipping below 100%
// removes it and reveals the back track's right-cap — retail's scissor-fill.
//
// ShowDetail icon overlays ride their authored containers: the dim BACK
// icon draws over the full track (its 0x100004A9 child draws AFTER the
// three slice children — higher ReadOrder within the back container);
// the bright FRONT icon is clipped with the rest of the front layer to
// the fill fraction (retail clips the whole element-id-2 child).
bool detail = _detailConfigured
&& ActiveRetailStateId == RetailUiStateIds.ShowDetail;
DrawHBar(ctx, resolve, BackLeft, BackTile, BackRight, Width);
if (detail)
{
DrawDetailIcon(
ctx, resolve, _detailBack.Sprite,
ComputeDetailOverlayRect(in _detailBack, Width, Height),
Width);
}
if (pct is not null && p > 0f)
{
DrawHBar(ctx, resolve, FrontLeft, FrontTile, FrontRight, Width * p);
if (detail)
{
DrawDetailIcon(
ctx, resolve, _detailFront.Sprite,
ComputeDetailOverlayRect(in _detailFront, Width, Height),
Width * p);
}
}
}
}
else
{
// Placeholder solid-color fallback.
ctx.DrawRect(0, 0, Width, Height, BgColor);
if (pct is not null && p > 0f)
{
var (fx, fy, fw, fh) = ComputeFillRect(p, Width, Height);
if (fw > 0f) ctx.DrawRect(fx, fy, fw, fh, BarColor);
}
}
// A live provider (vitals "cur/max") wins and stays centered;
// otherwise the active retail-state caption shows with its own
// authored justification (the powerbar modes author Right).
string? label = Label();
UiMeterLabelAlign align = UiMeterLabelAlign.Center;
if (string.IsNullOrEmpty(label) && _activeStateLabel is { } stateLabel)
(label, align) = stateLabel;
if (!string.IsNullOrEmpty(label))
{
if (DatFont is { } datFont)
{
// Retail path: the dat font's two-pass blit.
float tw = datFont.MeasureWidth(label);
float tx = AlignX(align, Width, tw);
float ty = (Height - datFont.LineHeight) * 0.5f;
ctx.DrawStringDat(datFont, label, tx, ty, LabelColor, Outline, OutlineColor);
}
else if (ctx.DefaultFont is { } font)
{
// Fallback: debug bitmap font (no dat font available).
float tw = font.MeasureWidth(label);
float tx = AlignX(align, Width, tw);
float ty = (Height - font.LineHeight) * 0.5f;
ctx.DrawString(label, tx, ty, LabelColor);
}
}
}
///
/// Draws the full-width horizontal 3-slice (native-width left-cap, stretched
/// middle, native-width right-cap) over this meter's rect, horizontally CLIPPED
/// so nothing past (local px from the left) is drawn.
/// The back track passes clipW = Width; the front fill passes
/// clipW = Width * fraction. Clipping UV-crops each slice proportionally,
/// so the fill ends cleanly and the back's right-cap shows through when partial.
/// A 0 id skips that slice.
///
private void DrawHBar(
UiRenderContext ctx, Func resolve,
uint leftId, uint midId, uint rightId, float clipW)
{
if (clipW <= 0f) return;
float w = Width, h = Height;
// Only resolve a slice when its id is non-zero. resolve(0) returns the 1x1 MAGENTA
// placeholder with a NON-ZERO GL handle, so resolving a zero (absent) cap id and then
// testing `tex != 0` would draw a 1px magenta cap. The single-image meter (toolbar
// selected-object bar) has no left/right caps (ids 0); the 3-slice vitals meter sets
// all six ids. Guard on the id, not the resolved handle.
var (lt, lw, _) = leftId != 0 ? resolve(leftId) : (0u, 0, 0);
var (mt, mw, _) = midId != 0 ? resolve(midId) : (0u, 0, 0);
var (rt, rw, _) = rightId != 0 ? resolve(rightId) : (0u, 0, 0);
float capL = lt != 0 ? MathF.Min(lw, w) : 0f;
float capR = rt != 0 ? MathF.Min(rw, w - capL) : 0f;
float midW = w - capL - capR;
// Each slice's texture repeats every NATIVE-width px (UV-repeat; the UI
// texture is GL_REPEAT-wrapped — TextureCache.UploadRgba8). Caps span their
// own native width → a single 1:1 copy. The wide middle spans many native
// widths → it TILES, matching retail's "fill-tile" + ImgTex::TileCSI rather
// than stretching one copy. (Same UV-repeat the chrome border already uses.)
DrawPiece(ctx, lt, 0f, capL, lw, h, clipW);
DrawPiece(ctx, mt, capL, midW, mw, h, clipW);
DrawPiece(ctx, rt, w - capR, capR, rw, h, clipW);
}
/// Draws a single-tile vertical bar slice. The track (
/// false) spans the full height; the fill spans px from the
/// bottom () or top, UV-cropped to that fraction of the
/// sprite so the fill reveals the matching part of the art (retail
/// UIElement_Meter::DrawChildren Box2D clip, direction 2/4). A 0 id is a no-op.
private void DrawVBar(UiRenderContext ctx, Func resolve,
uint tileId, float visibleH, bool fromBottom, bool isFill)
{
if (tileId == 0 || visibleH <= 0f) return;
var (tex, _, _) = resolve(tileId);
if (tex == 0) return;
float w = Width, h = Height;
if (visibleH > h) visibleH = h;
float frac = h > 0f ? visibleH / h : 0f;
// Bottom-up fill: bottom of the rect AND bottom of the sprite (v in [1-frac, 1]).
// Top-down / track: top of the rect AND top of the sprite (v in [0, frac]).
float y = isFill && fromBottom ? h - visibleH : 0f;
float v0 = isFill && fromBottom ? 1f - frac : 0f;
float v1 = isFill && fromBottom ? 1f : frac;
ctx.DrawSprite(tex, 0f, y, w, visibleH, 0f, v0, 1f, v1, System.Numerics.Vector4.One);
}
///
/// Draws one ShowDetail icon overlay at its authored meter-local rect,
/// horizontally clipped to local px (the back icon
/// passes the full width; the front icon passes Width * fraction,
/// mirroring retail's whole-front-container fill clip). The visible portion
/// is UV-cropped so the icon reveals left-to-right with the fill. Height is
/// clamped to the meter's box — retail clips children to the parent rect
/// (the stamina FRONT overlay authors H=28 in a 16px bar; retail shows 16).
///
private void DrawDetailIcon(
UiRenderContext ctx, Func resolve,
uint spriteId, (float X, float Y, float W, float H) rect, float clipW)
{
if (spriteId == 0 || rect.W <= 0f || rect.H <= 0f) return;
var (tex, _, _) = resolve(spriteId);
if (tex == 0) return;
float visibleW = MathF.Min(rect.W, clipW - rect.X);
if (visibleW <= 0f) return;
float h = MathF.Min(rect.H, Height - rect.Y);
if (h <= 0f) return;
float u1 = visibleW / rect.W;
ctx.DrawSprite(tex, rect.X, rect.Y, visibleW, h, 0f, 0f, u1, 1f, Vector4.One);
}
/// Draw a slice over local [,
/// pieceX+], with the texture repeating every
/// px (UV-repeat — the UI texture is GL_REPEAT-wrapped).
/// Clipped so nothing past shows. For a cap (span == native)
/// this is one 1:1 copy; for the wide middle it tiles; a partial last copy is
/// UV-cropped.
private static void DrawPiece(
UiRenderContext ctx, uint tex, float pieceX, float pieceW, float nativeW, float h, float clipW)
{
if (tex == 0 || pieceW <= 0f || nativeW <= 0f) return;
float visibleW = MathF.Min(pieceW, clipW - pieceX);
if (visibleW <= 0f) return;
float u1 = visibleW / nativeW; // >1 ⇒ texture repeats (tiles); ≤1 ⇒ a partial copy
ctx.DrawSprite(tex, pieceX, 0f, visibleW, h, 0f, 0f, u1, 1f, Vector4.One);
}
}