const std = @import("std"); const w = @import("../win32.zig"); const mach = @import("../main.zig"); const Core = @import("../Core.zig"); const InputState = @import("InputState.zig"); const gpu = mach.gpu; const InitOptions = Core.InitOptions; const Event = Core.Event; const KeyEvent = Core.KeyEvent; const MouseButtonEvent = Core.MouseButtonEvent; const MouseButton = Core.MouseButton; const Size = Core.Size; const DisplayMode = Core.DisplayMode; const CursorShape = Core.CursorShape; const VSyncMode = Core.VSyncMode; const CursorMode = Core.CursorMode; const Position = Core.Position; const Key = Core.Key; const KeyMods = Core.KeyMods; const Joystick = Core.Joystick; const EventQueue = std.fifo.LinearFifo(Event, .Dynamic); const Win32 = @This(); // -------------------------- // Module state // -------------------------- allocator: std.mem.Allocator, core: *Core, // Core platform interface surface_descriptor: gpu.Surface.Descriptor, display_mode: DisplayMode, vsync_mode: VSyncMode, cursor_mode: CursorMode, cursor_shape: CursorShape, border: bool, headless: bool, size: Size, // Internals window: w.HWND, refresh_rate: u32, surrogate: u16 = 0, dinput: *w.IDirectInput8W, saved_window_rect: w.RECT, surface_descriptor_from_hwnd: gpu.Surface.DescriptorFromWindowsHWND, events: EventQueue, input_state: InputState, oom: std.Thread.ResetEvent = .{}, // ------------------------------ // Platform interface // ------------------------------ pub fn init( self: *Win32, options: InitOptions, ) !void { self.allocator = options.allocator; self.core = @fieldParentPtr("platform", self); self.events = EventQueue.init(self.allocator); self.size = options.size; self.input_state = .{}; self.saved_window_rect = .{ .top = 0, .left = 0, .right = 0, .bottom = 0 }; const hInstance = w.GetModuleHandleW(null); const class_name = w.L("mach"); const class = std.mem.zeroInit(w.WNDCLASSW, .{ .style = w.CS_OWNDC, .lpfnWndProc = wndProc, .hInstance = hInstance, .hIcon = w.LoadIconW(null, @as([*:0]align(1) const u16, @ptrFromInt(@as(u32, w.IDI_APPLICATION)))), .hCursor = w.LoadCursorW(null, @as([*:0]align(1) const u16, @ptrFromInt(@as(u32, w.IDC_ARROW)))), .lpszClassName = class_name, }); if (w.RegisterClassW(&class) == 0) return error.Unexpected; const title = try std.unicode.utf8ToUtf16LeAllocZ(self.allocator, options.title); defer self.allocator.free(title); var request_window_width: i32 = @bitCast(self.size.width); var request_window_height: i32 = @bitCast(self.size.height); const window_ex_style: w.WINDOW_EX_STYLE = .{ .APPWINDOW = 1 }; const window_style: w.WINDOW_STYLE = if (options.border) w.WS_OVERLAPPEDWINDOW else w.WS_POPUPWINDOW; // w.WINDOW_STYLE{.POPUP = 1}; // TODO (win32): should border == false mean borderless display_mode? var rect: w.RECT = .{ .left = 0, .top = 0, .right = request_window_width, .bottom = request_window_height }; if (w.TRUE == w.AdjustWindowRectEx(&rect, window_style, w.FALSE, window_ex_style)) { request_window_width = rect.right - rect.left; request_window_height = rect.bottom - rect.top; } const window = w.CreateWindowExW( window_ex_style, class_name, title, window_style, w.CW_USEDEFAULT, w.CW_USEDEFAULT, request_window_width, request_window_height, null, null, hInstance, null, ) orelse return error.Unexpected; self.window = window; var dinput: ?*w.IDirectInput8W = undefined; const ptr: ?*?*anyopaque = @ptrCast(&dinput); if (w.DirectInput8Create(hInstance, w.DIRECTINPUT_VERSION, w.IID_IDirectInput8W, ptr, null) != w.DI_OK) { return error.Unexpected; } self.dinput = dinput.?; self.surface_descriptor_from_hwnd = .{ .hinstance = std.os.windows.kernel32.GetModuleHandleW(null).?, .hwnd = window, }; self.surface_descriptor = .{ .next_in_chain = .{ .from_windows_hwnd = &self.surface_descriptor_from_hwnd, } }; self.border = options.border; self.headless = options.headless; self.refresh_rate = 60; // TODO (win32) get monitor refresh rate self.vsync_mode = .triple; _ = w.SetWindowLongPtrW(window, w.GWLP_USERDATA, @bitCast(@intFromPtr(self))); if (!options.headless) { setDisplayMode(self, options.display_mode); } self.size = getClientRect(self); _ = w.GetWindowRect(self.window, &self.saved_window_rect); } pub fn deinit(self: *Win32) void { self.events.deinit(); _ = self.dinput.IUnknown_Release(); } pub fn update(self: *Win32) !void { _ = self; var msg: w.MSG = undefined; while (w.PeekMessageW(&msg, null, 0, 0, w.PM_REMOVE) != 0) { _ = w.TranslateMessage(&msg); _ = w.DispatchMessageW(&msg); } } pub const EventIterator = struct { queue: *EventQueue, pub fn next(self: *EventIterator) ?Event { return self.queue.readItem(); } }; pub fn pollEvents(self: *Win32) EventIterator { return .{ .queue = &self.events }; } pub fn setTitle(self: *Win32, title: [:0]const u8) void { const wtitle = std.unicode.utf8ToUtf16LeAllocZ(self.allocator, title) catch { self.oom.set(); return; }; defer self.allocator.free(wtitle); _ = w.SetWindowTextW(self.window, wtitle); } pub fn setDisplayMode(self: *Win32, mode: DisplayMode) void { self.display_mode = mode; switch (mode) { .windowed => { const window_style: w.WINDOW_STYLE = if (self.border) w.WS_OVERLAPPEDWINDOW else w.WS_POPUPWINDOW; const window_ex_style = w.WINDOW_EX_STYLE{ .APPWINDOW = 1 }; _ = w.SetWindowLongW(self.window, w.GWL_STYLE, @bitCast(window_style)); _ = w.SetWindowLongW(self.window, w.GWL_EXSTYLE, @bitCast(window_ex_style)); restoreWindowPosition(self); }, .fullscreen => { // TODO (win32) - change to use exclusive fullscreen using ChangeDisplaySetting _ = w.GetWindowRect(self.window, &self.saved_window_rect); const window_style = w.WINDOW_STYLE{ .POPUP = 1, .VISIBLE = 1 }; const window_ex_style = w.WINDOW_EX_STYLE{ .APPWINDOW = 1 }; _ = w.SetWindowLongW(self.window, w.GWL_STYLE, @bitCast(window_style)); _ = w.SetWindowLongW(self.window, w.GWL_EXSTYLE, @bitCast(window_ex_style)); const monitor = w.MonitorFromWindow(self.window, w.MONITOR_DEFAULTTONEAREST); var monitor_info: w.MONITORINFO = undefined; monitor_info.cbSize = @sizeOf(w.MONITORINFO); if (w.GetMonitorInfoW(monitor, &monitor_info) == w.TRUE) { _ = w.SetWindowPos(self.window, null, monitor_info.rcMonitor.left, monitor_info.rcMonitor.top, monitor_info.rcMonitor.right - monitor_info.rcMonitor.left, monitor_info.rcMonitor.bottom - monitor_info.rcMonitor.top, w.SWP_NOZORDER); } }, .borderless => { _ = w.GetWindowRect(self.window, &self.saved_window_rect); const window_style = w.WINDOW_STYLE{ .POPUP = 1, .VISIBLE = 1 }; const window_ex_style = w.WINDOW_EX_STYLE{ .APPWINDOW = 1 }; _ = w.SetWindowLongW(self.window, w.GWL_STYLE, @bitCast(window_style)); _ = w.SetWindowLongW(self.window, w.GWL_EXSTYLE, @bitCast(window_ex_style)); const monitor = w.MonitorFromWindow(self.window, w.MONITOR_DEFAULTTONEAREST); var monitor_info: w.MONITORINFO = undefined; monitor_info.cbSize = @sizeOf(w.MONITORINFO); if (w.GetMonitorInfoW(monitor, &monitor_info) == w.TRUE) { _ = w.SetWindowPos(self.window, null, monitor_info.rcMonitor.left, monitor_info.rcMonitor.top, monitor_info.rcMonitor.right - monitor_info.rcMonitor.left, monitor_info.rcMonitor.bottom - monitor_info.rcMonitor.top, w.SWP_NOZORDER); } }, } } pub fn setBorder(self: *Win32, value: bool) void { const overlappedwindow: i32 = @bitCast(w.WS_OVERLAPPEDWINDOW); const popupwindow: i32 = @bitCast(w.WS_POPUPWINDOW); _ = w.SetWindowLongW(self.window, w.GWL_STYLE, if (value) overlappedwindow else popupwindow); self.border = value; } pub fn setHeadless(self: *Win32, value: bool) void { _ = w.ShowWindow(self.window, if (value) w.SW_HIDE else w.SW_SHOW); self.headless = value; } pub fn setVSync(self: *Win32, mode: VSyncMode) void { self.vsync_mode = mode; } pub fn setSize(self: *Win32, value: Size) void { // TODO (win32) - use AdjustClientRect to get correct client rect. _ = w.SetWindowPos(self.window, null, 0, 0, @as(i32, @intCast(value.width)), @as(i32, @intCast(value.height)), w.SET_WINDOW_POS_FLAGS{ .NOMOVE = 1, .NOZORDER = 1, .NOACTIVATE = 1 }); self.size = value; } pub fn setCursorMode(self: *Win32, mode: CursorMode) void { switch (mode) { .normal => while (w.ShowCursor(w.TRUE) < 0) {}, .hidden => while (w.ShowCursor(w.FALSE) >= 0) {}, .disabled => {}, } self.cursor_mode = mode; } pub fn setCursorShape(self: *Win32, shape: CursorShape) void { const name: i32 = switch (shape) { .arrow => w.IDC_ARROW, .ibeam => w.IDC_IBEAM, .crosshair => w.IDC_CROSS, .pointing_hand => w.IDC_HAND, .resize_ew => w.IDC_SIZEWE, .resize_ns => w.IDC_SIZENS, .resize_nwse => w.IDC_SIZENWSE, .resize_nesw => w.IDC_SIZENESW, .resize_all => w.IDC_SIZEALL, .not_allowed => w.IDC_NO, }; _ = w.SetCursor(w.LoadCursorW(null, @ptrFromInt(@as(usize, @intCast(name))))); self.cursor_shape = shape; } pub fn keyPressed(self: *Win32, key: Key) bool { return self.input_state.isKeyPressed(key); } pub fn keyReleased(self: *Win32, key: Key) bool { return self.input_state.isKeyReleased(key); } pub fn mousePressed(self: *Win32, button: MouseButton) bool { return self.input_state.isMouseButtonPressed(button); } pub fn mouseReleased(self: *Win32, button: MouseButton) bool { return self.input_state.isMouseButtonReleased(button); } pub fn mousePosition(self: *Win32) Position { return self.input_state.mouse_position; } pub fn joystickPresent(_: *Win32, _: Joystick) bool { @panic("NOT IMPLEMENTED"); } pub fn joystickName(_: *Win32, _: Joystick) ?[:0]const u8 { @panic("NOT IMPLEMENTED"); } pub fn joystickButtons(_: *Win32, _: Joystick) ?[]const bool { @panic("NOT IMPLEMENTED"); } // May be called from any thread. pub fn joystickAxes(_: *Win32, _: Joystick) ?[]const f32 { @panic("NOT IMPLEMENTED"); } pub fn nativeWindowWin32(self: *Win32) w.HWND { return self.window; } // ----------------------------- // Internal functions // ----------------------------- fn getClientRect(self: *Win32) Size { var rect: w.RECT = undefined; _ = w.GetClientRect(self.window, &rect); const width: u32 = @intCast(rect.right - rect.left); const height: u32 = @intCast(rect.bottom - rect.top); return .{ .width = width, .height = height }; } fn restoreWindowPosition(self: *Win32) void { if (self.saved_window_rect.right - self.saved_window_rect.left == 0) { _ = w.ShowWindow(self.window, w.SW_RESTORE); } else { _ = w.SetWindowPos(self.window, null, self.saved_window_rect.left, self.saved_window_rect.top, self.saved_window_rect.right - self.saved_window_rect.left, self.saved_window_rect.bottom - self.saved_window_rect.top, w.SWP_SHOWWINDOW); } } pub fn outOfMemory(self: *Win32) bool { if (self.oom.isSet()) { self.oom.reset(); return true; } return false; } fn pushEvent(self: *Win32, event: Event) void { self.events.writeItem(event) catch self.oom.set(); } fn getKeyboardModifiers() mach.Core.KeyMods { return .{ .shift = w.GetKeyState(@as(i32, @intFromEnum(w.VK_SHIFT))) < 0, //& 0x8000 == 0x8000, .control = w.GetKeyState(@as(i32, @intFromEnum(w.VK_CONTROL))) < 0, // & 0x8000 == 0x8000, .alt = w.GetKeyState(@as(i32, @intFromEnum(w.VK_MENU))) < 0, // & 0x8000 == 0x8000, .super = (w.GetKeyState(@as(i32, @intFromEnum(w.VK_LWIN)))) < 0 // & 0x8000 == 0x8000) or (w.GetKeyState(@as(i32, @intFromEnum(w.VK_RWIN)))) < 0, // & 0x8000 == 0x8000), .caps_lock = w.GetKeyState(@as(i32, @intFromEnum(w.VK_CAPITAL))) & 1 == 1, .num_lock = w.GetKeyState(@as(i32, @intFromEnum(w.VK_NUMLOCK))) & 1 == 1, }; } fn wndProc(wnd: w.HWND, msg: u32, wParam: w.WPARAM, lParam: w.LPARAM) callconv(w.WINAPI) w.LRESULT { const self = blk: { const userdata: usize = @bitCast(w.GetWindowLongPtrW(wnd, w.GWLP_USERDATA)); const ptr: ?*Win32 = @ptrFromInt(userdata); break :blk ptr orelse return w.DefWindowProcW(wnd, msg, wParam, lParam); }; switch (msg) { w.WM_CLOSE => { self.pushEvent(.close); return 0; }, w.WM_SIZE => { const width: u32 = @as(u32, @intCast(lParam & 0xFFFF)); const height: u32 = @as(u32, @intCast((lParam >> 16) & 0xFFFF)); self.size = .{ .width = width, .height = height }; // TODO (win32): only send resize event when sizing is done. // the main mach loops does not run while resizing. // Which means if events are pushed here they will // queue up until resize is done. self.core.swap_chain_update.set(); return 0; }, w.WM_KEYDOWN, w.WM_KEYUP, w.WM_SYSKEYDOWN, w.WM_SYSKEYUP => { const vkey: w.VIRTUAL_KEY = @enumFromInt(wParam); if (vkey == w.VK_PROCESSKEY) return 0; if (msg == w.WM_SYSKEYDOWN and vkey == w.VK_F4) { self.pushEvent(.close); return 0; } const flags = lParam >> 16; const scancode: u9 = @intCast(flags & 0x1FF); if (scancode == 0x1D) { // right alt sends left control first var next: w.MSG = undefined; const time = w.GetMessageTime(); if (w.PeekMessageW(&next, self.window, 0, 0, w.PM_NOREMOVE) != 0 and next.time == time and (next.message == msg or (msg == w.WM_SYSKEYDOWN and next.message == w.WM_KEYUP)) and ((next.lParam >> 16) & 0x1FF) == 0x138) { return 0; } } const mods = getKeyboardModifiers(); const key = keyFromScancode(scancode); if (msg == w.WM_KEYDOWN or msg == w.WM_SYSKEYDOWN) { if (flags & w.KF_REPEAT == 0) { self.pushEvent(.{ .key_press = .{ .key = key, .mods = mods } }); self.input_state.keys.setValue(@intFromEnum(key), true); } else { self.pushEvent(.{ .key_repeat = .{ .key = key, .mods = mods } }); } } else { self.pushEvent(.{ .key_release = .{ .key = key, .mods = mods } }); self.input_state.keys.setValue(@intFromEnum(key), false); } return 0; }, w.WM_CHAR => { const char: u16 = @truncate(wParam); var chars: []const u16 = undefined; if (self.surrogate != 0) { chars = &.{ self.surrogate, char }; self.surrogate = 0; } else if (std.unicode.utf16IsHighSurrogate(char)) { self.surrogate = char; return 0; } else { chars = &.{char}; } var iter = std.unicode.Utf16LeIterator.init(chars); if (iter.nextCodepoint()) |codepoint| { self.pushEvent(.{ .char_input = .{ .codepoint = codepoint.? } }); } else |err| { err catch {}; } return 0; }, w.WM_LBUTTONDOWN, w.WM_LBUTTONUP, w.WM_RBUTTONDOWN, w.WM_RBUTTONUP, w.WM_MBUTTONDOWN, w.WM_MBUTTONUP, w.WM_XBUTTONDOWN, w.WM_XBUTTONUP, => { const mods = getKeyboardModifiers(); const x: f64 = @floatFromInt(@as(i16, @truncate(lParam & 0xFFFF))); const y: f64 = @floatFromInt(@as(i16, @truncate((lParam >> 16) & 0xFFFF))); const xbutton: u32 = @truncate(wParam >> 16); const button: MouseButton = switch (msg) { w.WM_LBUTTONDOWN, w.WM_LBUTTONUP => .left, w.WM_RBUTTONDOWN, w.WM_RBUTTONUP => .right, w.WM_MBUTTONDOWN, w.WM_MBUTTONUP => .middle, else => if (xbutton == @as(u32, @bitCast(w.XBUTTON1))) .four else .five, }; switch (msg) { w.WM_LBUTTONDOWN, w.WM_MBUTTONDOWN, w.WM_RBUTTONDOWN, w.WM_XBUTTONDOWN, => { self.pushEvent(.{ .mouse_press = .{ .button = button, .mods = mods, .pos = .{ .x = x, .y = y } } }); self.input_state.mouse_buttons.setValue(@intFromEnum(button), true); }, else => { self.pushEvent(.{ .mouse_release = .{ .button = button, .mods = mods, .pos = .{ .x = x, .y = y } } }); self.input_state.mouse_buttons.setValue(@intFromEnum(button), false); }, } return if (msg == w.WM_XBUTTONDOWN or msg == w.WM_XBUTTONUP) w.TRUE else 0; }, w.WM_MOUSEMOVE => { const x: f64 = @floatFromInt(@as(i16, @truncate(lParam & 0xFFFF))); const y: f64 = @floatFromInt(@as(i16, @truncate((lParam >> 16) & 0xFFFF))); self.pushEvent(.{ .mouse_motion = .{ .pos = .{ .x = x, .y = y, }, }, }); self.input_state.mouse_position = .{ .x = x, .y = y }; return 0; }, w.WM_MOUSEWHEEL => { const WHEEL_DELTA = 120.0; const wheel_high_word: u16 = @truncate((wParam >> 16) & 0xffff); const delta_y: f32 = @as(f32, @floatFromInt(@as(i16, @bitCast(wheel_high_word)))) / WHEEL_DELTA; self.pushEvent(.{ .mouse_scroll = .{ .xoffset = 0, .yoffset = delta_y, }, }); return 0; }, w.WM_SETFOCUS => { self.pushEvent(.{ .focus_gained = {} }); return 0; }, w.WM_KILLFOCUS => { self.pushEvent(.{ .focus_lost = {} }); // Clear input state when focus is lost to avoid "stuck" button when focus is regained. self.input_state = .{}; return 0; }, else => return w.DefWindowProcW(wnd, msg, wParam, lParam), } } fn keyFromScancode(scancode: u9) Key { comptime var table: [0x15D]Key = undefined; comptime for (&table, 1..) |*ptr, i| { ptr.* = switch (i) { 0x1 => .escape, 0x2 => .one, 0x3 => .two, 0x4 => .three, 0x5 => .four, 0x6 => .five, 0x7 => .six, 0x8 => .seven, 0x9 => .eight, 0xA => .nine, 0xB => .zero, 0xC => .minus, 0xD => .equal, 0xE => .backspace, 0xF => .tab, 0x10 => .q, 0x11 => .w, 0x12 => .e, 0x13 => .r, 0x14 => .t, 0x15 => .y, 0x16 => .u, 0x17 => .i, 0x18 => .o, 0x19 => .p, 0x1A => .left_bracket, 0x1B => .right_bracket, 0x1C => .enter, 0x1D => .left_control, 0x1E => .a, 0x1F => .s, 0x20 => .d, 0x21 => .f, 0x22 => .g, 0x23 => .h, 0x24 => .j, 0x25 => .k, 0x26 => .l, 0x27 => .semicolon, 0x28 => .apostrophe, 0x29 => .grave, 0x2A => .left_shift, 0x2B => .backslash, 0x2C => .z, 0x2D => .x, 0x2E => .c, 0x2F => .v, 0x30 => .b, 0x31 => .n, 0x32 => .m, 0x33 => .comma, 0x34 => .period, 0x35 => .slash, 0x36 => .right_shift, 0x37 => .kp_multiply, 0x38 => .left_alt, 0x39 => .space, 0x3A => .caps_lock, 0x3B => .f1, 0x3C => .f2, 0x3D => .f3, 0x3E => .f4, 0x3F => .f5, 0x40 => .f6, 0x41 => .f7, 0x42 => .f8, 0x43 => .f9, 0x44 => .f10, 0x45 => .pause, 0x46 => .scroll_lock, 0x47 => .kp_7, 0x48 => .kp_8, 0x49 => .kp_9, 0x4A => .kp_subtract, 0x4B => .kp_4, 0x4C => .kp_5, 0x4D => .kp_6, 0x4E => .kp_add, 0x4F => .kp_1, 0x50 => .kp_2, 0x51 => .kp_3, 0x52 => .kp_0, 0x53 => .kp_decimal, 0x54 => .print, // sysrq 0x56 => .iso_backslash, //0x56 => .europe2, 0x57 => .f11, 0x58 => .f12, 0x59 => .kp_equal, 0x5B => .left_super, // sent by touchpad gestures //0x5C => .international6, 0x64 => .f13, 0x65 => .f14, 0x66 => .f15, 0x67 => .f16, 0x68 => .f17, 0x69 => .f18, 0x6A => .f19, 0x6B => .f20, 0x6C => .f21, 0x6D => .f22, 0x6E => .f23, //0x70 => .international2, //0x73 => .international1, //0x76 => .lang5, 0x73 => .international1, 0x76 => .f24, //0x77 => .lang4, //0x78 => .lang3, //0x79 => .international4, //0x7B => .international5, //0x7D => .international3, 0x7E => .kp_comma, //0xF1 => .lang2, //0xF2 => .lang1, 0x11C => .kp_enter, 0x11D => .right_control, 0x135 => .kp_divide, 0x136 => .right_shift, // sent by IME 0x137 => .print, 0x138 => .right_alt, 0x145 => .num_lock, 0x146 => .pause, 0x147 => .home, 0x148 => .up, 0x149 => .page_up, 0x14B => .left, 0x14D => .right, 0x14F => .end, 0x150 => .down, 0x151 => .page_down, 0x152 => .insert, 0x153 => .delete, 0x15B => .left_super, 0x15C => .right_super, 0x15D => .menu, else => .unknown, }; }; return if (scancode > 0 and scancode <= table.len) table[scancode - 1] else .unknown; } // TODO (win32) Implement consistent error handling when interfacing with the Windows API. // TODO (win32) Support High DPI awareness // TODO (win32) Consider to add support for mouse capture // TODO (win32) Change to using WM_INPUT for mouse movement.