From 337b6ce6266a581c4b48ddd89a3453688d639892 Mon Sep 17 00:00:00 2001 From: Jonathan Marler Date: Tue, 7 Jan 2025 13:14:55 -0700 Subject: [PATCH] win32 gui: rework startup/hwnd sync --- src/keybind/builtin/emacs.json | 3 +- src/renderer/vaxis/renderer.zig | 2 +- src/renderer/win32/renderer.zig | 60 ++++++++--- src/tui/tui.zig | 17 +++- src/win32/gui.zig | 170 ++++++++++++++------------------ 5 files changed, 137 insertions(+), 115 deletions(-) diff --git a/src/keybind/builtin/emacs.json b/src/keybind/builtin/emacs.json index 9649ac48..27abb545 100644 --- a/src/keybind/builtin/emacs.json +++ b/src/keybind/builtin/emacs.json @@ -71,7 +71,8 @@ "on_match_failure": "ignore", "press": [ ["ctrl+h ctrl+a", "open_help"], - ["ctrl+x ctrl+f", "open_recent"], + ["ctrl+x ctrl+f", "open_file"], + ["ctrl+x b", "open_recent"], ["alt+x", "open_command_palette"], ["ctrl+x ctrl+c", "quit"] ] diff --git a/src/renderer/vaxis/renderer.zig b/src/renderer/vaxis/renderer.zig index 7221c969..01d43e68 100644 --- a/src/renderer/vaxis/renderer.zig +++ b/src/renderer/vaxis/renderer.zig @@ -39,7 +39,7 @@ logger: log.Logger, loop: Loop, -pub fn init(allocator: std.mem.Allocator, handler_ctx: *anyopaque, no_alternate: bool) !Self { +pub fn init(allocator: std.mem.Allocator, handler_ctx: *anyopaque, no_alternate: bool, _: *const fn (ctx: *anyopaque) void) !Self { const opts: vaxis.Vaxis.Options = .{ .kitty_keyboard_flags = .{ .disambiguate = true, diff --git a/src/renderer/win32/renderer.zig b/src/renderer/win32/renderer.zig index 234c2f46..b09909b6 100644 --- a/src/renderer/win32/renderer.zig +++ b/src/renderer/win32/renderer.zig @@ -23,6 +23,7 @@ allocator: std.mem.Allocator, vx: vaxis.Vaxis, handler_ctx: *anyopaque, +dispatch_initialized: *const fn (ctx: *anyopaque) void, 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, @@ -30,7 +31,9 @@ dispatch_event: ?*const fn (ctx: *anyopaque, cbor_msg: []const u8) void = null, thread: ?std.Thread = null, +hwnd: ?win32.HWND = null, title_buf: std.ArrayList(u16), +style: ?Style = null, const global = struct { var init_called: bool = false; @@ -44,6 +47,7 @@ pub fn init( allocator: std.mem.Allocator, handler_ctx: *anyopaque, no_alternate: bool, + dispatch_initialized: *const fn (ctx: *anyopaque) void, ) !Self { std.debug.assert(!global.init_called); global.init_called = true; @@ -66,6 +70,7 @@ pub fn init( .vx = try vaxis.init(allocator, opts), .handler_ctx = handler_ctx, .title_buf = std.ArrayList(u16).init(allocator), + .dispatch_initialized = dispatch_initialized, }; result.vx.caps.unicode = .unicode; result.vx.screen.width_method = .unicode; @@ -122,9 +127,13 @@ pub fn fmtmsg(buf: []u8, value: anytype) []const u8 { pub fn render(self: *Self) error{}!void { _ = gui.updateScreen(&self.vx.screen); + if (self.hwnd) |hwnd| win32.invalidateHwnd(hwnd); } pub fn stop(self: *Self) void { - gui.stop(); + // this is guaranteed because stop won't be called until after + // the window is created and we call dispatch_initialized + const hwnd = self.hwnd orelse unreachable; + gui.stop(hwnd); if (self.thread) |thread| thread.join(); } @@ -207,8 +216,6 @@ pub fn process_renderer_event(self: *Self, msg: []const u8) !void { var buf: [200]u8 = undefined; if (self.dispatch_event) |f| f(self.handler_ctx, fmtmsg(&buf, .{"resize"})); } - if (self.title_buf.items.len > 0) - self.set_terminal_title_internal(); return; } } @@ -308,30 +315,57 @@ pub fn process_renderer_event(self: *Self, msg: []const u8) !void { return; } } + { + var hwnd: usize = undefined; + if (try cbor.match(msg, .{ + cbor.any, + "WindowCreated", + cbor.extract(&hwnd), + })) { + std.debug.assert(self.hwnd == null); + self.hwnd = @ptrFromInt(hwnd); + self.dispatch_initialized(self.handler_ctx); + self.update_window_title(); + self.update_window_style(); + return; + } + } return error.UnexpectedRendererEvent; } pub fn set_terminal_title(self: *Self, text: []const u8) void { + self.title_buf.clearRetainingCapacity(); std.unicode.utf8ToUtf16LeArrayList(&self.title_buf, text) catch { std.log.err("title is invalid UTF-8", .{}); return; }; - self.set_terminal_title_internal(); + self.update_window_title(); } -fn set_terminal_title_internal(self: *Self) void { - const title = self.title_buf.toOwnedSliceSentinel(0) catch @panic("OOM:set_terminal_title"); - gui.set_window_title(title) catch { - // leave self.title_buf to try again later +fn update_window_title(self: *Self) void { + if (self.title_buf.items.len == 0) return; + + // keep the title buf around if the window isn't created yet + const hwnd = self.hwnd orelse return; + + const title = self.title_buf.toOwnedSliceSentinel(0) catch @panic("OOM:update_window_title"); + if (win32.SetWindowTextW(hwnd, title) == 0) { + std.log.warn("SetWindowText failed with {}", .{win32.GetLastError().fmt()}); self.title_buf = std.ArrayList(u16).fromOwnedSlice(self.allocator, title); - return; - }; - self.allocator.free(title); + } else { + self.allocator.free(title); + } } pub fn set_terminal_style(self: *Self, style_: Style) void { - _ = self; - if (style_.bg) |color| gui.set_window_background(@intCast(color.color)); + self.style = style_; + self.update_window_style(); +} +fn update_window_style(self: *Self) void { + const hwnd = self.hwnd orelse return; + if (self.style) |style_| { + if (style_.bg) |color| gui.set_window_background(hwnd, @intCast(color.color)); + } } pub fn set_terminal_cursor_color(self: *Self, color: Color) void { diff --git a/src/tui/tui.zig b/src/tui/tui.zig index 02d6ca81..ed4cc478 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -106,7 +106,7 @@ fn init(allocator: Allocator) !*Self { self.* = .{ .allocator = allocator, .config = conf, - .rdr = try renderer.init(allocator, self, tp.env.get().is("no-alternate")), + .rdr = try renderer.init(allocator, self, tp.env.get().is("no-alternate"), dispatch_initialized), .frame_time = frame_time, .frame_clock = frame_clock, .frame_clock_running = true, @@ -115,7 +115,9 @@ fn init(allocator: Allocator) !*Self { .message_filters = MessageFilter.List.init(allocator), .input_listeners = EventHandler.List.init(allocator), .logger = log.logger("tui"), - .init_timer = try tp.timeout.init_ms(init_delay, tp.message.fmt(.{"init"})), + .init_timer = if (build_options.gui) null else try tp.timeout.init_ms(init_delay, tp.message.fmt( + .{"init"}, + )), .theme = theme, .no_sleep = tp.env.get().is("no-sleep"), }; @@ -339,7 +341,7 @@ fn receive_safe(self: *Self, from: tp.pid_ref, m: tp.message) !void { if (self.init_timer) |*timer| { timer.deinit(); self.init_timer = null; - } else { + } else if (!build_options.gui) { return tp.unexpected(m); } return; @@ -446,6 +448,13 @@ fn dispatch_flush_input_event(self: *Self) !void { if (mode.event_handler) |eh| try eh.send(tp.self_pid(), try tp.message.fmtbuf(&buf, .{"F"})); } +fn dispatch_initialized(ctx: *anyopaque) void { + _ = ctx; + tp.self_pid().send(.{"init"}) catch |e| switch (e) { + error.Exit => {}, // safe to ignore + }; +} + fn dispatch_input(ctx: *anyopaque, cbor_msg: []const u8) void { const self: *Self = @ptrCast(@alignCast(ctx)); const m: tp.message = .{ .buf = cbor_msg }; @@ -1109,7 +1118,7 @@ pub const fallbacks: []const FallBack = &[_]FallBack{ }; fn set_terminal_style(self: *Self) void { - if (self.config.enable_terminal_color_scheme) { + if (build_options.gui or self.config.enable_terminal_color_scheme) { self.rdr.set_terminal_style(self.theme.editor); self.rdr.set_terminal_cursor_color(self.theme.editor_cursor.bg.?); } diff --git a/src/win32/gui.zig b/src/win32/gui.zig index 4359d169..033ad611 100644 --- a/src/win32/gui.zig +++ b/src/win32/gui.zig @@ -11,6 +11,7 @@ const cbor = @import("cbor"); const thespian = @import("thespian"); const vaxis = @import("vaxis"); +const RGB = @import("color").RGB; const input = @import("input"); const windowmsg = @import("windowmsg.zig"); @@ -19,6 +20,9 @@ const HResultError = ddui.HResultError; const WM_APP_EXIT = win32.WM_APP + 1; const WM_APP_SET_BACKGROUND = win32.WM_APP + 2; +const WM_APP_EXIT_RESULT = 0x45feaa11; +const WM_APP_SET_BACKGROUND_RESULT = 0x369a26cd; + pub const DropWriter = struct { pub const WriteError = error{}; pub const Writer = std.io.Writer(DropWriter, WriteError, write); @@ -41,15 +45,18 @@ fn onexit(e: error{Exit}) void { } 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 shared_screen = struct { + var mutex: std.Thread.Mutex = .{}; + // only access arena/obj while the mutex is locked + var arena: std.heap.ArenaAllocator = std.heap.ArenaAllocator.init(std.heap.page_allocator); + var obj: vaxis.Screen = .{}; + }; }; const window_style_ex = win32.WINDOW_EX_STYLE{ //.ACCEPTFILES = 1, @@ -115,7 +122,7 @@ 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]); + const rgb = RGB.from_u24(xterm_colors[idx]); break :blk .{ .r = @as(f32, @floatFromInt(rgb.r)) / 255.0, .g = @as(f32, @floatFromInt(rgb.g)) / 255.0, @@ -220,22 +227,7 @@ const State = struct { 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; - } + background: ?u32 = null, }; fn stateFromHwnd(hwnd: win32.HWND) *State { const addr: usize = @bitCast(win32.GetWindowLongPtrW(hwnd, @enumFromInt(0))); @@ -245,12 +237,13 @@ fn stateFromHwnd(hwnd: win32.HWND) *State { fn paint( d2d: *const D2d, + background: RGB, screen: *const vaxis.Screen, text_format_editor: *win32.IDWriteTextFormat, cell_size: XY(i32), ) void { { - const color = ddui.rgb8(31, 31, 31); + const color = ddui.rgb8(background.r, background.g, background.b); d2d.target.ID2D1RenderTarget.Clear(&color); } @@ -406,27 +399,24 @@ fn entry(pid: thespian.pid) !void { global.icons = getIcons(initial_placement.dpi); // 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(), - ); - } + const wc = win32.WNDCLASSEXW{ + .cbSize = @sizeOf(win32.WNDCLASSEXW), + .style = .{}, + .lpfnWndProc = WndProc, + .cbClsExtra = 0, + .cbWndExtra = @sizeOf(*State), + .hInstance = win32.GetModuleHandleW(null), + .hIcon = global.icons.large, + .hCursor = win32.LoadCursorW(null, win32.IDC_ARROW), + .hbrBackground = null, + .lpszMenuName = null, + .lpszClassName = CLASS_NAME, + .hIconSm = global.icons.small, + }; + if (0 == win32.RegisterClassExW(&wc)) fatalWin32( + "RegisterClass for main window", + win32.GetLastError(), + ); var create_args = CreateWindowArgs{ .allocator = arena_instance.allocator(), @@ -446,20 +436,14 @@ fn entry(pid: thespian.pid) !void { 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; - } + // NEVER DESTROY THE WINDOW! + // This allows us to send the hwnd to other thread/parts + // of the app and it will always be valid. + pid.send(.{ + "RDR", + "WindowCreated", + @intFromPtr(hwnd), + }) catch |e| return onexit(e); { // TODO: maybe use DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1 if applicable @@ -491,42 +475,33 @@ fn entry(pid: thespian.pid) !void { pid.send(.{"quit"}) catch |e| onexit(e); } -pub fn stop() void { - const hwnd = global.hwnd orelse return; - _ = win32.SendMessageW(hwnd, WM_APP_EXIT, 0, 0); -} - -pub fn set_window_title(title: [*:0]const u16) error{ NoWindow, Win32 }!void { - global.mutex.lock(); - defer global.mutex.unlock(); - const hwnd = global.hwnd orelse return error.NoWindow; - if (win32.SetWindowTextW(hwnd, title) == 0) { - std.log.warn("error in SetWindowText: {}", .{win32.GetLastError()}); - return error.Win32; - } +pub fn stop(hwnd: win32.HWND) void { + std.debug.assert(WM_APP_EXIT_RESULT == win32.SendMessageW(hwnd, WM_APP_EXIT, 0, 0)); } -pub fn set_window_background(color: u32) void { - const hwnd = global.hwnd orelse return; - _ = win32.SendMessageW(hwnd, WM_APP_SET_BACKGROUND, color, 0); +pub fn set_window_background(hwnd: win32.HWND, color: u32) void { + std.debug.assert(WM_APP_SET_BACKGROUND_RESULT == win32.SendMessageW( + hwnd, + WM_APP_SET_BACKGROUND, + color, + 0, + )); } -// 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); +pub fn updateScreen(screen: *const vaxis.Screen) void { + global.shared_screen.mutex.lock(); + defer global.shared_screen.mutex.unlock(); + _ = global.shared_screen.arena.reset(.retain_capacity); - const buf = state.shared_screen_arena.allocator().alloc(vaxis.Cell, screen.buf.len) catch |e| oom(e); + const buf = global.shared_screen.arena.allocator().alloc(vaxis.Cell, screen.buf.len) catch |e| oom(e); @memcpy(buf, screen.buf); for (buf) |*cell| { - cell.char.grapheme = state.shared_screen_arena.allocator().dupe(u8, cell.char.grapheme) catch |e| oom(e); + cell.char.grapheme = global.shared_screen.arena.allocator().dupe( + u8, + cell.char.grapheme, + ) catch |e| oom(e); } - state.shared_screen = .{ + global.shared_screen.obj = .{ .width = screen.width, .height = screen.height, .width_pix = screen.width_pix, @@ -540,8 +515,6 @@ pub fn updateScreen(screen: *const vaxis.Screen) bool { .mouse_shape = screen.mouse_shape, .cursor_shape = undefined, }; - win32.invalidateHwnd(hwnd); - return true; } // NOTE: we round the text metric up to the nearest integer which @@ -1056,11 +1029,12 @@ fn WndProc( state.currently_rendered_cell_size = getCellSize(text_format_editor); { - global.mutex.lock(); - defer global.mutex.unlock(); + global.shared_screen.mutex.lock(); + defer global.shared_screen.mutex.unlock(); paint( &state.maybe_d2d.?, - &state.shared_screen, + RGB.from_u24(if (state.background) |b| @intCast(0xffffff & b) else 0), + &global.shared_screen.obj, text_format_editor, state.currently_rendered_cell_size.?, ); @@ -1137,7 +1111,13 @@ fn WndProc( }, WM_APP_EXIT => { win32.PostQuitMessage(0); - return 0; + return WM_APP_EXIT_RESULT; + }, + WM_APP_SET_BACKGROUND => { + const state = stateFromHwnd(hwnd); + state.background = @intCast(wparam); + win32.invalidateHwnd(hwnd); + return WM_APP_SET_BACKGROUND_RESULT; }, win32.WM_CREATE => { const create_struct: *win32.CREATESTRUCTW = @ptrFromInt(@as(usize, @bitCast(lparam))); @@ -1146,7 +1126,6 @@ fn WndProc( state.* = .{ .pid = create_args.pid, - .shared_screen_arena = std.heap.ArenaAllocator.init(std.heap.page_allocator), }; const existing = win32.SetWindowLongPtrW( hwnd, @@ -1159,10 +1138,9 @@ fn WndProc( return 0; }, win32.WM_DESTROY => { - const state = stateFromHwnd(hwnd); - state.deinit(); - // no need to free, it was allocated via an arena - return 0; + // the window should never be destroyed so as to not to invalidate + // hwnd reference + @panic("gui window erroneously destroyed"); }, else => return win32.DefWindowProcW(hwnd, msg, wparam, lparam), }