/************************************************************/ /** * * @file: win32_app.c * @author: Martin Fouilleul * @date: 16/12/2022 * @revision: * *****************************************************************/ #include "app.c" #include "platform/platform_thread.h" #include void oc_init_keys() { memset(oc_appData.keyCodes, OC_KEY_UNKNOWN, 256 * sizeof(int)); oc_appData.keyCodes[0x00B] = OC_KEY_0; oc_appData.keyCodes[0x002] = OC_KEY_1; oc_appData.keyCodes[0x003] = OC_KEY_2; oc_appData.keyCodes[0x004] = OC_KEY_3; oc_appData.keyCodes[0x005] = OC_KEY_4; oc_appData.keyCodes[0x006] = OC_KEY_5; oc_appData.keyCodes[0x007] = OC_KEY_6; oc_appData.keyCodes[0x008] = OC_KEY_7; oc_appData.keyCodes[0x009] = OC_KEY_8; oc_appData.keyCodes[0x00A] = OC_KEY_9; oc_appData.keyCodes[0x01E] = OC_KEY_A; oc_appData.keyCodes[0x030] = OC_KEY_B; oc_appData.keyCodes[0x02E] = OC_KEY_C; oc_appData.keyCodes[0x020] = OC_KEY_D; oc_appData.keyCodes[0x012] = OC_KEY_E; oc_appData.keyCodes[0x021] = OC_KEY_F; oc_appData.keyCodes[0x022] = OC_KEY_G; oc_appData.keyCodes[0x023] = OC_KEY_H; oc_appData.keyCodes[0x017] = OC_KEY_I; oc_appData.keyCodes[0x024] = OC_KEY_J; oc_appData.keyCodes[0x025] = OC_KEY_K; oc_appData.keyCodes[0x026] = OC_KEY_L; oc_appData.keyCodes[0x032] = OC_KEY_M; oc_appData.keyCodes[0x031] = OC_KEY_N; oc_appData.keyCodes[0x018] = OC_KEY_O; oc_appData.keyCodes[0x019] = OC_KEY_P; oc_appData.keyCodes[0x010] = OC_KEY_Q; oc_appData.keyCodes[0x013] = OC_KEY_R; oc_appData.keyCodes[0x01F] = OC_KEY_S; oc_appData.keyCodes[0x014] = OC_KEY_T; oc_appData.keyCodes[0x016] = OC_KEY_U; oc_appData.keyCodes[0x02F] = OC_KEY_V; oc_appData.keyCodes[0x011] = OC_KEY_W; oc_appData.keyCodes[0x02D] = OC_KEY_X; oc_appData.keyCodes[0x015] = OC_KEY_Y; oc_appData.keyCodes[0x02C] = OC_KEY_Z; oc_appData.keyCodes[0x028] = OC_KEY_APOSTROPHE; oc_appData.keyCodes[0x02B] = OC_KEY_BACKSLASH; oc_appData.keyCodes[0x033] = OC_KEY_COMMA; oc_appData.keyCodes[0x00D] = OC_KEY_EQUAL; oc_appData.keyCodes[0x029] = OC_KEY_GRAVE_ACCENT; oc_appData.keyCodes[0x01A] = OC_KEY_LEFT_BRACKET; oc_appData.keyCodes[0x00C] = OC_KEY_MINUS; oc_appData.keyCodes[0x034] = OC_KEY_PERIOD; oc_appData.keyCodes[0x01B] = OC_KEY_RIGHT_BRACKET; oc_appData.keyCodes[0x027] = OC_KEY_SEMICOLON; oc_appData.keyCodes[0x035] = OC_KEY_SLASH; oc_appData.keyCodes[0x056] = OC_KEY_WORLD_2; oc_appData.keyCodes[0x00E] = OC_KEY_BACKSPACE; oc_appData.keyCodes[0x153] = OC_KEY_DELETE; oc_appData.keyCodes[0x14F] = OC_KEY_END; oc_appData.keyCodes[0x01C] = OC_KEY_ENTER; oc_appData.keyCodes[0x001] = OC_KEY_ESCAPE; oc_appData.keyCodes[0x147] = OC_KEY_HOME; oc_appData.keyCodes[0x152] = OC_KEY_INSERT; oc_appData.keyCodes[0x15D] = OC_KEY_MENU; oc_appData.keyCodes[0x151] = OC_KEY_PAGE_DOWN; oc_appData.keyCodes[0x149] = OC_KEY_PAGE_UP; oc_appData.keyCodes[0x045] = OC_KEY_PAUSE; oc_appData.keyCodes[0x146] = OC_KEY_PAUSE; oc_appData.keyCodes[0x039] = OC_KEY_SPACE; oc_appData.keyCodes[0x00F] = OC_KEY_TAB; oc_appData.keyCodes[0x03A] = OC_KEY_CAPS_LOCK; oc_appData.keyCodes[0x145] = OC_KEY_NUM_LOCK; oc_appData.keyCodes[0x046] = OC_KEY_SCROLL_LOCK; oc_appData.keyCodes[0x03B] = OC_KEY_F1; oc_appData.keyCodes[0x03C] = OC_KEY_F2; oc_appData.keyCodes[0x03D] = OC_KEY_F3; oc_appData.keyCodes[0x03E] = OC_KEY_F4; oc_appData.keyCodes[0x03F] = OC_KEY_F5; oc_appData.keyCodes[0x040] = OC_KEY_F6; oc_appData.keyCodes[0x041] = OC_KEY_F7; oc_appData.keyCodes[0x042] = OC_KEY_F8; oc_appData.keyCodes[0x043] = OC_KEY_F9; oc_appData.keyCodes[0x044] = OC_KEY_F10; oc_appData.keyCodes[0x057] = OC_KEY_F11; oc_appData.keyCodes[0x058] = OC_KEY_F12; oc_appData.keyCodes[0x064] = OC_KEY_F13; oc_appData.keyCodes[0x065] = OC_KEY_F14; oc_appData.keyCodes[0x066] = OC_KEY_F15; oc_appData.keyCodes[0x067] = OC_KEY_F16; oc_appData.keyCodes[0x068] = OC_KEY_F17; oc_appData.keyCodes[0x069] = OC_KEY_F18; oc_appData.keyCodes[0x06A] = OC_KEY_F19; oc_appData.keyCodes[0x06B] = OC_KEY_F20; oc_appData.keyCodes[0x06C] = OC_KEY_F21; oc_appData.keyCodes[0x06D] = OC_KEY_F22; oc_appData.keyCodes[0x06E] = OC_KEY_F23; oc_appData.keyCodes[0x076] = OC_KEY_F24; oc_appData.keyCodes[0x038] = OC_KEY_LEFT_ALT; oc_appData.keyCodes[0x01D] = OC_KEY_LEFT_CONTROL; oc_appData.keyCodes[0x02A] = OC_KEY_LEFT_SHIFT; oc_appData.keyCodes[0x15B] = OC_KEY_LEFT_SUPER; oc_appData.keyCodes[0x137] = OC_KEY_PRINT_SCREEN; oc_appData.keyCodes[0x138] = OC_KEY_RIGHT_ALT; oc_appData.keyCodes[0x11D] = OC_KEY_RIGHT_CONTROL; oc_appData.keyCodes[0x036] = OC_KEY_RIGHT_SHIFT; oc_appData.keyCodes[0x15C] = OC_KEY_RIGHT_SUPER; oc_appData.keyCodes[0x150] = OC_KEY_DOWN; oc_appData.keyCodes[0x14B] = OC_KEY_LEFT; oc_appData.keyCodes[0x14D] = OC_KEY_RIGHT; oc_appData.keyCodes[0x148] = OC_KEY_UP; oc_appData.keyCodes[0x052] = OC_KEY_KP_0; oc_appData.keyCodes[0x04F] = OC_KEY_KP_1; oc_appData.keyCodes[0x050] = OC_KEY_KP_2; oc_appData.keyCodes[0x051] = OC_KEY_KP_3; oc_appData.keyCodes[0x04B] = OC_KEY_KP_4; oc_appData.keyCodes[0x04C] = OC_KEY_KP_5; oc_appData.keyCodes[0x04D] = OC_KEY_KP_6; oc_appData.keyCodes[0x047] = OC_KEY_KP_7; oc_appData.keyCodes[0x048] = OC_KEY_KP_8; oc_appData.keyCodes[0x049] = OC_KEY_KP_9; oc_appData.keyCodes[0x04E] = OC_KEY_KP_ADD; oc_appData.keyCodes[0x053] = OC_KEY_KP_DECIMAL; oc_appData.keyCodes[0x135] = OC_KEY_KP_DIVIDE; oc_appData.keyCodes[0x11C] = OC_KEY_KP_ENTER; oc_appData.keyCodes[0x037] = OC_KEY_KP_MULTIPLY; oc_appData.keyCodes[0x04A] = OC_KEY_KP_SUBTRACT; memset(oc_appData.nativeKeys, 0, sizeof(int) * OC_KEY_COUNT); for(int nativeKey = 0; nativeKey < 256; nativeKey++) { oc_key_code mpKey = oc_appData.keyCodes[nativeKey]; if(mpKey) { oc_appData.nativeKeys[mpKey] = nativeKey; } } } void oc_init() { if(!oc_appData.init) { memset(&oc_appData, 0, sizeof(oc_appData)); oc_clock_init(); oc_init_common(); oc_init_keys(); oc_appData.win32.savedConsoleCodePage = GetConsoleOutputCP(); SetConsoleOutputCP(CP_UTF8); DWORD mode; GetConsoleMode(GetStdHandle(STD_OUTPUT_HANDLE), &mode); mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING; SetConsoleMode(GetStdHandle(STD_OUTPUT_HANDLE), mode); SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); u32 wheelScrollLines = 3; SystemParametersInfo(SPI_GETWHEELSCROLLLINES, 0, &wheelScrollLines, 0); oc_appData.win32.wheelScrollLines = wheelScrollLines; } } void oc_terminate() { if(oc_appData.init) { SetConsoleOutputCP(oc_appData.win32.savedConsoleCodePage); oc_terminate_common(); oc_appData = (oc_app){ 0 }; } } static oc_key_code oc_convert_win32_key(int code) { return (oc_appData.keyCodes[code]); } static oc_keymod_flags oc_get_mod_keys() { oc_keymod_flags mods = 0; if(GetKeyState(VK_SHIFT) & 0x8000) { mods |= OC_KEYMOD_SHIFT; } if(GetKeyState(VK_CONTROL) & 0x8000) { mods |= OC_KEYMOD_CTRL; mods |= OC_KEYMOD_MAIN_MODIFIER; } if(GetKeyState(VK_MENU) & 0x8000) { mods |= OC_KEYMOD_ALT; } if((GetKeyState(VK_LWIN) | GetKeyState(VK_RWIN)) & 0x8000) { mods |= OC_KEYMOD_CMD; } return (mods); } static void oc_win32_process_mouse_event(oc_window_data* window, oc_key_action action, oc_key_code button) { if(action == OC_KEY_PRESS) { if(!oc_appData.win32.mouseCaptureMask) { SetCapture(window->win32.hWnd); } oc_appData.win32.mouseCaptureMask |= (1 << button); } else if(action == OC_KEY_RELEASE) { oc_appData.win32.mouseCaptureMask &= ~(1 << button); if(!oc_appData.win32.mouseCaptureMask) { ReleaseCapture(); } } //TODO click/double click oc_event event = { 0 }; event.window = oc_window_handle_from_ptr(window); event.type = OC_EVENT_MOUSE_BUTTON; event.key.action = action; event.key.code = button; event.key.mods = oc_get_mod_keys(); oc_queue_event(&event); } static void oc_win32_process_wheel_event(oc_window_data* window, f32 x, f32 y) { oc_event event = { 0 }; event.window = oc_window_handle_from_ptr(window); event.type = OC_EVENT_MOUSE_WHEEL; // Borrowed from https://source.chromium.org/chromium/chromium/src/+/3e1a26c44c024d97dc9a4c09bbc6a2365398ca2c:ui/events/blink/web_input_event_builders_win.cc;l=318-330 f32 scrollMultiplier = oc_appData.win32.wheelScrollLines * 100.0 / 3.0; event.mouse.deltaX = x / WHEEL_DELTA * scrollMultiplier; event.mouse.deltaY = -y / WHEEL_DELTA * scrollMultiplier; event.mouse.mods = oc_get_mod_keys(); oc_queue_event(&event); } static void oc_win32_update_child_layers(oc_window_data* window) { RECT clientRect; GetClientRect(window->win32.hWnd, &clientRect); POINT point = { 0 }; ClientToScreen(window->win32.hWnd, &point); int clientWidth = (clientRect.right - clientRect.left); int clientHeight = (clientRect.bottom - clientRect.top); HWND insertAfter = window->win32.hWnd; oc_list_for(&window->win32.layers, layer, oc_layer, listElt) { SetWindowPos(layer->hWnd, insertAfter, point.x, point.y, clientWidth, clientHeight, SWP_NOACTIVATE | SWP_NOOWNERZORDER | SWP_NOZORDER); insertAfter = layer->hWnd; } } LRESULT oc_win32_win_proc(HWND windowHandle, UINT message, WPARAM wParam, LPARAM lParam) { LRESULT result = 0; oc_window_data* mpWindow = GetPropW(windowHandle, L"MilePost"); //TODO: put messages in queue bool handled = true; switch(message) { case WM_CLOSE: { oc_event event = { 0 }; event.window = oc_window_handle_from_ptr(mpWindow); event.type = OC_EVENT_WINDOW_CLOSE; oc_queue_event(&event); } break; case WM_DPICHANGED: { u32 dpi = HIWORD(wParam); RECT rect = *(RECT*)lParam; SetWindowPos(mpWindow->win32.hWnd, HWND_TOP, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, SWP_NOACTIVATE | SWP_NOZORDER); //TODO: send a message } break; //TODO: enter/exit size & move case WM_WINDOWPOSCHANGED: { oc_win32_update_child_layers(mpWindow); result = DefWindowProc(windowHandle, message, wParam, lParam); } break; case WM_SIZING: case WM_MOVING: { RECT* rect = (RECT*)lParam; oc_event event = { 0 }; event.type = message == WM_SIZING ? OC_EVENT_WINDOW_RESIZE : OC_EVENT_WINDOW_MOVE; event.window = oc_window_handle_from_ptr(mpWindow); event.move.frame = oc_window_get_frame_rect(event.window); event.move.content = oc_window_get_content_rect(event.window); oc_queue_event(&event); oc_win32_update_child_layers(mpWindow); } break; case WM_SETFOCUS: { oc_event event = { 0 }; event.window = oc_window_handle_from_ptr(mpWindow); event.type = OC_EVENT_WINDOW_FOCUS; oc_queue_event(&event); } break; case WM_KILLFOCUS: { oc_event event = { 0 }; event.window = oc_window_handle_from_ptr(mpWindow); event.type = OC_EVENT_WINDOW_UNFOCUS; oc_queue_event(&event); } break; case WM_SIZE: { bool minimized = (wParam == SIZE_MINIMIZED); if(minimized != mpWindow->minimized) { mpWindow->minimized = minimized; oc_event event = { 0 }; event.window = oc_window_handle_from_ptr(mpWindow); if(minimized) { event.type = OC_EVENT_WINDOW_HIDE; } else if(mpWindow->minimized) { event.type = OC_EVENT_WINDOW_SHOW; } oc_queue_event(&event); } } break; case WM_LBUTTONDOWN: { oc_win32_process_mouse_event(mpWindow, OC_KEY_PRESS, OC_MOUSE_LEFT); } break; case WM_RBUTTONDOWN: { oc_win32_process_mouse_event(mpWindow, OC_KEY_PRESS, OC_MOUSE_RIGHT); } break; case WM_MBUTTONDOWN: { oc_win32_process_mouse_event(mpWindow, OC_KEY_PRESS, OC_MOUSE_MIDDLE); } break; case WM_LBUTTONUP: { oc_win32_process_mouse_event(mpWindow, OC_KEY_RELEASE, OC_MOUSE_LEFT); } break; case WM_RBUTTONUP: { oc_win32_process_mouse_event(mpWindow, OC_KEY_RELEASE, OC_MOUSE_RIGHT); } break; case WM_MBUTTONUP: { oc_win32_process_mouse_event(mpWindow, OC_KEY_RELEASE, OC_MOUSE_MIDDLE); } break; case WM_MOUSEMOVE: { RECT rect; GetClientRect(mpWindow->win32.hWnd, &rect); u32 dpi = GetDpiForWindow(mpWindow->win32.hWnd); f32 scaling = (f32)dpi / 96.; oc_event event = { 0 }; event.window = oc_window_handle_from_ptr(mpWindow); event.type = OC_EVENT_MOUSE_MOVE; event.mouse.x = LOWORD(lParam) / scaling; event.mouse.y = HIWORD(lParam) / scaling; if(oc_appData.win32.mouseTracked || oc_appData.win32.mouseCaptureMask) { event.mouse.deltaX = event.mouse.x - oc_appData.win32.lastMousePos.x; event.mouse.deltaY = event.mouse.y - oc_appData.win32.lastMousePos.y; } oc_appData.win32.lastMousePos = (oc_vec2){ event.mouse.x, event.mouse.y }; if(!oc_appData.win32.mouseTracked) { oc_appData.win32.mouseTracked = true; TRACKMOUSEEVENT track; memset(&track, 0, sizeof(track)); track.cbSize = sizeof(track); track.dwFlags = TME_LEAVE; track.hwndTrack = mpWindow->win32.hWnd; TrackMouseEvent(&track); oc_event enter = { .window = event.window, .type = OC_EVENT_MOUSE_ENTER, .mouse.x = event.mouse.x, .mouse.y = event.mouse.y }; oc_queue_event(&enter); } oc_queue_event(&event); } break; case WM_MOUSELEAVE: { oc_appData.win32.mouseTracked = false; oc_event event = { 0 }; event.window = oc_window_handle_from_ptr(mpWindow); event.type = OC_EVENT_MOUSE_LEAVE; oc_queue_event(&event); } break; case WM_MOUSEWHEEL: { oc_win32_process_wheel_event(mpWindow, 0, (float)((i16)HIWORD(wParam))); } break; case WM_MOUSEHWHEEL: { oc_win32_process_wheel_event(mpWindow, (float)((i16)HIWORD(wParam)), 0); } break; case WM_KEYDOWN: case WM_SYSKEYDOWN: { // Need to pass these through to the normal event handler to handle system shortcuts like Alt+F4. // Same for WM_SYSKEYUP if(message == WM_SYSKEYDOWN) { handled = false; } oc_event event = { 0 }; event.window = oc_window_handle_from_ptr(mpWindow); event.type = OC_EVENT_KEYBOARD_KEY; event.key.action = (lParam & 0x40000000) ? OC_KEY_REPEAT : OC_KEY_PRESS; event.key.code = oc_convert_win32_key(HIWORD(lParam) & 0x1ff); event.key.mods = oc_get_mod_keys(); oc_queue_event(&event); } break; case WM_KEYUP: case WM_SYSKEYUP: { if(message == WM_SYSKEYUP) { handled = false; } oc_event event = { 0 }; event.window = oc_window_handle_from_ptr(mpWindow); event.type = OC_EVENT_KEYBOARD_KEY; event.key.action = OC_KEY_RELEASE; event.key.code = oc_convert_win32_key(HIWORD(lParam) & 0x1ff); event.key.mods = oc_get_mod_keys(); oc_queue_event(&event); } break; case WM_CHAR: { if((u32)wParam >= 32) { oc_event event = { 0 }; event.window = oc_window_handle_from_ptr(mpWindow); event.type = OC_EVENT_KEYBOARD_CHAR; event.character.codepoint = (oc_utf32)wParam; oc_str8 seq = oc_utf8_encode(event.character.sequence, event.character.codepoint); event.character.seqLen = seq.len; oc_queue_event(&event); } } break; case WM_SETTINGCHANGE: { if((u32)wParam == SPI_SETWHEELSCROLLLINES) { u32 wheelScrollLines; if(SystemParametersInfo(SPI_GETWHEELSCROLLLINES, 0, &wheelScrollLines, 0) != 0) { oc_appData.win32.wheelScrollLines = wheelScrollLines; } } } break; case WM_DROPFILES: { //TODO } break; case OC_WM_USER_DISPATCH_PROC: { oc_dispatch_proc proc = (oc_dispatch_proc)wParam; void* user = (void*)lParam; result = proc(user); } break; default: { handled = false; } break; } if(handled == false) { result = DefWindowProc(windowHandle, message, wParam, lParam); } return (result); } //-------------------------------------------------------------------- // app management //-------------------------------------------------------------------- bool oc_should_quit() { return (oc_appData.shouldQuit); } void oc_cancel_quit() { oc_appData.shouldQuit = false; } void oc_request_quit() { oc_appData.shouldQuit = true; } void oc_pump_events(f64 timeout) { MSG message; if(timeout < 0) { WaitMessage(); } else if(timeout > 0) { MsgWaitForMultipleObjects(0, NULL, FALSE, (DWORD)(timeout * 1e3), QS_ALLEVENTS); } while(PeekMessage(&message, 0, 0, 0, PM_REMOVE)) { TranslateMessage(&message); DispatchMessage(&message); } } i32 oc_dispatch_on_main_thread_sync(oc_window main_window, oc_dispatch_proc proc, void* user) { oc_window_data* window_data = oc_window_ptr_from_handle(main_window); OC_DEBUG_ASSERT(window_data != NULL); LRESULT result = SendMessage(window_data->win32.hWnd, OC_WM_USER_DISPATCH_PROC, (WPARAM)proc, (LPARAM)user); return result; } //-------------------------------------------------------------------- // window management //-------------------------------------------------------------------- //WARN: the following header pulls in objbase.h (even with WIN32_LEAN_AND_MEAN), which // #defines interface to struct... so make sure to #undef interface since it's a // name we want to be able to use throughout the codebase #include #undef interface oc_window oc_window_create(oc_rect rect, oc_str8 title, oc_window_style style) { WNDCLASS windowClass = { .style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC, .lpfnWndProc = oc_win32_win_proc, .hInstance = GetModuleHandleW(NULL), .lpszClassName = "ApplicationWindowClass", .hCursor = LoadCursor(0, IDC_ARROW) }; if(!RegisterClass(&windowClass)) { //TODO: error goto quit; } u32 dpiX, dpiY; HMONITOR monitor = MonitorFromPoint((POINT){ rect.x, rect.y }, MONITOR_DEFAULTTOPRIMARY); GetDpiForMonitor(monitor, MDT_EFFECTIVE_DPI, &dpiX, &dpiY); f32 scaleX = (f32)dpiX / 96.; f32 scaleY = (f32)dpiY / 96.; RECT frame = { rect.x * scaleX, rect.y * scaleY, (rect.x + rect.w) * scaleX, (rect.y + rect.h) * scaleY }; DWORD winStyle = WS_OVERLAPPEDWINDOW; AdjustWindowRect(&frame, winStyle, FALSE); oc_arena_scope scratch = oc_scratch_begin(); const char* titleCString = oc_str8_to_cstring(scratch.arena, title); HWND windowHandle = CreateWindow("ApplicationWindowClass", titleCString, winStyle, frame.left, frame.top, frame.right - frame.left, frame.bottom - frame.top, 0, 0, windowClass.hInstance, 0); oc_scratch_end(scratch); if(!windowHandle) { //TODO: error goto quit; } UpdateWindow(windowHandle); //TODO: return wrapped window quit:; oc_window_data* window = oc_window_alloc(); window->win32.hWnd = windowHandle; window->win32.layers = (oc_list){ 0 }; SetPropW(windowHandle, L"MilePost", window); return (oc_window_handle_from_ptr(window)); } void oc_window_destroy(oc_window window) { oc_window_data* windowData = oc_window_ptr_from_handle(window); if(windowData) { DestroyWindow(windowData->win32.hWnd); //TODO: check when to unregister class oc_window_recycle_ptr(windowData); } } void* oc_window_native_pointer(oc_window window) { oc_window_data* windowData = oc_window_ptr_from_handle(window); if(windowData) { return (windowData->win32.hWnd); } else { return (0); } } bool oc_window_should_close(oc_window window) { oc_window_data* windowData = oc_window_ptr_from_handle(window); if(windowData) { return (windowData->shouldClose); } else { return (false); } } void oc_window_request_close(oc_window window) { oc_window_data* windowData = oc_window_ptr_from_handle(window); if(windowData) { windowData->shouldClose = true; PostMessage(windowData->win32.hWnd, WM_CLOSE, 0, 0); } } void oc_window_cancel_close(oc_window window) { oc_window_data* windowData = oc_window_ptr_from_handle(window); if(windowData) { windowData->shouldClose = false; } } bool oc_window_is_hidden(oc_window window) { oc_window_data* windowData = oc_window_ptr_from_handle(window); if(windowData) { return (IsWindowVisible(windowData->win32.hWnd)); } else { return (false); } } void oc_window_hide(oc_window window) { oc_window_data* windowData = oc_window_ptr_from_handle(window); if(windowData) { ShowWindow(windowData->win32.hWnd, SW_HIDE); } } void oc_window_show(oc_window window) { oc_window_data* windowData = oc_window_ptr_from_handle(window); if(windowData) { ShowWindow(windowData->win32.hWnd, SW_NORMAL); } } bool oc_window_is_minimized(oc_window window) { oc_window_data* windowData = oc_window_ptr_from_handle(window); if(windowData) { return (windowData->minimized); } else { return (false); } } void oc_window_minimize(oc_window window) { oc_window_data* windowData = oc_window_ptr_from_handle(window); if(windowData) { ShowWindow(windowData->win32.hWnd, SW_MINIMIZE); } } void oc_window_maximize(oc_window window) { oc_window_data* windowData = oc_window_ptr_from_handle(window); if(windowData) { ShowWindow(windowData->win32.hWnd, SW_MAXIMIZE); } } void oc_window_restore(oc_window window) { oc_window_data* windowData = oc_window_ptr_from_handle(window); if(windowData) { ShowWindow(windowData->win32.hWnd, SW_RESTORE); } } bool oc_window_has_focus(oc_window window) { oc_window_data* windowData = oc_window_ptr_from_handle(window); if(windowData) { return (GetActiveWindow() == windowData->win32.hWnd); } else { return (false); } } void oc_window_focus(oc_window window) { oc_window_data* windowData = oc_window_ptr_from_handle(window); if(windowData) { SetFocus(windowData->win32.hWnd); } } void oc_window_unfocus(oc_window window) { oc_window_data* windowData = oc_window_ptr_from_handle(window); if(windowData) { SetFocus(0); } } void oc_window_send_to_back(oc_window window) { oc_window_data* windowData = oc_window_ptr_from_handle(window); if(windowData) { SetWindowPos(windowData->win32.hWnd, HWND_BOTTOM, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE); } } void oc_window_bring_to_front(oc_window window) { oc_window_data* windowData = oc_window_ptr_from_handle(window); if(windowData) { if(!IsWindowVisible(windowData->win32.hWnd)) { ShowWindow(windowData->win32.hWnd, SW_NORMAL); } SetWindowPos(windowData->win32.hWnd, HWND_TOP, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE); } } oc_rect oc_window_get_frame_rect(oc_window window) { oc_rect rect = { 0 }; oc_window_data* windowData = oc_window_ptr_from_handle(window); if(windowData) { //NOTE: GetWindowRect() includes the drop shadow, which we don't want, so we call // DwmGetWindowAttribute() instead. // Note that contrary to what the GetWindowRect() docs suggests when mentionning // this, DwmGetWindowAttributes() _does_ seem to adjust for DPI. u32 dpi = GetDpiForWindow(windowData->win32.hWnd); f32 scale = (float)dpi / 96.; RECT frame; HRESULT res = DwmGetWindowAttribute(windowData->win32.hWnd, DWMWA_EXTENDED_FRAME_BOUNDS, &frame, sizeof(RECT)); if(res == S_OK) { rect = (oc_rect){ frame.left / scale, frame.top / scale, (frame.right - frame.left) / scale, (frame.bottom - frame.top) / scale }; } } return (rect); } static oc_rect oc_win32_get_drop_shadow_offsets(HWND hWnd) { RECT frameIncludingShadow; RECT frameExcludingShadow; GetWindowRect(hWnd, &frameIncludingShadow); DwmGetWindowAttribute(hWnd, DWMWA_EXTENDED_FRAME_BOUNDS, &frameExcludingShadow, sizeof(RECT)); oc_rect extents = { .x = frameIncludingShadow.left - frameExcludingShadow.left, .y = frameIncludingShadow.top - frameExcludingShadow.top, .w = frameIncludingShadow.right - frameExcludingShadow.right, .h = frameIncludingShadow.bottom - frameExcludingShadow.bottom }; return (extents); } void oc_window_set_frame_rect(oc_window window, oc_rect rect) { oc_window_data* windowData = oc_window_ptr_from_handle(window); if(windowData) { u32 dpi = GetDpiForWindow(windowData->win32.hWnd); f32 scale = (float)dpi / 96.; //NOTE compute the size of the drop shadow to add it in setwindowpos oc_rect shadowOffsets = oc_win32_get_drop_shadow_offsets(windowData->win32.hWnd); RECT frame = { rect.x * scale + shadowOffsets.x, rect.y * scale + shadowOffsets.y, (rect.x + rect.w) * scale + shadowOffsets.w, (rect.y + rect.h) * scale + shadowOffsets.h }; SetWindowPos(windowData->win32.hWnd, HWND_TOP, frame.left, frame.top, frame.right - frame.left, frame.bottom - frame.top, SWP_NOZORDER | SWP_NOACTIVATE); } } oc_rect oc_window_get_content_rect(oc_window window) { oc_rect rect = { 0 }; oc_window_data* windowData = oc_window_ptr_from_handle(window); if(windowData) { RECT client; if(GetClientRect(windowData->win32.hWnd, &client)) { u32 dpi = GetDpiForWindow(windowData->win32.hWnd); f32 scale = (float)dpi / 96.; POINT origin = { 0, 0 }; ClientToScreen(windowData->win32.hWnd, &origin); rect = (oc_rect){ origin.x / scale, origin.y / scale, (client.right - client.left) / scale, (client.bottom - client.top) / scale }; } } return (rect); } void oc_window_set_content_rect(oc_window window, oc_rect rect) { oc_window_data* windowData = oc_window_ptr_from_handle(window); if(windowData) { u32 dpi = GetDpiForWindow(windowData->win32.hWnd); f32 scale = (float)dpi / 96.; RECT frame = { rect.x * scale, rect.y * scale, (rect.x + rect.w) * scale, (rect.y + rect.h) * scale }; DWORD style = GetWindowLong(windowData->win32.hWnd, GWL_STYLE); BOOL menu = (GetMenu(windowData->win32.hWnd) != NULL); AdjustWindowRect(&frame, style, menu); SetWindowPos(windowData->win32.hWnd, HWND_TOP, frame.left, frame.top, frame.right - frame.left, frame.bottom - frame.top, SWP_NOZORDER | SWP_NOACTIVATE); } } void oc_window_center(oc_window window) { oc_window_data* windowData = oc_window_ptr_from_handle(window); if(windowData) { oc_rect frame = oc_window_get_frame_rect(window); HMONITOR monitor = MonitorFromWindow(windowData->win32.hWnd, MONITOR_DEFAULTTOPRIMARY); if(monitor) { MONITORINFO monitorInfo = { .cbSize = sizeof(MONITORINFO) }; GetMonitorInfoW(monitor, &monitorInfo); int dpiX, dpiY; GetDpiForMonitor(monitor, MDT_EFFECTIVE_DPI, &dpiX, &dpiY); f32 scaleX = dpiX / 96.; f32 scaleY = dpiY / 96.; f32 monX = monitorInfo.rcWork.left / scaleX; f32 monY = monitorInfo.rcWork.top / scaleY; f32 monW = (monitorInfo.rcWork.right - monitorInfo.rcWork.left) / scaleX; f32 monH = (monitorInfo.rcWork.bottom - monitorInfo.rcWork.top) / scaleY; frame.x = monX + 0.5 * (monW - frame.w); frame.y = monY + 0.5 * (monH - frame.h); oc_window_set_frame_rect(window, frame); } } } //-------------------------------------------------------------------------------- // clipboard functions //-------------------------------------------------------------------------------- void oc_clipboard_clear(void) { if(OpenClipboard(NULL)) { EmptyClipboard(); CloseClipboard(); } } void oc_clipboard_set_string(oc_str8 string) { if(OpenClipboard(NULL)) { EmptyClipboard(); int wideCount = MultiByteToWideChar(CP_UTF8, 0, string.ptr, string.len, 0, 0); HANDLE handle = GlobalAlloc(GMEM_MOVEABLE, (wideCount + 1) * sizeof(wchar_t)); if(handle) { char* memory = GlobalLock(handle); if(memory) { MultiByteToWideChar(CP_UTF8, 0, string.ptr, string.len, (wchar_t*)memory, wideCount); ((wchar_t*)memory)[wideCount] = '\0'; GlobalUnlock(handle); SetClipboardData(CF_UNICODETEXT, handle); } } CloseClipboard(); } } oc_str8 oc_clipboard_get_string(oc_arena* arena) { oc_str8 string = { 0 }; if(OpenClipboard(NULL)) { HANDLE handle = GetClipboardData(CF_UNICODETEXT); if(handle) { char* memory = GlobalLock(handle); if(memory) { u64 size = WideCharToMultiByte(CP_UTF8, 0, (wchar_t*)memory, -1, 0, 0, 0, 0); if(size) { string.ptr = oc_arena_push(arena, size); string.len = size - 1; WideCharToMultiByte(CP_UTF8, 0, (wchar_t*)memory, -1, string.ptr, size, 0, 0); GlobalUnlock(handle); } } } CloseClipboard(); } return (string); } oc_str8 oc_clipboard_copy_string(oc_str8 backing) { //TODO return ((oc_str8){ 0 }); } //-------------------------------------------------------------------------------- // win32 surfaces //-------------------------------------------------------------------------------- #include "graphics/graphics_surface.h" oc_vec2 oc_win32_surface_contents_scaling(oc_surface_data* surface) { u32 dpi = GetDpiForWindow(surface->layer.hWnd); oc_vec2 contentsScaling = (oc_vec2){ (float)dpi / 96., (float)dpi / 96. }; return (contentsScaling); } oc_vec2 oc_win32_surface_get_size(oc_surface_data* surface) { oc_vec2 size = { 0 }; RECT rect; if(GetClientRect(surface->layer.hWnd, &rect)) { u32 dpi = GetDpiForWindow(surface->layer.hWnd); f32 scale = (float)dpi / 96.; size = (oc_vec2){ (rect.right - rect.left) / scale, (rect.bottom - rect.top) / scale }; } return (size); } bool oc_win32_surface_get_hidden(oc_surface_data* surface) { bool hidden = !IsWindowVisible(surface->layer.hWnd); return (hidden); } void oc_win32_surface_set_hidden(oc_surface_data* surface, bool hidden) { ShowWindow(surface->layer.hWnd, hidden ? SW_HIDE : SW_NORMAL); } void* oc_win32_surface_native_layer(oc_surface_data* surface) { return ((void*)surface->layer.hWnd); } oc_surface_id oc_win32_surface_remote_id(oc_surface_data* surface) { return ((oc_surface_id)surface->layer.hWnd); } void oc_win32_surface_host_connect(oc_surface_data* surface, oc_surface_id remoteID) { HWND dstWnd = surface->layer.hWnd; HWND srcWnd = (HWND)remoteID; RECT dstRect; GetClientRect(dstWnd, &dstRect); SetParent(srcWnd, dstWnd); ShowWindow(srcWnd, SW_NORMAL); SetWindowPos(srcWnd, HWND_TOP, 0, 0, dstRect.right - dstRect.left, dstRect.bottom - dstRect.top, SWP_NOACTIVATE | SWP_NOZORDER); } void oc_surface_cleanup(oc_surface_data* surface) { oc_list_remove(&surface->layer.parent->win32.layers, &surface->layer.listElt); DestroyWindow(surface->layer.hWnd); } LRESULT LayerWinProc(HWND windowHandle, UINT message, WPARAM wParam, LPARAM lParam) { if(message == WM_NCHITTEST) { return (HTTRANSPARENT); } else { return (DefWindowProc(windowHandle, message, wParam, lParam)); } } void oc_surface_init_for_window(oc_surface_data* surface, oc_window_data* window) { surface->contentsScaling = oc_win32_surface_contents_scaling; surface->getSize = oc_win32_surface_get_size; surface->getHidden = oc_win32_surface_get_hidden; surface->setHidden = oc_win32_surface_set_hidden; surface->nativeLayer = oc_win32_surface_native_layer; //NOTE(martin): create a child window for the surface WNDCLASS layerWindowClass = { .style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC, .lpfnWndProc = LayerWinProc, .hInstance = GetModuleHandleW(NULL), .lpszClassName = "layer_window_class", .hCursor = LoadCursor(0, IDC_ARROW) }; RegisterClass(&layerWindowClass); RECT parentRect; GetClientRect(window->win32.hWnd, &parentRect); POINT point = { 0 }; ClientToScreen(window->win32.hWnd, &point); int clientWidth = parentRect.right - parentRect.left; int clientHeight = parentRect.bottom - parentRect.top; surface->layer.hWnd = CreateWindow("layer_window_class", "layer", WS_POPUP | WS_VISIBLE, point.x, point.y, clientWidth, clientHeight, window->win32.hWnd, 0, layerWindowClass.hInstance, 0); HRGN region = CreateRectRgn(0, 0, -1, -1); DWM_BLURBEHIND bb = { 0 }; bb.dwFlags = DWM_BB_ENABLE | DWM_BB_BLURREGION; bb.hRgnBlur = region; bb.fEnable = TRUE; HRESULT res = DwmEnableBlurBehindWindow(surface->layer.hWnd, &bb); DeleteObject(region); if(res != S_OK) { oc_log_error("couldn't enable blur behind\n"); } //NOTE(reuben): Creating the child window takes focus away from the main window, but we want to keep //the focus on the main window SetFocus(window->win32.hWnd); surface->layer.parent = window; oc_list_append(&window->win32.layers, &surface->layer.listElt); } void oc_surface_init_remote(oc_surface_data* surface, u32 width, u32 height) { surface->contentsScaling = oc_win32_surface_contents_scaling; surface->getSize = oc_win32_surface_get_size; surface->getHidden = oc_win32_surface_get_hidden; surface->setHidden = oc_win32_surface_set_hidden; surface->nativeLayer = oc_win32_surface_native_layer; surface->remoteID = oc_win32_surface_remote_id; WNDCLASS layerWindowClass = { .style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC, .lpfnWndProc = DefWindowProc, .hInstance = GetModuleHandleW(NULL), .lpszClassName = "server_layer_window_class", .hCursor = LoadCursor(0, IDC_ARROW) }; RegisterClass(&layerWindowClass); //NOTE(martin): create a temporary parent window. This seems like a necessary hack, because if layer window is created as // a normal window first, and then parented to the client window, it breaks resizing the parent // window for some reason... HWND tmpParent = CreateWindow("server_layer_window_class", "layerParent", WS_OVERLAPPED, 0, 0, width, height, 0, 0, layerWindowClass.hInstance, 0); //NOTE: create the layer window surface->layer.hWnd = CreateWindowEx(WS_EX_NOACTIVATE, "server_layer_window_class", "layer", WS_CHILD, 0, 0, width, height, tmpParent, 0, layerWindowClass.hInstance, 0); //NOTE: unparent it and destroy tmp parent SetParent(surface->layer.hWnd, 0); DestroyWindow(tmpParent); } oc_surface_data* oc_win32_surface_create_host(oc_window window) { oc_surface_data* surface = 0; oc_window_data* windowData = oc_window_ptr_from_handle(window); if(windowData) { surface = oc_malloc_type(oc_surface_data); if(surface) { memset(surface, 0, sizeof(oc_surface_data)); oc_surface_init_for_window(surface, windowData); surface->api = OC_HOST; surface->hostConnect = oc_win32_surface_host_connect; } } return (surface); } //-------------------------------------------------------------------- // native open/save/alert windows //-------------------------------------------------------------------- //TODO: GetOpenFileName() doesn't seem to support selecting folders, and // requires filters which pair a "descriptive" name with an extension #define interface struct #include #include #undef interface oc_str8 oc_open_dialog(oc_arena* arena, oc_str8 title, oc_str8 defaultPath, oc_str8_list filters, bool directory) { oc_str8 res = { 0 }; HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE); if(SUCCEEDED(hr)) { IFileOpenDialog* dialog = 0; hr = CoCreateInstance(&CLSID_FileOpenDialog, NULL, CLSCTX_ALL, &IID_IFileOpenDialog, (void**)&dialog); if(SUCCEEDED(hr)) { if(directory) { FILEOPENDIALOGOPTIONS opt; dialog->lpVtbl->GetOptions(dialog, &opt); dialog->lpVtbl->SetOptions(dialog, opt | FOS_PICKFOLDERS); } if(filters.eltCount) { oc_arena_scope tmp = oc_arena_scope_begin(arena); COMDLG_FILTERSPEC* filterSpecs = oc_arena_push_array(arena, COMDLG_FILTERSPEC, filters.eltCount); int i = 0; oc_list_for(&filters.list, elt, oc_str8_elt, listElt) { oc_str8_list list = { 0 }; oc_str8_list_push(arena, &list, OC_STR8("*.")); oc_str8_list_push(arena, &list, elt->string); oc_str8 filter = oc_str8_list_join(arena, list); int filterWideSize = 1 + MultiByteToWideChar(CP_UTF8, 0, filter.ptr, filter.len, NULL, 0); filterSpecs[i].pszSpec = oc_arena_push_array(arena, wchar_t, filterWideSize); MultiByteToWideChar(CP_UTF8, 0, filter.ptr, filter.len, (LPWSTR)filterSpecs[i].pszSpec, filterWideSize); ((LPWSTR)(filterSpecs[i].pszSpec))[filterWideSize - 1] = 0; filterSpecs[i].pszName = filterSpecs[i].pszSpec; i++; } hr = dialog->lpVtbl->SetFileTypes(dialog, i, filterSpecs); oc_arena_scope_end(tmp); } if(defaultPath.len) { oc_arena_scope tmp = oc_arena_scope_begin(arena); int pathWideSize = MultiByteToWideChar(CP_UTF8, 0, defaultPath.ptr, defaultPath.len, NULL, 0); LPWSTR pathWide = oc_arena_push_array(arena, wchar_t, pathWideSize + 1); MultiByteToWideChar(CP_UTF8, 0, defaultPath.ptr, defaultPath.len, pathWide, pathWideSize); pathWide[pathWideSize] = '\0'; IShellItem* item = 0; hr = SHCreateItemFromParsingName(pathWide, NULL, &IID_IShellItem, (void**)&item); if(SUCCEEDED(hr)) { hr = dialog->lpVtbl->SetFolder(dialog, item); item->lpVtbl->Release(item); } oc_arena_scope_end(tmp); } hr = dialog->lpVtbl->Show(dialog, NULL); if(SUCCEEDED(hr)) { IShellItem* item; hr = dialog->lpVtbl->GetResult(dialog, &item); if(SUCCEEDED(hr)) { PWSTR filePath; hr = item->lpVtbl->GetDisplayName(item, SIGDN_FILESYSPATH, &filePath); if(SUCCEEDED(hr)) { int oc_utf8Size = WideCharToMultiByte(CP_UTF8, 0, filePath, -1, NULL, 0, NULL, NULL); if(oc_utf8Size > 0) { res.ptr = oc_arena_push(arena, oc_utf8Size); res.len = oc_utf8Size - 1; WideCharToMultiByte(CP_UTF8, 0, filePath, -1, res.ptr, oc_utf8Size, NULL, NULL); } CoTaskMemFree(filePath); } item->lpVtbl->Release(item); } } } } CoUninitialize(); return (res); } oc_str8 oc_save_dialog(oc_arena* arena, oc_str8 title, oc_str8 defaultPath, oc_str8_list filters) { oc_str8 res = { 0 }; HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE); if(SUCCEEDED(hr)) { IFileOpenDialog* dialog = 0; hr = CoCreateInstance(&CLSID_FileSaveDialog, NULL, CLSCTX_ALL, &IID_IFileSaveDialog, (void**)&dialog); if(SUCCEEDED(hr)) { if(filters.eltCount) { oc_arena_scope tmp = oc_arena_scope_begin(arena); COMDLG_FILTERSPEC* filterSpecs = oc_arena_push_array(arena, COMDLG_FILTERSPEC, filters.eltCount); int i = 0; oc_list_for(&filters.list, elt, oc_str8_elt, listElt) { oc_str8_list list = { 0 }; oc_str8_list_push(arena, &list, OC_STR8("*.")); oc_str8_list_push(arena, &list, elt->string); oc_str8 filter = oc_str8_list_join(arena, list); int filterWideSize = 1 + MultiByteToWideChar(CP_UTF8, 0, filter.ptr, filter.len, NULL, 0); filterSpecs[i].pszSpec = oc_arena_push_array(arena, wchar_t, filterWideSize); MultiByteToWideChar(CP_UTF8, 0, filter.ptr, filter.len, (LPWSTR)filterSpecs[i].pszSpec, filterWideSize); ((LPWSTR)(filterSpecs[i].pszSpec))[filterWideSize - 1] = 0; filterSpecs[i].pszName = filterSpecs[i].pszSpec; i++; } hr = dialog->lpVtbl->SetFileTypes(dialog, i, filterSpecs); oc_arena_scope_end(tmp); } if(defaultPath.len) { oc_arena_scope tmp = oc_arena_scope_begin(arena); int pathWideSize = MultiByteToWideChar(CP_UTF8, 0, defaultPath.ptr, defaultPath.len, NULL, 0); LPWSTR pathWide = oc_arena_push_array(arena, wchar_t, pathWideSize + 1); MultiByteToWideChar(CP_UTF8, 0, defaultPath.ptr, defaultPath.len, pathWide, pathWideSize); pathWide[pathWideSize] = '\0'; IShellItem* item = 0; hr = SHCreateItemFromParsingName(pathWide, NULL, &IID_IShellItem, (void**)&item); if(SUCCEEDED(hr)) { hr = dialog->lpVtbl->SetFolder(dialog, item); item->lpVtbl->Release(item); } oc_arena_scope_end(tmp); } hr = dialog->lpVtbl->Show(dialog, NULL); if(SUCCEEDED(hr)) { IShellItem* item; hr = dialog->lpVtbl->GetResult(dialog, &item); if(SUCCEEDED(hr)) { PWSTR filePath; hr = item->lpVtbl->GetDisplayName(item, SIGDN_FILESYSPATH, &filePath); if(SUCCEEDED(hr)) { int oc_utf8Size = WideCharToMultiByte(CP_UTF8, 0, filePath, -1, NULL, 0, NULL, NULL); if(oc_utf8Size > 0) { res.ptr = oc_arena_push(arena, oc_utf8Size); res.len = oc_utf8Size - 1; WideCharToMultiByte(CP_UTF8, 0, filePath, -1, res.ptr, oc_utf8Size, NULL, NULL); } CoTaskMemFree(filePath); } item->lpVtbl->Release(item); } } } } CoUninitialize(); return (res); } #include int oc_alert_popup(oc_str8 title, oc_str8 message, oc_str8_list options) { oc_arena_scope scratch = oc_scratch_begin(); TASKDIALOG_BUTTON* buttons = oc_arena_push_array(scratch.arena, TASKDIALOG_BUTTON, options.eltCount); int i = 0; oc_list_for(&options.list, elt, oc_str8_elt, listElt) { int textWideSize = MultiByteToWideChar(CP_UTF8, 0, elt->string.ptr, elt->string.len, NULL, 0); wchar_t* textWide = oc_arena_push_array(scratch.arena, wchar_t, textWideSize + 1); MultiByteToWideChar(CP_UTF8, 0, elt->string.ptr, elt->string.len, textWide, textWideSize); textWide[textWideSize] = '\0'; buttons[i].nButtonID = i + 1; buttons[i].pszButtonText = textWide; i++; } int titleWideSize = MultiByteToWideChar(CP_UTF8, 0, title.ptr, title.len, NULL, 0); wchar_t* titleWide = oc_arena_push_array(scratch.arena, wchar_t, titleWideSize + 1); MultiByteToWideChar(CP_UTF8, 0, title.ptr, title.len, titleWide, titleWideSize); titleWide[titleWideSize] = '\0'; int messageWideSize = MultiByteToWideChar(CP_UTF8, 0, message.ptr, message.len, NULL, 0); wchar_t* messageWide = oc_arena_push_array(scratch.arena, wchar_t, messageWideSize + 1); MultiByteToWideChar(CP_UTF8, 0, message.ptr, message.len, messageWide, messageWideSize); messageWide[messageWideSize] = '\0'; TASKDIALOGCONFIG config = { .cbSize = sizeof(TASKDIALOGCONFIG), .hwndParent = NULL, .hInstance = NULL, .dwFlags = 0, .dwCommonButtons = 0, .pszWindowTitle = titleWide, .hMainIcon = 0, .pszMainIcon = TD_WARNING_ICON, .pszMainInstruction = messageWide, .pszContent = NULL, .cButtons = options.eltCount, .pButtons = buttons, .nDefaultButton = 0, }; int button = -1; HRESULT hRes = TaskDialogIndirect(&config, &button, NULL, NULL); if(hRes == S_OK) { if(button == IDCANCEL) { button = -1; } else { button--; } } oc_scratch_end(scratch); return (button); }