using System.Numerics; using AcDream.App.Rendering; namespace AcDream.App.UI; /// /// Per-frame drawing context passed through the /// tree. Wraps a (our 2D sprite batcher) and a /// transform stack so elements can draw in local coordinates. /// /// Retail equivalent: the implicit context FUN_005da8f0 walks with /// when iterating the UI tree. Our version is explicit so it plugs /// cleanly into Silk.NET. /// public sealed class UiRenderContext { public TextRenderer TextRenderer { get; } public BitmapFont? DefaultFont { get; set; } public Vector2 ScreenSize { get; } // Transform stack — simple 2D translate (no rotation/scale for UI). private readonly System.Collections.Generic.List _stack = new(); private Vector2 _current; // Alpha (opacity) stack — a window pushes its Opacity so its background/sprite // draws fade (retail's translucent-chat effect). Text draws bypass this (they go // straight to TextRenderer), so text stays sharp over a translucent background. private readonly System.Collections.Generic.List _alphaStack = new(); private float _alpha = 1f; /// Current cumulative opacity multiplier applied to sprite + rect draws. public float AlphaMod => _alpha; /// Multiply into the running opacity. Pair with . public void PushAlpha(float a) { _alphaStack.Add(_alpha); _alpha *= a; } /// Push an ABSOLUTE opacity (replaces, not multiplies) — for popups/overlays /// that must stay opaque even inside a translucent window. Pair with . public void PushAlphaAbsolute(float a) { _alphaStack.Add(_alpha); _alpha = a; } public void PopAlpha() { if (_alphaStack.Count == 0) return; _alpha = _alphaStack[^1]; _alphaStack.RemoveAt(_alphaStack.Count - 1); } public UiRenderContext(TextRenderer tr, Vector2 screenSize, BitmapFont? defaultFont = null) { TextRenderer = tr; ScreenSize = screenSize; DefaultFont = defaultFont; } /// Push a relative translate. Must be paired with . public void PushTransform(float dx, float dy) { _stack.Add(_current); _current += new Vector2(dx, dy); } public void PopTransform() { if (_stack.Count == 0) return; _current = _stack[^1]; _stack.RemoveAt(_stack.Count - 1); } public Vector2 CurrentOrigin => _current; /// Route subsequent draws to the overlay layer (flushed on top of the whole /// UI). Used by the root for the popup/overlay traversal. Pair with . public void BeginOverlayLayer() => TextRenderer.OverlayMode = true; public void EndOverlayLayer() => TextRenderer.OverlayMode = false; // ── Pass-through draw helpers (add current translate) ────────────── public void DrawRect(float x, float y, float w, float h, Vector4 color) => TextRenderer.DrawRect(_current.X + x, _current.Y + y, w, h, ApplyAlpha(color)); /// Solid-colour fill drawn in the SPRITE bucket (painter order with text), for /// a panel BACKGROUND that text draws on top of. composites after /// all sprites and would cover the text — use this for backgrounds, that for foreground /// fills (carets, vital bars). public void DrawFill(float x, float y, float w, float h, Vector4 color) => TextRenderer.DrawFill(_current.X + x, _current.Y + y, w, h, ApplyAlpha(color)); public void DrawRectOutline(float x, float y, float w, float h, Vector4 color, float thickness = 1f) => TextRenderer.DrawRectOutline(_current.X + x, _current.Y + y, w, h, ApplyAlpha(color), thickness); public void DrawSprite(uint texture, float x, float y, float w, float h, float u0, float v0, float u1, float v1, Vector4 tint) => TextRenderer.DrawSprite(texture, _current.X + x, _current.Y + y, w, h, u0, v0, u1, v1, ApplyAlpha(tint)); /// Multiply the current window opacity into a draw color's alpha. private Vector4 ApplyAlpha(Vector4 c) => _alpha >= 1f ? c : new Vector4(c.X, c.Y, c.Z, c.W * _alpha); public void DrawString(string text, float x, float y, Vector4 color, BitmapFont? font = null) { var f = font ?? DefaultFont; if (f is null) return; TextRenderer.DrawString(f, text, _current.X + x, _current.Y + y, color); } /// /// Draw a single line of text with a retail dat font (), /// at , = the top-left of the /// typographic block (in this element's local space). Mirrors retail's /// SurfaceWindow::DrawCharacter (acclient 0x00442bd0): for each glyph /// the BACKGROUND atlas sub-rect is blitted first tinted black (the outline), /// then the FOREGROUND atlas sub-rect tinted (the /// fill). The pen advances by /// HorizontalOffsetBefore + Width + HorizontalOffsetAfter and each /// glyph is positioned at pen + HorizontalOffsetBefore on the X axis /// and at baseline + VerticalOffsetBefore - (BaselineOffset) via the /// glyph's OffsetY into the atlas. /// /// gates the black outline pass. Retail decides /// this PER text element: UIElement_Text::DrawSelf (acclient 0x00467aa0) /// runs the outline pass only when m_bitField & 0x10 is set — i.e. the /// element called SetOutline(true) (LayoutDesc property 0xd). The DEFAULT /// is OFF (one fill-only pass): the talk-focus menu items set no outline, so an /// always-on outline shows as a grey halo over the solid menu panel. Pass /// outline:true only for elements retail outlines. /// public void DrawStringDat(UiDatFont font, string text, float x, float y, Vector4 color, bool outline = false) { if (font is null || string.IsNullOrEmpty(text)) return; // Baseline of this line in local space; retail draws glyphs whose // descriptor OffsetY already places them relative to the line top, so we // anchor each glyph's quad at the line top (y) plus its VerticalOffsetBefore. float originX = _current.X + x; float originY = _current.Y + y; float pen = originX; // Snap the LINE baseline to a whole pixel ONCE. Retail's // SurfaceWindow::DrawCharacter (acclient 0x00442bd0) takes an int32 pen Y // (arg3) and adds the glyph's integer m_VerticalOffsetBefore (a schar) — every // glyph on a line shares one integer baseline. If we instead round EACH glyph's // Y independently and the caller passes a fractional line Y (e.g. a channel-menu // item centered in a 17px row over a 16px font → y = 0.5), adjacent letters round // to different rows and the line looks crooked ("letters dip down"). The vitals // digits never showed it because their bar baseline lands on an integer; chat text // does. Snapping the baseline once, then adding the integer offset, keeps the whole // line on one row and pixel-aligned. float baseY = System.MathF.Round(originY); var outlineTint = new Vector4(0f, 0f, 0f, color.W); for (int i = 0; i < text.Length; i++) { if (!font.TryGetGlyph(text[i], out var g)) continue; // Horizontal: snap each glyph's dest X to a whole pixel (the pen keeps its // true fractional advance). Vertical: integer baseline + integer per-glyph // offset — never an independent per-glyph round (see baseY note above). float gx = System.MathF.Round(pen + g.HorizontalOffsetBefore); float gy = baseY + g.VerticalOffsetBefore; float gw = g.Width; float gh = g.Height; if (gw > 0f && gh > 0f) { // Background (outline) atlas pass, tinted black — drawn behind. Gated by // `outline` (retail's per-element m_bitField & 0x10); off by default so UI // text is crisp fill-only and free of the grey halo over solid panels. if (outline && font.BackgroundTexture != 0) { var (bu0, bv0, bu1, bv1) = AtlasUv( g.OffsetX, g.OffsetY, g.Width, g.Height, font.BackgroundWidth, font.BackgroundHeight); TextRenderer.DrawSprite(font.BackgroundTexture, gx, gy, gw, gh, bu0, bv0, bu1, bv1, outlineTint); } // Foreground (fill) atlas pass, tinted with the requested color. var (fu0, fv0, fu1, fv1) = AtlasUv( g.OffsetX, g.OffsetY, g.Width, g.Height, font.ForegroundWidth, font.ForegroundHeight); TextRenderer.DrawSprite(font.ForegroundTexture, gx, gy, gw, gh, fu0, fv0, fu1, fv1, color); } pen += UiDatFont.GlyphAdvance(g); } } /// Convert an (OffsetX,OffsetY,Width,Height) atlas pixel sub-rect to /// normalized UVs for an atlas of x /// . Guards against a zero-sized atlas. private static (float u0, float v0, float u1, float v1) AtlasUv( int offsetX, int offsetY, int width, int height, int atlasW, int atlasH) { if (atlasW <= 0 || atlasH <= 0) return (0f, 0f, 0f, 0f); float u0 = offsetX / (float)atlasW; float v0 = offsetY / (float)atlasH; float u1 = (offsetX + width) / (float)atlasW; float v1 = (offsetY + height) / (float)atlasH; return (u0, v0, u1, v1); } }