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; } 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); } 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)) { // 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; var (lt, lw, _) = resolve(leftId); var (mt, mw, _) = resolve(midId); var (rt, rw, _) = resolve(rightId); 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); } /// 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); } }