win32 gui: rework startup/hwnd sync
This commit is contained in:
parent
ff7bdeef6b
commit
337b6ce626
5 changed files with 137 additions and 115 deletions
|
@ -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"]
|
||||
]
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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.?);
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue