MinecraftConsoles/Minecraft.Client/Common/UI/NativeUIRenderer.cpp
MrTheShy 4f2352361a Add Mojang/Ely.by/offline authentication system
The old Windows64 port had no real player identity — it used hardcoded
fake XUIDs, so anyone could impersonate anyone. This replaces that with
proper auth supporting Mojang, Ely.by, and offline accounts.

MCAuth library (new, MCAuth/):
  Mojang auth via MSA device code flow (XBL, SISU, MC services),
  Ely.by via Yggdrasil with 2FA, offline UUID generation matching
  Java Edition (MD5 v3 from "OfflinePlayer:<name>"). Multi-account
  manager with background token refresh, per-slot sessions, and
  on-disk token persistence. Server-side session verification via
  Mojang/Ely.by hasJoined API. Skin fetching and PNG validation
  from texture servers.

Network protocol (version bumped to 80):
  Three new packets (AuthScheme, AuthResponse, AuthResult) implement
  a server-driven auth handshake before login completes. Player
  identity migrated from 64-bit XUID to 128-bit GameUUID backed by
  two uint64 fields (hi/lo). readPlayerUID/writePlayerUID now
  serialize 16 bytes on the wire. Old and new clients cannot connect
  to each other — version mismatch is rejected at PreLogin.

Save migration:
  Map data mappings auto-migrate from old format: the old 64-bit
  XUID is placed in hi, lo is set to 0 as a sentinel. On first
  access by the real player, the sentinel entry is upgraded in-place
  to the full 128-bit UUID. Format detection is by file size (2080,
  2112, or 4160 bytes). Player .dat filenames inside saveData.ms
  change from decimal XUID to dashed UUID — old saves need manual
  entry renaming in the archive.

UI:
  NativeUIRenderer: immediate-mode drawing system (quads, text,
  9-slice panels, scrollbars, focus lists) for rendering auth
  screens without Flash/Scaleform. UIScene_MSAuth handles device
  code display, Ely.by credential input with 2FA, per-account
  skin head preview, and multi-account add/remove/switch.

Server:
  online-mode and auth-provider (mojang/elyby) in server.properties.
  Whitelist and ban checks validate against the server-verified UUID.
  Incompatible auth scheme logs which provider the server expects
  vs what the client is using.

Also fixes a pre-existing exploit where any client could send a
DebugOptionsPacket to grant themselves CraftAnything and other debug
privileges on any server — now requires OP status server-side.
2026-03-23 01:14:23 +01:00

2031 lines
66 KiB
C++

