#include "stdafx.h" #include "NativeUIRenderer.h" #include "UI.h" #include #include #include #include #pragma comment(lib, "windowscodecs.lib") #ifdef _WINDOWS64 #include #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 #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 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(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(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(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 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 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