WND_W32_SharedState WND_W32_shared_state = Zi; //////////////////////////////////////////////////////////// //~ @hookimpl Bootstrap void WND_Bootstrap(void) { WND_W32_SharedState *g = &WND_W32_shared_state; //- Initialize btn table { ZeroFixedArray(g->vk_to_button); for (u32 i = 'A', j = Button_A; i <= 'Z'; ++i, ++j) { g->vk_to_button[i] = (Button)j; } for (u32 i = '0', j = Button_0; i <= '9'; ++i, ++j) { g->vk_to_button[i] = (Button)j; } for (u32 i = VK_F1, j = Button_F1; i <= VK_F24; ++i, ++j) { g->vk_to_button[i] = (Button)j; } g->vk_to_button[VK_ESCAPE] = Button_Escape; g->vk_to_button[VK_OEM_3] = Button_GraveAccent; g->vk_to_button[VK_OEM_MINUS] = Button_Minus; g->vk_to_button[VK_OEM_PLUS] = Button_Equal; g->vk_to_button[VK_BACK] = Button_Backspace; g->vk_to_button[VK_TAB] = Button_Tab; g->vk_to_button[VK_SPACE] = Button_Space; g->vk_to_button[VK_RETURN] = Button_Enter; g->vk_to_button[VK_CONTROL] = Button_Ctrl; g->vk_to_button[VK_SHIFT] = Button_Shift; g->vk_to_button[VK_MENU] = Button_Alt; g->vk_to_button[VK_UP] = Button_Up; g->vk_to_button[VK_LEFT] = Button_Left; g->vk_to_button[VK_DOWN] = Button_Down; g->vk_to_button[VK_RIGHT] = Button_Right; g->vk_to_button[VK_DELETE] = Button_Delete; g->vk_to_button[VK_PRIOR] = Button_PageUp; g->vk_to_button[VK_NEXT] = Button_PageDown; g->vk_to_button[VK_HOME] = Button_Home; g->vk_to_button[VK_END] = Button_End; g->vk_to_button[VK_OEM_2] = Button_ForwardSlash; g->vk_to_button[VK_OEM_PERIOD] = Button_Period; g->vk_to_button[VK_OEM_COMMA] = Button_Comma; g->vk_to_button[VK_OEM_7] = Button_Quote; g->vk_to_button[VK_OEM_4] = Button_LeftBracket; g->vk_to_button[VK_OEM_6] = Button_RightBracket; g->vk_to_button[VK_INSERT] = Button_Insert; g->vk_to_button[VK_OEM_1] = Button_Semicolon; } //- Initialize cursors { HCURSOR arrow = LoadCursor(0, IDC_ARROW); for (u64 cursor_idx = 0; cursor_idx < countof(g->cursors); ++cursor_idx) { g->cursors[cursor_idx] = arrow; } g->cursors[WND_CursorKind_Text] = LoadCursor(0, IDC_IBEAM); g->cursors[WND_CursorKind_No] = LoadCursor(0, IDC_NO); g->cursors[WND_CursorKind_Hand] = LoadCursor(0, IDC_HAND); g->cursors[WND_CursorKind_Move] = LoadCursor(0, IDC_SIZEALL); g->cursors[WND_CursorKind_HorizontalResize] = LoadCursor(0, IDC_SIZEWE); g->cursors[WND_CursorKind_VerticalResize] = LoadCursor(0, IDC_SIZENS); g->cursors[WND_CursorKind_TlBrResize] = LoadCursor(0, IDC_SIZENWSE); g->cursors[WND_CursorKind_TrBlResize] = LoadCursor(0, IDC_SIZENESW); } //- Create window class { HMODULE instance = GetModuleHandle(0); /* Register the window class */ WNDCLASSEXW *wc = &g->window_class; wc->cbSize = sizeof(WNDCLASSEX); wc->lpszClassName = WND_W32_WindowClassName; wc->hCursor = LoadCursor(0, IDC_ARROW); wc->style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC; // wc->hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH); wc->lpfnWndProc = WND_W32_WindowProc; wc->hInstance = instance; /* Use first icon resource as window icon (same as explorer) */ wchar_t path[4096] = Zi; GetModuleFileNameW(instance, path, countof(path)); ExtractIconExW(path, 0, &wc->hIcon, &wc->hIconSm, 1); if (!RegisterClassExW(wc)) { Panic(Lit("Failed to register window class")); } } //- Register raw mouse input { RAWINPUTDEVICE rid = Zi; rid.usUsagePage = 0x01; /* HID_USAGE_PAGE_GENERIC */ rid.usUsage = 0x02; /* HID_USAGE_GENERIC_MOUSE */ RegisterRawInputDevices(&rid, 1, sizeof(rid)); } //- Dispatch message processor DispatchWave(Lit("Win32 msg loop"), 1, WND_W32_ProcessMessagesForever, 0); } //////////////////////////////////////////////////////////// //~ Window helpers WND_W32_Window *WND_W32_WindowFromHandle(WND_Handle handle) { return (WND_W32_Window *)handle.v; } //////////////////////////////////////////////////////////// //~ Initialization /* Win32 limitation: Window must be initialized on same thread that processes events */ void WND_W32_ProcessMessagesForever(WaveLaneCtx *lane) { WND_W32_SharedState *g = &WND_W32_shared_state; WND_W32_Window *window = &g->window; window->w2u_events_arena = AcquireArena(Gibi(64)); //- Initialize hwnd { /* * From martins (https://gist.github.com/mmozeiko/5e727f845db182d468a34d524508ad5f#file-win32_d3d11-c-L66-L70): * WS_EX_NOREDIRECTIONBITMAP flag here is needed to fix ugly bug with Windows 10 * when window is resized and DXGI swap chain uses FLIP presentation model * DO NOT use it if you choose to use non-FLIP presentation model * read about the bug here: https://stackoverflow.com/q/63096226 and here: https://stackoverflow.com/q/53000291 */ DWORD exstyle = WS_EX_APPWINDOW | WS_EX_NOREDIRECTIONBITMAP; /* TODO: Check for hwnd success */ window->hwnd = CreateWindowExW( exstyle, g->window_class.lpszClassName, L"", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, 0, 0, g->window_class.hInstance, 0 ); /* Dark mode */ BOOL dark_mode = 1; DwmSetWindowAttribute(window->hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE, (LPCVOID)&dark_mode, sizeof(dark_mode)); /* Set window as userdata */ /* FIXME: Necessary? */ SetWindowLongPtrW(window->hwnd, GWLP_USERDATA, (LONG_PTR)window); } //- Acquire swapchain { window->swapchain = G_AcquireSwapchain((u64)window->hwnd); } //- Begin processing messages Atomic32Set(&window->is_ready, 1); for (;;) { // SetFocus(window->hwnd); MSG msg = Zi; GetMessageW(&msg, 0, 0, 0); { TranslateMessage(&msg); DispatchMessageW(&msg); } } /* Destroy window */ DestroyWindow(window->hwnd); } //////////////////////////////////////////////////////////// //~ Message processing void WND_W32_PushEvent(WND_W32_Window *window, ControllerEvent event) { LockTicketMutex(&window->w2u_tm); { *PushStructNoZero(window->w2u_events_arena, ControllerEvent) = event; } UnlockTicketMutex(&window->w2u_tm); } LRESULT CALLBACK WND_W32_WindowProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) { WND_W32_SharedState *g = &WND_W32_shared_state; WND_W32_Window *window = &g->window; LRESULT result = 0; { switch(msg) { //- Default default: { result = DefWindowProcW(hwnd, msg, wparam, lparam); } break; //- Quit case WM_QUIT: case WM_CLOSE: case WM_DESTROY: { WND_W32_PushEvent(window, (ControllerEvent) { .kind = ControllerEventKind_Quit }); } break; //- Cursor kind case WM_SETCURSOR: { /* FIXME */ HCURSOR new_cursor = (HCURSOR)Atomic64Fetch(&window->new_cursor); if (new_cursor != window->active_cursor) { SetCursor(new_cursor); window->active_cursor = new_cursor; result = 1; } } break; //- Keyboard button case WM_SYSKEYUP: case WM_SYSKEYDOWN: case WM_KEYUP: case WM_KEYDOWN: { WORD vk_code = LOWORD(wparam); ControllerEvent event = Zi; if (msg == WM_KEYDOWN || msg == WM_SYSKEYDOWN) { event.kind = ControllerEventKind_ButtonDown; event.is_repeat = (lparam & 0x40000000) != 0; } else if (msg == WM_KEYUP || msg == WM_SYSKEYUP) { event.kind = ControllerEventKind_ButtonUp; } if (vk_code < countof(g->vk_to_button)) { event.button = g->vk_to_button[vk_code]; } WND_W32_PushEvent(window, event); if (msg == WM_SYSKEYUP || msg == WM_SYSKEYDOWN) { result = DefWindowProcW(hwnd, msg, wparam, lparam); } } break; //- Text case WM_SYSCHAR: case WM_CHAR: { u32 codepoint = 0; { u16 utf16_char = (u32)wparam; if (IsUtf16HighSurrogate(utf16_char)) { window->previous_utf16_high_surrogate = utf16_char; } else if (IsUtf16LowSurrogate(utf16_char)) { u16 high = window->previous_utf16_high_surrogate; u16 low = utf16_char; if (high) { u16 utf16_pair_bytes[2] = { high, low }; Utf16DecodeResult decoded = DecodeUtf16((String16) { .len = countof(utf16_pair_bytes), .text = utf16_pair_bytes }); if (decoded.advance16 == 2 && decoded.codepoint < U32Max) { codepoint = decoded.codepoint; } } window->previous_utf16_high_surrogate = 0; } else { window->previous_utf16_high_surrogate = 0; codepoint = utf16_char; } if (codepoint != 0) { if (codepoint == '\r') { codepoint = '\n'; /* Just treat all \r as newline */ } } } if ((codepoint >= 32 && codepoint != 127) || codepoint == '\t' || codepoint == '\n') { ControllerEvent event = Zi; event.kind = ControllerEventKind_Text; event.text_codepoint = codepoint; WND_W32_PushEvent(window, event); } } break; //- Mouse buttons case WM_LBUTTONUP: case WM_MBUTTONUP: case WM_RBUTTONUP: case WM_XBUTTONUP: case WM_LBUTTONDOWN: case WM_MBUTTONDOWN: case WM_RBUTTONDOWN: case WM_XBUTTONDOWN: { ControllerEvent event = Zi; b32 is_down = msg == WM_LBUTTONDOWN || msg == WM_MBUTTONDOWN || msg == WM_RBUTTONDOWN || msg == WM_XBUTTONDOWN; if (is_down) { event.kind = ControllerEventKind_ButtonDown; SetCapture(hwnd); } else { event.kind = ControllerEventKind_ButtonUp; ReleaseCapture(); } switch (msg) { case WM_LBUTTONUP: case WM_LBUTTONDOWN: event.button = Button_M1; break; case WM_RBUTTONUP: case WM_RBUTTONDOWN: event.button = Button_M2; break; case WM_MBUTTONUP: case WM_MBUTTONDOWN: event.button = Button_M3; break; case WM_XBUTTONUP: case WM_XBUTTONDOWN: { u32 wparam_xbutton = GET_XBUTTON_WPARAM(wparam); if (wparam_xbutton == XBUTTON1) { event.button = Button_M4; } else if (wparam_xbutton == XBUTTON2) { event.button = Button_M5; } } break; } if (event.button) { WND_W32_PushEvent(window, event); } } break; //- Mouse wheel case WM_MOUSEWHEEL: { int delta = GET_WHEEL_DELTA_WPARAM(wparam); i32 dir = delta >= 0 ? 1 : -1; Button btn = dir >= 0 ? Button_MWheelUp : Button_MWheelDown; for (i32 i = 0; i < (dir * delta); i += WHEEL_DELTA) { /* Send a button down & button up event simultaneously */ WND_W32_PushEvent(window, (ControllerEvent) { .kind = ControllerEventKind_ButtonDown, .button = btn }); WND_W32_PushEvent(window, (ControllerEvent) { .kind = ControllerEventKind_ButtonUp, .button = btn }); } } break; //- Cursor move case WM_MOUSEMOVE: { i32 x = GET_X_LPARAM(lparam); i32 y = GET_Y_LPARAM(lparam); ControllerEvent event = Zi; event.kind = ControllerEventKind_CursorMove; event.cursor_pos = VEC2I32(x, y); WND_W32_PushEvent(window, event); } break; //- Raw mouse move case WM_INPUT: { TempArena scratch = BeginScratchNoConflict(); { /* Read raw input buffer */ UINT buff_size; GetRawInputData((HRAWINPUT)lparam, RID_INPUT, 0, &buff_size, sizeof(RAWINPUTHEADER)); u8 *buff = PushStructs(scratch.arena, u8, buff_size); if (GetRawInputData((HRAWINPUT)lparam, RID_INPUT, buff, &buff_size, sizeof(RAWINPUTHEADER)) != buff_size) { LogErrorF("GetRawInputData did not return correct size"); break; } RAWINPUT raw = Zi; CopyBytes(&raw, buff, sizeof(RAWINPUT)); if (raw.header.dwType == RIM_TYPEMOUSE) { i32 x = raw.data.mouse.lLastX; i32 y = raw.data.mouse.lLastY; ControllerEvent event = Zi; event.kind = ControllerEventKind_MouseMove; event.mouse_delta = VEC2I32(x, y); WND_W32_PushEvent(window, event); } } EndScratch(scratch); } break; } } return result; } //////////////////////////////////////////////////////////// //~ @hookimpl Command void WND_PushCmd_(WND_Frame frame, WND_Cmd desc) { WND_W32_Window *window = WND_W32_WindowFromHandle(frame.window); WND_W32_CmdNode *n = PushStruct(window->frame_arena, WND_W32_CmdNode); n->cmd = desc; SllQueuePush(window->first_cmd, window->last_cmd, n); if (desc.kind == WND_CmdKind_Restore) { n->cmd.restore = PushString(window->frame_arena, desc.restore); } } //////////////////////////////////////////////////////////// //~ @hookimpl Frame WND_Frame WND_BeginFrame(G_Format backbuffer_format, WND_BackbufferSizeMode backbuffer_size_mode) { WND_W32_SharedState *g = &WND_W32_shared_state; WND_W32_Window *window = &g->window; WND_Frame result = Zi; while (!Atomic32Fetch(&window->is_ready)) { _mm_pause(); } HWND hwnd = window->hwnd; result.window.v = (u64)window; /* Grab monitor info */ RECT monitor_rect = Zi; { MONITORINFO monitor_info = { .cbSize = sizeof(monitor_info) }; GetMonitorInfo(MonitorFromWindow(hwnd, MONITOR_DEFAULTTOPRIMARY), &monitor_info); monitor_rect = monitor_info.rcMonitor; } result.monitor_size = VEC2I32(monitor_rect.right - monitor_rect.left, monitor_rect.bottom - monitor_rect.top); result.monitor_size.x = MaxI32(result.monitor_size.x, 1); result.monitor_size.y = MaxI32(result.monitor_size.y, 1); /* Client rect */ RECT client_rect = Zi; GetClientRect(hwnd, (LPRECT)&client_rect); /* Screen rect */ RECT screen_rect = client_rect; ClientToScreen(hwnd, (LPPOINT)&screen_rect.left); ClientToScreen(hwnd, (LPPOINT)&screen_rect.right); result.draw_size = VEC2I32(screen_rect.right - screen_rect.left, screen_rect.bottom - screen_rect.top); result.draw_size.x = MaxI32(result.draw_size.x, 1); result.draw_size.y = MaxI32(result.draw_size.y, 1); /* Prepare backbuffer */ Vec2I32 backbuffer_size = Zi; if (backbuffer_size_mode == WND_BackbufferSizeMode_MatchWindow) { backbuffer_size = result.draw_size; } else if (backbuffer_size_mode == WND_BackbufferSizeMode_MatchMonitor) { backbuffer_size = result.monitor_size; } result.backbuffer = G_PrepareBackbuffer(window->swapchain, backbuffer_format, backbuffer_size); /* Reset per-frame data */ if (!window->frame_arena) { window->frame_arena = AcquireArena(Gibi(64)); } ResetArena(window->frame_arena); window->first_cmd = 0; window->last_cmd = 0; /* Pop user input */ { LockTicketMutex(&window->w2u_tm); { ControllerEvent *src = ArenaFirst(window->w2u_events_arena, ControllerEvent); result.controller_events.count = ArenaCount(window->w2u_events_arena, ControllerEvent); result.controller_events.events = PushStructsNoZero(window->frame_arena, ControllerEvent, result.controller_events.count); CopyStructs(result.controller_events.events, src, result.controller_events.count); ResetArena(window->w2u_events_arena); } UnlockTicketMutex(&window->w2u_tm); } /* Minimized / maximized / fullscreen */ DWORD style = (DWORD)GetWindowLongPtr(hwnd, GWL_STYLE); DWORD ex_style = (DWORD)GetWindowLongPtr(hwnd, GWL_EXSTYLE); WINDOWPLACEMENT placement = { .length = sizeof(placement) }; GetWindowPlacement(hwnd, &placement); { if (placement.showCmd == SW_MAXIMIZE) { result.maximized = 1; } if (placement.showCmd == SW_MINIMIZE) { result.minimized = 1; } if ( screen_rect.left == monitor_rect.left && screen_rect.top == monitor_rect.top && screen_rect.right == monitor_rect.right && screen_rect.bottom == monitor_rect.bottom ) { result.fullscreen = 1; } } result.forced_top = window->is_forced_top; result.fullscreen = window->is_fullscreen; result.has_focus = GetForegroundWindow() == hwnd; /* Generate restore data */ { WND_W32_RestorableData restore = Zi; { restore.magic = WND_W32_RestoreMagic; restore.placement = placement; restore.style = style; restore.ex_style = ex_style; restore.is_forced_top = window->is_forced_top; restore.is_fullscreen = window->is_fullscreen; restore.fullscreen_restore_rect = window->fullscreen_restore_rect; restore.has_focus = result.has_focus; if (IsWindowArranged(hwnd)) { restore.is_snapped = 1; restore.snapped_screen_rect = screen_rect; } } result.restore = PushString(window->frame_arena, StringFromStruct(&restore)); } return result; } void WND_EndFrame(WND_Frame frame, i32 vsync) { TempArena scratch = BeginScratchNoConflict(); WND_W32_SharedState *g = &WND_W32_shared_state; WND_W32_Window *window = WND_W32_WindowFromHandle(frame.window); HWND hwnd = window->hwnd; /* Process cmds */ b32 was_restored = 0; WND_CursorKind new_cursor = (WND_CursorKind)Atomic64Fetch(&window->new_cursor); for (WND_W32_CmdNode *n = window->first_cmd; n; n = n->next) { WND_Cmd cmd = n->cmd; switch(cmd.kind) { default: break; //- Minimize case WND_CmdKind_SetMinimized: { ShowWindow(hwnd, SW_MINIMIZE); } break; //- Maximize case WND_CmdKind_SetMaximized: { ShowWindow(hwnd, SW_MAXIMIZE); } break; //- Fullscreen case WND_CmdKind_SetFullscreen: { if (cmd.v != window->is_fullscreen) { DWORD old_style = (DWORD)GetWindowLongPtr(hwnd, GWL_STYLE); RECT old_rect = Zi; { GetClientRect(hwnd, (LPRECT)&old_rect); ClientToScreen(hwnd, (LPPOINT)&old_rect.left); ClientToScreen(hwnd, (LPPOINT)&old_rect.right); AdjustWindowRect(&old_rect, old_style, 0); } RECT new_rect = Zi; HWND new_hwnd = 0; DWORD new_style = old_style; if (cmd.v) { /* Enter fullscreen */ { MONITORINFO monitor_info = { .cbSize = sizeof(monitor_info) }; GetMonitorInfo(MonitorFromWindow(hwnd, MONITOR_DEFAULTTOPRIMARY), &monitor_info); new_rect = monitor_info.rcMonitor; } new_style &= ~WS_OVERLAPPEDWINDOW; new_style |= WS_POPUP; window->fullscreen_restore_rect = old_rect; new_hwnd = HWND_TOP; } else { /* Exit fullscreen */ new_rect = window->fullscreen_restore_rect; new_style &= ~WS_POPUP; new_style |= WS_OVERLAPPEDWINDOW; new_hwnd = window->is_forced_top ? HWND_TOPMOST : HWND_NOTOPMOST; } window->is_fullscreen = cmd.v; SetWindowLongPtr(hwnd, GWL_STYLE, new_style); SetWindowPos(hwnd, new_hwnd, new_rect.left, new_rect.top, new_rect.right - new_rect.left, new_rect.bottom - new_rect.top, 0); } } break; //- Force top case WND_CmdKind_SetForcedTop: { window->is_forced_top = cmd.v; SetWindowPos(hwnd, window->is_forced_top ? HWND_TOPMOST : HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE); } break; //- Cursor case WND_CmdKind_SetCursor: { new_cursor = cmd.cursor; } break; //- Restore case WND_CmdKind_Restore: { /* FIXME: Cap bounds */ String restore_str = cmd.restore; if (restore_str.len == sizeof(WND_W32_RestorableData)) { WND_W32_RestorableData *restore = PushStructNoZero(scratch.arena, WND_W32_RestorableData); CopyBytes(restore, restore_str.text, restore_str.len); if (restore->magic == WND_W32_RestoreMagic) { WINDOWPLACEMENT placement = restore->placement; was_restored = 1; SetWindowPlacement(hwnd, &placement); SetWindowLongPtr(hwnd, GWL_STYLE, restore->style); SetWindowLongPtr(hwnd, GWL_EXSTYLE, restore->ex_style); if (restore->is_fullscreen) { MONITORINFO monitor_info = { .cbSize = sizeof(monitor_info) }; GetMonitorInfo(MonitorFromWindow(hwnd, MONITOR_DEFAULTTOPRIMARY), &monitor_info); RECT rect = monitor_info.rcMonitor; window->is_fullscreen = 1; window->fullscreen_restore_rect = restore->fullscreen_restore_rect; SetWindowPos(hwnd, HWND_TOP, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, 0); } if (restore->is_forced_top) { window->is_forced_top = 1; SetWindowPos(hwnd, window->is_forced_top ? HWND_TOPMOST : HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE); } if (restore->is_snapped) { RECT rect = restore->snapped_screen_rect; HWND pos_hwnd = window->is_forced_top ? HWND_TOPMOST : HWND_NOTOPMOST; SetWindowPos(hwnd, pos_hwnd, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, 0); } if (restore->has_focus) { ShowWindow(hwnd, SW_SHOW); SetForegroundWindow(hwnd); BringWindowToTop(hwnd); } } } } break; } } /* Bring window to front on first frame */ if (!was_restored && window->frame_gen == 0) { ShowWindow(hwnd, SW_SHOW); SetForegroundWindow(hwnd); BringWindowToTop(hwnd); } /* Set cursor */ Atomic64Set(&window->new_cursor, (u64)g->cursors[new_cursor]); /* Commit backbuffer */ G_CommitBackbuffer(frame.backbuffer, vsync); ++window->frame_gen; EndScratch(scratch); }