using System.Numerics;
namespace AcDream.App.UI;
///
/// 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
{
/// 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 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; }
/// 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;
/// 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.
DrawHBar(ctx, resolve, BackLeft, BackTile, BackRight, Width);
if (pct is not null && p > 0f)
DrawHBar(ctx, resolve, FrontLeft, FrontTile, FrontRight, 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);
}
}
string? label = Label();
if (!string.IsNullOrEmpty(label))
{
if (DatFont is { } datFont)
{
// Retail path: centered cur/max via the dat font's two-pass blit.
float tw = datFont.MeasureWidth(label);
float tx = (Width - tw) * 0.5f;
float ty = (Height - datFont.LineHeight) * 0.5f;
ctx.DrawStringDat(datFont, label, tx, ty, LabelColor);
}
else if (ctx.DefaultFont is { } font)
{
// Fallback: debug bitmap font (no dat font available).
float tw = font.MeasureWidth(label);
float tx = (Width - tw) * 0.5f;
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);
}
/// 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);
}
}