MinecraftConsoles/Minecraft.Client/Common/UI/NativeUIRenderer.cpp

1869 lines
59 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"
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;
};
enum BatchMode { BATCH_COLOR, BATCH_TEXT, BATCH_TEXTURE };
static ID3D11VertexShader* s_pVS = nullptr;
static ID3D11PixelShader* s_pPS_Color = nullptr;
static ID3D11PixelShader* s_pPS_Tex = nullptr;
static ID3D11PixelShader* s_pPS_TexFull = nullptr;
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;
static ID3D11SamplerState* s_pSamplerLinear = nullptr;
static bool s_initialized = false;
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;
struct FileTexEntry { int id; int w; int h; };
static std::unordered_map<std::string, FileTexEntry> s_fileTexCache;
struct ClipRect { float x, y, w, h; };
static ClipRect s_clipStack[kMaxClipStack];
static int s_clipDepth = 0;
static float s_bbWidth = kVW;
static float s_bbHeight = kVH;
static float s_vpX = 0.0f;
static float s_vpY = 0.0f;
static float s_vpW = kVW;
static float s_vpH = kVH;
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;
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";
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";
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";
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;
}
static void InitD3D()
{
if (s_initialized || !g_pd3dDevice) return;
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;
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;
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);
rasDesc.ScissorEnable = TRUE;
g_pd3dDevice->CreateRasterizerState(&rasDesc, &s_pRastScissor);
D3D11_DEPTH_STENCIL_DESC dsDesc = {};
dsDesc.DepthEnable = FALSE;
dsDesc.StencilEnable = FALSE;
g_pd3dDevice->CreateDepthStencilState(&dsDesc, &s_pDepthState);
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);
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);
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;
}
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);
}
}
}
}
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);
}
}
}
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);
BindBatchTexture(ctx);
}
else
{
ctx->PSSetShader(s_pPS_TexFull, nullptr, 0);
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;
}
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;
}
{
float bbAspect = s_bbWidth / s_bbHeight;
constexpr float targetAspect = kVW / kVH;
if (bbAspect > targetAspect)
{
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)
{
s_vpW = s_bbWidth;
s_vpH = s_bbWidth / targetAspect;
s_vpX = 0.0f;
s_vpY = (s_bbHeight - s_vpH) * 0.5f;
}
else
{
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);
}
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;
}
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;
}
static void ApplyScissor()
{
ID3D11DeviceContext* ctx = g_pImmediateContext;
if (s_clipDepth <= 0)
{
ctx->RSSetState(s_pRastState);
return;
}
ctx->RSSetState(s_pRastScissor);
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);
}
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];
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;
}
}
namespace NativeUI
{
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();
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;
}
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;
FlushBatch();
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);
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();
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);
float maxR = fminf(w, h) * 0.5f;
if (radius > maxR) radius = maxR;
static constexpr float kHalfPi = 1.5707963f;
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);
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;
float maxR = fminf(w, h) * 0.5f;
if (radius > maxR) radius = maxR;
float ri = fmaxf(0.0f, radius - thickness);
DrawRect(x + radius, y, w - 2 * radius, thickness, color);
DrawRect(x + radius, y + h - thickness, w - 2 * radius, thickness, color);
DrawRect(x, y + radius, thickness, h - 2 * radius, color);
DrawRect(x + w - thickness, y + radius, thickness, h - 2 * radius, color);
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;
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;
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);
PushGradientQuad(x + radius, y, x + w - radius, y + h,
tr, tg, tb, ta, br, bg, bb, ba);
float midTopY = y + radius;
float midBotY = y + h - radius;
PushGradientQuad(x, midTopY, x + radius, midBotY,
tr, tg, tb, ta, br, bg, bb, ba);
PushGradientQuad(x + w - radius, midTopY, x + w, midBotY,
tr, tg, tb, ta, br, bg, bb, ba);
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 };
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;
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);
}
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;
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;
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);
}
void PushClipRect(float x, float y, float w, float h)
{
if (!s_inFrame) return;
FlushBatch();
ClipRect newClip = { x, y, w, h };
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();
}
static int s_guiTexId = -2;
static int GetGuiTexture()
{
if (s_guiTexId == -2)
s_guiTexId = LoadTexture(L"/gui/gui.png");
return s_guiTexId;
}
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;
float scale = h / srcH;
float cL = capL * scale;
float cR = capR * scale;
float cT = capT * scale;
float cB = capB * scale;
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;
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;
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);
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);
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)
{
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])
{
uint32_t textColor = active ? 0xFFFFFF55u : 0xFFFFFFFFu;
DrawShadowText(x + w * 0.5f, y + h * 0.5f, label, textColor,
labelSize, ALIGN_CENTER_X | ALIGN_CENTER_Y);
}
}
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)
{
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;
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;
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)
{
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
{
float radius = h * 0.5f;
DrawRoundedRect(x, y, w, h, radius, trackColor);
}
if (s_sliderBtnTex >= 0)
{
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
{
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;
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);
}
int LoadTexture(const wchar_t* path)
{
if (!path || !path[0]) return -1;
Minecraft* mc = Minecraft::GetInstance();
if (!mc || !mc->textures) return -1;
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;
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;
}
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);
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;
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;
}
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; }
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);
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);
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;
ns.tl = LoadTextureFromFile((base + "_TL.png").c_str(), &cw, &ch);
if (ns.tl >= 0)
{
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
{
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;
float cw = (float)ns.cornerW;
float ch = (float)ns.cornerH;
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;
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);
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);
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;
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;
PushClipRect(x + radius, y, w - radius * 2, h);
DrawTexture(x, y, w, h, textureId, tint);
PopClipRect();
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();
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;
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)
{
drawW = w;
drawH = w / aspect;
}
else
{
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);
}
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;
float pixX = (float)g_KBMInput.GetMouseX() / (float)winW * s_bbWidth;
float pixY = (float)g_KBMInput.GetMouseY() / (float)winH * s_bbHeight;
outX = (pixX - s_vpX) / s_vpW * kVW;
outY = (pixY - s_vpY) / s_vpH * kVH;
return true;
#else
return false;
#endif
}
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
if (m_mouseConsumed && !g_KBMInput.IsMouseButtonDown(KeyboardMouseInput::MOUSE_LEFT))
m_mouseConsumed = false;
float mx, my;
if (!NativeUI::GetMouseVirtual(mx, my))
{
ClearHover();
m_lastHoveredId = -1;
return;
}
bool mouseMoved = (mx != m_lastMouseX || my != m_lastMouseY);
m_lastMouseX = mx;
m_lastMouseY = my;
if (!mouseMoved && m_lastDevice == eDevice_Gamepad)
{
return;
}
if (mouseMoved)
m_lastDevice = eDevice_Mouse;
int prevHover = m_lastHoveredId;
UpdateHover(mx, my);
m_lastHoveredId = m_hoveredId;
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();
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
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;
}
if (mx >= panelX && mx <= panelX + panelW &&
my >= panelY && my <= panelY + panelH)
{
m_mouseConsumed = true;
return RESULT_UNHANDLED;
}
}
}
#endif
m_lastDevice = eDevice_Gamepad;
ui.PlayUISFX(eSFX_Press);
return GetFocused();
}
case ACTION_MENU_CANCEL:
ui.PlayUISFX(eSFX_Back);
return backId;
default:
return RESULT_UNHANDLED;
}
}
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);
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)
{
color = 0xFFFFFF55u;
}
else
{
color = 0xFF4DC3FFu;
}
DrawShadowText(x, y, text, color, size, align);
float underlineY = drawY + th + 1.0f;
float underlineThick = (focused || hovered) ? 2.0f : 1.0f;
DrawRect(drawX, underlineY, tw, underlineThick, color);
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
}
}