Compare commits

...

7 Commits

Author SHA1 Message Date
Philosoph228
682cab60c4
Merge addea58fea into 7c9506bc31 2025-03-26 15:28:30 -07:00
Philosoph228
addea58fea
Merge branch 'nicbarker:main' into win32-gdi 2025-03-25 11:40:09 +05:00
Philosoph228
fd02c528d3
Add missing font resource 2025-03-25 09:49:24 +05:00
Philosoph228
b2bbdf8760
Implement accurate rectangle blending and rounding
The implementation works by handling special cases where rectangles have semi-transparent background colors or rounded corners. For these cases, it first copies the underlying image region from the window device context, blends the rectangle on top of this captured background, and then draws the final result back to the window DC because we can't access and modify window dc bits directly. Also this approach requires custom pixel processing for color blending and uses sqrtf for precise anti-aliased corners, which isn't ideal for performance. A `gdi_fabulous` flag is provided to toggle this feature when needed.
2025-03-25 09:47:04 +05:00
Philosoph228
5a1d13f0a4
Add support for font loading and rendering 2025-03-24 05:50:10 +05:00
Philosoph228
d9c68cd32d
Fix CreateWindow client size
CreateWindow(Ex) size parameters actually accepts outer window dimensions containing caption and border, which we can calculate with AdjustWindowRect by giving client size window dimensions.
2025-03-24 03:21:19 +05:00
Philosoph228
e0fffb64fe
Add CMake target for win32_gdi example 2025-03-24 02:55:18 +05:00
5 changed files with 396 additions and 16 deletions

View File

@ -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,4 +38,10 @@ 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

View File

@ -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)

View File

@ -25,6 +25,10 @@ void CenterWindow(HWND hWnd);
long lastMsgTime = 0;
bool ui_debug_mode;
HFONT fonts[1];
#define RECTWIDTH(rc) ((rc).right - (rc).left)
#define RECTHEIGHT(rc) ((rc).bottom - (rc).top)
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
@ -113,7 +117,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 +155,10 @@ 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);
// 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 +172,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 +183,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,

Binary file not shown.

View File

@ -1,11 +1,264 @@
#include <Windows.h>
// #define USE_INTRINSICS
// #define USE_FAST_SQRT
#if defined(USE_INTRINSICS)
#include <immintrin.h>
#endif
#include "../../clay.h"
HDC renderer_hdcMem = {0};
HBITMAP renderer_hbmMem = {0};
HANDLE renderer_hOld = {0};
bool gdi_fabulous = true;
void Clay_Win32_Render(HWND hwnd, Clay_RenderCommandArray renderCommands)
#define RECTWIDTH(rc) ((rc).right - (rc).left)
#define RECTHEIGHT(rc) ((rc).bottom - (rc).top)
/*----------------------------------------------------------------------------+
| Math stuff start |
+----------------------------------------------------------------------------*/
#if defined(USE_INTRINSICS)
#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
// Use intrinsics
#if defined(USE_INTRINSICS)
inline float intrin_sqrtf(const float f)
{
__m128 temp = _mm_set_ss(f);
temp = _mm_sqrt_ss(temp);
return _mm_cvtss_f32(temp);
}
#endif // defined(USE_INTRINSICS)
// 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
/*----------------------------------------------------------------------------+
| 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,10 +301,17 @@ 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:
@ -64,9 +324,21 @@ void Clay_Win32_Render(HWND hwnd, Clay_RenderCommandArray renderCommands)
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 (rrd.cornerRadius.topLeft > 0)
if (has_rounded_corners)
{
HRGN roundedRectRgn = CreateRoundRectRgn(
r.left, r.top, r.right + 1, r.bottom + 1,
@ -81,6 +353,8 @@ void Clay_Win32_Render(HWND hwnd, Clay_RenderCommandArray renderCommands)
}
DeleteObject(recColor);
}
break;
}
@ -216,6 +490,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(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;
@ -239,3 +544,45 @@ static inline Clay_Dimensions Clay_Win32_MeasureText(Clay_StringSlice text, Clay
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;
}