clay/renderers/win32_gdi/clay_renderer_gdi.c

591 lines
20 KiB
C

#include <Windows.h>
#if !defined(CLAY_DISABLE_SIMD) && (defined(__x86_64__) || defined(_M_X64) || defined(_M_AMD64))
#include <immintrin.h> // AVX intrinsincs for faster sqrtf
#endif
#include "../../clay.h"
HDC renderer_hdcMem = {0};
HBITMAP renderer_hbmMem = {0};
HANDLE renderer_hOld = {0};
bool gdi_fabulous = true;
#ifndef RECTWIDTH
#define RECTWIDTH(rc) ((rc).right - (rc).left)
#endif
#ifndef RECTHEIGHT
#define RECTHEIGHT(rc) ((rc).bottom - (rc).top)
#endif
/*----------------------------------------------------------------------------+
| Math stuff start |
+----------------------------------------------------------------------------*/
// Intrinsincs wrappers
#if !defined(CLAY_DISABLE_SIMD) && (defined(__x86_64__) || defined(_M_X64) || defined(_M_AMD64))
inline float intrin_sqrtf(const float f)
{
__m128 temp = _mm_set_ss(f);
temp = _mm_sqrt_ss(temp);
return _mm_cvtss_f32(temp);
}
#endif
// Use fast inverse square root
#if defined(USE_FAST_SQRT)
float fast_inv_sqrtf(float number)
{
const float threehalfs = 1.5f;
float x2 = number * 0.5f;
float y = number;
// Evil bit-level hacking
uint32_t i = *(uint32_t*)&y;
i = 0x5f3759df - (i >> 1); // Initial guess for Newton's method
y = *(float*)&i;
// One iteration of Newton's method
y = y * (threehalfs - (x2 * y * y)); // y = y * (1.5 - 0.5 * x * y^2)
return y;
}
// Fast square root approximation using the inverse square root
float fast_sqrtf(float number)
{
if (number < 0.0f) return 0.0f; // Handle negative input
return number * fast_inv_sqrtf(number);
}
#endif
// sqrtf_impl implementation chooser
#if !defined(CLAY_DISABLE_SIMD) && (defined(__x86_64__) || defined(_M_X64) || defined(_M_AMD64))
#define sqrtf_impl(x) intrin_sqrtf(x)
#elif defined(USE_FAST_SQRT)
#define sqrtf_impl(x) fast_sqrtf(x)
#else
#define sqrtf_impl(x) sqrtf(x) // Fallback to std sqrtf
#endif
/*----------------------------------------------------------------------------+
| Math stuff end |
+----------------------------------------------------------------------------*/
static inline Clay_Color ColorBlend(Clay_Color base, Clay_Color overlay, float factor)
{
Clay_Color blended;
// Normalize alpha values for multiplications
float base_a = base.a / 255.0f;
float overlay_a = overlay.a / 255.0f;
overlay_a *= factor;
float out_a = overlay_a + base_a * (1.0f - overlay_a);
// Avoid division by zero and fully transparent cases
if (out_a <= 0.0f)
{
return (Clay_Color) { .a = 0, .r = 0, .g = 0, .b = 0 };
}
blended.r = (overlay.r * overlay_a + base.r * base_a * (1.0f - overlay_a)) / out_a;
blended.g = (overlay.g * overlay_a + base.g * base_a * (1.0f - overlay_a)) / out_a;
blended.b = (overlay.b * overlay_a + base.b * base_a * (1.0f - overlay_a)) / out_a;
blended.a = out_a * 255.0f; // Denormalize alpha back
return blended;
}
static float RoundedRectPixelCoverage(int x, int y, const Clay_CornerRadius radius, int width, int height) {
// Check if the pixel is in one of the four rounded corners
if (x < radius.topLeft && y < radius.topLeft) {
// Top-left corner
float dx = radius.topLeft - x - 1;
float dy = radius.topLeft - y - 1;
float distance = sqrtf_impl(dx * dx + dy * dy);
if (distance > radius.topLeft)
return 0.0f;
if (distance <= radius.topLeft - 1)
return 1.0f;
return radius.topLeft - distance;
}
else if (x >= width - radius.topRight && y < radius.topRight) {
// Top-right corner
float dx = x - (width - radius.topRight);
float dy = radius.topRight - y - 1;
float distance = sqrtf_impl(dx * dx + dy * dy);
if (distance > radius.topRight)
return 0.0f;
if (distance <= radius.topRight - 1)
return 1.0f;
return radius.topRight - distance;
}
else if (x < radius.bottomLeft && y >= height - radius.bottomLeft) {
// Bottom-left corner
float dx = radius.bottomLeft - x - 1;
float dy = y - (height - radius.bottomLeft);
float distance = sqrtf_impl(dx * dx + dy * dy);
if (distance > radius.bottomLeft)
return 0.0f;
if (distance <= radius.bottomLeft - 1)
return 1.0f;
return radius.bottomLeft - distance;
}
else if (x >= width - radius.bottomRight && y >= height - radius.bottomRight) {
// Bottom-right corner
float dx = x - (width - radius.bottomRight);
float dy = y - (height - radius.bottomRight);
float distance = sqrtf_impl(dx * dx + dy * dy);
if (distance > radius.bottomRight)
return 0.0f;
if (distance <= radius.bottomRight - 1)
return 1.0f;
return radius.bottomRight - distance;
}
else {
// Not in a corner, full coverage
return 1.0f;
}
}
typedef struct {
HDC hdcMem;
HBITMAP hbmMem;
HBITMAP hbmMemPrev;
void* pBits;
SIZE size;
} HDCSubstitute;
static void CreateHDCSubstitute(HDCSubstitute* phdcs, HDC hdcSrc, PRECT prc)
{
if (prc == NULL)
return;
phdcs->size = (SIZE){ RECTWIDTH(*prc), RECTHEIGHT(*prc) };
if (phdcs->size.cx <= 0 || phdcs->size.cy <= 0)
return;
phdcs->hdcMem = CreateCompatibleDC(hdcSrc);
if (phdcs->hdcMem == NULL)
return;
// Create a 32-bit DIB section for the memory DC
BITMAPINFO bmi = { 0 };
bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
bmi.bmiHeader.biWidth = phdcs->size.cx;
bmi.bmiHeader.biHeight = -phdcs->size.cy; // I think it's faster? Probably
bmi.bmiHeader.biPlanes = 1;
bmi.bmiHeader.biBitCount = 32;
bmi.bmiHeader.biCompression = BI_RGB;
phdcs->pBits = NULL;
phdcs->hbmMem = CreateDIBSection(phdcs->hdcMem, &bmi, DIB_RGB_COLORS, &phdcs->pBits, NULL, 0);
if (phdcs->hbmMem == NULL)
{
DeleteDC(phdcs->hdcMem);
return;
}
// Select the DIB section into the memory DC
phdcs->hbmMemPrev = SelectObject(phdcs->hdcMem, phdcs->hbmMem);
// Copy the content of the target DC to the memory DC
BitBlt(phdcs->hdcMem, 0, 0, phdcs->size.cx, phdcs->size.cy, hdcSrc, prc->left, prc->top, SRCCOPY);
}
static void DestroyHDCSubstitute(HDCSubstitute* phdcs)
{
if (phdcs == NULL)
return;
// Clean up
SelectObject(phdcs->hdcMem, phdcs->hbmMemPrev);
DeleteObject(phdcs->hbmMem);
DeleteDC(phdcs->hdcMem);
ZeroMemory(phdcs, sizeof(HDCSubstitute));
}
static void __Clay_Win32_FillRoundRect(HDC hdc, PRECT prc, Clay_Color color, Clay_CornerRadius radius)
{
HDCSubstitute substitute = { 0 };
CreateHDCSubstitute(&substitute, hdc, prc);
bool has_corner_radius = radius.topLeft || radius.topRight || radius.bottomLeft || radius.bottomRight;
if (has_corner_radius)
{
// Limit the corner radius to the minimum of half the width and half the height
float max_radius = (float)fmin(substitute.size.cx / 2.0f, substitute.size.cy / 2.0f);
if (radius.topLeft > max_radius) radius.topLeft = max_radius;
if (radius.topRight > max_radius) radius.topRight = max_radius;
if (radius.bottomLeft > max_radius) radius.bottomLeft = max_radius;
if (radius.bottomRight > max_radius) radius.bottomRight = max_radius;
}
// Iterate over each pixel in the DIB section
uint32_t* pixels = (uint32_t*)substitute.pBits;
for (int y = 0; y < substitute.size.cy; ++y)
{
for (int x = 0; x < substitute.size.cx; ++x)
{
float coverage = 1.0f;
if (has_corner_radius)
coverage = RoundedRectPixelCoverage(x, y, radius, substitute.size.cx, substitute.size.cy);
if (coverage > 0.0f)
{
uint32_t pixel = pixels[y * substitute.size.cx + x];
Clay_Color dst_color = {
.r = (float)((pixel >> 16) & 0xFF), // Red
.g = (float)((pixel >> 8) & 0xFF), // Green
.b = (float)(pixel & 0xFF), // Blue
.a = 255.0f // Fully opaque
};
Clay_Color blended = ColorBlend(dst_color, color, coverage);
pixels[y * substitute.size.cx + x] =
((uint32_t)(blended.b) << 0) |
((uint32_t)(blended.g) << 8) |
((uint32_t)(blended.r) << 16);
}
}
}
// Copy the blended content back to the target DC
BitBlt(hdc, prc->left, prc->top, substitute.size.cx, substitute.size.cy, substitute.hdcMem, 0, 0, SRCCOPY);
DestroyHDCSubstitute(&substitute);
}
void Clay_Win32_Render(HWND hwnd, Clay_RenderCommandArray renderCommands, HFONT* fonts)
{
bool is_clipping = false;
HRGN clipping_region = {0};
PAINTSTRUCT ps;
HDC hdc;
RECT rc; // Top left of our window
GetWindowRect(hwnd, &rc);
hdc = BeginPaint(hwnd, &ps);
int win_width = rc.right - rc.left,
win_height = rc.bottom - rc.top;
// Create an off-screen DC for double-buffering
renderer_hdcMem = CreateCompatibleDC(hdc);
renderer_hbmMem = CreateCompatibleBitmap(hdc, win_width, win_height);
renderer_hOld = SelectObject(renderer_hdcMem, renderer_hbmMem);
// draw
for (int j = 0; j < renderCommands.length; j++)
{
Clay_RenderCommand *renderCommand = Clay_RenderCommandArray_Get(&renderCommands, j);
Clay_BoundingBox boundingBox = renderCommand->boundingBox;
switch (renderCommand->commandType)
{
case CLAY_RENDER_COMMAND_TYPE_TEXT:
{
Clay_Color c = renderCommand->renderData.text.textColor;
SetTextColor(renderer_hdcMem, RGB(c.r, c.g, c.b));
SetBkMode(renderer_hdcMem, TRANSPARENT);
RECT r = rc;
r.left = boundingBox.x;
r.top = boundingBox.y;
r.right = boundingBox.x + boundingBox.width + r.right;
r.bottom = boundingBox.y + boundingBox.height + r.bottom;
uint16_t font_id = renderCommand->renderData.text.fontId;
HFONT hFont = fonts[font_id];
HFONT hPrevFont = SelectObject(renderer_hdcMem, hFont);
// Actually draw text
DrawTextA(renderer_hdcMem, renderCommand->renderData.text.stringContents.chars,
renderCommand->renderData.text.stringContents.length,
&r, DT_TOP | DT_LEFT);
SelectObject(renderer_hdcMem, hPrevFont);
break;
}
case CLAY_RENDER_COMMAND_TYPE_RECTANGLE:
{
Clay_RectangleRenderData rrd = renderCommand->renderData.rectangle;
RECT r = rc;
r.left = boundingBox.x;
r.top = boundingBox.y;
r.right = boundingBox.x + boundingBox.width;
r.bottom = boundingBox.y + boundingBox.height;
bool translucid = rrd.backgroundColor.a > 0.0f && rrd.backgroundColor.a < 255.0f;
bool has_rounded_corners = rrd.cornerRadius.topLeft > 0.0f
|| rrd.cornerRadius.topRight > 0.0f
|| rrd.cornerRadius.bottomLeft > 0.0f
|| rrd.cornerRadius.bottomRight > 0.0f;
if (gdi_fabulous && (translucid || has_rounded_corners))
{
__Clay_Win32_FillRoundRect(renderer_hdcMem, &r, rrd.backgroundColor, rrd.cornerRadius);
}
else
{
HBRUSH recColor = CreateSolidBrush(RGB(rrd.backgroundColor.r, rrd.backgroundColor.g, rrd.backgroundColor.b));
if (has_rounded_corners)
{
HRGN roundedRectRgn = CreateRoundRectRgn(
r.left, r.top, r.right + 1, r.bottom + 1,
rrd.cornerRadius.topLeft * 2, rrd.cornerRadius.topLeft * 2);
FillRgn(renderer_hdcMem, roundedRectRgn, recColor);
DeleteObject(roundedRectRgn);
}
else
{
FillRect(renderer_hdcMem, &r, recColor);
}
DeleteObject(recColor);
}
break;
}
// The renderer should begin clipping all future draw commands, only rendering content that falls within the provided boundingBox.
case CLAY_RENDER_COMMAND_TYPE_SCISSOR_START:
{
is_clipping = true;
clipping_region = CreateRectRgn(boundingBox.x,
boundingBox.y,
boundingBox.x + boundingBox.width,
boundingBox.y + boundingBox.height);
SelectClipRgn(renderer_hdcMem, clipping_region);
break;
}
// The renderer should finish any previously active clipping, and begin rendering elements in full again.
case CLAY_RENDER_COMMAND_TYPE_SCISSOR_END:
{
SelectClipRgn(renderer_hdcMem, NULL);
if (clipping_region)
{
DeleteObject(clipping_region);
}
is_clipping = false;
clipping_region = NULL;
break;
}
// The renderer should draw a colored border inset into the bounding box.
case CLAY_RENDER_COMMAND_TYPE_BORDER:
{
Clay_BorderRenderData brd = renderCommand->renderData.border;
RECT r = rc;
r.left = boundingBox.x;
r.top = boundingBox.y;
r.right = boundingBox.x + boundingBox.width;
r.bottom = boundingBox.y + boundingBox.height;
HPEN topPen = CreatePen(PS_SOLID, brd.width.top, RGB(brd.color.r, brd.color.g, brd.color.b));
HPEN leftPen = CreatePen(PS_SOLID, brd.width.left, RGB(brd.color.r, brd.color.g, brd.color.b));
HPEN bottomPen = CreatePen(PS_SOLID, brd.width.bottom, RGB(brd.color.r, brd.color.g, brd.color.b));
HPEN rightPen = CreatePen(PS_SOLID, brd.width.right, RGB(brd.color.r, brd.color.g, brd.color.b));
HPEN oldPen = SelectObject(renderer_hdcMem, topPen);
if (brd.cornerRadius.topLeft == 0)
{
MoveToEx(renderer_hdcMem, r.left, r.top, NULL);
LineTo(renderer_hdcMem, r.right, r.top);
SelectObject(renderer_hdcMem, leftPen);
MoveToEx(renderer_hdcMem, r.left, r.top, NULL);
LineTo(renderer_hdcMem, r.left, r.bottom);
SelectObject(renderer_hdcMem, bottomPen);
MoveToEx(renderer_hdcMem, r.left, r.bottom, NULL);
LineTo(renderer_hdcMem, r.right, r.bottom);
SelectObject(renderer_hdcMem, rightPen);
MoveToEx(renderer_hdcMem, r.right, r.top, NULL);
LineTo(renderer_hdcMem, r.right, r.bottom);
}
else
{
// todo: i should be rounded
MoveToEx(renderer_hdcMem, r.left, r.top, NULL);
LineTo(renderer_hdcMem, r.right, r.top);
SelectObject(renderer_hdcMem, leftPen);
MoveToEx(renderer_hdcMem, r.left, r.top, NULL);
LineTo(renderer_hdcMem, r.left, r.bottom);
SelectObject(renderer_hdcMem, bottomPen);
MoveToEx(renderer_hdcMem, r.left, r.bottom, NULL);
LineTo(renderer_hdcMem, r.right, r.bottom);
SelectObject(renderer_hdcMem, rightPen);
MoveToEx(renderer_hdcMem, r.right, r.top, NULL);
LineTo(renderer_hdcMem, r.right, r.bottom);
}
SelectObject(renderer_hdcMem, oldPen);
DeleteObject(topPen);
DeleteObject(leftPen);
DeleteObject(bottomPen);
DeleteObject(rightPen);
break;
}
// case CLAY_RENDER_COMMAND_TYPE_IMAGE:
// {
// // TODO: i couldnt get the win 32 api to load a bitmap.... So im punting on this one :(
// break;
// }
default:
printf("Unhandled render command %d\r\n", renderCommand->commandType);
break;
}
}
BitBlt(hdc, 0, 0, win_width, win_height, renderer_hdcMem, 0, 0, SRCCOPY);
// Free-up the off-screen DC
SelectObject(renderer_hdcMem, renderer_hOld);
DeleteObject(renderer_hbmMem);
DeleteDC(renderer_hdcMem);
EndPaint(hwnd, &ps);
}
/*
Hacks due to the windows api not making sence to use.... may measure too large, but never too small
*/
#ifndef WIN32_FONT_HEIGHT
#define WIN32_FONT_HEIGHT (16)
#endif
#ifndef WIN32_FONT_WIDTH
#define WIN32_FONT_WIDTH (8)
#endif
static inline Clay_Dimensions Clay_Win32_MeasureText(Clay_StringSlice text, Clay_TextElementConfig *config, void *userData)
{
Clay_Dimensions textSize = {0};
if (userData != NULL)
{
HFONT* fonts = (HFONT*)userData;
HFONT hFont = fonts[config->fontId];
if (hFont != NULL)
{
HDC hScreenDC = GetDC(HWND_DESKTOP);
HDC hTempDC = CreateCompatibleDC(hScreenDC);
if (hTempDC != NULL)
{
HFONT hPrevFont = SelectObject(hTempDC, hFont);
SIZE size;
GetTextExtentPoint32(hTempDC, text.chars, text.length, &size);
textSize.width = size.cx;
textSize.height = size.cy;
SelectObject(hScreenDC, hPrevFont);
DeleteDC(hTempDC);
return textSize;
}
ReleaseDC(HWND_DESKTOP, hScreenDC);
}
}
// Fallback for system bitmap font
float maxTextWidth = 0.0f;
float lineTextWidth = 0;
float textHeight = WIN32_FONT_HEIGHT;
for (int i = 0; i < text.length; ++i)
{
if (text.chars[i] == '\n')
{
maxTextWidth = fmax(maxTextWidth, lineTextWidth);
lineTextWidth = 0;
continue;
}
lineTextWidth += WIN32_FONT_WIDTH;
}
maxTextWidth = fmax(maxTextWidth, lineTextWidth);
textSize.width = maxTextWidth;
textSize.height = textHeight;
return textSize;
}
HFONT Clay_Win32_SimpleCreateFont(const char* filePath, const char* family, int height, int weight)
{
// Add the font resource to the application instance
int fontAdded = AddFontResourceEx(filePath, FR_PRIVATE, NULL);
if (fontAdded == 0) {
return NULL;
}
int fontHeight = height;
// If negative, treat height as Pt rather than pixels
if (height < 0) {
// Get the screen DPI
HDC hScreenDC = GetDC(HWND_DESKTOP);
int iScreenDPI = GetDeviceCaps(hScreenDC, LOGPIXELSY);
ReleaseDC(HWND_DESKTOP, hScreenDC);
// Convert font height from points to pixels
fontHeight = MulDiv(height, iScreenDPI, 72);
}
// Create the font using the calculated height and the font name
HFONT hFont = CreateFont(
fontHeight, // Height
0, // Width (0 means default width)
0, // Escapement angle
0, // Orientation angle
weight, // Font weight
FALSE, // Italic
FALSE, // Underline
FALSE, // Strikeout
ANSI_CHARSET, // Character set
OUT_DEFAULT_PRECIS, // Output precision
CLIP_DEFAULT_PRECIS, // Clipping precision
DEFAULT_QUALITY, // Font quality
DEFAULT_PITCH, // Pitch and family
family // Font name
);
return hFont;
}