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;
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;
// ── 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, 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, 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, tint);
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. If the font has no background atlas the
/// outline pass is skipped.
///
public void DrawStringDat(UiDatFont font, string text, float x, float y, Vector4 color)
{
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;
var outline = new Vector4(0f, 0f, 0f, color.W);
for (int i = 0; i < text.Length; i++)
{
if (!font.TryGetGlyph(text[i], out var g))
continue;
// Pixel-snap each glyph's destination to whole pixels so the atlas samples
// texel-aligned. Without this, a fractional bar width after resize puts the
// centered number on a sub-pixel x and linear filtering smears the glyphs
// (the "unsharp at certain sizes" artifact). The pen keeps its true
// fractional advance, so only the per-glyph dest is snapped.
float gx = System.MathF.Round(pen + g.HorizontalOffsetBefore);
float gy = System.MathF.Round(originY + g.VerticalOffsetBefore);
float gw = g.Width;
float gh = g.Height;
if (gw > 0f && gh > 0f)
{
// Background (outline) atlas pass, tinted black — drawn behind.
if (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, outline);
}
// 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);
}
}