diff --git a/CMakeLists.txt b/CMakeLists.txt index 570b314..b825769 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,6 +9,7 @@ option(CLAY_INCLUDE_CPP_EXAMPLE "Build C++ example" OFF) option(CLAY_INCLUDE_RAYLIB_EXAMPLES "Build raylib examples" OFF) option(CLAY_INCLUDE_SDL2_EXAMPLES "Build SDL 2 examples" OFF) option(CLAY_INCLUDE_SDL3_EXAMPLES "Build SDL 3 examples" OFF) +option(CLAY_INCLUDE_WIN32_GDI_EXAMPLES "Build Win32 GDI examples" OFF) message(STATUS "CLAY_INCLUDE_DEMOS: ${CLAY_INCLUDE_DEMOS}") @@ -37,6 +38,12 @@ if(NOT MSVC AND (CLAY_INCLUDE_ALL_EXAMPLES OR CLAY_INCLUDE_SDL3_EXAMPLES)) add_subdirectory("examples/SDL3-simple-demo") endif() +if(WIN32) # Build only for Win or Wine + if(CLAY_INCLUDE_ALL_EXAMPLES OR CLAY_INCLUDE_WIN32_GDI_EXAMPLES) + add_subdirectory("examples/win32_gdi") + endif() +endif() + # add_subdirectory("examples/cairo-pdf-rendering") Some issue with github actions populating cairo, disable for now #add_library(${PROJECT_NAME} INTERFACE) diff --git a/examples/win32_gdi/CMakeLists.txt b/examples/win32_gdi/CMakeLists.txt new file mode 100644 index 0000000..e401e1d --- /dev/null +++ b/examples/win32_gdi/CMakeLists.txt @@ -0,0 +1,15 @@ +cmake_minimum_required(VERSION 3.27) +project(win32_gdi C) + +set(CMAKE_C_STANDARD 99) + +add_executable(win32_gdi WIN32 main.c) + +target_compile_options(win32_gdi PUBLIC) +target_include_directories(win32_gdi PUBLIC .) + +add_custom_command( + TARGET win32_gdi POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory + ${CMAKE_CURRENT_SOURCE_DIR}/resources + ${CMAKE_CURRENT_BINARY_DIR}/resources) diff --git a/examples/win32_gdi/main.c b/examples/win32_gdi/main.c index 761c59e..e2e44fd 100644 --- a/examples/win32_gdi/main.c +++ b/examples/win32_gdi/main.c @@ -25,6 +25,14 @@ void CenterWindow(HWND hWnd); long lastMsgTime = 0; bool ui_debug_mode; +HFONT fonts[1]; + +#ifndef RECTWIDTH +#define RECTWIDTH(rc) ((rc).right - (rc).left) +#endif +#ifndef RECTHEIGHT +#define RECTHEIGHT(rc) ((rc).bottom - (rc).top) +#endif LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { @@ -113,7 +121,7 @@ LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) case WM_PAINT: { Clay_RenderCommandArray renderCommands = ClayVideoDemo_CreateLayout(&demo_data); - Clay_Win32_Render(hwnd, renderCommands); + Clay_Win32_Render(hwnd, renderCommands, fonts); break; } @@ -151,7 +159,12 @@ int APIENTRY WinMain( uint64_t clayRequiredMemory = Clay_MinMemorySize(); Clay_Arena clayMemory = Clay_CreateArenaWithCapacityAndMemory(clayRequiredMemory, malloc(clayRequiredMemory)); Clay_Initialize(clayMemory, (Clay_Dimensions){.width = 800, .height = 600}, (Clay_ErrorHandler){HandleClayErrors}); // This final argument is new since the video was published - Clay_SetMeasureTextFunction(Clay_Win32_MeasureText, NULL); + + Clay_Win32_SetRendererFlags(CLAYGDI_RF_ALPHABLEND | CLAYGDI_RF_SMOOTHCORNERS); + + // Initialize clay fonts and text drawing + fonts[FONT_ID_BODY_16] = Clay_Win32_SimpleCreateFont("resources/Roboto-Regular.ttf", "Roboto", -11, FW_NORMAL); + Clay_SetMeasureTextFunction(Clay_Win32_MeasureText, fonts); ZeroMemory(&wc, sizeof wc); wc.hInstance = hInstance; @@ -165,6 +178,10 @@ int APIENTRY WinMain( if (FALSE == RegisterClass(&wc)) return 0; + // Calculate window rectangle by given client size + // TODO: AdjustWindowRectExForDpi for DPI support + RECT rcWindow = { .right = 800, .bottom = 600 }; + AdjustWindowRect(&rcWindow, WS_OVERLAPPEDWINDOW, FALSE); hwnd = CreateWindow( szAppName, @@ -172,8 +189,8 @@ int APIENTRY WinMain( WS_OVERLAPPEDWINDOW | WS_VISIBLE, CW_USEDEFAULT, CW_USEDEFAULT, - 800, // CW_USEDEFAULT, - 600, // CW_USEDEFAULT, + RECTWIDTH(rcWindow), // CW_USEDEFAULT, + RECTHEIGHT(rcWindow), // CW_USEDEFAULT, 0, 0, hInstance, diff --git a/examples/win32_gdi/resources/Roboto-Regular.ttf b/examples/win32_gdi/resources/Roboto-Regular.ttf new file mode 100644 index 0000000..ddf4bfa Binary files /dev/null and b/examples/win32_gdi/resources/Roboto-Regular.ttf differ diff --git a/renderers/win32_gdi/clay_renderer_gdi.c b/renderers/win32_gdi/clay_renderer_gdi.c index e031189..d9f8504 100644 --- a/renderers/win32_gdi/clay_renderer_gdi.c +++ b/renderers/win32_gdi/clay_renderer_gdi.c @@ -1,11 +1,290 @@ #include + +#if !defined(CLAY_DISABLE_SIMD) && (defined(__x86_64__) || defined(_M_X64) || defined(_M_AMD64)) +#include // AVX intrinsincs for faster sqrtf +#endif + #include "../../clay.h" HDC renderer_hdcMem = {0}; HBITMAP renderer_hbmMem = {0}; HANDLE renderer_hOld = {0}; +DWORD g_dwGdiRenderFlags; -void Clay_Win32_Render(HWND hwnd, Clay_RenderCommandArray renderCommands) +#ifndef RECTWIDTH +#define RECTWIDTH(rc) ((rc).right - (rc).left) +#endif +#ifndef RECTHEIGHT +#define RECTHEIGHT(rc) ((rc).bottom - (rc).top) +#endif + +// Renderer options bit flags +// RF clearly stated in the name to avoid confusion with possible macro definitions for other purposes +#define CLAYGDI_RF_ALPHABLEND 0x00000001 +#define CLAYGDI_RF_SMOOTHCORNERS 0x00000002 +// These are bitflags, not indexes. Next would be 0x00000004 + +inline DWORD Clay_Win32_GetRendererFlags() { return g_dwGdiRenderFlags; } + +// Replaces the rendering flags with new ones provided +inline void Clay_Win32_SetRendererFlags(DWORD dwFlags) { g_dwGdiRenderFlags = dwFlags; } + +// Returns `true` if flags were modified +inline bool Clay_Win32_ModifyRendererFlags(DWORD dwRemove, DWORD dwAdd) +{ + DWORD dwSavedFlags = g_dwGdiRenderFlags; + DWORD dwNewFlags = (dwSavedFlags & ~dwRemove) | dwAdd; + + if (dwSavedFlags == dwNewFlags) + return false; + + Clay_Win32_SetRendererFlags(dwNewFlags); + return true; +} + + +/*----------------------------------------------------------------------------+ + | 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}; @@ -48,14 +327,22 @@ void Clay_Win32_Render(HWND hwnd, Clay_RenderCommandArray renderCommands) 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: { + DWORD dwFlags = Clay_Win32_GetRendererFlags(); Clay_RectangleRenderData rrd = renderCommand->renderData.rectangle; RECT r = rc; @@ -64,23 +351,44 @@ void Clay_Win32_Render(HWND hwnd, Clay_RenderCommandArray renderCommands) r.right = boundingBox.x + boundingBox.width; r.bottom = boundingBox.y + boundingBox.height; - HBRUSH recColor = CreateSolidBrush(RGB(rrd.backgroundColor.r, rrd.backgroundColor.g, rrd.backgroundColor.b)); + bool translucid = false; + // There is need to check that only if alphablending is enabled. + // In other case the blending will be always opaque and we can jump to simpler FillRgn/Rect + if (dwFlags & CLAYGDI_RF_ALPHABLEND) + 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 (rrd.cornerRadius.topLeft > 0) + // We go here if CLAYGDI_RF_SMOOTHCORNERS flag is set and one of the corners is rounded + // Also we go here if GLAYGDI_RF_ALPHABLEND flag is set and the fill color is translucid + if ((dwFlags & CLAYGDI_RF_ALPHABLEND) && translucid || (dwFlags & CLAYGDI_RF_SMOOTHCORNERS) && 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); + __Clay_Win32_FillRoundRect(renderer_hdcMem, &r, rrd.backgroundColor, rrd.cornerRadius); } else { - FillRect(renderer_hdcMem, &r, recColor); + 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); } - DeleteObject(recColor); break; } @@ -216,6 +524,37 @@ static inline Clay_Dimensions Clay_Win32_MeasureText(Clay_StringSlice text, Clay { Clay_Dimensions textSize = {0}; + if (userData != NULL) + { + HFONT* fonts = (HFONT*)userData; + HFONT hFont = fonts[config->fontId]; + + if (hFont != NULL) + { + HDC hScreenDC = GetDC(NULL); + 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; @@ -238,4 +577,33 @@ static inline Clay_Dimensions Clay_Win32_MeasureText(Clay_StringSlice text, Clay textSize.height = textHeight; return textSize; -} \ No newline at end of file +} + +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(NULL); + 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, 0, 0, 0, weight, FALSE, FALSE, FALSE, + ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, + DEFAULT_PITCH, family); + + return hFont; +}