#include "stdafx.h"
#include "NativeUIRenderer.h"
#include "UI.h"
#include <cmath>
#include <d3d11.h>
#include <d3dcompiler.h>
#include <wincodec.h>
#pragma comment(lib, "windowscodecs.lib")
#ifdef _WINDOWS64
#include <shellapi.h>
#include "../../Windows64/KeyboardMouseInput.h"
#pragma comment(lib, "shell32.lib")
extern KeyboardMouseInput g_KBMInput;
extern HWND g_hWnd;
#endif
#pragma comment(lib, "d3dcompiler.lib")
extern ID3D11Device* g_pd3dDevice;
extern ID3D11DeviceContext* g_pImmediateContext;
extern ID3D11RenderTargetView* g_pRenderTargetView;
extern ID3D11DepthStencilView* g_pDepthStencilView;
#include <unordered_map>
#include "../../Font.h"
#include "../../Textures.h"
#include "../../ResourceLocation.h"
// Internal constants & types
namespace
{
static constexpr float kVW = 1280.0f;
static constexpr float kVH = 720.0f;
static constexpr float kFontUnitH = 8.0f;
static constexpr int kMaxVerts = 8192;
static constexpr int kMaxClipStack = 16;
inline Font* GetFont()
{
return Minecraft::GetInstance()->font;
}
inline float ScaleForSize(float size)
{
return size / kFontUnitH;
}
struct UIVertex
{
float x, y;
float r, g, b, a;
float u, v;
};
// Batch mode determines which pixel shader to use
enum BatchMode { BATCH_COLOR, BATCH_TEXT, BATCH_TEXTURE };
// --- D3D11 resources ---
static ID3D11VertexShader* s_pVS = nullptr;
static ID3D11PixelShader* s_pPS_Color = nullptr;
static ID3D11PixelShader* s_pPS_Tex = nullptr; // font alpha-only
static ID3D11PixelShader* s_pPS_TexFull = nullptr; // full RGBA texture
static ID3D11InputLayout* s_pInputLayout = nullptr;
static ID3D11Buffer* s_pVB = nullptr;
static ID3D11RasterizerState* s_pRastState = nullptr;
static ID3D11RasterizerState* s_pRastScissor = nullptr;
static ID3D11DepthStencilState* s_pDepthState = nullptr;
static ID3D11BlendState* s_pBlendState = nullptr;
static ID3D11SamplerState* s_pSampler = nullptr; // point (fonts)
static ID3D11SamplerState* s_pSamplerLinear = nullptr; // bilinear (textures)
static bool s_initialized = false;
// Batching
static UIVertex s_vertices[kMaxVerts];
static int s_vertCount = 0;
static bool s_inFrame = false;
static BatchMode s_batchMode = BATCH_COLOR;
static int s_batchTexId = -1; // for BATCH_TEXTURE mode
// File texture cache: path → {texId, width, height}
struct FileTexEntry { int id; int w; int h; };
static std::unordered_map<std::string, FileTexEntry> s_fileTexCache;
// Clip stack
struct ClipRect { float x, y, w, h; };
static ClipRect s_clipStack[kMaxClipStack];
static int s_clipDepth = 0;
// Backbuffer dimensions (cached per frame)
static float s_bbWidth = kVW;
static float s_bbHeight = kVH;
// 16:9 viewport within the backbuffer (pillarboxed/letterboxed)
static float s_vpX = 0.0f; // viewport offset X in backbuffer pixels
static float s_vpY = 0.0f; // viewport offset Y in backbuffer pixels
static float s_vpW = kVW; // viewport width in backbuffer pixels
static float s_vpH = kVH; // viewport height in backbuffer pixels
// Saved D3D11 state
static ID3D11RenderTargetView* s_savedRTV = nullptr;
static ID3D11DepthStencilView* s_savedDSV = nullptr;
static D3D11_VIEWPORT s_savedViewport = {};
static ID3D11RasterizerState* s_savedRast = nullptr;
static ID3D11DepthStencilState* s_savedDepth = nullptr;
static UINT s_savedStencilRef = 0;
static ID3D11BlendState* s_savedBlend = nullptr;
static float s_savedBlendFactor[4] = {};
static UINT s_savedSampleMask = 0;
static ID3D11VertexShader* s_savedVS = nullptr;
static ID3D11PixelShader* s_savedPS = nullptr;
static ID3D11InputLayout* s_savedIL = nullptr;
static D3D11_PRIMITIVE_TOPOLOGY s_savedTopo = D3D11_PRIMITIVE_TOPOLOGY_UNDEFINED;
// HLSL shaders
static const char* s_vsCode =
"struct VS_IN { float2 pos : POSITION; float4 col : COLOR; float2 uv : TEXCOORD0; };\n"
"struct VS_OUT { float4 pos : SV_Position; float4 col : COLOR; float2 uv : TEXCOORD0; };\n"
"VS_OUT main(VS_IN i)\n"
"{\n"
" VS_OUT o;\n"
" o.pos.x = i.pos.x / 640.0 - 1.0;\n"
" o.pos.y = 1.0 - i.pos.y / 360.0;\n"
" o.pos.z = 0.0;\n"
" o.pos.w = 1.0;\n"
" o.col = i.col;\n"
" o.uv = i.uv;\n"
" return o;\n"
"}\n";
static const char* s_psColorCode =
"struct PS_IN { float4 pos : SV_Position; float4 col : COLOR; float2 uv : TEXCOORD0; };\n"
"float4 main(PS_IN i) : SV_Target { return i.col; }\n";
// Font text: sample alpha from atlas, color from vertex
static const char* s_psTexCode =
"Texture2D tex : register(t0);\n"
"SamplerState samp : register(s0);\n"
"struct PS_IN { float4 pos : SV_Position; float4 col : COLOR; float2 uv : TEXCOORD0; };\n"
"float4 main(PS_IN i) : SV_Target\n"
"{\n"
" float4 t = tex.Sample(samp, i.uv);\n"
" return float4(i.col.rgb, i.col.a * t.a);\n"
"}\n";
// Full-color texture: sample RGBA from texture, multiply by vertex color (tint)
static const char* s_psTexFullCode =
"Texture2D tex : register(t0);\n"
"SamplerState samp : register(s0);\n"
"struct PS_IN { float4 pos : SV_Position; float4 col : COLOR; float2 uv : TEXCOORD0; };\n"
"float4 main(PS_IN i) : SV_Target\n"
"{\n"
" float4 t = tex.Sample(samp, i.uv);\n"
" return t * i.col;\n"
"}\n";
// Helpers
static void DecodeColor(uint32_t color, float& r, float& g, float& b, float& a)
{
a = ((color >> 24) & 0xFF) / 255.0f;
r = ((color >> 16) & 0xFF) / 255.0f;
g = ((color >> 8) & 0xFF) / 255.0f;
b = ((color ) & 0xFF) / 255.0f;
}
static ID3D11PixelShader* CompilePS(const char* code, const char* name)
{
ID3DBlob* blob = nullptr;
ID3DBlob* err = nullptr;
HRESULT hr = D3DCompile(code, strlen(code), name, nullptr, nullptr,
"main", "ps_4_0", D3DCOMPILE_ENABLE_STRICTNESS, 0, &blob, &err);
if (FAILED(hr)) { if (err) err->Release(); return nullptr; }
if (err) err->Release();
ID3D11PixelShader* ps = nullptr;
hr = g_pd3dDevice->CreatePixelShader(blob->GetBufferPointer(),
blob->GetBufferSize(), nullptr, &ps);
blob->Release();
return SUCCEEDED(hr) ? ps : nullptr;
}
// Init / Shutdown
static void InitD3D()
{
if (s_initialized || !g_pd3dDevice) return;
// Clean up any leftover objects from a previous failed attempt
auto SafeRelease = [](auto*& p) { if (p) { p->Release(); p = nullptr; } };
SafeRelease(s_pVS);
SafeRelease(s_pInputLayout);
SafeRelease(s_pPS_Color);
SafeRelease(s_pPS_Tex);
SafeRelease(s_pPS_TexFull);
SafeRelease(s_pVB);
SafeRelease(s_pRastState);
SafeRelease(s_pRastScissor);
SafeRelease(s_pDepthState);
SafeRelease(s_pBlendState);
SafeRelease(s_pSampler);
SafeRelease(s_pSamplerLinear);
HRESULT hr;
ID3DBlob* vsBlob = nullptr;
ID3DBlob* err = nullptr;
hr = D3DCompile(s_vsCode, strlen(s_vsCode), "NativeUI_VS", nullptr, nullptr,
"main", "vs_4_0", D3DCOMPILE_ENABLE_STRICTNESS, 0, &vsBlob, &err);
if (FAILED(hr)) { if (err) err->Release(); return; }
if (err) { err->Release(); err = nullptr; }
hr = g_pd3dDevice->CreateVertexShader(vsBlob->GetBufferPointer(),
vsBlob->GetBufferSize(), nullptr, &s_pVS);
if (FAILED(hr)) { vsBlob->Release(); return; }
D3D11_INPUT_ELEMENT_DESC layout[] = {
{ "POSITION", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
{ "COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, sizeof(float) * 2, D3D11_INPUT_PER_VERTEX_DATA, 0 },
{ "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, sizeof(float) * 6, D3D11_INPUT_PER_VERTEX_DATA, 0 },
};
hr = g_pd3dDevice->CreateInputLayout(layout, 3, vsBlob->GetBufferPointer(),
vsBlob->GetBufferSize(), &s_pInputLayout);
vsBlob->Release();
if (FAILED(hr)) return;
s_pPS_Color = CompilePS(s_psColorCode, "NativeUI_PS_Color");
if (!s_pPS_Color) return;
s_pPS_Tex = CompilePS(s_psTexCode, "NativeUI_PS_Tex");
if (!s_pPS_Tex) return;
s_pPS_TexFull = CompilePS(s_psTexFullCode, "NativeUI_PS_TexFull");
if (!s_pPS_TexFull) return;
// Dynamic vertex buffer
D3D11_BUFFER_DESC vbDesc = {};
vbDesc.ByteWidth = sizeof(s_vertices);
vbDesc.Usage = D3D11_USAGE_DYNAMIC;
vbDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER;
vbDesc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
hr = g_pd3dDevice->CreateBuffer(&vbDesc, nullptr, &s_pVB);
if (FAILED(hr)) return;
// Rasterizer — no scissor
D3D11_RASTERIZER_DESC rasDesc = {};
rasDesc.FillMode = D3D11_FILL_SOLID;
rasDesc.CullMode = D3D11_CULL_NONE;
rasDesc.DepthClipEnable = FALSE;
rasDesc.ScissorEnable = FALSE;
g_pd3dDevice->CreateRasterizerState(&rasDesc, &s_pRastState);
// Rasterizer — with scissor
rasDesc.ScissorEnable = TRUE;
g_pd3dDevice->CreateRasterizerState(&rasDesc, &s_pRastScissor);
// Depth off
D3D11_DEPTH_STENCIL_DESC dsDesc = {};
dsDesc.DepthEnable = FALSE;
dsDesc.StencilEnable = FALSE;
g_pd3dDevice->CreateDepthStencilState(&dsDesc, &s_pDepthState);
// Alpha blending
D3D11_BLEND_DESC blendDesc = {};
blendDesc.RenderTarget[0].BlendEnable = TRUE;
blendDesc.RenderTarget[0].SrcBlend = D3D11_BLEND_SRC_ALPHA;
blendDesc.RenderTarget[0].DestBlend = D3D11_BLEND_INV_SRC_ALPHA;
blendDesc.RenderTarget[0].BlendOp = D3D11_BLEND_OP_ADD;
blendDesc.RenderTarget[0].SrcBlendAlpha = D3D11_BLEND_ONE;
blendDesc.RenderTarget[0].DestBlendAlpha = D3D11_BLEND_INV_SRC_ALPHA;
blendDesc.RenderTarget[0].BlendOpAlpha = D3D11_BLEND_OP_ADD;
blendDesc.RenderTarget[0].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL;
g_pd3dDevice->CreateBlendState(&blendDesc, &s_pBlendState);
// Point sampler for crisp pixel font
D3D11_SAMPLER_DESC sampDesc = {};
sampDesc.Filter = D3D11_FILTER_MIN_MAG_MIP_POINT;
sampDesc.AddressU = D3D11_TEXTURE_ADDRESS_CLAMP;
sampDesc.AddressV = D3D11_TEXTURE_ADDRESS_CLAMP;
sampDesc.AddressW = D3D11_TEXTURE_ADDRESS_CLAMP;
g_pd3dDevice->CreateSamplerState(&sampDesc, &s_pSampler);
// Bilinear sampler for texture rendering
D3D11_SAMPLER_DESC sampLinDesc = {};
sampLinDesc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR;
sampLinDesc.AddressU = D3D11_TEXTURE_ADDRESS_CLAMP;
sampLinDesc.AddressV = D3D11_TEXTURE_ADDRESS_CLAMP;
sampLinDesc.AddressW = D3D11_TEXTURE_ADDRESS_CLAMP;
g_pd3dDevice->CreateSamplerState(&sampLinDesc, &s_pSamplerLinear);
s_initialized = true;
}
// Bind the right texture SRV for the current batch mode
static void BindBatchTexture(ID3D11DeviceContext* ctx)
{
if (s_batchMode == BATCH_TEXT)
{
Font* font = GetFont();
if (font)
{
ResourceLocation* loc = font->getTextureLocation();
if (loc && loc->isPreloaded())
{
int texId = font->getTextures()->loadTexture(loc->getTexture());
ID3D11ShaderResourceView* srv = RenderManager.TextureGetTexture(texId);
if (srv)
{
ctx->PSSetShaderResources(0, 1, &srv);
ctx->PSSetSamplers(0, 1, &s_pSampler); // point sampler for fonts
}
}
}
}
else if (s_batchMode == BATCH_TEXTURE && s_batchTexId >= 0)
{
ID3D11ShaderResourceView* srv = RenderManager.TextureGetTexture(s_batchTexId);
if (srv)
{
ctx->PSSetShaderResources(0, 1, &srv);
ctx->PSSetSamplers(0, 1, &s_pSampler); // nearest (point) for textures
}
}
}
static void FlushBatch()
{
if (s_vertCount == 0 || !s_initialized) return;
ID3D11DeviceContext* ctx = g_pImmediateContext;
D3D11_MAPPED_SUBRESOURCE mapped;
HRESULT hr = ctx->Map(s_pVB, 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped);
if (FAILED(hr)) { s_vertCount = 0; return; }
memcpy(mapped.pData, s_vertices, s_vertCount * sizeof(UIVertex));
ctx->Unmap(s_pVB, 0);
if (s_batchMode == BATCH_COLOR)
ctx->PSSetShader(s_pPS_Color, nullptr, 0);
else if (s_batchMode == BATCH_TEXT)
{
ctx->PSSetShader(s_pPS_Tex, nullptr, 0); // alpha-only font shader
BindBatchTexture(ctx);
}
else // BATCH_TEXTURE
{
ctx->PSSetShader(s_pPS_TexFull, nullptr, 0); // full RGBA texture shader
BindBatchTexture(ctx);
}
ctx->Draw(s_vertCount, 0);
s_vertCount = 0;
}
static void EnsureBatchMode(BatchMode mode, int texId = -1)
{
if (s_vertCount > 0 && (s_batchMode != mode || (mode == BATCH_TEXTURE && s_batchTexId != texId)))
FlushBatch();
s_batchMode = mode;
s_batchTexId = texId;
}
// State save/restore
static void SaveD3DState()
{
ID3D11DeviceContext* ctx = g_pImmediateContext;
ctx->OMGetRenderTargets(1, &s_savedRTV, &s_savedDSV);
UINT numVP = 1;
ctx->RSGetViewports(&numVP, &s_savedViewport);
ctx->RSGetState(&s_savedRast);
ctx->OMGetDepthStencilState(&s_savedDepth, &s_savedStencilRef);
ctx->OMGetBlendState(&s_savedBlend, s_savedBlendFactor, &s_savedSampleMask);
ctx->VSGetShader(&s_savedVS, nullptr, nullptr);
ctx->PSGetShader(&s_savedPS, nullptr, nullptr);
ctx->IAGetInputLayout(&s_savedIL);
ctx->IAGetPrimitiveTopology(&s_savedTopo);
}
static void RestoreD3DState()
{
ID3D11DeviceContext* ctx = g_pImmediateContext;
ctx->OMSetRenderTargets(1, &s_savedRTV, s_savedDSV);
ctx->RSSetViewports(1, &s_savedViewport);
ctx->RSSetState(s_savedRast);
ctx->OMSetDepthStencilState(s_savedDepth, s_savedStencilRef);
ctx->OMSetBlendState(s_savedBlend, s_savedBlendFactor, s_savedSampleMask);
ctx->VSSetShader(s_savedVS, nullptr, 0);
ctx->PSSetShader(s_savedPS, nullptr, 0);
ctx->IASetInputLayout(s_savedIL);
ctx->IASetPrimitiveTopology(s_savedTopo);
if (s_savedRTV) { s_savedRTV->Release(); s_savedRTV = nullptr; }
if (s_savedDSV) { s_savedDSV->Release(); s_savedDSV = nullptr; }
if (s_savedRast) { s_savedRast->Release(); s_savedRast = nullptr; }
if (s_savedDepth) { s_savedDepth->Release(); s_savedDepth = nullptr; }
if (s_savedBlend) { s_savedBlend->Release(); s_savedBlend = nullptr; }
if (s_savedVS) { s_savedVS->Release(); s_savedVS = nullptr; }
if (s_savedPS) { s_savedPS->Release(); s_savedPS = nullptr; }
if (s_savedIL) { s_savedIL->Release(); s_savedIL = nullptr; }
}
static void SetupD3DPipeline()
{
ID3D11DeviceContext* ctx = g_pImmediateContext;
ctx->OMSetRenderTargets(1, &g_pRenderTargetView, nullptr);
ID3D11Resource* rtvResource = nullptr;
g_pRenderTargetView->GetResource(&rtvResource);
ID3D11Texture2D* rtvTex = nullptr;
rtvResource->QueryInterface(__uuidof(ID3D11Texture2D), (void**)&rtvTex);
rtvResource->Release();
if (rtvTex)
{
D3D11_TEXTURE2D_DESC desc;
rtvTex->GetDesc(&desc);
s_bbWidth = (float)desc.Width;
s_bbHeight = (float)desc.Height;
rtvTex->Release();
}
else
{
s_bbWidth = kVW;
s_bbHeight = kVH;
}
// Compute a 16:9 viewport centered in the backbuffer (pillarbox on ultrawide)
{
float bbAspect = s_bbWidth / s_bbHeight;
constexpr float targetAspect = kVW / kVH; // 16:9
if (bbAspect > targetAspect)
{
// Wider than 16:9 → pillarbox (bars on left/right)
s_vpH = s_bbHeight;
s_vpW = s_bbHeight * targetAspect;
s_vpX = (s_bbWidth - s_vpW) * 0.5f;
s_vpY = 0.0f;
}
else if (bbAspect < targetAspect)
{
// Taller than 16:9 → letterbox (bars on top/bottom)
s_vpW = s_bbWidth;
s_vpH = s_bbWidth / targetAspect;
s_vpX = 0.0f;
s_vpY = (s_bbHeight - s_vpH) * 0.5f;
}
else
{
// Exactly 16:9
s_vpX = 0.0f;
s_vpY = 0.0f;
s_vpW = s_bbWidth;
s_vpH = s_bbHeight;
}
}
D3D11_VIEWPORT vp = {};
vp.TopLeftX = s_vpX;
vp.TopLeftY = s_vpY;
vp.Width = s_vpW;
vp.Height = s_vpH;
vp.MinDepth = 0.0f;
vp.MaxDepth = 1.0f;
ctx->RSSetViewports(1, &vp);
ctx->IASetInputLayout(s_pInputLayout);
ctx->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
UINT stride = sizeof(UIVertex);
UINT offset = 0;
ctx->IASetVertexBuffers(0, 1, &s_pVB, &stride, &offset);
ctx->VSSetShader(s_pVS, nullptr, 0);
ctx->PSSetShader(s_pPS_Color, nullptr, 0);
ctx->RSSetState(s_pRastState);
ctx->OMSetDepthStencilState(s_pDepthState, 0);
constexpr float bf[4] = { 0, 0, 0, 0 };
ctx->OMSetBlendState(s_pBlendState, bf, 0xFFFFFFFF);
}
// Vertex push helpers
static void PushQuad(float x0, float y0, float x1, float y1,
float r, float g, float b, float a,
float u0, float v0, float u1, float v1)
{
if (s_vertCount + 6 > kMaxVerts) FlushBatch();
UIVertex* v = &s_vertices[s_vertCount];
v[0] = { x0, y0, r, g, b, a, u0, v0 };
v[1] = { x1, y0, r, g, b, a, u1, v0 };
v[2] = { x0, y1, r, g, b, a, u0, v1 };
v[3] = { x0, y1, r, g, b, a, u0, v1 };
v[4] = { x1, y0, r, g, b, a, u1, v0 };
v[5] = { x1, y1, r, g, b, a, u1, v1 };
s_vertCount += 6;
}
// Gradient quad — top color to bottom color
static void PushGradientQuad(float x0, float y0, float x1, float y1,
float tr, float tg, float tb, float ta,
float br, float bg, float bb, float ba)
{
if (s_vertCount + 6 > kMaxVerts) FlushBatch();
UIVertex* v = &s_vertices[s_vertCount];
v[0] = { x0, y0, tr, tg, tb, ta, 0, 0 };
v[1] = { x1, y0, tr, tg, tb, ta, 0, 0 };
v[2] = { x0, y1, br, bg, bb, ba, 0, 0 };
v[3] = { x0, y1, br, bg, bb, ba, 0, 0 };
v[4] = { x1, y0, tr, tg, tb, ta, 0, 0 };
v[5] = { x1, y1, br, bg, bb, ba, 0, 0 };
s_vertCount += 6;
}
// Clip rect helpers
static void ApplyScissor()
{
ID3D11DeviceContext* ctx = g_pImmediateContext;
if (s_clipDepth <= 0)
{
ctx->RSSetState(s_pRastState);
return;
}
ctx->RSSetState(s_pRastScissor);
// Convert virtual coords to backbuffer pixels using the 16:9 viewport
const ClipRect& c = s_clipStack[s_clipDepth - 1];
float scaleX = s_vpW / kVW;
float scaleY = s_vpH / kVH;
D3D11_RECT r;
r.left = (LONG)(s_vpX + c.x * scaleX);
r.top = (LONG)(s_vpY + c.y * scaleY);
r.right = (LONG)(s_vpX + (c.x + c.w) * scaleX);
r.bottom = (LONG)(s_vpY + (c.y + c.h) * scaleY);
ctx->RSSetScissorRects(1, &r);
}
// Glyph rendering
static void DrawGlyphString(Font* font, const std::wstring& str,
float startX, float startY, float scale,
float cr, float cg, float cb, float ca)
{
EnsureBatchMode(BATCH_TEXT);
const int cols = font->getCols();
const int cw = font->getCharWidth();
const int ch = font->getCharHeight();
const float atlasW = (float)(cols * cw);
const float atlasH = (float)(font->getRows() * ch);
float curX = startX;
for (size_t i = 0; i < str.length(); ++i)
{
wchar_t c = str[i];
// Skip section sign format codes
if (c == 167 && i + 1 < str.length())
{
++i;
continue;
}
wchar_t mapped = font->mapChar(c);
int charW = font->getCharPixelWidth(c);
float uOff = (float)(mapped % cols * cw);
float vOff = (float)(mapped / cols * ch);
float u0 = uOff / atlasW;
float v0 = vOff / atlasH;
float u1 = (uOff + (float)charW - 0.01f) / atlasW;
float v1 = (vOff + 7.99f) / atlasH;
float gx0 = curX;
float gy0 = startY;
float gx1 = curX + ((float)charW - 0.01f) * scale;
float gy1 = startY + (float)ch * scale;
PushQuad(gx0, gy0, gx1, gy1, cr, cg, cb, ca, u0, v0, u1, v1);
curX += (float)charW * scale;
}
}
static void ApplyAlignment(Font* font, const std::wstring& wstr,
float x, float y,
float scale, uint32_t align,
float& drawX, float& drawY)
{
float textW = static_cast<float>(font->width(wstr)) * scale;
float textH = kFontUnitH * scale;
drawX = x;
if (align & NativeUI::ALIGN_CENTER_X) drawX = x - textW * 0.5f;
else if (align & NativeUI::ALIGN_RIGHT) drawX = x - textW;
drawY = y;
if (align & NativeUI::ALIGN_CENTER_Y) drawY = y - textH * 0.5f;
else if (align & NativeUI::ALIGN_BOTTOM) drawY = y - textH;
}
}
// =======================================================================
// NativeUI public API
// =======================================================================
namespace NativeUI
{
// ---- Lifecycle --------------------------------------------------------
void BeginFrame()
{
InitD3D();
if (!s_initialized) return;
SaveD3DState();
SetupD3DPipeline();
s_vertCount = 0;
s_batchMode = BATCH_COLOR;
s_batchTexId = -1;
s_clipDepth = 0;
s_inFrame = true;
}
void EndFrame()
{
if (!s_inFrame) return;
FlushBatch();
// Unbind SRV to avoid hazards
ID3D11ShaderResourceView* nullSrv = nullptr;
g_pImmediateContext->PSSetShaderResources(0, 1, &nullSrv);
RestoreD3DState();
s_inFrame = false;
}
void Shutdown()
{
auto SafeRelease = [](auto*& p) { if (p) { p->Release(); p = nullptr; } };
SafeRelease(s_pBlendState);
SafeRelease(s_pDepthState);
SafeRelease(s_pRastState);
SafeRelease(s_pRastScissor);
SafeRelease(s_pSampler);
SafeRelease(s_pSamplerLinear);
SafeRelease(s_pVB);
SafeRelease(s_pInputLayout);
SafeRelease(s_pPS_TexFull);
SafeRelease(s_pPS_Tex);
SafeRelease(s_pPS_Color);
SafeRelease(s_pVS);
s_initialized = false;
}
// ---- Primitives -------------------------------------------------------
void DrawRect(float x, float y, float w, float h, uint32_t color)
{
if (!s_inFrame) return;
EnsureBatchMode(BATCH_COLOR);
float r, g, b, a;
DecodeColor(color, r, g, b, a);
PushQuad(x, y, x + w, y + h, r, g, b, a, 0, 0, 0, 0);
}
void DrawRectFullscreen(uint32_t color)
{
if (!s_inFrame) return;
// Flush pending geometry that uses the 16:9 viewport
FlushBatch();
// Switch to full-backbuffer viewport
D3D11_VIEWPORT fullVP = {};
fullVP.TopLeftX = 0;
fullVP.TopLeftY = 0;
fullVP.Width = s_bbWidth;
fullVP.Height = s_bbHeight;
fullVP.MinDepth = 0.0f;
fullVP.MaxDepth = 1.0f;
g_pImmediateContext->RSSetViewports(1, &fullVP);
// Emit a fullscreen quad (in 1280x720 virtual, which the shader maps to NDC)
EnsureBatchMode(BATCH_COLOR);
float r, g, b, a;
DecodeColor(color, r, g, b, a);
PushQuad(0, 0, kVW, kVH, r, g, b, a, 0, 0, 0, 0);
FlushBatch();
// Restore the 16:9 centered viewport
D3D11_VIEWPORT vp = {};
vp.TopLeftX = s_vpX;
vp.TopLeftY = s_vpY;
vp.Width = s_vpW;
vp.Height = s_vpH;
vp.MinDepth = 0.0f;
vp.MaxDepth = 1.0f;
g_pImmediateContext->RSSetViewports(1, &vp);
}
static void EmitCornerFans(const float cx[4], const float cy[4],
const float startAngle[4], float radius,
const float cr[4], const float cg[4],
const float cb[4], const float ca[4])
{
static constexpr int kSeg = 6;
static constexpr float kHalfPi = 1.5707963f;
for (int corner = 0; corner < 4; ++corner)
{
for (int seg = 0; seg < kSeg; ++seg)
{
float a0 = startAngle[corner] + kHalfPi * seg / kSeg;
float a1 = startAngle[corner] + kHalfPi * (seg + 1) / kSeg;
float px0 = cx[corner] + cosf(a0) * radius;
float py0 = cy[corner] + sinf(a0) * radius;
float px1 = cx[corner] + cosf(a1) * radius;
float py1 = cy[corner] + sinf(a1) * radius;
if (s_vertCount + 3 > kMaxVerts) FlushBatch();
UIVertex* v = &s_vertices[s_vertCount];
v[0] = { cx[corner], cy[corner], cr[corner], cg[corner], cb[corner], ca[corner], 0, 0 };
v[1] = { px0, py0, cr[corner], cg[corner], cb[corner], ca[corner], 0, 0 };
v[2] = { px1, py1, cr[corner], cg[corner], cb[corner], ca[corner], 0, 0 };
s_vertCount += 3;
}
}
}
void DrawRoundedRect(float x, float y, float w, float h,
float radius, uint32_t color)
{
if (!s_inFrame) return;
EnsureBatchMode(BATCH_COLOR);
float r, g, b, a;
DecodeColor(color, r, g, b, a);
// Clamp radius
float maxR = fminf(w, h) * 0.5f;
if (radius > maxR) radius = maxR;
static constexpr float kHalfPi = 1.5707963f;
// Center cross
PushQuad(x + radius, y, x + w - radius, y + h, r, g, b, a, 0, 0, 0, 0);
PushQuad(x, y + radius, x + radius, y + h - radius, r, g, b, a, 0, 0, 0, 0);
PushQuad(x + w - radius, y + radius, x + w, y + h - radius, r, g, b, a, 0, 0, 0, 0);
// Corner fans (TL, TR, BL, BR)
float cx[4] = { x + radius, x + w - radius, x + radius, x + w - radius };
float cy[4] = { y + radius, y + radius, y + h - radius, y + h - radius };
float startAngle[4] = { kHalfPi * 2, kHalfPi * 3, kHalfPi, 0 };
float cr[4] = { r, r, r, r };
float cg[4] = { g, g, g, g };
float cb[4] = { b, b, b, b };
float ca[4] = { a, a, a, a };
EmitCornerFans(cx, cy, startAngle, radius, cr, cg, cb, ca);
}
void DrawRoundedBorder(float x, float y, float w, float h,
float radius, float thickness, uint32_t color)
{
if (!s_inFrame) return;
// Draw as four rounded-rect strips (outer minus inner)
float maxR = fminf(w, h) * 0.5f;
if (radius > maxR) radius = maxR;
float ri = fmaxf(0.0f, radius - thickness);
// Top edge
DrawRect(x + radius, y, w - 2 * radius, thickness, color);
// Bottom edge
DrawRect(x + radius, y + h - thickness, w - 2 * radius, thickness, color);
// Left edge
DrawRect(x, y + radius, thickness, h - 2 * radius, color);
// Right edge
DrawRect(x + w - thickness, y + radius, thickness, h - 2 * radius, color);
// Corner arcs
EnsureBatchMode(BATCH_COLOR);
float r, g, b, a;
DecodeColor(color, r, g, b, a);
static constexpr int kSeg = 6;
static constexpr float kHalfPi = 1.5707963f;
float cx_[4] = { x + radius, x + w - radius, x + radius, x + w - radius };
float cy_[4] = { y + radius, y + radius, y + h - radius, y + h - radius };
float startA[4] = { kHalfPi * 2, kHalfPi * 3, kHalfPi, 0 };
for (int corner = 0; corner < 4; ++corner)
{
for (int seg = 0; seg < kSeg; ++seg)
{
float a0 = startA[corner] + kHalfPi * seg / kSeg;
float a1 = startA[corner] + kHalfPi * (seg + 1) / kSeg;
// Outer edge
float ox0 = cx_[corner] + cosf(a0) * radius;
float oy0 = cy_[corner] + sinf(a0) * radius;
float ox1 = cx_[corner] + cosf(a1) * radius;
float oy1 = cy_[corner] + sinf(a1) * radius;
// Inner edge
float ix0 = cx_[corner] + cosf(a0) * ri;
float iy0 = cy_[corner] + sinf(a0) * ri;
float ix1 = cx_[corner] + cosf(a1) * ri;
float iy1 = cy_[corner] + sinf(a1) * ri;
if (s_vertCount + 6 > kMaxVerts) FlushBatch();
UIVertex* v = &s_vertices[s_vertCount];
v[0] = { ox0, oy0, r, g, b, a, 0, 0 };
v[1] = { ox1, oy1, r, g, b, a, 0, 0 };
v[2] = { ix0, iy0, r, g, b, a, 0, 0 };
v[3] = { ix0, iy0, r, g, b, a, 0, 0 };
v[4] = { ox1, oy1, r, g, b, a, 0, 0 };
v[5] = { ix1, iy1, r, g, b, a, 0, 0 };
s_vertCount += 6;
}
}
}
void DrawBorder(float x, float y, float w, float h,
float thickness, uint32_t color)
{
DrawRect(x, y, w, thickness, color);
DrawRect(x, y + h - thickness, w, thickness, color);
DrawRect(x, y + thickness, thickness, h - 2.0f * thickness, color);
DrawRect(x + w - thickness, y + thickness, thickness, h - 2.0f * thickness, color);
}
void DrawLine(float x, float y, float length, float thickness, uint32_t color)
{
DrawRect(x, y, length, thickness, color);
}
void DrawLineV(float x, float y, float length, float thickness, uint32_t color)
{
DrawRect(x, y, thickness, length, color);
}
void DrawGradientRect(float x, float y, float w, float h,
uint32_t topColor, uint32_t bottomColor)
{
if (!s_inFrame) return;
EnsureBatchMode(BATCH_COLOR);
float tr, tg, tb, ta, br, bg, bb, ba;
DecodeColor(topColor, tr, tg, tb, ta);
DecodeColor(bottomColor, br, bg, bb, ba);
PushGradientQuad(x, y, x + w, y + h, tr, tg, tb, ta, br, bg, bb, ba);
}
void DrawGradientRoundedRect(float x, float y, float w, float h,
float radius,
uint32_t topColor, uint32_t bottomColor)
{
if (!s_inFrame) return;
EnsureBatchMode(BATCH_COLOR);
float maxR = fminf(w, h) * 0.5f;
if (radius > maxR) radius = maxR;
float tr, tg, tb, ta, br, bg, bb, ba;
DecodeColor(topColor, tr, tg, tb, ta);
DecodeColor(bottomColor, br, bg, bb, ba);
// Center cross with gradient
PushGradientQuad(x + radius, y, x + w - radius, y + h,
tr, tg, tb, ta, br, bg, bb, ba);
// Left strip — interpolate color based on vertical position
float midTopY = y + radius;
float midBotY = y + h - radius;
PushGradientQuad(x, midTopY, x + radius, midBotY,
tr, tg, tb, ta, br, bg, bb, ba);
// Right strip
PushGradientQuad(x + w - radius, midTopY, x + w, midBotY,
tr, tg, tb, ta, br, bg, bb, ba);
// Corners — use top color for TL/TR, bottom for BL/BR
static constexpr float kHalfPi = 1.5707963f;
float cx_[4] = { x + radius, x + w - radius, x + radius, x + w - radius };
float cy_[4] = { y + radius, y + radius, y + h - radius, y + h - radius };
float startA[4] = { kHalfPi * 2, kHalfPi * 3, kHalfPi, 0 };
// TL=top, TR=top, BL=bottom, BR=bottom
float cr[4] = { tr, tr, br, br };
float cg[4] = { tg, tg, bg, bg };
float cb[4] = { tb, tb, bb, bb };
float ca[4] = { ta, ta, ba, ba };
EmitCornerFans(cx_, cy_, startA, radius, cr, cg, cb, ca);
}
void DrawDropShadow(float x, float y, float w, float h,
float offset, float spread, uint32_t color)
{
if (!s_inFrame) return;
// Multi-layer shadow for soft appearance
float r, g, b, a;
DecodeColor(color, r, g, b, a);
int layers = 3;
for (int i = 0; i < layers; ++i)
{
float t = (float)(i + 1) / (float)layers;
float expand = spread * t;
float off = offset * t;
float layerA = a * (1.0f - t * 0.6f) / (float)layers;
DrawRect(x - expand + off, y - expand + off,
w + expand * 2, h + expand * 2,
((uint32_t)(layerA * 255.0f) << 24) | (color & 0x00FFFFFFu));
}
}
void DrawPanel(float x, float y, float w, float h,
float radius, uint32_t bgColor, uint32_t borderColor,
float borderThick, bool shadow)
{
if (shadow)
DrawDropShadow(x, y, w, h, 4.0f, 6.0f, 0x60000000u);
if (radius > 0.0f)
{
DrawRoundedRect(x, y, w, h, radius, bgColor);
if (borderThick > 0.0f)
DrawRoundedBorder(x, y, w, h, radius, borderThick, borderColor);
}
else
{
DrawRect(x, y, w, h, bgColor);
if (borderThick > 0.0f)
DrawBorder(x, y, w, h, borderThick, borderColor);
}
}
void DrawDivider(float x, float y, float w, uint32_t color, float thickness)
{
DrawRect(x, y, w, thickness, color);
}
// ---- Text -------------------------------------------------------------
void DrawText(float x, float y, const wchar_t* text,
uint32_t color, float size, uint32_t align)
{
if (!text || !text[0] || !s_inFrame) return;
Font* font = GetFont();
if (!font) return;
std::wstring wstr = font->sanitize(std::wstring(text));
const float scale = ScaleForSize(size);
float drawX, drawY;
ApplyAlignment(font, wstr, x, y, scale, align, drawX, drawY);
if ((color & 0xFC000000) == 0) color |= 0xFF000000;
float r, g, b, a;
DecodeColor(color, r, g, b, a);
DrawGlyphString(font, wstr, drawX, drawY, scale, r, g, b, a);
}
void DrawShadowText(float x, float y, const wchar_t* text,
uint32_t color, float size, uint32_t align)
{
if (!text || !text[0] || !s_inFrame) return;
Font* font = GetFont();
if (!font) return;
std::wstring wstr = font->sanitize(std::wstring(text));
const float scale = ScaleForSize(size);
float drawX, drawY;
ApplyAlignment(font, wstr, x, y, scale, align, drawX, drawY);
if ((color & 0xFC000000) == 0) color |= 0xFF000000;
uint32_t shadow = (color & 0xfcfcfc) >> 2 | (color & 0xFF000000);
float sr, sg, sb, sa;
DecodeColor(shadow, sr, sg, sb, sa);
DrawGlyphString(font, wstr, drawX + scale, drawY + scale, scale, sr, sg, sb, sa);
float r, g, b, a;
DecodeColor(color, r, g, b, a);
DrawGlyphString(font, wstr, drawX, drawY, scale, r, g, b, a);
}
float DrawTextWrapped(float x, float y, const wchar_t* text,
float maxWidth, uint32_t color,
float size, uint32_t align)
{
if (!text || !text[0] || !s_inFrame) return 0.0f;
Font* font = GetFont();
if (!font) return 0.0f;
std::wstring wstr = font->sanitize(std::wstring(text));
const float scale = ScaleForSize(size);
const float lineH = LineHeight(size);
float curY = y;
size_t lineStart = 0;
size_t lastSpace = std::wstring::npos;
for (size_t i = 0; i <= wstr.length(); ++i)
{
bool endOfString = (i == wstr.length());
bool isSpace = !endOfString && wstr[i] == L' ';
if (isSpace) lastSpace = i;
// Measure current segment
std::wstring segment = wstr.substr(lineStart, i - lineStart);
float segW = static_cast<float>(font->width(segment)) * scale;
if (segW > maxWidth || endOfString)
{
size_t breakAt;
if (endOfString)
breakAt = i;
else if (lastSpace != std::wstring::npos && lastSpace > lineStart)
breakAt = lastSpace;
else
breakAt = i > lineStart ? i - 1 : i;
std::wstring line = wstr.substr(lineStart, breakAt - lineStart);
if (!line.empty())
{
float drawX, drawY;
ApplyAlignment(font, line, x, curY, scale, align, drawX, drawY);
if ((color & 0xFC000000) == 0) color |= 0xFF000000;
float r, g, b, a;
DecodeColor(color, r, g, b, a);
DrawGlyphString(font, line, drawX, drawY, scale, r, g, b, a);
curY += lineH;
}
lineStart = breakAt;
// Skip the space at the break point
if (lineStart < wstr.length() && wstr[lineStart] == L' ')
++lineStart;
lastSpace = std::wstring::npos;
i = lineStart;
}
}
return curY - y;
}
void MeasureText(const wchar_t* text, float size,
float* outWidth, float* outHeight)
{
if (!text || !text[0])
{
if (outWidth) *outWidth = 0.0f;
if (outHeight) *outHeight = 0.0f;
return;
}
Font* font = GetFont();
if (!font)
{
if (outWidth) *outWidth = 0.0f;
if (outHeight) *outHeight = 0.0f;
return;
}
const float scale = ScaleForSize(size);
if (outWidth) *outWidth = static_cast<float>(font->width(std::wstring(text))) * scale;
if (outHeight) *outHeight = size;
}
float LineHeight(float size)
{
return size + 2.0f * ScaleForSize(size);
}
// ---- Clipping ---------------------------------------------------------
void PushClipRect(float x, float y, float w, float h)
{
if (!s_inFrame) return;
FlushBatch();
ClipRect newClip = { x, y, w, h };
// Intersect with parent clip
if (s_clipDepth > 0)
{
const ClipRect& parent = s_clipStack[s_clipDepth - 1];
float nx0 = fmaxf(x, parent.x);
float ny0 = fmaxf(y, parent.y);
float nx1 = fminf(x + w, parent.x + parent.w);
float ny1 = fminf(y + h, parent.y + parent.h);
newClip.x = nx0;
newClip.y = ny0;
newClip.w = fmaxf(0.0f, nx1 - nx0);
newClip.h = fmaxf(0.0f, ny1 - ny0);
}
if (s_clipDepth < kMaxClipStack)
s_clipStack[s_clipDepth++] = newClip;
ApplyScissor();
}
void PopClipRect()
{
if (!s_inFrame) return;
FlushBatch();
if (s_clipDepth > 0) --s_clipDepth;
ApplyScissor();
}
// ---- Widgets ----------------------------------------------------------
// Cached gui/gui.png texture ID
static int s_guiTexId = -2; // -2 = not yet loaded
static int GetGuiTexture()
{
if (s_guiTexId == -2)
s_guiTexId = LoadTexture(L"/gui/gui.png");
return s_guiTexId;
}
// Draw a 9-slice sub-region from gui.png using UV sub-rects.
// srcY/srcH in texel coords (256x256 atlas).
// capL/capR = horizontal border in texels, capT/capB = vertical border in texels.
// Borders are scaled uniformly (scale = h/srcH), only center stretches.
static void DrawGuiSlice(float x, float y, float w, float h,
int texId, float srcY, float srcH,
float capL = 2.0f, float capR = 2.0f,
float capT = 2.0f, float capB = 3.0f)
{
const float texSz = 256.0f;
const float srcW = 200.0f;
// Scale: each texel → this many virtual pixels
float scale = h / srcH;
float cL = capL * scale;
float cR = capR * scale;
float cT = capT * scale;
float cB = capB * scale;
// Clamp if too small
if (cL + cR > w) { float s = w / (cL + cR); cL *= s; cR *= s; }
if (cT + cB > h) { float s = h / (cT + cB); cT *= s; cB *= s; }
float midW = w - cL - cR;
float midH = h - cT - cB;
// UV coordinates
float u0 = 0.0f;
float uL = capL / texSz;
float uR = (srcW - capR) / texSz;
float u1 = srcW / texSz;
float v0 = srcY / texSz;
float vT = (srcY + capT) / texSz;
float vB = (srcY + srcH - capB) / texSz;
float v1 = (srcY + srcH) / texSz;
// Top row
DrawTextureUV(x, y, cL, cT, texId, u0, v0, uL, vT);
DrawTextureUV(x + cL, y, midW, cT, texId, uL, v0, uR, vT);
DrawTextureUV(x + w - cR, y, cR, cT, texId, uR, v0, u1, vT);
// Middle row
DrawTextureUV(x, y + cT, cL, midH, texId, u0, vT, uL, vB);
DrawTextureUV(x + cL, y + cT, midW, midH, texId, uL, vT, uR, vB);
DrawTextureUV(x + w - cR, y + cT, cR, midH, texId, uR, vT, u1, vB);
// Bottom row
DrawTextureUV(x, y + h - cB, cL, cB, texId, u0, vB, uL, v1);
DrawTextureUV(x + cL, y + h - cB, midW, cB, texId, uL, vB, uR, v1);
DrawTextureUV(x + w - cR, y + h - cB, cR, cB, texId, uR, vB, u1, v1);
}
void DrawButton(float x, float y, float w, float h,
const wchar_t* label, bool focused, bool hovered,
float labelSize)
{
int texId = GetGuiTexture();
bool active = focused || hovered;
if (texId >= 0)
{
// gui.png: button normal at y=66 (20px), hovered/focused at y=86 (20px)
float srcY = active ? 86.0f : 66.0f;
DrawGuiSlice(x, y, w, h, texId, srcY, 20.0f, 2.0f, 2.0f, 2.0f, 3.0f);
}
else
{
uint32_t bg = active ? 0xFF1B5A8Cu : 0xFF282828u;
DrawRoundedRect(x, y, w, h, 4.0f, bg);
}
if (label && label[0])
{
// White normally, yellow (#FFFF55) when focused/hovered — standard MC behavior
uint32_t textColor = active ? 0xFFFFFF55u : 0xFFFFFFFFu;
DrawShadowText(x + w * 0.5f, y + h * 0.5f, label, textColor,
labelSize, ALIGN_CENTER_X | ALIGN_CENTER_Y);
}
}
// Cached texture IDs for reusable widgets
static int s_enchantBtnTex = -2;
static int s_sliderTrackTex = -2;
static int s_sliderBtnTex = -2;
void DrawTextBox(float x, float y, float w, float h, uint32_t tint)
{
if (s_enchantBtnTex == -2)
s_enchantBtnTex = LoadTextureFromFile(
"Common/Media/Graphics/EnchantmentButtonEmpty.png");
if (s_enchantBtnTex >= 0)
{
// EnchantmentButtonEmpty.png = 240x42, 3-slice with ~6 texel caps
float scale = h / 42.0f;
float capW = 6.0f * scale;
if (capW * 2 > w) capW = w * 0.5f;
float midW = w - capW * 2;
float uCap = 6.0f / 240.0f;
float uMid = (240.0f - 6.0f) / 240.0f;
DrawTextureUV(x, y, capW, h, s_enchantBtnTex, 0.0f, 0.0f, uCap, 1.0f, tint);
DrawTextureUV(x + capW, y, midW, h, s_enchantBtnTex, uCap, 0.0f, uMid, 1.0f, tint);
DrawTextureUV(x + w - capW, y, capW, h, s_enchantBtnTex, uMid, 0.0f, 1.0f, 1.0f, tint);
}
else
{
DrawRoundedRect(x, y, w, h, 2.0f, 0xFF1A1A2Au);
DrawRoundedBorder(x, y, w, h, 2.0f, 1.0f, 0xFF444444u);
}
}
void DrawProgressBar(float x, float y, float w, float h,
float progress,
uint32_t fillColor, uint32_t trackColor)
{
float radius = h * 0.5f;
DrawRoundedRect(x, y, w, h, radius, trackColor);
float fill = w * (progress < 0.0f ? 0.0f : progress > 1.0f ? 1.0f : progress);
if (fill > radius * 2.0f)
DrawRoundedRect(x, y, fill, h, radius, fillColor);
else if (fill > 0.0f)
DrawRoundedRect(x, y, fmaxf(fill, h), h, radius, fillColor);
}
void DrawSpinner(float cx, float cy, float radius, int tick, uint32_t color)
{
static constexpr int kDots = 10;
static constexpr float kTwoPi = 6.28318530f;
const uint32_t baseA = (color >> 24) & 0xFFu;
const uint32_t rgb = color & 0x00FFFFFFu;
for (int i = 0; i < kDots; ++i)
{
const float angle = (float)i / kDots * kTwoPi - kTwoPi * 0.25f;
const float dx = cosf(angle) * radius;
const float dy = sinf(angle) * radius;
const int age = (i - tick % kDots + kDots) % kDots;
const float t = 1.0f - (float)age / kDots;
// Scale dot size based on fade — leading dot is bigger
const float dotR = radius * (0.10f + 0.12f * t);
const uint32_t a = (uint32_t)(baseA * (0.15f + 0.85f * t)) & 0xFFu;
DrawRoundedRect(cx + dx - dotR, cy + dy - dotR,
dotR * 2.0f, dotR * 2.0f, dotR, (a << 24) | rgb);
}
}
void DrawCheckbox(float x, float y, float size, bool checked, bool focused,
bool hovered)
{
uint32_t bgColor, borderColor;
float radius = 3.0f;
if (focused)
{
bgColor = 0xFF1A3A55u;
borderColor = 0xFF4DC3FFu;
}
else if (hovered)
{
bgColor = 0xFF2A3A48u;
borderColor = 0xFF3DA8E0u;
}
else
{
bgColor = 0xFF222222u;
borderColor = 0xFF555555u;
}
DrawRoundedRect(x, y, size, size, radius, bgColor);
DrawRoundedBorder(x, y, size, size, radius, 2.0f, borderColor);
if (checked)
{
float pad = size * 0.22f;
DrawRoundedRect(x + pad, y + pad, size - pad * 2, size - pad * 2,
2.0f, 0xFF4DC3FFu);
}
}
void DrawSlider(float x, float y, float w, float h,
float value, bool focused, bool hovered,
uint32_t fillColor, uint32_t trackColor)
{
value = value < 0.0f ? 0.0f : value > 1.0f ? 1.0f : value;
// Load slider textures once
if (s_sliderTrackTex == -2)
s_sliderTrackTex = LoadTextureFromFile(
"Common/Media/Graphics/Slider_Track.png");
if (s_sliderBtnTex == -2)
s_sliderBtnTex = LoadTextureFromFile(
"Common/Media/Graphics/Slider_Button.png");
if (s_sliderTrackTex >= 0)
{
// Slider_Track.png = 600x32, 3-slice with ~8 texel caps
float scale = h / 32.0f;
float capW = 8.0f * scale;
if (capW * 2 > w) capW = w * 0.5f;
float midW = w - capW * 2;
float uCap = 8.0f / 600.0f;
float uMid = (600.0f - 8.0f) / 600.0f;
DrawTextureUV(x, y, capW, h, s_sliderTrackTex, 0.0f, 0.0f, uCap, 1.0f);
DrawTextureUV(x + capW, y, midW, h, s_sliderTrackTex, uCap, 0.0f, uMid, 1.0f);
DrawTextureUV(x + w - capW, y, capW, h, s_sliderTrackTex, uMid, 0.0f, 1.0f, 1.0f);
}
else
{
// Fallback
float radius = h * 0.5f;
DrawRoundedRect(x, y, w, h, radius, trackColor);
}
// Thumb — Slider_Button.png = 16x32
if (s_sliderBtnTex >= 0)
{
// Scale thumb to match track height, keep aspect ratio (16:32 = 1:2)
float thumbH = h;
float thumbW = thumbH * (16.0f / 32.0f);
float thumbX = x + value * (w - thumbW);
if (thumbX < x) thumbX = x;
if (thumbX + thumbW > x + w) thumbX = x + w - thumbW;
DrawTexture(thumbX, y, thumbW, thumbH, s_sliderBtnTex);
}
else
{
// Fallback thumb
float thumbR = h * 0.8f;
float thumbX = x + w * value;
if (thumbX < x + thumbR) thumbX = x + thumbR;
if (thumbX > x + w - thumbR) thumbX = x + w - thumbR;
uint32_t thumbColor = (focused || hovered) ? 0xFFFFFFFFu : 0xFFCCCCCCu;
DrawRoundedRect(thumbX - thumbR, y + h * 0.5f - thumbR,
thumbR * 2, thumbR * 2, thumbR, thumbColor);
}
}
void DrawTooltip(float x, float y, const wchar_t* text, float size)
{
if (!text || !text[0]) return;
float tw, th;
MeasureText(text, size, &tw, &th);
float padX = 10.0f, padY = 6.0f;
float boxW = tw + padX * 2;
float boxH = th + padY * 2;
float radius = 4.0f;
// Position above the point, centered
float bx = x - boxW * 0.5f;
float by = y - boxH - 6.0f;
DrawDropShadow(bx, by, boxW, boxH, 3.0f, 4.0f, 0x50000000u);
DrawRoundedRect(bx, by, boxW, boxH, radius, 0xF0181818u);
DrawRoundedBorder(bx, by, boxW, boxH, radius, 1.0f, 0xFF555555u);
DrawShadowText(x, by + padY, text, 0xFFFFFFFFu, size, ALIGN_CENTER_X);
}
// ---- Texture drawing --------------------------------------------------
int LoadTexture(const wchar_t* path)
{
if (!path || !path[0]) return -1;
Minecraft* mc = Minecraft::GetInstance();
if (!mc || !mc->textures) return -1;
// loadTexture(TN_COUNT, path) loads from file and caches by path.
// Returns the RenderManager texture ID.
return mc->textures->loadTextureByPath(std::wstring(path));
}
int LoadTextureByName(int textureName)
{
Minecraft* mc = Minecraft::GetInstance();
if (!mc || !mc->textures) return -1;
return mc->textures->loadTexture(textureName);
}
int LoadTextureFromFile(const char* filePath, int* outWidth, int* outHeight)
{
if (!filePath || !filePath[0]) return -1;
// Check cache first
std::string key(filePath);
auto it = s_fileTexCache.find(key);
if (it != s_fileTexCache.end())
{
if (outWidth) *outWidth = it->second.w;
if (outHeight) *outHeight = it->second.h;
return it->second.id;
}
// Load pixel data from file via RenderManager
D3DXIMAGE_INFO info;
ZeroMemory(&info, sizeof(info));
int* pixelData = nullptr;
HRESULT hr = RenderManager.LoadTextureData(filePath, &info, &pixelData);
if (FAILED(hr) || !pixelData)
return -1;
int texId = RenderManager.TextureCreate();
if (texId < 0)
{
free(pixelData);
return -1;
}
RenderManager.TextureBind(texId);
RenderManager.TextureData(info.Width, info.Height, pixelData, 0);
free(pixelData);
// Cache it
s_fileTexCache[key] = { texId, info.Width, info.Height };
if (outWidth) *outWidth = info.Width;
if (outHeight) *outHeight = info.Height;
return texId;
}
int LoadTextureFromFileDirect(const char* filePath, int* outWidth, int* outHeight)
{
if (!filePath || !filePath[0]) return -1;
// Check cache
std::string key = std::string("$direct$") + filePath;
auto it = s_fileTexCache.find(key);
if (it != s_fileTexCache.end())
{
if (outWidth) *outWidth = it->second.w;
if (outHeight) *outHeight = it->second.h;
return it->second.id;
}
// Decode PNG via WIC into RGBA pixel buffer
HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
bool needUninit = SUCCEEDED(hr);
IWICImagingFactory* factory = nullptr;
IWICBitmapDecoder* decoder = nullptr;
IWICBitmapFrameDecode*frame = nullptr;
IWICFormatConverter* converter = nullptr;
auto cleanup = [&]() {
if (converter) converter->Release();
if (frame) frame->Release();
if (decoder) decoder->Release();
if (factory) factory->Release();
if (needUninit) CoUninitialize();
};
hr = CoCreateInstance(CLSID_WICImagingFactory, nullptr, CLSCTX_INPROC_SERVER,
IID_PPV_ARGS(&factory));
if (FAILED(hr)) { cleanup(); return -1; }
// Convert narrow path to wide
int wlen = MultiByteToWideChar(CP_UTF8, 0, filePath, -1, nullptr, 0);
std::vector<wchar_t> wpath(wlen);
MultiByteToWideChar(CP_UTF8, 0, filePath, -1, wpath.data(), wlen);
hr = factory->CreateDecoderFromFilename(wpath.data(), nullptr,
GENERIC_READ, WICDecodeMetadataCacheOnDemand, &decoder);
if (FAILED(hr)) { cleanup(); return -1; }
hr = decoder->GetFrame(0, &frame);
if (FAILED(hr)) { cleanup(); return -1; }
UINT w = 0, h = 0;
frame->GetSize(&w, &h);
// Convert to 32bpp RGBA (matches TEXTURE_FORMAT_RxGyBzAw)
hr = factory->CreateFormatConverter(&converter);
if (FAILED(hr)) { cleanup(); return -1; }
converter->Initialize(frame, GUID_WICPixelFormat32bppRGBA,
WICBitmapDitherTypeNone, nullptr, 0.0, WICBitmapPaletteTypeCustom);
UINT stride = w * 4;
std::vector<BYTE> pixels(stride * h);
converter->CopyPixels(nullptr, stride, (UINT)pixels.size(), pixels.data());
cleanup();
int texId = RenderManager.TextureCreate();
if (texId < 0) return -1;
RenderManager.TextureBind(texId);
RenderManager.TextureData(w, h, pixels.data(), 0); // TEXTURE_FORMAT_RxGyBzAw (default)
s_fileTexCache[key] = { texId, (int)w, (int)h };
if (outWidth) *outWidth = (int)w;
if (outHeight) *outHeight = (int)h;
return texId;
}
NineSlice LoadNineSlice(const char* basePath)
{
NineSlice ns = {};
ns.valid = false;
if (!basePath || !basePath[0]) return ns;
std::string base(basePath);
int cw = 0, ch = 0;
// Try convention A first: _TL, _TM, _TR, _ML, _MM, _MR, _BL, _BM, _BR
ns.tl = LoadTextureFromFile((base + "_TL.png").c_str(), &cw, &ch);
if (ns.tl >= 0)
{
// Convention A
ns.tm = LoadTextureFromFile((base + "_TM.png").c_str());
ns.tr = LoadTextureFromFile((base + "_TR.png").c_str());
ns.ml = LoadTextureFromFile((base + "_ML.png").c_str());
ns.mm = LoadTextureFromFile((base + "_MM.png").c_str());
ns.mr = LoadTextureFromFile((base + "_MR.png").c_str());
ns.bl = LoadTextureFromFile((base + "_BL.png").c_str());
ns.bm = LoadTextureFromFile((base + "_BM.png").c_str());
ns.br = LoadTextureFromFile((base + "_BR.png").c_str());
}
else
{
// Convention B: _Top_L, _Top_M, _Top_R, _Mid_L, _Mid_M, _Mid_R, _Bot_L, _Bot_M, _Bot_R
ns.tl = LoadTextureFromFile((base + "_Top_L.png").c_str(), &cw, &ch);
ns.tm = LoadTextureFromFile((base + "_Top_M.png").c_str());
ns.tr = LoadTextureFromFile((base + "_Top_R.png").c_str());
ns.ml = LoadTextureFromFile((base + "_Mid_L.png").c_str());
ns.mm = LoadTextureFromFile((base + "_Mid_M.png").c_str());
ns.mr = LoadTextureFromFile((base + "_Mid_R.png").c_str());
ns.bl = LoadTextureFromFile((base + "_Bot_L.png").c_str());
ns.bm = LoadTextureFromFile((base + "_Bot_M.png").c_str());
ns.br = LoadTextureFromFile((base + "_Bot_R.png").c_str());
}
ns.cornerW = cw;
ns.cornerH = ch;
ns.valid = (ns.tl >= 0 && ns.tm >= 0 && ns.tr >= 0 &&
ns.ml >= 0 && ns.mm >= 0 && ns.mr >= 0 &&
ns.bl >= 0 && ns.bm >= 0 && ns.br >= 0 &&
cw > 0 && ch > 0);
return ns;
}
ThreeSlice LoadThreeSlice(const char* basePath)
{
ThreeSlice ts = {};
ts.valid = false;
if (!basePath || !basePath[0]) return ts;
std::string base(basePath);
int cw = 0, ch = 0;
ts.left = LoadTextureFromFile((base + "_Left.png").c_str(), &cw, &ch);
ts.mid = LoadTextureFromFile((base + "_Middle.png").c_str());
ts.right = LoadTextureFromFile((base + "_Right.png").c_str());
ts.capW = cw;
ts.capH = ch;
ts.valid = (ts.left >= 0 && ts.mid >= 0 && ts.right >= 0 &&
cw > 0 && ch > 0);
return ts;
}
void DrawNineSlice(float x, float y, float w, float h,
const NineSlice& ns, uint32_t tint)
{
if (!ns.valid || !s_inFrame) return;
// Corner size in virtual pixels (scale from texture pixels)
float cw = (float)ns.cornerW;
float ch = (float)ns.cornerH;
// Clamp corners if panel is too small
if (cw * 2 > w) cw = w * 0.5f;
if (ch * 2 > h) ch = h * 0.5f;
float midW = w - cw * 2;
float midH = h - ch * 2;
// Top row
DrawTexture(x, y, cw, ch, ns.tl, tint);
DrawTexture(x + cw, y, midW, ch, ns.tm, tint);
DrawTexture(x + w - cw, y, cw, ch, ns.tr, tint);
// Middle row
DrawTexture(x, y + ch, cw, midH, ns.ml, tint);
DrawTexture(x + cw, y + ch, midW, midH, ns.mm, tint);
DrawTexture(x + w - cw, y + ch, cw, midH, ns.mr, tint);
// Bottom row
DrawTexture(x, y + h - ch, cw, ch, ns.bl, tint);
DrawTexture(x + cw, y + h - ch, midW, ch, ns.bm, tint);
DrawTexture(x + w - cw, y + h - ch, cw, ch, ns.br, tint);
}
void DrawThreeSlice(float x, float y, float w, float h,
const ThreeSlice& ts, uint32_t tint)
{
if (!ts.valid || !s_inFrame) return;
float capW = (float)ts.capW;
// Clamp caps if strip is too narrow
if (capW * 2 > w) capW = w * 0.5f;
float midW = w - capW * 2;
DrawTexture(x, y, capW, h, ts.left, tint);
DrawTexture(x + capW, y, midW, h, ts.mid, tint);
DrawTexture(x + w - capW, y, capW, h, ts.right, tint);
}
void DrawTexture(float x, float y, float w, float h,
int textureId, uint32_t tint)
{
DrawTextureUV(x, y, w, h, textureId, 0.0f, 0.0f, 1.0f, 1.0f, tint);
}
void DrawTextureUV(float x, float y, float w, float h,
int textureId,
float u0, float v0, float u1, float v1,
uint32_t tint)
{
if (!s_inFrame || textureId < 0) return;
EnsureBatchMode(BATCH_TEXTURE, textureId);
float r, g, b, a;
DecodeColor(tint, r, g, b, a);
PushQuad(x, y, x + w, y + h, r, g, b, a, u0, v0, u1, v1);
}
void DrawTextureRounded(float x, float y, float w, float h,
int textureId, float radius, uint32_t tint)
{
if (!s_inFrame || textureId < 0) return;
// Use clip rect to approximate rounded corners
// Draw full texture, clipped to the rounded region
PushClipRect(x + radius, y, w - radius * 2, h);
DrawTexture(x, y, w, h, textureId, tint);
PopClipRect();
// Left/right strips (excluding corners)
PushClipRect(x, y + radius, radius, h - radius * 2);
DrawTexture(x, y, w, h, textureId, tint);
PopClipRect();
PushClipRect(x + w - radius, y + radius, radius, h - radius * 2);
DrawTexture(x, y, w, h, textureId, tint);
PopClipRect();
// Corner arcs — approximate with small clip rects
static constexpr int kSteps = 8;
static constexpr float kHalfPi = 1.5707963f;
float cx_[4] = { x + radius, x + w - radius, x + radius, x + w - radius };
float cy_[4] = { y + radius, y + radius, y + h - radius, y + h - radius };
float startA[4] = { kHalfPi * 2, kHalfPi * 3, kHalfPi, 0 };
for (int corner = 0; corner < 4; ++corner)
{
for (int step = 0; step < kSteps; ++step)
{
float a0 = startA[corner] + kHalfPi * step / kSteps;
float a1 = startA[corner] + kHalfPi * (step + 1) / kSteps;
// Bounding box of this arc slice
float px0 = cx_[corner] + fminf(0.0f, fminf(cosf(a0), cosf(a1))) * radius;
float py0 = cy_[corner] + fminf(0.0f, fminf(sinf(a0), sinf(a1))) * radius;
float px1 = cx_[corner] + fmaxf(0.0f, fmaxf(cosf(a0), cosf(a1))) * radius;
float py1 = cy_[corner] + fmaxf(0.0f, fmaxf(sinf(a0), sinf(a1))) * radius;
if (px1 > px0 && py1 > py0)
{
PushClipRect(px0, py0, px1 - px0, py1 - py0);
DrawTexture(x, y, w, h, textureId, tint);
PopClipRect();
}
}
}
}
void DrawTextureFit(float x, float y, float w, float h,
int textureId, int texW, int texH,
uint32_t tint)
{
if (!s_inFrame || textureId < 0 || texW <= 0 || texH <= 0) return;
float aspect = (float)texW / (float)texH;
float boxAspect = w / h;
float drawW, drawH;
if (aspect > boxAspect)
{
// Texture is wider — fit to width
drawW = w;
drawH = w / aspect;
}
else
{
// Texture is taller — fit to height
drawH = h;
drawW = h * aspect;
}
float drawX = x + (w - drawW) * 0.5f;
float drawY = y + (h - drawH) * 0.5f;
DrawTexture(drawX, drawY, drawW, drawH, textureId, tint);
}
// ---- Input helpers ----------------------------------------------------
bool NativeUI::GetMouseVirtual(float& outX, float& outY)
{
#ifdef _WINDOWS64
if (!g_hWnd) return false;
RECT rc;
GetClientRect(g_hWnd, &rc);
int winW = rc.right - rc.left;
int winH = rc.bottom - rc.top;
if (winW <= 0 || winH <= 0) return false;
// Map window pixel coords to backbuffer coords, then to the 16:9 viewport
float pixX = (float)g_KBMInput.GetMouseX() / (float)winW * s_bbWidth;
float pixY = (float)g_KBMInput.GetMouseY() / (float)winH * s_bbHeight;
// Convert from backbuffer pixel space to virtual canvas (relative to viewport)
outX = (pixX - s_vpX) / s_vpW * kVW;
outY = (pixY - s_vpY) / s_vpH * kVH;
return true;
#else
return false;
#endif
}
// ---- FocusList --------------------------------------------------------
void FocusList::Add(int id, float x, float y, float w, float h)
{
m_entries.push_back({ id, x, y, w, h });
if (m_focusIdx >= (int)m_entries.size())
m_focusIdx = 0;
}
int FocusList::GetFocused() const
{
if (m_entries.empty()) return -1;
return m_entries[m_focusIdx].id;
}
void FocusList::MoveNext()
{
if (m_entries.empty()) return;
m_focusIdx = (m_focusIdx + 1) % (int)m_entries.size();
ui.PlayUISFX(eSFX_Focus);
}
void FocusList::MovePrev()
{
if (m_entries.empty()) return;
m_focusIdx = (m_focusIdx - 1 + (int)m_entries.size()) % (int)m_entries.size();
ui.PlayUISFX(eSFX_Focus);
}
void FocusList::SetFocus(int id)
{
for (int i = 0; i < (int)m_entries.size(); ++i)
{
if (m_entries[i].id == id)
{
m_focusIdx = i;
return;
}
}
}
bool FocusList::HitTest(float mx, float my, int& outId) const
{
for (const Entry& e : m_entries)
{
if (mx >= e.x && mx <= e.x + e.w &&
my >= e.y && my <= e.y + e.h)
{
outId = e.id;
return true;
}
}
return false;
}
bool FocusList::UpdateHover(float mx, float my)
{
int hitId;
if (HitTest(mx, my, hitId))
{
m_hoveredId = hitId;
return true;
}
m_hoveredId = -1;
return false;
}
void FocusList::TickMouse()
{
#ifdef _WINDOWS64
// Release consumption lock when the mouse button is no longer held.
if (m_mouseConsumed && !g_KBMInput.IsMouseButtonDown(KeyboardMouseInput::MOUSE_LEFT))
m_mouseConsumed = false;
// Only process mouse hover if the mouse has actually moved recently.
// This prevents a stationary mouse cursor from stealing focus from gamepad.
float mx, my;
if (!NativeUI::GetMouseVirtual(mx, my))
{
ClearHover();
m_lastHoveredId = -1;
return;
}
// Track mouse movement — only switch to mouse device if cursor position changed
bool mouseMoved = (mx != m_lastMouseX || my != m_lastMouseY);
m_lastMouseX = mx;
m_lastMouseY = my;
if (!mouseMoved && m_lastDevice == eDevice_Gamepad)
{
// Mouse is stationary and gamepad is active — don't update hover
return;
}
if (mouseMoved)
m_lastDevice = eDevice_Mouse;
int prevHover = m_lastHoveredId;
UpdateHover(mx, my);
m_lastHoveredId = m_hoveredId;
// Play focus sound when hover enters a different element
if (m_hoveredId >= 0 && m_hoveredId != prevHover)
ui.PlayUISFX(eSFX_Focus);
#endif
}
int FocusList::HandleMenuKey(int key, int backId,
float panelX, float panelY,
float panelW, float panelH)
{
switch (key)
{
case ACTION_MENU_UP:
case ACTION_MENU_LEFT:
m_lastDevice = eDevice_Gamepad;
ClearHover(); // dismiss mouse hover when gamepad takes over
MovePrev();
return RESULT_NAVIGATED;
case ACTION_MENU_DOWN:
case ACTION_MENU_RIGHT:
m_lastDevice = eDevice_Gamepad;
ClearHover();
MoveNext();
return RESULT_NAVIGATED;
case ACTION_MENU_OK:
{
#ifdef _WINDOWS64
// UIController converts mouse left-click to ACTION_MENU_OK.
// Only use mouse hit-test if mouse is the active device.
if (m_lastDevice == eDevice_Mouse &&
g_KBMInput.IsMouseButtonDown(KeyboardMouseInput::MOUSE_LEFT))
{
float mx, my;
if (NativeUI::GetMouseVirtual(mx, my))
{
int hitId;
if (HitTest(mx, my, hitId))
{
SetFocus(hitId);
ui.PlayUISFX(eSFX_Press);
m_mouseConsumed = true;
return hitId;
}
// Click inside panel but not on element — consume silently
if (mx >= panelX && mx <= panelX + panelW &&
my >= panelY && my <= panelY + panelH)
{
m_mouseConsumed = true;
return RESULT_UNHANDLED;
}
}
}
#endif
// Gamepad/keyboard press — use focused element
m_lastDevice = eDevice_Gamepad;
ui.PlayUISFX(eSFX_Press);
return GetFocused();
}
case ACTION_MENU_CANCEL:
ui.PlayUISFX(eSFX_Back);
return backId;
default:
return RESULT_UNHANDLED;
}
}
// ---- Link widget ------------------------------------------------------
void DrawLink(float x, float y, const wchar_t* text,
const char* /*url*/, bool focused, bool hovered,
float size, uint32_t align,
float* outX, float* outY, float* outW, float* outH)
{
if (!text || !text[0] || !s_inFrame) return;
float tw, th;
MeasureText(text, size, &tw, &th);
// Compute draw position based on alignment
float drawX = x, drawY = y;
if (align & ALIGN_CENTER_X) drawX = x - tw * 0.5f;
else if (align & ALIGN_RIGHT) drawX = x - tw;
if (align & ALIGN_CENTER_Y) drawY = y - th * 0.5f;
else if (align & ALIGN_BOTTOM) drawY = y - th;
uint32_t color;
float padX = 6.0f, padY = 3.0f;
if (focused || hovered)
{
// Yellow text, no background — standard MC hover
color = 0xFFFFFF55u;
}
else
{
// Blue link
color = 0xFF4DC3FFu;
}
// Draw the text
DrawShadowText(x, y, text, color, size, align);
// Underline — always visible, thicker on hover/focus
float underlineY = drawY + th + 1.0f;
float underlineThick = (focused || hovered) ? 2.0f : 1.0f;
DrawRect(drawX, underlineY, tw, underlineThick, color);
// Output bounding rect (includes padding for easier clicking)
if (outX) *outX = drawX - padX;
if (outY) *outY = drawY - padY;
if (outW) *outW = tw + padX * 2;
if (outH) *outH = th + padY * 2 + 2;
}
void OpenURL(const char* url)
{
if (!url || !url[0]) return;
#ifdef _WINDOWS64
ShellExecuteA(nullptr, "open", url, nullptr, nullptr, SW_SHOWNORMAL);
#endif
}
} // namespace NativeUI