#include "platform/platform.h" #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN #endif #include #include struct PlatformWindow { HWND hwnd; bool should_close; int32_t width; int32_t height; int32_t pending_menu_cmd; PlatformFrameCallback frame_callback; void *frame_callback_user_data; PlatformInput input; B32 prev_mouse_down; }; static PlatformWindow *g_current_window = nullptr; static LRESULT CALLBACK win32_wndproc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) { switch (msg) { case WM_SIZE: if (g_current_window && wparam != SIZE_MINIMIZED) { g_current_window->width = (int32_t)LOWORD(lparam); g_current_window->height = (int32_t)HIWORD(lparam); // Render a frame during the modal resize loop so the UI // stays responsive instead of showing a stretched image. if (g_current_window->frame_callback) { g_current_window->frame_callback(g_current_window->frame_callback_user_data); } } return 0; case WM_CHAR: if (g_current_window && wparam >= 32 && wparam < 0xFFFF) { PlatformInput *ev = &g_current_window->input; if (ev->char_count < PLATFORM_MAX_CHARS_PER_FRAME) ev->chars[ev->char_count++] = (uint16_t)wparam; } return 0; case WM_KEYDOWN: case WM_SYSKEYDOWN: if (g_current_window) { PlatformInput *ev = &g_current_window->input; if (ev->key_count < PLATFORM_MAX_KEYS_PER_FRAME) ev->keys[ev->key_count++] = (uint8_t)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 (g_current_window) { int16_t wheel_delta = (int16_t)HIWORD(wparam); g_current_window->input.scroll_delta.y += (F32)wheel_delta / (F32)WHEEL_DELTA * 6.0f; } return 0; case WM_COMMAND: if (g_current_window && HIWORD(wparam) == 0) g_current_window->pending_menu_cmd = (int32_t)LOWORD(wparam); return 0; case WM_SETCURSOR: // When the cursor is in our client area, force it to an arrow. // Without this, moving from a resize border back into the client // area would leave the resize cursor shape stuck. if (LOWORD(lparam) == HTCLIENT) { SetCursor(LoadCursor(nullptr, IDC_ARROW)); return TRUE; } break; case WM_CLOSE: if (g_current_window) g_current_window->should_close = true; return 0; case WM_DESTROY: 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) { WNDCLASSEXW wc = {}; wc.cbSize = sizeof(wc); wc.style = CS_CLASSDC; wc.lpfnWndProc = win32_wndproc; wc.hInstance = GetModuleHandleW(nullptr); wc.hCursor = LoadCursor(nullptr, IDC_ARROW); wc.lpszClassName = L"autosample_wc"; RegisterClassExW(&wc); 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; RECT rect = { 0, 0, (LONG)desc->width, (LONG)desc->height }; AdjustWindowRect(&rect, WS_OVERLAPPEDWINDOW, FALSE); int wchar_count = MultiByteToWideChar(CP_UTF8, 0, desc->title, -1, nullptr, 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, wc.lpszClassName, wtitle, WS_OVERLAPPEDWINDOW, x, y, rect.right - rect.left, rect.bottom - rect.top, nullptr, nullptr, wc.hInstance, nullptr ); _freea(wtitle); if (!hwnd) return nullptr; ShowWindow(hwnd, SW_SHOWDEFAULT); UpdateWindow(hwnd); PlatformWindow *window = new PlatformWindow(); window->hwnd = hwnd; window->should_close = false; window->width = desc->width; window->height = desc->height; g_current_window = window; return window; } void platform_destroy_window(PlatformWindow *window) { if (!window) return; if (window->hwnd) { DestroyWindow(window->hwnd); UnregisterClassW(L"autosample_wc", GetModuleHandleW(nullptr)); } if (g_current_window == window) g_current_window = nullptr; delete window; } bool platform_poll_events(PlatformWindow *window) { MSG msg; while (PeekMessageW(&msg, nullptr, 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, int32_t *w, int32_t *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, int32_t menu_count) { HMENU menu_bar = CreateMenu(); for (int32_t i = 0; i < menu_count; i++) { HMENU submenu = CreatePopupMenu(); for (int32_t j = 0; j < menus[i].item_count; j++) { PlatformMenuItem *item = &menus[i].items[j]; if (!item->label) { AppendMenuW(submenu, MF_SEPARATOR, 0, nullptr); } else { int wchar_count = MultiByteToWideChar(CP_UTF8, 0, item->label, -1, nullptr, 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, nullptr, 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); } int32_t platform_poll_menu_command(PlatformWindow *window) { int32_t 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 window->input = {}; return result; } void platform_clipboard_set(const char *text) { if (!text) return; int len = (int)strlen(text); if (len == 0) return; // Convert UTF-8 to wide string for Windows clipboard int wlen = MultiByteToWideChar(CP_UTF8, 0, text, len, nullptr, 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_current_window ? g_current_window->hwnd : nullptr; 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_current_window ? g_current_window->hwnd : nullptr; if (!OpenClipboard(hwnd)) return nullptr; 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, nullptr, nullptr); buf[len > 0 ? len - 1 : 0] = '\0'; // WideCharToMultiByte includes null in count GlobalUnlock(hmem); } } CloseClipboard(); return buf[0] ? buf : nullptr; }