diff --git a/build.zig b/build.zig index c9b0a7e..e5ad86e 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,51 @@ 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 }, + }, + }); + 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 +445,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 +465,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/win32/renderer.zig b/src/renderer/win32/renderer.zig new file mode 100644 index 0000000..5af72d7 --- /dev/null +++ b/src/renderer/win32/renderer.zig @@ -0,0 +1,363 @@ +const Self = @This(); +pub const log_name = "renderer"; + +const std = @import("std"); +const cbor = @import("cbor"); +const thespian = @import("thespian"); +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 input_fd_blocking(self: Self) i32 { + _ = self; + @panic("todo"); +} +pub fn leave_alternate_screen(self: *Self) void { + _ = self; + @panic("todo"); +} +pub fn process_gui_event(self: *Self, m: thespian.message) !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 m.match(.{ + thespian.any, + "I", + thespian.extract(&args.kind), + thespian.extract(&args.codepoint), + thespian.extract(&args.shifted_codepoint), + thespian.extract(&args.text), + thespian.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 m.match(.{ + thespian.any, + "Resize", + thespian.extract(&args.cell_width), + thespian.extract(&args.cell_height), + thespian.extract(&args.pixel_width), + thespian.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 m.match(.{ + thespian.any, + "M", + thespian.extract(&args.col), + thespian.extract(&args.row), + thespian.extract(&args.xoffset), + thespian.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 m.match(.{ + thespian.any, + "B", + thespian.extract(&args.button.press), + thespian.extract(&args.button.id), + thespian.extract(&args.pos.col), + thespian.extract(&args.pos.row), + thespian.extract(&args.pos.xoffset), + thespian.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 thespian.unexpected(m); +} + +pub fn process_input_event(self: *Self, input_: []const u8, text: ?[]const u8) !void { + _ = self; + _ = input_; + _ = text; + @panic("todo"); +} +pub fn set_terminal_title(self: *Self, text: []const u8) void { + _ = self; + std.log.warn("TODO: set_terminal_title '{s}'", .{text}); +} +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; + @panic("todo"); +} +pub fn request_system_clipboard(self: *Self) void { + _ = self; + @panic("todo"); +} +pub fn request_windows_clipboard(self: *Self) ![]u8 { + _ = self; + @panic("todo"); +} +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"); +} +pub fn ucs32_to_utf8(ucs32: []const u32, utf8: []u8) !usize { + _ = ucs32; + _ = utf8; + @panic("todo"); +} diff --git a/src/tui/tui.zig b/src/tui/tui.zig index e8b5758..dc44cfa 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"); @@ -256,6 +257,13 @@ fn receive_safe(self: *Self, from: tp.pid_ref, m: tp.message) !void { return; } + if (build_options.gui) { + if (try m.match(.{ "GUI", tp.more })) { + try self.rdr.process_gui_event(m); + return; + } + } + if (self.message_filters.filter(from, m) catch |e| return self.logger.err("filter", e)) return; 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..8a61a42 --- /dev/null +++ b/src/win32/gui.zig @@ -0,0 +1,918 @@ +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 => @panic("todo: color index"), + .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) = .{}, + + // 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, + dpi: u32, + screen: *const vaxis.Screen, + text_format_editor: *win32.IDWriteTextFormat, +) void { + { + const color = ddui.rgb8(31, 31, 31); + d2d.target.ID2D1RenderTarget.Clear(&color); + } + const cell_size = getCellSize(dpi, text_format_editor); + 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); +} + +// 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( + dpi: u32, + text_format_editor: *win32.IDWriteTextFormat, +) XY(i32) { + const metrics = getTextFormatMetrics(text_format_editor); + + // TODO: get the actual font metrics + 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 width_scaled: i32 = @intFromFloat(@ceil(win32.scaleDpi(f32, width, dpi))); + + 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; + const height_scaled: i32 = @intFromFloat(@ceil(win32.scaleDpi(f32, height, dpi))); + // std.log.info( + // "CellSize font_size={d} size={d}x{d} scaled-size={}x{}", + // .{ font_size, width, height, width_scaled, height_scaled }, + // ); + return .{ + .x = width_scaled, + .y = height_scaled, + }; +} + +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 dpi = win32.dpiFromHwnd(hwnd); + const text_format = state.text_format_editor.getOrCreate(Dpi{ .value = dpi }); + const cell_size = getCellSize(dpi, text_format); + 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(.{ + "GUI", + "M", + cell.x, + cell.y, + cell_offset.x, + cell_offset.y, + }) catch |e| onexit(e), + else => |b| state.pid.send(.{ + "GUI", + "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 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 winkey = WinKey{ + .vk = @intCast(0xffff & wparam), + .extended = 0 != (lparam & 0x1000000), + }; + + 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(.{ + "GUI", + "I", + switch (kind) { + .press => input.event.press, + .release => input.event.release, + }, + @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) u32 { + _ = self; + @panic("todo"); + } +}; + +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_KEYDOWN => { + sendKey(hwnd, .press, wparam, lparam); + return 0; + }, + win32.WM_KEYUP => { + 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.width), + .height = @intCast(client_size.height), + }; + const hr = state.maybe_d2d.?.target.Resize(&size); + if (hr < 0) break :blk HResultError{ .context = "D2dResize", .hr = hr }; + } + state.maybe_d2d.?.target.ID2D1RenderTarget.BeginDraw(); + + { + global.mutex.lock(); + defer global.mutex.unlock(); + paint( + &state.maybe_d2d.?, + dpi, + &state.shared_screen, + state.text_format_editor.getOrCreate(Dpi{ .value = dpi }), + ); + } + + 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_SIZE => { + const client_pixel_size: XY(u16) = .{ + .x = win32.loword(lparam), + .y = win32.hiword(lparam), + }; + + 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( + dpi, + state.text_format_editor.getOrCreate(Dpi{ .value = @intCast(dpi) }), + ); + 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("new size {}x{} {}x{}", .{ new_size.x, new_size.y, new_cell_size.x, new_cell_size.y }); + state.pid.send(.{ + "GUI", + "Resize", + client_cell_size.x, + client_cell_size.y, + client_pixel_size.x, + client_pixel_size.y, + }) catch @panic("pid send failed"); + 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), + } +} + +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) win32.D2D_SIZE_U { + var rect: win32.RECT = undefined; + if (0 == win32.GetClientRect(hwnd, &rect)) + fatalWin32("GetClientRect", win32.GetLastError()); + return .{ + .width = @intCast(rect.right - rect.left), + .height = @intCast(rect.bottom - rect.top), + }; +} +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 }; + } + }; +} 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, + }; +}