Files
autosample/src/platform/platform_win32.c
2026-03-12 16:30:04 -04:00

373 lines
12 KiB
C

#include "platform/platform.h"
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#include <malloc.h>
#include <stdlib.h>
#include <string.h>
typedef struct PlatformWindow {
HWND hwnd;
B32 should_close;
S32 width;
S32 height;
S32 pending_menu_cmd;
PlatformFrameCallback frame_callback;
void *frame_callback_user_data;
PlatformInput input;
B32 prev_mouse_down;
} PlatformWindow;
// Main window receives menu commands
static PlatformWindow *g_main_window = NULL;
static HCURSOR g_current_cursor = NULL;
static B32 g_wndclass_registered = false;
static LRESULT CALLBACK win32_wndproc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
PlatformWindow *pw = (PlatformWindow *)GetWindowLongPtr(hwnd, GWLP_USERDATA);
switch (msg) {
case WM_SIZE:
if (pw && wparam != SIZE_MINIMIZED) {
pw->width = (S32)LOWORD(lparam);
pw->height = (S32)HIWORD(lparam);
// Render a frame during the modal resize loop so the UI
// stays responsive instead of showing a stretched image.
if (pw->frame_callback) {
pw->frame_callback(pw->frame_callback_user_data);
}
}
return 0;
case WM_CHAR:
if (pw && wparam >= 32 && wparam < 0xFFFF) {
PlatformInput *ev = &pw->input;
if (ev->char_count < PLATFORM_MAX_CHARS_PER_FRAME)
ev->chars[ev->char_count++] = (U16)wparam;
}
return 0;
case WM_KEYDOWN:
case WM_SYSKEYDOWN:
if (pw) {
PlatformInput *ev = &pw->input;
if (ev->key_count < PLATFORM_MAX_KEYS_PER_FRAME)
ev->keys[ev->key_count++] = (U8)wparam;
ev->ctrl_held = (GetKeyState(VK_CONTROL) & 0x8000) != 0;
ev->shift_held = (GetKeyState(VK_SHIFT) & 0x8000) != 0;
}
break; // fall through to DefWindowProc for system keys
case WM_MOUSEWHEEL:
if (pw) {
S16 wheel_delta = (S16)HIWORD(wparam);
pw->input.scroll_delta.y += (F32)wheel_delta / (F32)WHEEL_DELTA * 6.0f;
}
return 0;
case WM_COMMAND:
// Route menu commands to main window
if (g_main_window && HIWORD(wparam) == 0)
g_main_window->pending_menu_cmd = (S32)LOWORD(wparam);
return 0;
case WM_SETCURSOR:
// When the cursor is in our client area, use the app-set cursor.
if (LOWORD(lparam) == HTCLIENT) {
SetCursor(g_current_cursor ? g_current_cursor : LoadCursor(NULL, IDC_ARROW));
return TRUE;
}
break;
case WM_DPICHANGED:
if (pw) {
RECT *suggested = (RECT *)lparam;
SetWindowPos(hwnd, NULL, suggested->left, suggested->top,
suggested->right - suggested->left, suggested->bottom - suggested->top,
SWP_NOZORDER | SWP_NOACTIVATE);
}
return 0;
case WM_CLOSE:
if (pw)
pw->should_close = true;
return 0;
case WM_DESTROY:
if (pw == g_main_window)
PostQuitMessage(0);
return 0;
case WM_SYSCOMMAND:
if ((wparam & 0xfff0) == SC_KEYMENU)
return 0;
break;
}
return DefWindowProcW(hwnd, msg, wparam, lparam);
}
PlatformWindow *platform_create_window(PlatformWindowDesc *desc) {
SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
if (!g_wndclass_registered) {
WNDCLASSEXW wc = {0};
wc.cbSize = sizeof(wc);
wc.style = CS_CLASSDC;
wc.lpfnWndProc = win32_wndproc;
wc.hInstance = GetModuleHandleW(NULL);
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.lpszClassName = L"autosample_wc";
RegisterClassExW(&wc);
g_wndclass_registered = true;
}
UINT dpi = GetDpiForSystem();
int screen_w = GetSystemMetrics(SM_CXSCREEN);
int screen_h = GetSystemMetrics(SM_CYSCREEN);
int x = (screen_w - desc->width) / 2;
int y = (screen_h - desc->height) / 2;
DWORD style;
HWND parent_hwnd = NULL;
if (desc->style == PLATFORM_WINDOW_STYLE_POPUP) {
style = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU;
if (desc->parent && !desc->independent) parent_hwnd = desc->parent->hwnd;
} else if (desc->style == PLATFORM_WINDOW_STYLE_POPUP_RESIZABLE) {
style = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | WS_MAXIMIZEBOX;
if (desc->parent && !desc->independent) parent_hwnd = desc->parent->hwnd;
} else {
style = WS_OVERLAPPEDWINDOW;
}
RECT rect = { 0, 0, (LONG)desc->width, (LONG)desc->height };
AdjustWindowRectExForDpi(&rect, style, FALSE, 0, dpi);
int wchar_count = MultiByteToWideChar(CP_UTF8, 0, desc->title, -1, NULL, 0);
wchar_t *wtitle = (wchar_t *)_malloca(wchar_count * sizeof(wchar_t));
MultiByteToWideChar(CP_UTF8, 0, desc->title, -1, wtitle, wchar_count);
HWND hwnd = CreateWindowExW(
0, L"autosample_wc", wtitle,
style,
x, y,
rect.right - rect.left,
rect.bottom - rect.top,
parent_hwnd, NULL, GetModuleHandleW(NULL), NULL
);
_freea(wtitle);
if (!hwnd)
return NULL;
PlatformWindow *window = (PlatformWindow *)calloc(1, sizeof(PlatformWindow));
window->hwnd = hwnd;
window->should_close = false;
window->width = desc->width;
window->height = desc->height;
// Store PlatformWindow* on the HWND so WndProc can find it
SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)window);
// Track main window for menu commands
if (desc->style == PLATFORM_WINDOW_STYLE_NORMAL) {
g_main_window = window;
}
ShowWindow(hwnd, SW_SHOWDEFAULT);
UpdateWindow(hwnd);
return window;
}
void platform_destroy_window(PlatformWindow *window) {
if (!window) return;
if (window->hwnd) {
DestroyWindow(window->hwnd);
}
if (g_main_window == window)
g_main_window = NULL;
free(window);
}
B32 platform_poll_events(PlatformWindow *window) {
MSG msg;
while (PeekMessageW(&msg, NULL, 0, 0, PM_REMOVE)) {
TranslateMessage(&msg);
DispatchMessageW(&msg);
if (msg.message == WM_QUIT) {
window->should_close = true;
}
}
return !window->should_close;
}
void platform_get_size(PlatformWindow *window, S32 *w, S32 *h) {
if (w) *w = window->width;
if (h) *h = window->height;
}
void *platform_get_native_handle(PlatformWindow *window) {
return (void *)window->hwnd;
}
void platform_set_frame_callback(PlatformWindow *window, PlatformFrameCallback cb, void *user_data) {
window->frame_callback = cb;
window->frame_callback_user_data = user_data;
}
void platform_set_menu(PlatformWindow *window, PlatformMenu *menus, S32 menu_count) {
HMENU menu_bar = CreateMenu();
for (S32 i = 0; i < menu_count; i++) {
HMENU submenu = CreatePopupMenu();
for (S32 j = 0; j < menus[i].item_count; j++) {
PlatformMenuItem *item = &menus[i].items[j];
if (!item->label) {
AppendMenuW(submenu, MF_SEPARATOR, 0, NULL);
} else {
int wchar_count = MultiByteToWideChar(CP_UTF8, 0, item->label, -1, NULL, 0);
wchar_t *wlabel = (wchar_t *)_malloca(wchar_count * sizeof(wchar_t));
MultiByteToWideChar(CP_UTF8, 0, item->label, -1, wlabel, wchar_count);
AppendMenuW(submenu, MF_STRING, (UINT_PTR)item->id, wlabel);
_freea(wlabel);
}
}
int wchar_count = MultiByteToWideChar(CP_UTF8, 0, menus[i].label, -1, NULL, 0);
wchar_t *wlabel = (wchar_t *)_malloca(wchar_count * sizeof(wchar_t));
MultiByteToWideChar(CP_UTF8, 0, menus[i].label, -1, wlabel, wchar_count);
AppendMenuW(menu_bar, MF_POPUP, (UINT_PTR)submenu, wlabel);
_freea(wlabel);
}
SetMenu(window->hwnd, menu_bar);
}
S32 platform_poll_menu_command(PlatformWindow *window) {
S32 cmd = window->pending_menu_cmd;
window->pending_menu_cmd = 0;
return cmd;
}
PlatformInput platform_get_input(PlatformWindow *window) {
PlatformInput result = window->input;
// Poll mouse position
POINT cursor;
GetCursorPos(&cursor);
ScreenToClient(window->hwnd, &cursor);
result.mouse_pos = v2f32((F32)cursor.x, (F32)cursor.y);
// Poll mouse button
result.was_mouse_down = window->prev_mouse_down;
result.mouse_down = (GetAsyncKeyState(VK_LBUTTON) & 0x8000) != 0;
window->prev_mouse_down = result.mouse_down;
// Clear accumulated events for next frame
memset(&window->input, 0, sizeof(window->input));
return result;
}
B32 platform_window_should_close(PlatformWindow *window) {
return window ? window->should_close : 0;
}
void platform_focus_window(PlatformWindow *window) {
if (!window || !window->hwnd) return;
SetForegroundWindow(window->hwnd);
SetFocus(window->hwnd);
}
F32 platform_get_dpi_scale(PlatformWindow *window) {
if (!window || !window->hwnd) return 1.0f;
return (F32)GetDpiForWindow(window->hwnd) / 96.0f;
}
void platform_set_cursor(PlatformCursor cursor) {
switch (cursor) {
case PLATFORM_CURSOR_SIZE_WE: g_current_cursor = LoadCursor(NULL, IDC_SIZEWE); break;
case PLATFORM_CURSOR_SIZE_NS: g_current_cursor = LoadCursor(NULL, IDC_SIZENS); break;
default: g_current_cursor = LoadCursor(NULL, IDC_ARROW); break;
}
}
void platform_clipboard_set(const char *text) {
if (!text) return;
int len = (S32)strlen(text);
if (len == 0) return;
// Convert UTF-8 to wide string for Windows clipboard
int wlen = MultiByteToWideChar(CP_UTF8, 0, text, len, NULL, 0);
if (wlen == 0) return;
HGLOBAL hmem = GlobalAlloc(GMEM_MOVEABLE, (wlen + 1) * sizeof(wchar_t));
if (!hmem) return;
wchar_t *wbuf = (wchar_t *)GlobalLock(hmem);
MultiByteToWideChar(CP_UTF8, 0, text, len, wbuf, wlen);
wbuf[wlen] = L'\0';
GlobalUnlock(hmem);
HWND hwnd = g_main_window ? g_main_window->hwnd : NULL;
if (OpenClipboard(hwnd)) {
EmptyClipboard();
SetClipboardData(CF_UNICODETEXT, hmem);
CloseClipboard();
} else {
GlobalFree(hmem);
}
}
const char *platform_clipboard_get() {
static char buf[4096];
buf[0] = '\0';
HWND hwnd = g_main_window ? g_main_window->hwnd : NULL;
if (!OpenClipboard(hwnd)) return NULL;
HGLOBAL hmem = GetClipboardData(CF_UNICODETEXT);
if (hmem) {
wchar_t *wbuf = (wchar_t *)GlobalLock(hmem);
if (wbuf) {
int len = WideCharToMultiByte(CP_UTF8, 0, wbuf, -1, buf, sizeof(buf) - 1, NULL, NULL);
buf[len > 0 ? len - 1 : 0] = '\0'; // WideCharToMultiByte includes null in count
GlobalUnlock(hmem);
}
}
CloseClipboard();
return buf[0] ? buf : NULL;
}
S32 platform_message_box(PlatformWindow *parent, const char *title,
const char *message, PlatformMsgBoxType type) {
UINT mb_type;
switch (type) {
case PLATFORM_MSGBOX_OK: mb_type = MB_OK; break;
case PLATFORM_MSGBOX_OK_CANCEL: mb_type = MB_OKCANCEL; break;
case PLATFORM_MSGBOX_YES_NO: mb_type = MB_YESNO; break;
default: mb_type = MB_OK; break;
}
// Convert UTF-8 to wide strings
int title_wlen = MultiByteToWideChar(CP_UTF8, 0, title, -1, NULL, 0);
wchar_t *wtitle = (wchar_t *)_malloca(title_wlen * sizeof(wchar_t));
MultiByteToWideChar(CP_UTF8, 0, title, -1, wtitle, title_wlen);
int msg_wlen = MultiByteToWideChar(CP_UTF8, 0, message, -1, NULL, 0);
wchar_t *wmsg = (wchar_t *)_malloca(msg_wlen * sizeof(wchar_t));
MultiByteToWideChar(CP_UTF8, 0, message, -1, wmsg, msg_wlen);
HWND hwnd = parent ? parent->hwnd : NULL;
int result = MessageBoxW(hwnd, wmsg, wtitle, mb_type);
_freea(wmsg);
_freea(wtitle);
switch (result) {
case IDOK: return 0;
case IDYES: return 0;
case IDCANCEL: return 1;
case IDNO: return 1;
default: return -1;
}
}