diff --git a/build.zig b/build.zig index c9b0a7e..bf11011 100644 --- a/build.zig +++ b/build.zig @@ -8,6 +8,7 @@ pub fn build(b: *std.Build) void { const strip = b.option(bool, "strip", "Disable debug information (default: no)"); const use_llvm = b.option(bool, "use_llvm", "Enable llvm backend (default: none)"); const pie = b.option(bool, "pie", "Produce an executable with position independent code (default: none)"); + const gui = b.option(bool, "gui", "Standalone GUI mode") orelse false; const run_step = b.step("run", "Run the app"); const check_step = b.step("check", "Check the app"); @@ -25,6 +26,7 @@ pub fn build(b: *std.Build) void { strip, use_llvm, pie, + gui, ); } @@ -39,6 +41,7 @@ fn build_development( strip: ?bool, use_llvm: ?bool, pie: ?bool, + gui: bool, ) void { const target = b.standardTargetOptions(.{ .default_target = .{ .abi = if (builtin.os.tag == .linux and !tracy_enabled) .musl else null } }); const optimize = b.standardOptimizeOption(.{}); @@ -57,6 +60,7 @@ fn build_development( strip orelse false, use_llvm, pie, + gui, ); } @@ -71,6 +75,7 @@ fn build_release( strip: ?bool, use_llvm: ?bool, pie: ?bool, + gui: bool, ) void { const targets: []const std.Target.Query = &.{ .{ .cpu_arch = .x86_64, .os_tag = .linux, .abi = .musl }, @@ -112,6 +117,7 @@ fn build_release( strip orelse true, use_llvm, pie, + gui, ); } } @@ -130,11 +136,13 @@ pub fn build_exe( strip: bool, use_llvm: ?bool, pie: ?bool, + gui: bool, ) void { const options = b.addOptions(); options.addOption(bool, "enable_tracy", tracy_enabled); options.addOption(bool, "use_tree_sitter", use_tree_sitter); options.addOption(bool, "strip", strip); + options.addOption(bool, "gui", gui); const options_mod = options.createModule(); @@ -266,7 +274,7 @@ pub fn build_exe( }, }); - const renderer_mod = b.createModule(.{ + const tui_renderer_mod = b.createModule(.{ .root_source_file = b.path("src/renderer/vaxis/renderer.zig"), .imports = &.{ .{ .name = "vaxis", .module = vaxis_mod }, @@ -280,6 +288,52 @@ pub fn build_exe( }, }); + const renderer_mod = blk: { + if (gui) switch (target.result.os.tag) { + .windows => { + const direct2d_dep = b.lazyDependency("direct2d", .{}) orelse break :blk tui_renderer_mod; + + const win32_dep = direct2d_dep.builder.dependency("win32", .{}); + const win32_mod = win32_dep.module("zigwin32"); + const gui_mod = b.createModule(.{ + .root_source_file = b.path("src/win32/gui.zig"), + .imports = &.{ + .{ .name = "win32", .module = win32_mod }, + .{ .name = "ddui", .module = direct2d_dep.module("ddui") }, + .{ .name = "cbor", .module = cbor_mod }, + .{ .name = "thespian", .module = thespian_mod }, + .{ .name = "input", .module = input_mod }, + // TODO: we should be able to work without these modules + .{ .name = "vaxis", .module = vaxis_mod }, + .{ .name = "color", .module = color_mod }, + }, + }); + gui_mod.addIncludePath(b.path("src/win32")); + + const mod = b.createModule(.{ + .root_source_file = b.path("src/renderer/win32/renderer.zig"), + .imports = &.{ + .{ .name = "theme", .module = themes_dep.module("theme") }, + .{ .name = "win32", .module = win32_mod }, + .{ .name = "cbor", .module = cbor_mod }, + .{ .name = "thespian", .module = thespian_mod }, + .{ .name = "input", .module = input_mod }, + .{ .name = "gui", .module = gui_mod }, + // TODO: we should be able to work without these modules + .{ .name = "tuirenderer", .module = tui_renderer_mod }, + .{ .name = "vaxis", .module = vaxis_mod }, + }, + }); + break :blk mod; + }, + else => |tag| { + std.log.err("OS '{s}' does not support -Dgui mode", .{@tagName(tag)}); + std.process.exit(0xff); + }, + }; + break :blk tui_renderer_mod; + }; + const keybind_mod = b.createModule(.{ .root_source_file = b.path("src/keybind/keybind.zig"), .imports = &.{ @@ -392,6 +446,7 @@ pub fn build_exe( .target = target, .optimize = optimize, .strip = strip, + .win32_manifest = b.path("src/win32/flow.manifest"), }); if (use_llvm) |value| { @@ -411,6 +466,16 @@ pub fn build_exe( exe.root_module.addImport("input", input_mod); exe.root_module.addImport("syntax", syntax_mod); exe.root_module.addImport("version_info", b.createModule(.{ .root_source_file = version_info_file })); + + if (target.result.os.tag == .windows) { + exe.addWin32ResourceFile(.{ + .file = b.path("src/win32/flow.rc"), + }); + if (gui) { + exe.subsystem = .Windows; + } + } + const exe_install = b.addInstallArtifact(exe, exe_install_options); b.getInstallStep().dependOn(&exe_install.step); diff --git a/build.zig.zon b/build.zig.zon index b70702d..b9557a2 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -37,6 +37,11 @@ .url = "https://github.com/rockorager/zeit/archive/9cca8ec620a54c3b07cd249f25e5bcb3153d03d7.tar.gz", .hash = "1220755ea2a5aa6bb3713437aaafefd44812169fe43f1da755c3ee6101b85940f441", }, + .direct2d = .{ + .url = "https://github.com/marler8997/direct2d-zig/archive/0d031389a26653bb71f81c2340d1b8ba6bd339c3.tar.gz", + .hash = "122069b40656962c6ba9b9b3f9f882ba2e9cf4c5e1afebac7b7501404129e6bb4705", + .lazy = true, + }, }, .paths = .{ "include", diff --git a/src/renderer/vaxis/renderer.zig b/src/renderer/vaxis/renderer.zig index 80bccb3..6318757 100644 --- a/src/renderer/vaxis/renderer.zig +++ b/src/renderer/vaxis/renderer.zig @@ -142,11 +142,12 @@ pub fn input_fd_blocking(self: Self) i32 { return self.tty.fd; } -pub fn leave_alternate_screen(self: *Self) void { - self.vx.exitAltScreen() catch {}; -} - -pub fn process_input_event(self: *Self, input_: []const u8, text: ?[]const u8) !void { +pub fn process_renderer_event(self: *Self, msg: []const u8) !void { + var input_: []const u8 = undefined; + var text_: []const u8 = undefined; + if (!try cbor.match(msg, .{ "RDR", cbor.extract(&input_), cbor.extract(&text_) })) + return error.UnexpectedRendererEvent; + const text = if (text_.len > 0) text_ else null; const event = std.mem.bytesAsValue(vaxis.Event, input_); switch (event.*) { .key_press => |key__| { @@ -349,7 +350,7 @@ pub fn request_system_clipboard(self: *Self) void { self.vx.requestSystemClipboard(self.tty.anyWriter()) catch |e| log.logger(log_name).err("request_system_clipboard", e); } -pub fn request_windows_clipboard(self: *Self) ![]u8 { +pub fn request_windows_clipboard(allocator: std.mem.Allocator) ![]u8 { const windows = std.os.windows; const win32 = struct { pub extern "user32" fn OpenClipboard(hWndNewOwner: ?windows.HWND) callconv(windows.WINAPI) windows.BOOL; @@ -370,7 +371,7 @@ pub fn request_windows_clipboard(self: *Self) ![]u8 { const text = std.mem.span(data); defer _ = win32.GlobalUnlock(mem); - return self.allocator.dupe(u8, text); + return allocator.dupe(u8, text); } pub fn request_mouse_cursor_text(self: *Self, push_or_pop: bool) void { @@ -493,7 +494,7 @@ const Loop = struct { }, else => {}, } - self.pid.send(.{ "VXS", std.mem.asBytes(&event), text }) catch @panic("send VXS event failed"); + self.pid.send(.{ "RDR", std.mem.asBytes(&event), text }) catch @panic("send RDR event failed"); if (free_text) self.vaxis.opts.system_clipboard_allocator.?.free(text); } diff --git a/src/renderer/win32/renderer.zig b/src/renderer/win32/renderer.zig new file mode 100644 index 0000000..c8b5c08 --- /dev/null +++ b/src/renderer/win32/renderer.zig @@ -0,0 +1,397 @@ +const Self = @This(); +pub const log_name = "renderer"; + +const std = @import("std"); +const cbor = @import("cbor"); +const vaxis = @import("vaxis"); +const Style = @import("theme").Style; +const Color = @import("theme").Color; +pub const CursorShape = vaxis.Cell.CursorShape; + +pub const Plane = @import("tuirenderer").Plane; +const input = @import("input"); + +const win32 = @import("win32").everything; + +pub const Cell = @import("tuirenderer").Cell; +pub const StyleBits = @import("tuirenderer").style; +const gui = @import("gui"); +const DropWriter = gui.DropWriter; +pub const style = StyleBits; + +allocator: std.mem.Allocator, +vx: vaxis.Vaxis, + +renders_missed: u32 = 0, + +handler_ctx: *anyopaque, +dispatch_input: ?*const fn (ctx: *anyopaque, cbor_msg: []const u8) void = null, +dispatch_mouse: ?*const fn (ctx: *anyopaque, y: c_int, x: c_int, cbor_msg: []const u8) void = null, +dispatch_mouse_drag: ?*const fn (ctx: *anyopaque, y: c_int, x: c_int, cbor_msg: []const u8) void = null, +dispatch_event: ?*const fn (ctx: *anyopaque, cbor_msg: []const u8) void = null, + +thread: ?std.Thread = null, + +const global = struct { + var init_called: bool = false; +}; + +fn oom(e: error{OutOfMemory}) noreturn { + @panic(@errorName(e)); +} + +pub fn init( + allocator: std.mem.Allocator, + handler_ctx: *anyopaque, + no_alternate: bool, +) !Self { + std.debug.assert(!global.init_called); + global.init_called = true; + + _ = no_alternate; + + gui.init(); + const opts: vaxis.Vaxis.Options = .{ + .kitty_keyboard_flags = .{ + .disambiguate = true, + .report_events = true, + .report_alternate_keys = true, + .report_all_as_ctl_seqs = true, + .report_text = true, + }, + .system_clipboard_allocator = allocator, + }; + var result = .{ + .allocator = allocator, + .vx = try vaxis.init(allocator, opts), + .handler_ctx = handler_ctx, + }; + result.vx.caps.unicode = .unicode; + result.vx.screen.width_method = .unicode; + return result; +} + +pub fn deinit(self: *Self) void { + std.log.warn("TODO: implement win32 renderer deinit", .{}); + var drop_writer = DropWriter{}; + self.vx.deinit(self.allocator, drop_writer.writer().any()); +} + +threadlocal var thread_is_panicing = false; + +pub fn panic( + msg: []const u8, + error_return_trace: ?*std.builtin.StackTrace, + ret_addr: ?usize, +) noreturn { + if (!thread_is_panicing) { + thread_is_panicing = true; + const msg_z: [:0]const u8 = if (std.fmt.allocPrintZ( + std.heap.page_allocator, + "{s}", + .{msg}, + )) |msg_z| msg_z else |_| "failed allocate error message"; + _ = win32.MessageBoxA(null, msg_z, "Flow Panic", .{ .ICONASTERISK = 1 }); + } + std.builtin.default_panic(msg, error_return_trace, ret_addr); +} + +pub fn run(self: *Self) !void { + if (self.thread) |_| return; + + // dummy resize to fully init vaxis + const drop_writer = DropWriter{}; + try self.vx.resize( + self.allocator, + drop_writer.writer().any(), + .{ .rows = 25, .cols = 80, .x_pixel = 0, .y_pixel = 0 }, + ); + self.vx.queueRefresh(); + //if (self.dispatch_event) |f| f(self.handler_ctx, try self.fmtmsg(.{"resize"})); + self.thread = try gui.start(); +} + +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]; +} + +pub fn render(self: *Self) error{}!void { + if (!gui.updateScreen(&self.vx.screen)) { + self.renders_missed += 1; + std.log.warn("missed {} renders, no gui window yet", .{self.renders_missed}); + } +} +pub fn stop(self: *Self) void { + _ = self; + std.log.warn("TODO: implement stop", .{}); +} + +pub fn stdplane(self: *Self) Plane { + const name = "root"; + var plane: Plane = .{ + .window = self.vx.window(), + .name_buf = undefined, + .name_len = name.len, + }; + @memcpy(plane.name_buf[0..name.len], name); + return plane; +} + +pub fn process_renderer_event(self: *Self, msg: []const u8) !void { + const Input = struct { + kind: u8, + codepoint: u21, + shifted_codepoint: u21, + text: []const u8, + mods: u8, + }; + const MousePos = struct { + col: i32, + row: i32, + xoffset: i32, + yoffset: i32, + }; + const Winsize = struct { + cell_width: u16, + cell_height: u16, + pixel_width: u16, + pixel_height: u16, + }; + + { + var args: Input = undefined; + if (try cbor.match(msg, .{ + cbor.any, + "I", + cbor.extract(&args.kind), + cbor.extract(&args.codepoint), + cbor.extract(&args.shifted_codepoint), + cbor.extract(&args.text), + cbor.extract(&args.mods), + })) { + var buf: [300]u8 = undefined; + const cbor_msg = fmtmsg(&buf, .{ + "I", + args.kind, + args.codepoint, + args.shifted_codepoint, + args.text, + args.mods, + }); + if (self.dispatch_input) |f| f(self.handler_ctx, cbor_msg); + return; + } + } + + { + var args: Winsize = undefined; + if (try cbor.match(msg, .{ + cbor.any, + "Resize", + cbor.extract(&args.cell_width), + cbor.extract(&args.cell_height), + cbor.extract(&args.pixel_width), + cbor.extract(&args.pixel_height), + })) { + var drop_writer = DropWriter{}; + self.vx.resize(self.allocator, drop_writer.writer().any(), .{ + .rows = @intCast(args.cell_height), + .cols = @intCast(args.cell_width), + .x_pixel = @intCast(args.pixel_width), + .y_pixel = @intCast(args.pixel_height), + }) catch |err| std.debug.panic("resize failed with {s}", .{@errorName(err)}); + self.vx.queueRefresh(); + { + var buf: [200]u8 = undefined; + if (self.dispatch_event) |f| f(self.handler_ctx, fmtmsg(&buf, .{"resize"})); + } + return; + } + } + { + var args: MousePos = undefined; + if (try cbor.match(msg, .{ + cbor.any, + "M", + cbor.extract(&args.col), + cbor.extract(&args.row), + cbor.extract(&args.xoffset), + cbor.extract(&args.yoffset), + })) { + var buf: [200]u8 = undefined; + if (self.dispatch_mouse) |f| f( + self.handler_ctx, + @intCast(args.row), + @intCast(args.col), + fmtmsg(&buf, .{ + "M", + args.col, + args.row, + args.xoffset, + args.yoffset, + }), + ); + return; + } + } + { + var args: struct { + pos: MousePos, + button: struct { + press: u8, + id: u8, + }, + } = undefined; + if (try cbor.match(msg, .{ + cbor.any, + "B", + cbor.extract(&args.button.press), + cbor.extract(&args.button.id), + cbor.extract(&args.pos.col), + cbor.extract(&args.pos.row), + cbor.extract(&args.pos.xoffset), + cbor.extract(&args.pos.yoffset), + })) { + var buf: [200]u8 = undefined; + if (self.dispatch_mouse) |f| f( + self.handler_ctx, + @intCast(args.pos.row), + @intCast(args.pos.col), + fmtmsg(&buf, .{ + "B", + args.button.press, + args.button.id, + input.utils.button_id_string(@enumFromInt(args.button.id)), + args.pos.col, + args.pos.row, + args.pos.xoffset, + args.pos.yoffset, + }), + ); + return; + } + } + return error.UnexpectedRendererEvent; +} + +fn setEllipsis(str: []u16) void { + std.debug.assert(str.len >= 3); + str[str.len - 1] = '.'; + str[str.len - 2] = '.'; + str[str.len - 3] = '.'; +} + +const ConversionSizes = struct { + src_len: usize, + dst_len: usize, +}; +fn calcUtf8ToUtf16LeWithMax(utf8: []const u8, max_dst_len: usize) !ConversionSizes { + var src_len: usize = 0; + var dst_len: usize = 0; + while (src_len < utf8.len) { + if (dst_len >= max_dst_len) break; + const n = try std.unicode.utf8ByteSequenceLength(utf8[src_len]); + const next_src_len = src_len + n; + const codepoint = try std.unicode.utf8Decode(utf8[src_len..next_src_len]); + if (codepoint < 0x10000) { + dst_len += 1; + } else { + if (dst_len + 2 > max_dst_len) break; + dst_len += 2; + } + src_len = next_src_len; + } + return .{ .src_len = src_len, .dst_len = dst_len }; +} + +pub fn set_terminal_title(self: *Self, title_utf8: []const u8) void { + _ = self; + + const max_title_wide = 500; + const conversion_sizes = calcUtf8ToUtf16LeWithMax(title_utf8, max_title_wide) catch { + std.log.err("title is invalid UTF-8", .{}); + return; + }; + + var title_wide_buf: [max_title_wide + 1]u16 = undefined; + const len = @min(max_title_wide, conversion_sizes.dst_len); + title_wide_buf[len] = 0; + const title_wide = title_wide_buf[0..len :0]; + + const size = std.unicode.utf8ToUtf16Le(title_wide, title_utf8[0..conversion_sizes.src_len]) catch |err| switch (err) { + error.InvalidUtf8 => { + std.log.err("title is invalid UTF-8", .{}); + return; + }, + }; + std.debug.assert(size == conversion_sizes.dst_len); + if (conversion_sizes.src_len != title_utf8.len) { + setEllipsis(title_wide); + } + var win32_err: gui.Win32Error = undefined; + gui.setWindowTitle(title_wide, &win32_err) catch |err| switch (err) { + error.NoWindow => std.log.warn("no window to set the title for", .{}), + error.Win32 => std.log.err("{s} failed with {}", .{ win32_err.what, win32_err.code.fmt() }), + }; +} + +pub fn set_terminal_style(self: *Self, style_: Style) void { + _ = self; + _ = style_; + std.log.warn("TODO: implement set_terminal_style", .{}); + //if (style_.fg) |color| + //self.vx.setTerminalForegroundColor(self.tty.anyWriter(), vaxis.Cell.Color.rgbFromUint(@intCast(color.color)).rgb) catch {}; + //if (style_.bg) |color| + //self.vx.setTerminalBackgroundColor(self.tty.anyWriter(), vaxis.Cell.Color.rgbFromUint(@intCast(color.color)).rgb) catch {}; +} + +pub fn set_terminal_cursor_color(self: *Self, color: Color) void { + _ = self; + std.log.warn("TODO: set_terminal_cursor_color '{any}'", .{color}); + //self.vx.setTerminalCursorColor(self.tty.anyWriter(), vaxis.Cell.Color.rgbFromUint(@intCast(color.color)).rgb) catch {}; +} + +pub fn set_terminal_working_directory(self: *Self, absolute_path: []const u8) void { + _ = self; + std.log.warn("TODO: set_terminal_working_directory '{s}'", .{absolute_path}); + //self.vx.setTerminalWorkingDirectory(self.tty.anyWriter(), absolute_path) catch {}; +} + +pub fn copy_to_system_clipboard(self: *Self, text: []const u8) void { + _ = self; + _ = text; + std.log.warn("TODO: copy_to_system_clipboard", .{}); +} + +pub const request_windows_clipboard = @import("tuirenderer").request_windows_clipboard; + +pub fn request_mouse_cursor_text(self: *Self, push_or_pop: bool) void { + _ = self; + _ = push_or_pop; + //@panic("todo"); +} +pub fn request_mouse_cursor_pointer(self: *Self, push_or_pop: bool) void { + _ = self; + _ = push_or_pop; + //@panic("todo"); +} +pub fn request_mouse_cursor_default(self: *Self, push_or_pop: bool) void { + _ = self; + _ = push_or_pop; + //@panic("todo"); +} +pub fn cursor_enable(self: *Self, y: c_int, x: c_int, shape: CursorShape) !void { + _ = self; + _ = y; + _ = x; + _ = shape; + //@panic("todo"); +} +pub fn cursor_disable(self: *Self) void { + _ = self; + //@panic("todo"); +} diff --git a/src/tui/mainview.zig b/src/tui/mainview.zig index e5d00e1..ba54f9d 100644 --- a/src/tui/mainview.zig +++ b/src/tui/mainview.zig @@ -570,10 +570,10 @@ const cmds = struct { } pub const open_previous_file_meta = .{ .description = "Open the previous file" }; - pub fn system_paste(_: *Self, _: Ctx) Result { + pub fn system_paste(self: *Self, _: Ctx) Result { if (builtin.os.tag == .windows) { - const text = try tui.current().rdr.request_windows_clipboard(); - defer tui.current().rdr.allocator.free(text); + const text = try @import("renderer").request_windows_clipboard(self.allocator); + defer self.allocator.free(text); return command.executeName("paste", command.fmt(.{text})) catch {}; } tui.current().rdr.request_system_clipboard(); diff --git a/src/tui/tui.zig b/src/tui/tui.zig index e8b5758..025c12c 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const build_options = @import("build_options"); const tp = @import("thespian"); const cbor = @import("cbor"); const log = @import("log"); @@ -91,6 +92,7 @@ fn init(allocator: Allocator) !*Self { conf.input_mode = try allocator.dupe(u8, conf.input_mode); conf.top_bar = try allocator.dupe(u8, conf.top_bar); conf.bottom_bar = try allocator.dupe(u8, conf.bottom_bar); + if (build_options.gui) conf.enable_terminal_cursor = false; const frame_rate: usize = @intCast(tp.env.get().num("frame-rate")); if (frame_rate != 0) @@ -246,10 +248,11 @@ fn receive(self: *Self, from: tp.pid_ref, m: tp.message) tp.result { } fn receive_safe(self: *Self, from: tp.pid_ref, m: tp.message) !void { - var input: []const u8 = undefined; - var text: []const u8 = undefined; - if (try m.match(.{ "VXS", tp.extract(&input), tp.extract(&text) })) { - try self.rdr.process_input_event(input, if (text.len > 0) text else null); + if (try m.match(.{ "RDR", tp.more })) { + self.rdr.process_renderer_event(m.buf) catch |e| switch (e) { + error.UnexpectedRendererEvent => return tp.unexpected(m), + else => return e, + }; try self.dispatch_flush_input_event(); if (self.unrendered_input_events_count > 0 and !self.frame_clock_running) need_render(); @@ -304,6 +307,7 @@ fn receive_safe(self: *Self, from: tp.pid_ref, m: tp.message) !void { return; } + var text: []const u8 = undefined; if (try m.match(.{ "system_clipboard", tp.extract(&text) })) { try self.dispatch_flush_input_event(); return if (command.get_id("mini_mode_paste")) |id| diff --git a/src/win32/ResourceNames.h b/src/win32/ResourceNames.h new file mode 100644 index 0000000..5720263 --- /dev/null +++ b/src/win32/ResourceNames.h @@ -0,0 +1 @@ +#define ID_ICON_FLOW 1 diff --git a/src/win32/flow.ico b/src/win32/flow.ico new file mode 100644 index 0000000..0630ebf Binary files /dev/null and b/src/win32/flow.ico differ diff --git a/src/win32/flow.manifest b/src/win32/flow.manifest new file mode 100644 index 0000000..89e2857 --- /dev/null +++ b/src/win32/flow.manifest @@ -0,0 +1,14 @@ + + + + + true/pm + PerMonitorV2 + + + + + + + + diff --git a/src/win32/flow.rc b/src/win32/flow.rc new file mode 100644 index 0000000..00b7652 --- /dev/null +++ b/src/win32/flow.rc @@ -0,0 +1,34 @@ +#include "ResourceNames.h" + +// LANG_NEUTRAL(0), SUBLANG_NEUTRAL(0) +LANGUAGE 0, 0 + +ID_ICON_FLOW ICON "flow.ico" + +VS_VERSION_INFO VERSIONINFO +//FILEVERSION VERSION_MAJOR,VERSION_MINOR,VERSION_PATCH,VERSION_COMMIT_HEIGHT +//PRODUCTVERSION VERSION_MAJOR,VERSION_MINOR,VERSION_PATCH,VERSION_COMMIT_HEIGHT +//FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +//FILEFLAGS VER_DBG +//FILEOS VOS_NT +//FILETYPE VFT_APP +//FILESUBTYPE VFT2_UNKNOWN +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904b0" + BEGIN + //VALUE "CompanyName", "???" + //VALUE "FileDescription", "???" + //VALUE "FileVersion", VERSION + //VALUE "LegalCopyright", "(C) 2024 ???" + VALUE "OriginalFilename", "flow.exe" + VALUE "ProductName", "Flow Control" + //VALUE "ProductVersion", VERSION + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x0409,1200 + END +END diff --git a/src/win32/gui.zig b/src/win32/gui.zig new file mode 100644 index 0000000..6eb8a7b --- /dev/null +++ b/src/win32/gui.zig @@ -0,0 +1,1171 @@ +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 input = @import("input"); +const windowmsg = @import("windowmsg.zig"); + +const HResultError = ddui.HResultError; + +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 mutex: std.Thread.Mutex = .{}; + + 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; + var window_class: u16 = 0; + var hwnd: ?win32.HWND = null; +}; +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; + + global.icons = getIcons(); + + { + 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() Icons { + const small_x = win32.GetSystemMetrics(.CXSMICON); + const small_y = win32.GetSystemMetrics(.CYSMICON); + const large_x = win32.GetSystemMetrics(.CXICON); + const large_y = win32.GetSystemMetrics(.CYICON); + std.log.info("icons small={}x{} large={}x{}", .{ + small_x, small_y, + large_x, large_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 = @import("color").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"), + .center_x = true, + .center_y = true, + }) 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, + + // these fields should only be accessed inside the global mutex + shared_screen_arena: std.heap.ArenaAllocator, + shared_screen: vaxis.Screen = .{}, + pub fn deinit(self: *State) void { + { + global.mutex.lock(); + defer global.mutex.unlock(); + self.shared_screen.deinit(self.shared_screen_arena.allocator()); + self.shared_screen_arena.deinit(); + } + if (self.maybe_d2d) |*d2d| { + d2d.deinit(); + } + self.* = undefined; + } +}; +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, + screen: *const vaxis.Screen, + text_format_editor: *win32.IDWriteTextFormat, + cell_size: XY(i32), +) void { + { + const color = ddui.rgb8(31, 31, 31); + d2d.target.ID2D1RenderTarget.Clear(&color); + } + + for (0..screen.height) |y| { + const row_y = cell_size.y * @as(i32, @intCast(y)); + for (0..screen.width) |x| { + const column_x = 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 = std.unicode.wtf8ToWtf16Le(&buf_wtf16, cell.char.grapheme) catch |err| switch (err) { + error.InvalidWtf8 => @panic("TODO: handle invalid wtf8"), + }; + ddui.DrawText( + &d2d.target.ID2D1RenderTarget, + buf_wtf16[0..grapheme_len], + text_format_editor, + ddui.rectFloatFromInt(cell_rect), + d2d.solid(d2dColorFromVAxis(cell.style.fg)), + .{}, + .NATURAL, + ); + } + } +} + +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"); + + // we only need to register the window class once per process + if (global.window_class == 0) { + 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, + }; + global.window_class = win32.RegisterClassExW(&wc); + if (global.window_class == 0) 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, + win32.CW_USEDEFAULT, // x + win32.CW_USEDEFAULT, // y + win32.CW_USEDEFAULT, // width + win32.CW_USEDEFAULT, // height + null, // Parent window + null, // Menu + win32.GetModuleHandleW(null), + @ptrCast(&create_args), + ) orelse fatalWin32("CreateWindow", win32.GetLastError()); + defer if (0 == win32.DestroyWindow(hwnd)) fatalWin32("DestroyWindow", win32.GetLastError()); + + { + global.mutex.lock(); + defer global.mutex.unlock(); + std.debug.assert(global.hwnd == null); + global.hwnd = hwnd; + } + defer { + global.mutex.lock(); + defer global.mutex.unlock(); + std.debug.assert(global.hwnd == hwnd); + global.hwnd = null; + } + + { + // 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) { + // No need for TranslateMessage since we don't use WM_*CHAR messages + //_ = win32.TranslateMessage(&msg); + _ = win32.DispatchMessageW(&msg); + } + + const exit_code = std.math.cast(u32, msg.wParam) orelse 0xffffffff; + std.log.info("gui thread exit {} ({})", .{ exit_code, msg.wParam }); + pid.send(.{"quit"}) catch |e| onexit(e); +} + +pub const Win32Error = struct { + what: [:0]const u8, + code: win32.WIN32_ERROR, + pub fn set(self: *Win32Error, what: [:0]const u8, code: win32.WIN32_ERROR) error{Win32} { + self.* = .{ .what = what, .code = code }; + return error.Win32; + } +}; + +pub fn setWindowTitle(title: [*:0]const u16, err: *Win32Error) error{ NoWindow, Win32 }!void { + global.mutex.lock(); + defer global.mutex.unlock(); + + const hwnd = global.hwnd orelse return error.NoWindow; + if (0 == win32.SetWindowTextW(hwnd, title)) + return err.set("SetWindowText", win32.GetLastError()); +} + +// returns false if there is no hwnd +pub fn updateScreen(screen: *const vaxis.Screen) bool { + global.mutex.lock(); + defer global.mutex.unlock(); + + const hwnd = global.hwnd orelse return false; + const state = stateFromHwnd(hwnd); + + _ = state.shared_screen_arena.reset(.retain_capacity); + + const buf = state.shared_screen_arena.allocator().alloc(vaxis.Cell, screen.buf.len) catch |e| oom(e); + @memcpy(buf, screen.buf); + for (buf) |*cell| { + cell.char.grapheme = state.shared_screen_arena.allocator().dupe(u8, cell.char.grapheme) catch |e| oom(e); + } + state.shared_screen = .{ + .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, + }; + win32.invalidateHwnd(hwnd); + return true; +} + +fn getCellSize( + text_format_editor: *win32.IDWriteTextFormat, +) XY(i32) { + const metrics = getTextFormatMetrics(text_format_editor); + + const font_size = text_format_editor.GetFontSize(); + const pixels_per_design_unit: f32 = font_size / @as(f32, @floatFromInt(metrics.designUnitsPerEm)); + + const width: f32 = getTextFormatWidth(text_format_editor); + + const ascent = @as(f32, @floatFromInt(metrics.ascent)) * pixels_per_design_unit; + const descent = @as(f32, @floatFromInt(metrics.descent)) * pixels_per_design_unit; + const height: f32 = ascent + descent; + // std.log.info( + // "CellSize font_size={d} size={d}x{d}", + // .{ font_size, width, height }, + // ); + return .{ + .x = @intFromFloat(width), + .y = @intFromFloat(height), + }; +} + +fn getTextFormatWidth( + text_format: *win32.IDWriteTextFormat, +) f32 { + var text_layout: *win32.IDWriteTextLayout = undefined; + { + const hr = global.dwrite_factory.CreateTextLayout( + win32.L("0"), + 1, + text_format, // 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 metrics.width; +} + +fn getTextFormatMetrics( + text_format: *win32.IDWriteTextFormat, +) win32.DWRITE_FONT_METRICS { + var collection: *win32.IDWriteFontCollection = undefined; + + { + const hr = text_format.GetFontCollection(&collection); + if (hr < 0) fatalHr("GetFontCollection", hr); + } + defer _ = collection.IUnknown.Release(); + + const max_family_name_len = 300; + var family_name_buf: [max_family_name_len + 1]u16 = undefined; + const family_name_len = text_format.GetFontFamilyNameLength(); + if (family_name_len > max_family_name_len) std.debug.panic( + "family name len {} is too big", + .{family_name_len}, + ); + + family_name_buf[family_name_len] = 0xff; + { + const hr = text_format.GetFontFamilyName(@ptrCast(&family_name_buf), max_family_name_len); + if (hr < 0) fatalHr("GetFontFamilyName", hr); + } + std.debug.assert(family_name_buf[family_name_len] == 0); + + var family_index: u32 = undefined; + + { + var exists: win32.BOOL = undefined; + const hr = collection.FindFamilyName(@ptrCast(&family_name_buf), &family_index, &exists); + if (hr < 0) fatalHr("FindFamilyName", hr); + if (0 == exists) std.debug.panic( + "FontFamily '{}' does not exist?", + .{std.unicode.fmtUtf16le(family_name_buf[0..family_name_len])}, + ); + } + + var family: *win32.IDWriteFontFamily = undefined; + + { + const hr = collection.GetFontFamily(family_index, &family); + if (hr < 0) fatalHr("GetFontFamily", hr); + } + defer _ = family.IUnknown.Release(); + + var font: *win32.IDWriteFont = undefined; + + { + const hr = family.GetFirstMatchingFont( + text_format.GetFontWeight(), + text_format.GetFontStretch(), + text_format.GetFontStyle(), + &font, + ); + if (hr < 0) fatalHr("GetFirstMatchingFont", hr); + } + defer _ = font.IUnknown.Release(); + + var metrics: win32.DWRITE_FONT_METRICS = undefined; + font.GetMetrics(&metrics); + return metrics; +} + +fn cellFromPos(cell_size: XY(i32), x: i32, y: i32) XY(i32) { + return XY(i32){ + .x = @divTrunc(x, cell_size.x), + .y = @divTrunc(y, cell_size.y), + }; +} +fn cellOffsetFromPos(cell_size: XY(i32), x: i32, y: i32) XY(i32) { + return .{ + .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]; +} + +fn sendMouse( + hwnd: win32.HWND, + kind: enum { + move, + left_down, + left_up, + right_down, + right_up, + }, + lparam: win32.LPARAM, +) void { + const point = ddui.pointFromLparam(lparam); + const state = stateFromHwnd(hwnd); + const cell_size = state.currently_rendered_cell_size orelse { + std.log.info("dropping mouse event that occurred before first render", .{}); + return; + }; + const cell = cellFromPos(cell_size, point.x, point.y); + const cell_offset = cellOffsetFromPos(cell_size, point.x, point.y); + switch (kind) { + .move => state.pid.send(.{ + "RDR", + "M", + cell.x, + 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.x, + 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 { + std.log.info("dropping mouse whell event that occurred before first render", .{}); + return; + }; + const cell = cellFromPos(cell_size, point.x, point.y); + const cell_offset = cellOffsetFromPos(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.x, + cell.y, + cell_offset.x, + cell_offset.y, + }) catch |e| onexit(e); + } +} + +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? + // tn::ModifierFlags mod_flags = (tn::ModifierFlags)mod_flags_u32; + + const event = switch (kind) { + .press => input.event.press, + .release => input.event.release, + }; + + const winkey = WinKey{ + .vk = @intCast(0xffff & wparam), + .extended = 0 != (lparam & 0x1000000), + }; + if (winkey.toFlow(mods.shift)) |key| { + state.pid.send(.{ + "RDR", + "I", + event, + @as(u21, key), + @as(u21, key), + "", + @as(u8, @bitCast(mods)), + }) catch |e| onexit(e); + return; + } + + const max_char_count = 20; + var char_buf: [max_char_count + 1]u16 = undefined; + const unicode_result = win32.ToUnicode( + winkey.vk, + @intCast((@as(usize, @bitCast(lparam)) >> 16) & 0xff), + &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}", .{ i, codepoint }); + } + std.debug.panic("TODO: unicode result is {}", .{unicode_result}); + } + + for (char_buf[0..@intCast(unicode_result)]) |codepoint| { + state.pid.send(.{ + "RDR", + "I", + event, + @as(u21, codepoint), + // TODO: shifted_codepoint? + @as(u21, codepoint), + "", // text? + @as(u8, @bitCast(mods)), + }) catch |e| onexit(e); + } +} + +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 toFlow(self: WinKey, shift_down: bool) ?u16 { + if (self.extended) return switch (self.vk) { + @intFromEnum(win32.VK_RETURN) => input.key.kp_enter, + @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, + @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, + + '0'...'9' => |ascii| ascii, + 'A'...'Z' => |ascii| if (shift_down) ascii else ascii + ('a' - 'A'), + + @intFromEnum(win32.VK_LWIN) => input.key.left_meta, + @intFromEnum(win32.VK_RWIN) => input.key.right_meta, + @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_10) => input.key.left_shift, + @intFromEnum(win32.VK_RSHIFT) => input.key.right_shift, + @intFromEnum(win32.VK_LCONTROL) => input.key.left_control, + //@intFromEnum(win32.VK_11) => input.key.left_control, + @intFromEnum(win32.VK_RCONTROL) => input.key.right_control, + @intFromEnum(win32.VK_LMENU) => input.key.left_alt, + //@intFromEnum(win32.VK_12) => 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, + }; + } +}; + +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, lparam); + return 0; + }, + win32.WM_LBUTTONDOWN => { + sendMouse(hwnd, .left_down, lparam); + return 0; + }, + win32.WM_LBUTTONUP => { + sendMouse(hwnd, .left_up, lparam); + return 0; + }, + win32.WM_RBUTTONDOWN => { + sendMouse(hwnd, .right_down, lparam); + return 0; + }, + win32.WM_RBUTTONUP => { + sendMouse(hwnd, .right_up, 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.mutex.lock(); + defer global.mutex.unlock(); + paint( + &state.maybe_d2d.?, + &state.shared_screen, + 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 => { + win32.PostQuitMessage(0); + return 0; + }, + 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, + .shared_screen_arena = std.heap.ArenaAllocator.init(std.heap.page_allocator), + }; + const existing = win32.SetWindowLongPtrW( + hwnd, + @enumFromInt(0), + @as(isize, @bitCast(@intFromPtr(state))), + ); + std.debug.assert(existing == 0); + std.debug.assert(state == stateFromHwnd(hwnd)); + return 0; + }, + win32.WM_DESTROY => { + const state = stateFromHwnd(hwnd); + state.deinit(); + // no need to free, it was allocated via an arena + return 0; + }, + 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.info( + "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, +}; diff --git a/src/win32/windowmsg.zig b/src/win32/windowmsg.zig new file mode 100644 index 0000000..d16faed --- /dev/null +++ b/src/win32/windowmsg.zig @@ -0,0 +1,365 @@ +const std = @import("std"); +const win32 = @import("win32").everything; + +pub fn pointFromLparam(lparam: win32.LPARAM) win32.POINT { + return .{ + .x = @as(i16, @bitCast(win32.loword(lparam))), + .y = @as(i16, @bitCast(win32.hiword(lparam))), + }; +} + +pub const MessageNode = struct { + tail_ref: *?*MessageNode, + hwnd: win32.HWND, + msg: u32, + wparam: win32.WPARAM, + lparam: win32.LPARAM, + old_tail: ?*MessageNode, + pub fn init( + self: *MessageNode, + tail_ref: *?*MessageNode, + hwnd: win32.HWND, + msg: u32, + wparam: win32.WPARAM, + lparam: win32.LPARAM, + ) void { + if (tail_ref.*) |old_tail| { + std.debug.assert(old_tail.hwnd == hwnd); + } + self.* = .{ + .tail_ref = tail_ref, + .hwnd = hwnd, + .msg = msg, + .wparam = wparam, + .lparam = lparam, + .old_tail = tail_ref.*, + }; + tail_ref.* = self; + } + pub fn deinit(self: *MessageNode) void { + std.debug.assert(self.tail_ref.* == self); + self.tail_ref.* = self.old_tail; + } + pub fn fmtPath(self: *MessageNode) FmtPath { + return .{ .node = self }; + } +}; + +fn writeMessageNodePath( + writer: anytype, + node: *MessageNode, +) !void { + if (node.old_tail) |old_tail| { + try writeMessageNodePath(writer, old_tail); + try writer.writeAll(" > "); + } + try writer.print("{s}:{}", .{ msg_name(node.msg) orelse "?", node.msg }); + switch (node.msg) { + win32.WM_NCMOUSEMOVE, + win32.WM_NCLBUTTONDOWN, + => { + const hit_value = node.wparam; + const point = pointFromLparam(node.lparam); + try writer.print( + "(hit={s}:{},point={},{})", + .{ getHitName(@bitCast(hit_value)) orelse "?", hit_value, point.x, point.y }, + ); + }, + win32.WM_NCHITTEST, + win32.WM_MOUSEMOVE, + => { + const point = pointFromLparam(node.lparam); + try writer.print("({},{})", .{ point.x, point.y }); + }, + win32.WM_CAPTURECHANGED => { + try writer.print("({})", .{node.lparam}); + }, + win32.WM_SYSCOMMAND => { + try writer.print("(type=0x{x})", .{0xfff0 & node.wparam}); + }, + else => {}, + } +} + +const FmtPath = struct { + node: *MessageNode, + const Self = @This(); + pub fn format( + self: Self, + comptime fmt: []const u8, + options: std.fmt.FormatOptions, + writer: anytype, + ) @TypeOf(writer).Error!void { + _ = fmt; + _ = options; + try writeMessageNodePath(writer, self.node); + } +}; + +pub fn msg_name(msg: u32) ?[]const u8 { + return switch (msg) { + 0 => "WM_NULL", + 1 => "WM_CREATE", + 2 => "WM_DESTROY", + 3 => "WM_MOVE", + 5 => "WM_SIZE", + 6 => "WM_ACTIVATE", + 7 => "WM_SETFOCUS", + 8 => "WM_KILLFOCUS", + 10 => "WM_ENABLE", + 11 => "WM_SETREDRAW", + 12 => "WM_SETTEXT", + 13 => "WM_GETTEXT", + 14 => "WM_GETTEXTLENGTH", + 15 => "WM_PAINT", + 16 => "WM_CLOSE", + 17 => "WM_QUERYENDSESSION", + 18 => "WM_QUIT", + 19 => "WM_QUERYOPEN", + 20 => "WM_ERASEBKGND", + 21 => "WM_SYSCOLORCHANGE", + 22 => "WM_ENDSESSION", + 24 => "WM_SHOWWINDOW", + 25 => "WM_CTLCOLOR", + 26 => "WM_WININICHANGE", + 27 => "WM_DEVMODECHANGE", + 28 => "WM_ACTIVATEAPP", + 29 => "WM_FONTCHANGE", + 30 => "WM_TIMECHANGE", + 31 => "WM_CANCELMODE", + 32 => "WM_SETCURSOR", + 33 => "WM_MOUSEACTIVATE", + 34 => "WM_CHILDACTIVATE", + 35 => "WM_QUEUESYNC", + 36 => "WM_GETMINMAXINFO", + 38 => "WM_PAINTICON", + 39 => "WM_ICONERASEBKGND", + 40 => "WM_NEXTDLGCTL", + 42 => "WM_SPOOLERSTATUS", + 43 => "WM_DRAWITEM", + 44 => "WM_MEASUREITEM", + 45 => "WM_DELETEITEM", + 46 => "WM_VKEYTOITEM", + 47 => "WM_CHARTOITEM", + 48 => "WM_SETFONT", + 49 => "WM_GETFONT", + 50 => "WM_SETHOTKEY", + 51 => "WM_GETHOTKEY", + 55 => "WM_QUERYDRAGICON", + 57 => "WM_COMPAREITEM", + 61 => "WM_GETOBJECT", + 65 => "WM_COMPACTING", + 68 => "WM_COMMNOTIFY", + 70 => "WM_WINDOWPOSCHANGING", + 71 => "WM_WINDOWPOSCHANGED", + 72 => "WM_POWER", + 73 => "WM_COPYGLOBALDATA", + 74 => "WM_COPYDATA", + 75 => "WM_CANCELJOURNAL", + 78 => "WM_NOTIFY", + 80 => "WM_INPUTLANGCHANGEREQUEST", + 81 => "WM_INPUTLANGCHANGE", + 82 => "WM_TCARD", + 83 => "WM_HELP", + 84 => "WM_USERCHANGED", + 85 => "WM_NOTIFYFORMAT", + 123 => "WM_CONTEXTMENU", + 124 => "WM_STYLECHANGING", + 125 => "WM_STYLECHANGED", + 126 => "WM_DISPLAYCHANGE", + 127 => "WM_GETICON", + 128 => "WM_SETICON", + 129 => "WM_NCCREATE", + 130 => "WM_NCDESTROY", + 131 => "WM_NCCALCSIZE", + 132 => "WM_NCHITTEST", + 133 => "WM_NCPAINT", + 134 => "WM_NCACTIVATE", + 135 => "WM_GETDLGCODE", + 136 => "WM_SYNCPAINT", + 160 => "WM_NCMOUSEMOVE", + 161 => "WM_NCLBUTTONDOWN", + 162 => "WM_NCLBUTTONUP", + 163 => "WM_NCLBUTTONDBLCLK", + 164 => "WM_NCRBUTTONDOWN", + 165 => "WM_NCRBUTTONUP", + 166 => "WM_NCRBUTTONDBLCLK", + 167 => "WM_NCMBUTTONDOWN", + 168 => "WM_NCMBUTTONUP", + 169 => "WM_NCMBUTTONDBLCLK", + 171 => "WM_NCXBUTTONDOWN", + 172 => "WM_NCXBUTTONUP", + 173 => "WM_NCXBUTTONDBLCLK", + 255 => "WM_INPUT", + 256 => "WM_KEYDOWN", + 257 => "WM_KEYUP", + 258 => "WM_CHAR", + 259 => "WM_DEADCHAR", + 260 => "WM_SYSKEYDOWN", + 261 => "WM_SYSKEYUP", + 262 => "WM_SYSCHAR", + 263 => "WM_SYSDEADCHAR", + 265 => "WM_UNICHAR", + 266 => "WM_CONVERTREQUEST", + 267 => "WM_CONVERTRESULT", + 268 => "WM_INTERIM", + 269 => "WM_IME_STARTCOMPOSITION", + 270 => "WM_IME_ENDCOMPOSITION", + 271 => "WM_IME_COMPOSITION", + 272 => "WM_INITDIALOG", + 273 => "WM_COMMAND", + 274 => "WM_SYSCOMMAND", + 275 => "WM_TIMER", + 276 => "WM_HSCROLL", + 277 => "WM_VSCROLL", + 278 => "WM_INITMENU", + 279 => "WM_INITMENUPOPUP", + 280 => "WM_SYSTIMER", + 287 => "WM_MENUSELECT", + 288 => "WM_MENUCHAR", + 289 => "WM_ENTERIDLE", + 290 => "WM_MENURBUTTONUP", + 291 => "WM_MENUDRAG", + 292 => "WM_MENUGETOBJECT", + 293 => "WM_UNINITMENUPOPUP", + 294 => "WM_MENUCOMMAND", + 295 => "WM_CHANGEUISTATE", + 296 => "WM_UPDATEUISTATE", + 297 => "WM_QUERYUISTATE", + 305 => "WM_LBTRACKPOINT", + 306 => "WM_CTLCOLORMSGBOX", + 307 => "WM_CTLCOLOREDIT", + 308 => "WM_CTLCOLORLISTBOX", + 309 => "WM_CTLCOLORBTN", + 310 => "WM_CTLCOLORDLG", + 311 => "WM_CTLCOLORSCROLLBAR", + 312 => "WM_CTLCOLORSTATIC", + 512 => "WM_MOUSEMOVE", + 513 => "WM_LBUTTONDOWN", + 514 => "WM_LBUTTONUP", + 515 => "WM_LBUTTONDBLCLK", + 516 => "WM_RBUTTONDOWN", + 517 => "WM_RBUTTONUP", + 518 => "WM_RBUTTONDBLCLK", + 519 => "WM_MBUTTONDOWN", + 520 => "WM_MBUTTONUP", + 521 => "WM_MBUTTONDBLCLK", + 522 => "WM_MOUSEWHEEL", + 523 => "WM_XBUTTONDOWN", + 524 => "WM_XBUTTONUP", + 525 => "WM_XBUTTONDBLCLK", + 526 => "WM_MOUSEHWHEEL", + 528 => "WM_PARENTNOTIFY", + 529 => "WM_ENTERMENULOOP", + 530 => "WM_EXITMENULOOP", + 531 => "WM_NEXTMENU", + 532 => "WM_SIZING", + 533 => "WM_CAPTURECHANGED", + 534 => "WM_MOVING", + 536 => "WM_POWERBROADCAST", + 537 => "WM_DEVICECHANGE", + 544 => "WM_MDICREATE", + 545 => "WM_MDIDESTROY", + 546 => "WM_MDIACTIVATE", + 547 => "WM_MDIRESTORE", + 548 => "WM_MDINEXT", + 549 => "WM_MDIMAXIMIZE", + 550 => "WM_MDITILE", + 551 => "WM_MDICASCADE", + 552 => "WM_MDIICONARRANGE", + 553 => "WM_MDIGETACTIVE", + 560 => "WM_MDISETMENU", + 561 => "WM_ENTERSIZEMOVE", + 562 => "WM_EXITSIZEMOVE", + 563 => "WM_DROPFILES", + 564 => "WM_MDIREFRESHMENU", + 640 => "WM_IME_REPORT", + 641 => "WM_IME_SETCONTEXT", + 642 => "WM_IME_NOTIFY", + 643 => "WM_IME_CONTROL", + 644 => "WM_IME_COMPOSITIONFULL", + 645 => "WM_IME_SELECT", + 646 => "WM_IME_CHAR", + 648 => "WM_IME_REQUEST", + 656 => "WM_IME_KEYDOWN", + 657 => "WM_IME_KEYUP", + 672 => "WM_NCMOUSEHOVER", + 673 => "WM_MOUSEHOVER", + 674 => "WM_NCMOUSELEAVE", + 675 => "WM_MOUSELEAVE", + 768 => "WM_CUT", + 769 => "WM_COPY", + 770 => "WM_PASTE", + 771 => "WM_CLEAR", + 772 => "WM_UNDO", + 773 => "WM_RENDERFORMAT", + 774 => "WM_RENDERALLFORMATS", + 775 => "WM_DESTROYCLIPBOARD", + 776 => "WM_DRAWCLIPBOARD", + 777 => "WM_PAINTCLIPBOARD", + 778 => "WM_VSCROLLCLIPBOARD", + 779 => "WM_SIZECLIPBOARD", + 780 => "WM_ASKCBFORMATNAME", + 781 => "WM_CHANGECBCHAIN", + 782 => "WM_HSCROLLCLIPBOARD", + 783 => "WM_QUERYNEWPALETTE", + 784 => "WM_PALETTEISCHANGING", + 785 => "WM_PALETTECHANGED", + 786 => "WM_HOTKEY", + 791 => "WM_PRINT", + 792 => "WM_PRINTCLIENT", + 793 => "WM_APPCOMMAND", + 799 => "WM_DWMNCRENDERINGCHANGED", + 856 => "WM_HANDHELDFIRST", + 863 => "WM_HANDHELDLAST", + 864 => "WM_AFXFIRST", + 895 => "WM_AFXLAST", + 896 => "WM_PENWINFIRST", + 897 => "WM_RCRESULT", + 898 => "WM_HOOKRCRESULT", + 899 => "WM_GLOBALRCCHANGE", + 900 => "WM_SKB", + 901 => "WM_PENCTL", + 902 => "WM_PENMISC", + 903 => "WM_CTLINIT", + 904 => "WM_PENEVENT", + 911 => "WM_PENWINLAST", + 1024 => "WM_USER+0", + 1025 => "WM_USER+1", + 1026 => "WM_USER+2", + 1027 => "WM_USER+3", + 1028 => "WM_USER+4", + 1029 => "WM_USER+5", + 1030 => "WM_USER+6", + else => null, + }; +} + +pub fn getHitName(hit: win32.LRESULT) ?[]const u8 { + return switch (hit) { + win32.HTERROR => "err", + win32.HTTRANSPARENT => "transprnt", + win32.HTNOWHERE => "nowhere", + win32.HTCLIENT => "client", + win32.HTCAPTION => "caption", + win32.HTSYSMENU => "sysmnu", + win32.HTSIZE => "size", + win32.HTMENU => "menu", + win32.HTHSCROLL => "hscroll", + win32.HTVSCROLL => "vscroll", + win32.HTMINBUTTON => "minbtn", + win32.HTMAXBUTTON => "max", + win32.HTLEFT => "left", + win32.HTRIGHT => "right", + win32.HTTOP => "top", + win32.HTTOPLEFT => "topleft", + win32.HTTOPRIGHT => "topright", + win32.HTBOTTOM => "bottom", + win32.HTBOTTOMLEFT => "botmleft", + win32.HTBOTTOMRIGHT => "botmright", + win32.HTBORDER => "border", + win32.HTCLOSE => "close", + win32.HTHELP => "help", + else => null, + }; +}