const std = @import("std"); const c = @cImport({ @cInclude("ResourceNames.h"); }); const win32 = @import("win32").everything; const ddui = @import("ddui"); const cbor = @import("cbor"); const thespian = @import("thespian"); const vaxis = @import("vaxis"); const RGB = @import("color").RGB; const input = @import("input"); const windowmsg = @import("windowmsg.zig"); const HResultError = ddui.HResultError; const WM_APP_EXIT = win32.WM_APP + 1; const WM_APP_SET_BACKGROUND = win32.WM_APP + 2; const WM_APP_EXIT_RESULT = 0x45feaa11; const WM_APP_SET_BACKGROUND_RESULT = 0x369a26cd; pub const DropWriter = struct { pub const WriteError = error{}; pub const Writer = std.io.Writer(DropWriter, WriteError, write); pub fn writer(self: DropWriter) Writer { return .{ .context = self }; } pub fn write(self: DropWriter, bytes: []const u8) WriteError!usize { _ = self; return bytes.len; } }; fn oom(e: error{OutOfMemory}) noreturn { @panic(@errorName(e)); } fn onexit(e: error{Exit}) void { switch (e) { error.Exit => {}, } } const global = struct { var init_called: bool = false; var start_called: bool = false; var icons: Icons = undefined; var dwrite_factory: *win32.IDWriteFactory = undefined; var d2d_factory: *win32.ID2D1Factory = undefined; const shared_screen = struct { var mutex: std.Thread.Mutex = .{}; // only access arena/obj while the mutex is locked var arena: std.heap.ArenaAllocator = std.heap.ArenaAllocator.init(std.heap.page_allocator); var obj: vaxis.Screen = .{}; }; }; const window_style_ex = win32.WINDOW_EX_STYLE{ //.ACCEPTFILES = 1, }; const window_style = win32.WS_OVERLAPPEDWINDOW; pub fn init() void { std.debug.assert(!global.init_called); global.init_called = true; { const hr = win32.DWriteCreateFactory( win32.DWRITE_FACTORY_TYPE_SHARED, win32.IID_IDWriteFactory, @ptrCast(&global.dwrite_factory), ); if (hr < 0) fatalHr("DWriteCreateFactory", hr); } { var err: HResultError = undefined; global.d2d_factory = ddui.createFactory( .SINGLE_THREADED, .{}, &err, ) catch std.debug.panic("{}", .{err}); } } const Icons = struct { small: win32.HICON, large: win32.HICON, }; fn getIcons(dpi: XY(u32)) Icons { const small_x = win32.GetSystemMetricsForDpi(@intFromEnum(win32.SM_CXSMICON), dpi.x); const small_y = win32.GetSystemMetricsForDpi(@intFromEnum(win32.SM_CYSMICON), dpi.y); const large_x = win32.GetSystemMetricsForDpi(@intFromEnum(win32.SM_CXICON), dpi.x); const large_y = win32.GetSystemMetricsForDpi(@intFromEnum(win32.SM_CYICON), dpi.y); std.log.debug("icons small={}x{} large={}x{} at dpi {}x{}", .{ small_x, small_y, large_x, large_y, dpi.x, dpi.y, }); const small = win32.LoadImageW( win32.GetModuleHandleW(null), @ptrFromInt(c.ID_ICON_FLOW), .ICON, small_x, small_y, win32.LR_SHARED, ) orelse fatalWin32("LoadImage for small icon", win32.GetLastError()); const large = win32.LoadImageW( win32.GetModuleHandleW(null), @ptrFromInt(c.ID_ICON_FLOW), .ICON, large_x, large_y, win32.LR_SHARED, ) orelse fatalWin32("LoadImage for large icon", win32.GetLastError()); return .{ .small = @ptrCast(small), .large = @ptrCast(large) }; } fn d2dColorFromVAxis(color: vaxis.Cell.Color) win32.D2D_COLOR_F { return switch (color) { .default => .{ .r = 0, .g = 0, .b = 0, .a = 0 }, .index => |idx| blk: { const rgb = RGB.from_u24(xterm_colors[idx]); break :blk .{ .r = @as(f32, @floatFromInt(rgb.r)) / 255.0, .g = @as(f32, @floatFromInt(rgb.g)) / 255.0, .b = @as(f32, @floatFromInt(rgb.b)) / 255.0, .a = 1, }; }, .rgb => |rgb| .{ .r = @as(f32, @floatFromInt(rgb[0])) / 255.0, .g = @as(f32, @floatFromInt(rgb[1])) / 255.0, .b = @as(f32, @floatFromInt(rgb[2])) / 255.0, .a = 1, }, }; } const Dpi = struct { value: u32, pub fn eql(self: Dpi, other: Dpi) bool { return self.value == other.value; } }; fn createTextFormatEditor(dpi: Dpi) *win32.IDWriteTextFormat { var err: HResultError = undefined; return ddui.createTextFormat(global.dwrite_factory, &err, .{ .size = win32.scaleDpi(f32, 14, dpi.value), .family_name = win32.L("Cascadia Code"), }) catch std.debug.panic("{s} failed, hresult=0x{x}", .{ err.context, err.hr }); } const D2d = struct { target: *win32.ID2D1HwndRenderTarget, brush: *win32.ID2D1SolidColorBrush, pub fn init(hwnd: win32.HWND, err: *HResultError) error{HResult}!D2d { var target: *win32.ID2D1HwndRenderTarget = undefined; const target_props = win32.D2D1_RENDER_TARGET_PROPERTIES{ .type = .DEFAULT, .pixelFormat = .{ .format = .B8G8R8A8_UNORM, .alphaMode = .PREMULTIPLIED, }, .dpiX = 0, .dpiY = 0, .usage = .{}, .minLevel = .DEFAULT, }; const hwnd_target_props = win32.D2D1_HWND_RENDER_TARGET_PROPERTIES{ .hwnd = hwnd, .pixelSize = .{ .width = 0, .height = 0 }, .presentOptions = .{}, }; { const hr = global.d2d_factory.CreateHwndRenderTarget( &target_props, &hwnd_target_props, &target, ); if (hr < 0) return err.set(hr, "CreateHwndRenderTarget"); } errdefer _ = target.IUnknown.Release(); { var dc: *win32.ID2D1DeviceContext = undefined; { const hr = target.IUnknown.QueryInterface(win32.IID_ID2D1DeviceContext, @ptrCast(&dc)); if (hr < 0) return err.set(hr, "GetDeviceContext"); } defer _ = dc.IUnknown.Release(); // just make everything DPI aware, all applications should just do this dc.SetUnitMode(win32.D2D1_UNIT_MODE_PIXELS); } var brush: *win32.ID2D1SolidColorBrush = undefined; { const color: win32.D2D_COLOR_F = .{ .r = 0, .g = 0, .b = 0, .a = 0 }; const hr = target.ID2D1RenderTarget.CreateSolidColorBrush(&color, null, &brush); if (hr < 0) return err.set(hr, "CreateSolidBrush"); } errdefer _ = brush.IUnknown.Release(); return .{ .target = target, .brush = brush, }; } pub fn deinit(self: *D2d) void { _ = self.brush.IUnknown.Release(); _ = self.target.IUnknown.Release(); } pub fn solid(self: *const D2d, color: win32.D2D_COLOR_F) *win32.ID2D1Brush { self.brush.SetColor(&color); return &self.brush.ID2D1Brush; } }; const State = struct { pid: thespian.pid, maybe_d2d: ?D2d = null, erase_bg_done: bool = false, text_format_editor: ddui.TextFormatCache(Dpi, createTextFormatEditor) = .{}, scroll_delta: isize = 0, currently_rendered_cell_size: ?XY(i32) = null, background: ?u32 = null, }; fn stateFromHwnd(hwnd: win32.HWND) *State { const addr: usize = @bitCast(win32.GetWindowLongPtrW(hwnd, @enumFromInt(0))); if (addr == 0) @panic("window is missing it's state!"); return @ptrFromInt(addr); } fn paint( d2d: *const D2d, background: RGB, screen: *const vaxis.Screen, text_format_editor: *win32.IDWriteTextFormat, cell_size: XY(i32), ) void { { const color = ddui.rgb8(background.r, background.g, background.b); d2d.target.ID2D1RenderTarget.Clear(&color); } for (0..screen.height) |y| { const row_y: i32 = cell_size.y * @as(i32, @intCast(y)); for (0..screen.width) |x| { const column_x: i32 = cell_size.x * @as(i32, @intCast(x)); const cell_index = screen.width * y + x; const cell = &screen.buf[cell_index]; const cell_rect: win32.RECT = .{ .left = column_x, .top = row_y, .right = column_x + cell_size.x, .bottom = row_y + cell_size.y, }; ddui.FillRectangle( &d2d.target.ID2D1RenderTarget, cell_rect, d2d.solid(d2dColorFromVAxis(cell.style.bg)), ); // TODO: pre-caclulate the buffer size needed, for now this should just // cause out-of-bounds access var buf_wtf16: [100]u16 = undefined; const grapheme_len = blk: { break :blk std.unicode.wtf8ToWtf16Le(&buf_wtf16, cell.char.grapheme) catch |err| switch (err) { error.InvalidWtf8 => { buf_wtf16[0] = std.unicode.replacement_character; break :blk 1; }, }; }; const grapheme = buf_wtf16[0..grapheme_len]; if (std.mem.eql(u16, grapheme, &[_]u16{' '})) continue; ddui.DrawText( &d2d.target.ID2D1RenderTarget, grapheme, text_format_editor, ddui.rectFloatFromInt(cell_rect), d2d.solid(d2dColorFromVAxis(cell.style.fg)), .{ .CLIP = 1, .ENABLE_COLOR_FONT = 1, }, .NATURAL, ); } } } const WindowPlacement = struct { dpi: XY(u32), size: XY(i32), pos: XY(i32), pub const default: WindowPlacement = .{ .dpi = .{ .x = 96, .y = 96, }, .pos = .{ .x = win32.CW_USEDEFAULT, .y = win32.CW_USEDEFAULT, }, .size = .{ .x = win32.CW_USEDEFAULT, .y = win32.CW_USEDEFAULT, }, }; }; fn calcWindowPlacement() WindowPlacement { var result = WindowPlacement.default; const monitor = win32.MonitorFromPoint( .{ .x = 0, .y = 0 }, win32.MONITOR_DEFAULTTOPRIMARY, ) orelse { std.log.warn("MonitorFromPoint failed with {}", .{win32.GetLastError().fmt()}); return result; }; result.dpi = blk: { var dpi: XY(u32) = undefined; const hr = win32.GetDpiForMonitor( monitor, win32.MDT_EFFECTIVE_DPI, &dpi.x, &dpi.y, ); if (hr < 0) { std.log.warn("GetDpiForMonitor failed, hresult=0x{x}", .{@as(u32, @bitCast(hr))}); return result; } break :blk dpi; }; std.log.debug("primary monitor dpi {}x{}", .{ result.dpi.x, result.dpi.y }); const work_rect: win32.RECT = blk: { var info: win32.MONITORINFO = undefined; info.cbSize = @sizeOf(win32.MONITORINFO); if (0 == win32.GetMonitorInfoW(monitor, &info)) { std.log.warn("GetMonitorInfo failed with {}", .{win32.GetLastError().fmt()}); return result; } break :blk info.rcWork; }; const work_size: XY(i32) = .{ .x = work_rect.right - work_rect.left, .y = work_rect.bottom - work_rect.top, }; std.log.debug( "primary monitor work topleft={},{} size={}x{}", .{ work_rect.left, work_rect.top, work_size.x, work_size.y }, ); const wanted_size: XY(i32) = .{ .x = win32.scaleDpi(i32, 800, result.dpi.x), .y = win32.scaleDpi(i32, 1200, result.dpi.y), }; result.size = .{ .x = @min(wanted_size.x, work_size.x), .y = @min(wanted_size.y, work_size.y), }; result.pos = .{ // TODO: maybe we should shift this window away from the center? .x = work_rect.left + @divTrunc(work_size.x - result.size.x, 2), .y = work_rect.top + @divTrunc(work_size.y - result.size.y, 2), }; return result; } const CreateWindowArgs = struct { allocator: std.mem.Allocator, pid: thespian.pid, }; pub fn start() !std.Thread { std.debug.assert(!global.start_called); global.start_called = true; const pid = thespian.self_pid().clone(); return try std.Thread.spawn(.{}, entry, .{pid}); } fn entry(pid: thespian.pid) !void { var arena_instance = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena_instance.deinit(); const CLASS_NAME = win32.L("Flow"); const initial_placement = calcWindowPlacement(); global.icons = getIcons(initial_placement.dpi); // we only need to register the window class once per process const wc = win32.WNDCLASSEXW{ .cbSize = @sizeOf(win32.WNDCLASSEXW), .style = .{}, .lpfnWndProc = WndProc, .cbClsExtra = 0, .cbWndExtra = @sizeOf(*State), .hInstance = win32.GetModuleHandleW(null), .hIcon = global.icons.large, .hCursor = win32.LoadCursorW(null, win32.IDC_ARROW), .hbrBackground = null, .lpszMenuName = null, .lpszClassName = CLASS_NAME, .hIconSm = global.icons.small, }; if (0 == win32.RegisterClassExW(&wc)) fatalWin32( "RegisterClass for main window", win32.GetLastError(), ); var create_args = CreateWindowArgs{ .allocator = arena_instance.allocator(), .pid = pid, }; const hwnd = win32.CreateWindowExW( window_style_ex, CLASS_NAME, // Window class win32.L("Flow"), window_style, initial_placement.pos.x, initial_placement.pos.y, initial_placement.size.x, initial_placement.size.y, null, // Parent window null, // Menu win32.GetModuleHandleW(null), @ptrCast(&create_args), ) orelse fatalWin32("CreateWindow", win32.GetLastError()); // NEVER DESTROY THE WINDOW! // This allows us to send the hwnd to other thread/parts // of the app and it will always be valid. pid.send(.{ "RDR", "WindowCreated", @intFromPtr(hwnd), }) catch |e| return onexit(e); { // TODO: maybe use DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1 if applicable // see https://stackoverflow.com/questions/57124243/winforms-dark-title-bar-on-windows-10 //int attribute = DWMWA_USE_IMMERSIVE_DARK_MODE; const dark_value: c_int = 1; const hr = win32.DwmSetWindowAttribute( hwnd, win32.DWMWA_USE_IMMERSIVE_DARK_MODE, &dark_value, @sizeOf(@TypeOf(dark_value)), ); if (hr < 0) std.log.warn( "DwmSetWindowAttribute for dark={} failed, error={}", .{ dark_value, win32.GetLastError() }, ); } if (0 == win32.UpdateWindow(hwnd)) fatalWin32("UpdateWindow", win32.GetLastError()); _ = win32.ShowWindow(hwnd, win32.SW_SHOWNORMAL); var msg: win32.MSG = undefined; while (win32.GetMessageW(&msg, null, 0, 0) != 0) { _ = win32.TranslateMessage(&msg); _ = win32.DispatchMessageW(&msg); } const exit_code = std.math.cast(u32, msg.wParam) orelse 0xffffffff; std.log.debug("gui thread exit {} ({})", .{ exit_code, msg.wParam }); pid.send(.{"quit"}) catch |e| onexit(e); } pub fn stop(hwnd: win32.HWND) void { std.debug.assert(WM_APP_EXIT_RESULT == win32.SendMessageW(hwnd, WM_APP_EXIT, 0, 0)); } pub fn set_window_background(hwnd: win32.HWND, color: u32) void { std.debug.assert(WM_APP_SET_BACKGROUND_RESULT == win32.SendMessageW( hwnd, WM_APP_SET_BACKGROUND, color, 0, )); } pub fn updateScreen(screen: *const vaxis.Screen) void { global.shared_screen.mutex.lock(); defer global.shared_screen.mutex.unlock(); _ = global.shared_screen.arena.reset(.retain_capacity); const buf = global.shared_screen.arena.allocator().alloc(vaxis.Cell, screen.buf.len) catch |e| oom(e); @memcpy(buf, screen.buf); for (buf) |*cell| { cell.char.grapheme = global.shared_screen.arena.allocator().dupe( u8, cell.char.grapheme, ) catch |e| oom(e); } global.shared_screen.obj = .{ .width = screen.width, .height = screen.height, .width_pix = screen.width_pix, .height_pix = screen.height_pix, .buf = buf, .cursor_row = screen.cursor_row, .cursor_col = screen.cursor_col, .cursor_vis = screen.cursor_vis, .unicode = undefined, .width_method = undefined, .mouse_shape = screen.mouse_shape, .cursor_shape = undefined, }; } // NOTE: we round the text metric up to the nearest integer which // means our background rectangles will be aligned. We accomodate // for any gap added by doing this by centering the text. fn getCellSize(text_format: *win32.IDWriteTextFormat) XY(i32) { var text_layout: *win32.IDWriteTextLayout = undefined; { const hr = global.dwrite_factory.CreateTextLayout( win32.L("█"), 1, text_format, std.math.floatMax(f32), std.math.floatMax(f32), &text_layout, ); if (hr < 0) fatalHr("CreateTextLayout", hr); } defer _ = text_layout.IUnknown.Release(); var metrics: win32.DWRITE_TEXT_METRICS = undefined; { const hr = text_layout.GetMetrics(&metrics); if (hr < 0) fatalHr("GetMetrics", hr); } return .{ .x = @intFromFloat(@ceil(metrics.width)), .y = @intFromFloat(@ceil(metrics.height)), }; } const CellPos = struct { cell: XY(i32), offset: XY(i32), pub fn init(cell_size: XY(i32), x: i32, y: i32) CellPos { return .{ .cell = .{ .x = @divTrunc(x, cell_size.x), .y = @divTrunc(y, cell_size.y), }, .offset = .{ .x = @mod(x, cell_size.x), .y = @mod(y, cell_size.y), }, }; } }; pub fn fmtmsg(buf: []u8, value: anytype) []const u8 { var fbs = std.io.fixedBufferStream(buf); cbor.writeValue(fbs.writer(), value) catch |e| switch (e) { error.NoSpaceLeft => std.debug.panic("buffer of size {} not big enough", .{buf.len}), }; return buf[0..fbs.pos]; } const MouseFlags = packed struct(u8) { left_down: bool, right_down: bool, shift_down: bool, control_down: bool, middle_down: bool, xbutton1_down: bool, xbutton2_down: bool, _: bool, }; fn sendMouse( hwnd: win32.HWND, kind: enum { move, left_down, left_up, right_down, right_up, }, wparam: win32.WPARAM, lparam: win32.LPARAM, ) void { const point = ddui.pointFromLparam(lparam); const state = stateFromHwnd(hwnd); const cell_size = state.currently_rendered_cell_size orelse return; const cell = CellPos.init(cell_size, point.x, point.y); switch (kind) { .move => { const flags: MouseFlags = @bitCast(@as(u8, @intCast(0xff & wparam))); if (flags.left_down) state.pid.send(.{ "RDR", "D", @intFromEnum(input.mouse.BUTTON1), cell.cell.x, cell.cell.y, cell.offset.x, cell.offset.y, }) catch |e| onexit(e) else state.pid.send(.{ "RDR", "M", cell.cell.x, cell.cell.y, cell.offset.x, cell.offset.y, }) catch |e| onexit(e); }, else => |b| state.pid.send(.{ "RDR", "B", switch (b) { .move => unreachable, .left_down, .right_down => input.event.press, .left_up, .right_up => input.event.release, }, switch (b) { .move => unreachable, .left_down, .left_up => @intFromEnum(input.mouse.BUTTON1), .right_down, .right_up => @intFromEnum(input.mouse.BUTTON2), }, cell.cell.x, cell.cell.y, cell.offset.x, cell.offset.y, }) catch |e| onexit(e), } } fn sendMouseWheel( hwnd: win32.HWND, wparam: win32.WPARAM, lparam: win32.LPARAM, ) void { const point = ddui.pointFromLparam(lparam); const state = stateFromHwnd(hwnd); const cell_size = state.currently_rendered_cell_size orelse return; const cell = CellPos.init(cell_size, point.x, point.y); // const fwKeys = win32.loword(wparam); state.scroll_delta += @as(i16, @bitCast(win32.hiword(wparam))); while (@abs(state.scroll_delta) > win32.WHEEL_DELTA) { const button = blk: { if (state.scroll_delta > 0) { state.scroll_delta -= win32.WHEEL_DELTA; break :blk @intFromEnum(input.mouse.BUTTON4); } state.scroll_delta += win32.WHEEL_DELTA; break :blk @intFromEnum(input.mouse.BUTTON5); }; state.pid.send(.{ "RDR", "B", input.event.press, button, cell.cell.x, cell.cell.y, cell.offset.x, cell.offset.y, }) catch |e| onexit(e); } } const WinKeyFlags = packed struct(u32) { repeat_count: u16, scan_code: u8, extended: bool, reserved: u4, context: bool, previous: bool, transition: bool, }; fn sendKey( hwnd: win32.HWND, kind: enum { press, release, }, wparam: win32.WPARAM, lparam: win32.LPARAM, ) void { const state = stateFromHwnd(hwnd); var keyboard_state: [256]u8 = undefined; if (0 == win32.GetKeyboardState(&keyboard_state)) fatalWin32( "GetKeyboardState", win32.GetLastError(), ); const mods: vaxis.Key.Modifiers = .{ .shift = (0 != (keyboard_state[@intFromEnum(win32.VK_SHIFT)] & 0x80)), .alt = (0 != (keyboard_state[@intFromEnum(win32.VK_MENU)] & 0x80)), .ctrl = (0 != (keyboard_state[@intFromEnum(win32.VK_CONTROL)] & 0x80)), .super = false, .hyper = false, .meta = false, .caps_lock = (0 != (keyboard_state[@intFromEnum(win32.VK_CAPITAL)] & 1)), .num_lock = false, }; // if ((keyboard_state[VK_LWIN] & 0x80) || (keyboard_state[VK_RWIN] & 0x80)) mod_flags_u32 |= tn::ModifierFlags::Super; // if (m_winkey_down) mod_flags_u32 |= tn::ModifierFlags::Super; // // TODO: Numpad? // // TODO: Help? // // TODO: Fn? const event = switch (kind) { .press => input.event.press, .release => input.event.release, }; const win_key_flags: WinKeyFlags = @bitCast(@as(u32, @intCast(0xffffffff & lparam))); const winkey: WinKey = .{ .vk = @intCast(0xffff & wparam), .extended = win_key_flags.extended, }; if (winkey.skipToUnicode()) |codepoint| { state.pid.send(.{ "RDR", "I", event, @as(u21, codepoint), @as(u21, codepoint), "", @as(u8, @bitCast(mods)), }) catch |e| onexit(e); return; } const max_char_count = 20; var char_buf: [max_char_count + 1]u16 = undefined; // release control key when getting the unicode character of this key keyboard_state[@intFromEnum(win32.VK_CONTROL)] = 0; const unicode_result = win32.ToUnicode( winkey.vk, win_key_flags.scan_code, &keyboard_state, @ptrCast(&char_buf), max_char_count, 0, ); if (unicode_result < 0) { // < 0 means this is a dead key // The ToUnicode function should remember this dead key // and apply it to the next call return; } if (unicode_result > max_char_count) { for (char_buf[0..@intCast(unicode_result)], 0..) |codepoint, i| { std.log.err("UNICODE[{}] 0x{x} {d}", .{ i, codepoint, unicode_result }); } return; } if (unicode_result == 0) { std.log.warn("unknown virtual key {} (0x{x})", .{ winkey, winkey.vk }); return; } for (char_buf[0..@intCast(unicode_result)]) |codepoint| { const mod_bits = @as(u8, @bitCast(mods)); const is_modified = mod_bits & ~(input.mod.shift | input.mod.caps_lock) != 0; // ignore shift and caps var utf8_buf: [6]u8 = undefined; const utf8_len = if (event == input.event.press and !is_modified) std.unicode.utf8Encode(codepoint, &utf8_buf) catch { std.log.err("invalid codepoint {}", .{codepoint}); continue; } else 0; state.pid.send(.{ "RDR", "I", event, @as(u21, winkey.toKKPKeyCode()), @as(u21, codepoint), utf8_buf[0..utf8_len], mod_bits, }) catch |e| onexit(e); } } // TODO: move to libvaxis const WinKey = struct { vk: u16, extended: bool, pub fn eql(self: WinKey, other: WinKey) bool { return self.vk == other.vk and self.extended == other.extended; } pub fn format( self: WinKey, comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype, ) !void { _ = fmt; _ = options; const e_suffix: []const u8 = if (self.extended) "e" else ""; try writer.print("{}{s}", .{ self.vk, e_suffix }); } pub fn skipToUnicode(self: WinKey) ?u21 { if (self.extended) return switch (self.vk) { @intFromEnum(win32.VK_RETURN) => input.key.kp_enter, @intFromEnum(win32.VK_CONTROL) => input.key.right_control, @intFromEnum(win32.VK_MENU) => input.key.right_alt, @intFromEnum(win32.VK_LWIN) => input.key.left_super, @intFromEnum(win32.VK_RWIN) => input.key.right_super, @intFromEnum(win32.VK_PRIOR) => input.key.page_up, @intFromEnum(win32.VK_NEXT) => input.key.page_down, @intFromEnum(win32.VK_END) => input.key.end, @intFromEnum(win32.VK_HOME) => input.key.home, @intFromEnum(win32.VK_LEFT) => input.key.left, @intFromEnum(win32.VK_UP) => input.key.up, @intFromEnum(win32.VK_RIGHT) => input.key.right, @intFromEnum(win32.VK_DOWN) => input.key.down, @intFromEnum(win32.VK_INSERT) => input.key.insert, @intFromEnum(win32.VK_DELETE) => input.key.delete, @intFromEnum(win32.VK_DIVIDE) => input.key.kp_divide, else => null, }; return switch (self.vk) { @intFromEnum(win32.VK_BACK) => input.key.backspace, @intFromEnum(win32.VK_TAB) => input.key.tab, @intFromEnum(win32.VK_RETURN) => input.key.enter, // note: this could be left or right shift @intFromEnum(win32.VK_SHIFT) => input.key.left_shift, @intFromEnum(win32.VK_CONTROL) => input.key.left_control, @intFromEnum(win32.VK_MENU) => input.key.left_alt, @intFromEnum(win32.VK_PAUSE) => input.key.pause, @intFromEnum(win32.VK_CAPITAL) => input.key.caps_lock, @intFromEnum(win32.VK_ESCAPE) => input.key.escape, @intFromEnum(win32.VK_SPACE) => input.key.space, @intFromEnum(win32.VK_PRIOR) => input.key.kp_page_up, @intFromEnum(win32.VK_NEXT) => input.key.kp_page_down, @intFromEnum(win32.VK_END) => input.key.kp_end, @intFromEnum(win32.VK_HOME) => input.key.kp_home, @intFromEnum(win32.VK_LEFT) => input.key.kp_left, @intFromEnum(win32.VK_UP) => input.key.kp_up, @intFromEnum(win32.VK_RIGHT) => input.key.kp_right, @intFromEnum(win32.VK_DOWN) => input.key.kp_down, @intFromEnum(win32.VK_SNAPSHOT) => input.key.print_screen, @intFromEnum(win32.VK_INSERT) => input.key.kp_insert, @intFromEnum(win32.VK_DELETE) => input.key.kp_delete, @intFromEnum(win32.VK_LWIN) => input.key.left_super, @intFromEnum(win32.VK_RWIN) => input.key.right_super, @intFromEnum(win32.VK_NUMPAD0) => input.key.kp_0, @intFromEnum(win32.VK_NUMPAD1) => input.key.kp_1, @intFromEnum(win32.VK_NUMPAD2) => input.key.kp_2, @intFromEnum(win32.VK_NUMPAD3) => input.key.kp_3, @intFromEnum(win32.VK_NUMPAD4) => input.key.kp_4, @intFromEnum(win32.VK_NUMPAD5) => input.key.kp_5, @intFromEnum(win32.VK_NUMPAD6) => input.key.kp_6, @intFromEnum(win32.VK_NUMPAD7) => input.key.kp_7, @intFromEnum(win32.VK_NUMPAD8) => input.key.kp_8, @intFromEnum(win32.VK_NUMPAD9) => input.key.kp_9, @intFromEnum(win32.VK_MULTIPLY) => input.key.kp_multiply, @intFromEnum(win32.VK_ADD) => input.key.kp_add, @intFromEnum(win32.VK_SEPARATOR) => input.key.kp_separator, @intFromEnum(win32.VK_SUBTRACT) => input.key.kp_subtract, @intFromEnum(win32.VK_DECIMAL) => input.key.kp_decimal, // odd, for some reason the divide key is considered extended? //@intFromEnum(win32.VK_DIVIDE) => input.key.kp_divide, @intFromEnum(win32.VK_F1) => input.key.f1, @intFromEnum(win32.VK_F2) => input.key.f2, @intFromEnum(win32.VK_F3) => input.key.f3, @intFromEnum(win32.VK_F4) => input.key.f4, @intFromEnum(win32.VK_F5) => input.key.f5, @intFromEnum(win32.VK_F6) => input.key.f6, @intFromEnum(win32.VK_F7) => input.key.f8, @intFromEnum(win32.VK_F8) => input.key.f8, @intFromEnum(win32.VK_F9) => input.key.f9, @intFromEnum(win32.VK_F10) => input.key.f10, @intFromEnum(win32.VK_F11) => input.key.f11, @intFromEnum(win32.VK_F12) => input.key.f12, @intFromEnum(win32.VK_F13) => input.key.f13, @intFromEnum(win32.VK_F14) => input.key.f14, @intFromEnum(win32.VK_F15) => input.key.f15, @intFromEnum(win32.VK_F16) => input.key.f16, @intFromEnum(win32.VK_F17) => input.key.f17, @intFromEnum(win32.VK_F18) => input.key.f18, @intFromEnum(win32.VK_F19) => input.key.f19, @intFromEnum(win32.VK_F20) => input.key.f20, @intFromEnum(win32.VK_F21) => input.key.f21, @intFromEnum(win32.VK_F22) => input.key.f22, @intFromEnum(win32.VK_F23) => input.key.f23, @intFromEnum(win32.VK_F24) => input.key.f24, @intFromEnum(win32.VK_NUMLOCK) => input.key.num_lock, @intFromEnum(win32.VK_SCROLL) => input.key.scroll_lock, @intFromEnum(win32.VK_LSHIFT) => input.key.left_shift, @intFromEnum(win32.VK_RSHIFT) => input.key.right_shift, @intFromEnum(win32.VK_LCONTROL) => input.key.left_control, @intFromEnum(win32.VK_RCONTROL) => input.key.right_control, @intFromEnum(win32.VK_LMENU) => input.key.left_alt, @intFromEnum(win32.VK_RMENU) => input.key.right_alt, @intFromEnum(win32.VK_VOLUME_MUTE) => input.key.mute_volume, @intFromEnum(win32.VK_VOLUME_DOWN) => input.key.lower_volume, @intFromEnum(win32.VK_VOLUME_UP) => input.key.raise_volume, @intFromEnum(win32.VK_MEDIA_NEXT_TRACK) => input.key.media_track_next, @intFromEnum(win32.VK_MEDIA_PREV_TRACK) => input.key.media_track_previous, @intFromEnum(win32.VK_MEDIA_STOP) => input.key.media_stop, @intFromEnum(win32.VK_MEDIA_PLAY_PAUSE) => input.key.media_play_pause, else => null, }; } pub fn toKKPKeyCode(self: WinKey) u21 { if (self.extended) return self.vk; return switch (self.vk) { 'A'...'Z' => |char| char + ('a' - 'A'), @intFromEnum(win32.VK_OEM_1) => ';', @intFromEnum(win32.VK_OEM_PLUS) => '+', @intFromEnum(win32.VK_OEM_COMMA) => ',', @intFromEnum(win32.VK_OEM_MINUS) => '-', @intFromEnum(win32.VK_OEM_PERIOD) => '.', @intFromEnum(win32.VK_OEM_2) => '/', @intFromEnum(win32.VK_OEM_3) => '`', @intFromEnum(win32.VK_OEM_4) => '[', @intFromEnum(win32.VK_OEM_5) => '\\', @intFromEnum(win32.VK_OEM_6) => ']', @intFromEnum(win32.VK_OEM_7) => '\'', @intFromEnum(win32.VK_OEM_102) => '\\', else => |char| char, }; } }; var global_msg_tail: ?*windowmsg.MessageNode = null; fn WndProc( hwnd: win32.HWND, msg: u32, wparam: win32.WPARAM, lparam: win32.LPARAM, ) callconv(std.os.windows.WINAPI) win32.LRESULT { var msg_node: windowmsg.MessageNode = undefined; msg_node.init(&global_msg_tail, hwnd, msg, wparam, lparam); defer msg_node.deinit(); switch (msg) { // win32.WM_NCHITTEST, // win32.WM_SETCURSOR, // win32.WM_GETICON, // win32.WM_MOUSEMOVE, // win32.WM_NCMOUSEMOVE, // => {}, else => if (false) std.log.info("{}", .{msg_node.fmtPath()}), } switch (msg) { win32.WM_MOUSEMOVE => { sendMouse(hwnd, .move, wparam, lparam); return 0; }, win32.WM_LBUTTONDOWN => { sendMouse(hwnd, .left_down, wparam, lparam); return 0; }, win32.WM_LBUTTONUP => { sendMouse(hwnd, .left_up, wparam, lparam); return 0; }, win32.WM_RBUTTONDOWN => { sendMouse(hwnd, .right_down, wparam, lparam); return 0; }, win32.WM_RBUTTONUP => { sendMouse(hwnd, .right_up, wparam, lparam); return 0; }, win32.WM_MOUSEWHEEL => { sendMouseWheel(hwnd, wparam, lparam); return 0; }, win32.WM_KEYDOWN, win32.WM_SYSKEYDOWN => { sendKey(hwnd, .press, wparam, lparam); return 0; }, win32.WM_KEYUP, win32.WM_SYSKEYUP => { sendKey(hwnd, .release, wparam, lparam); return 0; }, win32.WM_PAINT => { const dpi = win32.dpiFromHwnd(hwnd); const client_size = getClientSize(hwnd); const state = stateFromHwnd(hwnd); const err: HResultError = blk: { var ps: win32.PAINTSTRUCT = undefined; _ = win32.BeginPaint(hwnd, &ps) orelse return fatalWin32( "BeginPaint", win32.GetLastError(), ); defer if (0 == win32.EndPaint(hwnd, &ps)) fatalWin32( "EndPaint", win32.GetLastError(), ); if (state.maybe_d2d == null) { var err: HResultError = undefined; state.maybe_d2d = D2d.init(hwnd, &err) catch break :blk err; } { const size: win32.D2D_SIZE_U = .{ .width = @intCast(client_size.x), .height = @intCast(client_size.y), }; const hr = state.maybe_d2d.?.target.Resize(&size); if (hr < 0) break :blk HResultError{ .context = "D2dResize", .hr = hr }; } state.maybe_d2d.?.target.ID2D1RenderTarget.BeginDraw(); const text_format_editor = state.text_format_editor.getOrCreate(Dpi{ .value = dpi }); state.currently_rendered_cell_size = getCellSize(text_format_editor); { global.shared_screen.mutex.lock(); defer global.shared_screen.mutex.unlock(); paint( &state.maybe_d2d.?, RGB.from_u24(if (state.background) |b| @intCast(0xffffff & b) else 0), &global.shared_screen.obj, text_format_editor, state.currently_rendered_cell_size.?, ); } break :blk HResultError{ .context = "D2dEndDraw", .hr = state.maybe_d2d.?.target.ID2D1RenderTarget.EndDraw(null, null), }; }; if (err.hr == win32.D2DERR_RECREATE_TARGET) { std.log.debug("D2DERR_RECREATE_TARGET", .{}); state.maybe_d2d.?.deinit(); state.maybe_d2d = null; win32.invalidateHwnd(hwnd); } else if (err.hr < 0) std.debug.panic("paint error: {}", .{err}); return 0; }, win32.WM_DPICHANGED => { const dpi = win32.dpiFromHwnd(hwnd); if (dpi != win32.hiword(wparam)) @panic("unexpected hiword dpi"); if (dpi != win32.loword(wparam)) @panic("unexpected loword dpi"); const rect: *win32.RECT = @ptrFromInt(@as(usize, @bitCast(lparam))); if (0 == win32.SetWindowPos( hwnd, null, // ignored via NOZORDER rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, .{ .NOZORDER = 1 }, )) fatalWin32("SetWindowPos", win32.GetLastError()); sendResize(hwnd); return 0; }, win32.WM_SIZE, => { const do_sanity_check = true; if (do_sanity_check) { const client_pixel_size: XY(u16) = .{ .x = win32.loword(lparam), .y = win32.hiword(lparam), }; const client_size = getClientSize(hwnd); std.debug.assert(client_pixel_size.x == client_size.x); std.debug.assert(client_pixel_size.y == client_size.y); } sendResize(hwnd); return 0; }, win32.WM_DISPLAYCHANGE => { win32.invalidateHwnd(hwnd); return 0; }, win32.WM_ERASEBKGND => { const state = stateFromHwnd(hwnd); if (!state.erase_bg_done) { state.erase_bg_done = true; const brush = win32.CreateSolidBrush(toColorRef(.{ .r = 29, .g = 29, .b = 29 })) orelse fatalWin32("CreateSolidBrush", win32.GetLastError()); defer deleteObject(brush); const hdc: win32.HDC = @ptrFromInt(wparam); var rect: win32.RECT = undefined; if (0 == win32.GetClientRect(hwnd, &rect)) @panic(""); if (0 == win32.FillRect(hdc, &rect, brush)) @panic(""); } return 1; // background erased }, win32.WM_CLOSE => { const state = stateFromHwnd(hwnd); state.pid.send(.{ "cmd", "quit" }) catch |e| onexit(e); return 0; }, WM_APP_EXIT => { win32.PostQuitMessage(0); return WM_APP_EXIT_RESULT; }, WM_APP_SET_BACKGROUND => { const state = stateFromHwnd(hwnd); state.background = @intCast(wparam); win32.invalidateHwnd(hwnd); return WM_APP_SET_BACKGROUND_RESULT; }, win32.WM_CREATE => { const create_struct: *win32.CREATESTRUCTW = @ptrFromInt(@as(usize, @bitCast(lparam))); const create_args: *CreateWindowArgs = @alignCast(@ptrCast(create_struct.lpCreateParams)); const state = create_args.allocator.create(State) catch |e| oom(e); state.* = .{ .pid = create_args.pid, }; const existing = win32.SetWindowLongPtrW( hwnd, @enumFromInt(0), @as(isize, @bitCast(@intFromPtr(state))), ); std.debug.assert(existing == 0); std.debug.assert(state == stateFromHwnd(hwnd)); sendResize(hwnd); return 0; }, win32.WM_DESTROY => { // the window should never be destroyed so as to not to invalidate // hwnd reference @panic("gui window erroneously destroyed"); }, else => return win32.DefWindowProcW(hwnd, msg, wparam, lparam), } } fn sendResize( hwnd: win32.HWND, ) void { const dpi = win32.dpiFromHwnd(hwnd); const state = stateFromHwnd(hwnd); if (state.maybe_d2d == null) { var err: HResultError = undefined; state.maybe_d2d = D2d.init(hwnd, &err) catch std.debug.panic( "D2d.init failed with {}", .{err}, ); } const single_cell_size = getCellSize( state.text_format_editor.getOrCreate(Dpi{ .value = @intCast(dpi) }), ); const client_pixel_size = getClientSize(hwnd); const client_cell_size: XY(u16) = .{ .x = @intCast(@divTrunc(client_pixel_size.x, single_cell_size.x)), .y = @intCast(@divTrunc(client_pixel_size.y, single_cell_size.y)), }; std.log.debug( "Resize Px={}x{} Cells={}x{}", .{ client_pixel_size.x, client_pixel_size.y, client_cell_size.x, client_cell_size.y }, ); state.pid.send(.{ "RDR", "Resize", client_cell_size.x, client_cell_size.y, client_pixel_size.x, client_pixel_size.y, }) catch @panic("pid send failed"); } pub const Rgb8 = struct { r: u8, g: u8, b: u8 }; fn toColorRef(rgb: Rgb8) u32 { return (@as(u32, rgb.r) << 0) | (@as(u32, rgb.g) << 8) | (@as(u32, rgb.b) << 16); } fn fatalWin32(what: []const u8, err: win32.WIN32_ERROR) noreturn { std.debug.panic("{s} failed with {}", .{ what, err.fmt() }); } fn fatalHr(what: []const u8, hresult: win32.HRESULT) noreturn { std.debug.panic("{s} failed, hresult=0x{x}", .{ what, @as(u32, @bitCast(hresult)) }); } fn deleteObject(obj: ?win32.HGDIOBJ) void { if (0 == win32.DeleteObject(obj)) fatalWin32("DeleteObject", win32.GetLastError()); } fn getClientSize(hwnd: win32.HWND) XY(i32) { var rect: win32.RECT = undefined; if (0 == win32.GetClientRect(hwnd, &rect)) fatalWin32("GetClientRect", win32.GetLastError()); std.debug.assert(rect.left == 0); std.debug.assert(rect.top == 0); return .{ .x = rect.right, .y = rect.bottom }; } pub fn XY(comptime T: type) type { return struct { x: T, y: T, pub fn init(x: T, y: T) @This() { return .{ .x = x, .y = y }; } }; } const xterm_colors: [256]u24 = .{ 0x000000, 0x800000, 0x008000, 0x808000, 0x000080, 0x800080, 0x008080, 0xc0c0c0, 0x808080, 0xff0000, 0x00ff00, 0xffff00, 0x0000ff, 0xff00ff, 0x00ffff, 0xffffff, 0x000000, 0x00005f, 0x000087, 0x0000af, 0x0000d7, 0x0000ff, 0x005f00, 0x005f5f, 0x005f87, 0x005faf, 0x005fd7, 0x005fff, 0x008700, 0x00875f, 0x008787, 0x0087af, 0x0087d7, 0x0087ff, 0x00af00, 0x00af5f, 0x00af87, 0x00afaf, 0x00afd7, 0x00afff, 0x00d700, 0x00d75f, 0x00d787, 0x00d7af, 0x00d7d7, 0x00d7ff, 0x00ff00, 0x00ff5f, 0x00ff87, 0x00ffaf, 0x00ffd7, 0x00ffff, 0x5f0000, 0x5f005f, 0x5f0087, 0x5f00af, 0x5f00d7, 0x5f00ff, 0x5f5f00, 0x5f5f5f, 0x5f5f87, 0x5f5faf, 0x5f5fd7, 0x5f5fff, 0x5f8700, 0x5f875f, 0x5f8787, 0x5f87af, 0x5f87d7, 0x5f87ff, 0x5faf00, 0x5faf5f, 0x5faf87, 0x5fafaf, 0x5fafd7, 0x5fafff, 0x5fd700, 0x5fd75f, 0x5fd787, 0x5fd7af, 0x5fd7d7, 0x5fd7ff, 0x5fff00, 0x5fff5f, 0x5fff87, 0x5fffaf, 0x5fffd7, 0x5fffff, 0x870000, 0x87005f, 0x870087, 0x8700af, 0x8700d7, 0x8700ff, 0x875f00, 0x875f5f, 0x875f87, 0x875faf, 0x875fd7, 0x875fff, 0x878700, 0x87875f, 0x878787, 0x8787af, 0x8787d7, 0x8787ff, 0x87af00, 0x87af5f, 0x87af87, 0x87afaf, 0x87afd7, 0x87afff, 0x87d700, 0x87d75f, 0x87d787, 0x87d7af, 0x87d7d7, 0x87d7ff, 0x87ff00, 0x87ff5f, 0x87ff87, 0x87ffaf, 0x87ffd7, 0x87ffff, 0xaf0000, 0xaf005f, 0xaf0087, 0xaf00af, 0xaf00d7, 0xaf00ff, 0xaf5f00, 0xaf5f5f, 0xaf5f87, 0xaf5faf, 0xaf5fd7, 0xaf5fff, 0xaf8700, 0xaf875f, 0xaf8787, 0xaf87af, 0xaf87d7, 0xaf87ff, 0xafaf00, 0xafaf5f, 0xafaf87, 0xafafaf, 0xafafd7, 0xafafff, 0xafd700, 0xafd75f, 0xafd787, 0xafd7af, 0xafd7d7, 0xafd7ff, 0xafff00, 0xafff5f, 0xafff87, 0xafffaf, 0xafffd7, 0xafffff, 0xd70000, 0xd7005f, 0xd70087, 0xd700af, 0xd700d7, 0xd700ff, 0xd75f00, 0xd75f5f, 0xd75f87, 0xd75faf, 0xd75fd7, 0xd75fff, 0xd78700, 0xd7875f, 0xd78787, 0xd787af, 0xd787d7, 0xd787ff, 0xd7af00, 0xd7af5f, 0xd7af87, 0xd7afaf, 0xd7afd7, 0xd7afff, 0xd7d700, 0xd7d75f, 0xd7d787, 0xd7d7af, 0xd7d7d7, 0xd7d7ff, 0xd7ff00, 0xd7ff5f, 0xd7ff87, 0xd7ffaf, 0xd7ffd7, 0xd7ffff, 0xff0000, 0xff005f, 0xff0087, 0xff00af, 0xff00d7, 0xff00ff, 0xff5f00, 0xff5f5f, 0xff5f87, 0xff5faf, 0xff5fd7, 0xff5fff, 0xff8700, 0xff875f, 0xff8787, 0xff87af, 0xff87d7, 0xff87ff, 0xffaf00, 0xffaf5f, 0xffaf87, 0xffafaf, 0xffafd7, 0xffafff, 0xffd700, 0xffd75f, 0xffd787, 0xffd7af, 0xffd7d7, 0xffd7ff, 0xffff00, 0xffff5f, 0xffff87, 0xffffaf, 0xffffd7, 0xffffff, 0x080808, 0x121212, 0x1c1c1c, 0x262626, 0x303030, 0x3a3a3a, 0x444444, 0x4e4e4e, 0x585858, 0x606060, 0x666666, 0x767676, 0x808080, 0x8a8a8a, 0x949494, 0x9e9e9e, 0xa8a8a8, 0xb2b2b2, 0xbcbcbc, 0xc6c6c6, 0xd0d0d0, 0xdadada, 0xe4e4e4, 0xeeeeee, };