win32 gui: rework startup/hwnd sync

This commit is contained in:
Jonathan Marler 2025-01-07 13:14:55 -07:00 committed by CJ van den Berg
parent ff7bdeef6b
commit 337b6ce626
5 changed files with 137 additions and 115 deletions

View file

@ -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"]
]

View file

@ -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,

View file

@ -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;
};
} 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 {

View file

@ -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.?);
}

View file

@ -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,7 +399,6 @@ 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 = .{},
@ -421,12 +413,10 @@ fn entry(pid: thespian.pid) !void {
.lpszClassName = CLASS_NAME,
.hIconSm = global.icons.small,
};
global.window_class = win32.RegisterClassExW(&wc);
if (global.window_class == 0) fatalWin32(
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 stop(hwnd: win32.HWND) void {
std.debug.assert(WM_APP_EXIT_RESULT == 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 set_window_background(hwnd: win32.HWND, color: u32) void {
std.debug.assert(WM_APP_SET_BACKGROUND_RESULT == win32.SendMessageW(
hwnd,
WM_APP_SET_BACKGROUND,
color,
0,
));
}
pub fn set_window_background(color: u32) void {
const hwnd = global.hwnd orelse return;
_ = win32.SendMessageW(hwnd, WM_APP_SET_BACKGROUND, color, 0);
}
pub fn updateScreen(screen: *const vaxis.Screen) void {
global.shared_screen.mutex.lock();
defer global.shared_screen.mutex.unlock();
_ = global.shared_screen.arena.reset(.retain_capacity);
// 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);
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),
}