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); /// 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 stretched gradient middle, and a fixed-width right-cap. The "back" slice is // the empty track (drawn full width); the "front" slice is the coloured fill // (drawn from the left, grown to the fill fraction — the track owns the right // end, so the fill omits its own right-cap). Ids come from the vitals LayoutDesc // (0x21000014) via tools/dump-vitals-bars; 0 = none. /// Empty-track left-cap RenderSurface id. public uint BackLeft { get; set; } /// Empty-track middle (stretched 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 (stretched gradient) RenderSurface id. public uint FrontTile { get; set; } /// Coloured-fill right-cap RenderSurface id. public uint FrontRight { get; set; } public UiMeter() { ClickThrough = 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)) { // Empty track: full-width 3-slice (left-cap + stretched gradient + right-cap). DrawHBar(ctx, resolve, BackLeft, BackTile, BackRight, 0, 0, Width, Height, withRightCap: true); // Coloured fill: grows from the left to the value, no right-cap of its own. if (pct is not null && p > 0f) DrawHBar(ctx, resolve, FrontLeft, FrontTile, FrontRight, 0, 0, Width * p, Height, withRightCap: false); } 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) && ctx.DefaultFont is { } font) { 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 a horizontal 3-slice into x at /// (,): a native-width left-cap, a stretched /// middle, and (when ) a native-width right-cap. Caps /// are clamped so a narrow bar never overdraws. A 0 id skips that slice. /// private static void DrawHBar( UiRenderContext ctx, Func resolve, uint leftId, uint tileId, uint rightId, float x, float y, float w, float h, bool withRightCap) { if (w <= 0f) return; var (lt, lw, _) = resolve(leftId); var (tt, _, _) = resolve(tileId); var (rt, rw, _) = resolve(rightId); float rcap = withRightCap && rt != 0 ? MathF.Min(rw, w) : 0f; float lcap = lt != 0 ? MathF.Min(lw, w - rcap) : 0f; if (lt != 0 && lcap > 0f) ctx.DrawSprite(lt, x, y, lcap, h, 0, 0, 1, 1, Vector4.One); float midX = x + lcap; float midW = w - lcap - rcap; if (tt != 0 && midW > 0f) ctx.DrawSprite(tt, midX, y, midW, h, 0, 0, 1, 1, Vector4.One); if (rcap > 0f) ctx.DrawSprite(rt, x + w - rcap, y, rcap, h, 0, 0, 1, 1, Vector4.One); } }