const Self = @This(); pub const log_name = "renderer"; const std = @import("std"); const cbor = @import("cbor"); const vaxis = @import("vaxis"); const Style = @import("theme").Style; const Color = @import("theme").Color; pub const CursorShape = vaxis.Cell.CursorShape; pub const Plane = @import("tuirenderer").Plane; const input = @import("input"); const win32 = @import("win32").everything; pub const Cell = @import("tuirenderer").Cell; pub const StyleBits = @import("tuirenderer").style; const gui = @import("gui"); const DropWriter = gui.DropWriter; pub const style = StyleBits; allocator: std.mem.Allocator, vx: vaxis.Vaxis, 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, 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; }; fn oom(e: error{OutOfMemory}) noreturn { @panic(@errorName(e)); } 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; _ = 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: Self = .{ .allocator = allocator, .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; return result; } pub fn deinit(self: *Self) void { std.debug.assert(self.thread == null); var drop_writer = DropWriter{}; self.vx.deinit(self.allocator, drop_writer.writer().any()); self.title_buf.deinit(); } 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.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 { _ = gui.updateScreen(&self.vx.screen); if (self.hwnd) |hwnd| win32.invalidateHwnd(hwnd); } pub fn stop(self: *Self) void { // 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(); } pub fn stdplane(self: *Self) Plane { const name = "root"; var plane: Plane = .{ .window = self.vx.window(), .name_buf = undefined, .name_len = name.len, }; @memcpy(plane.name_buf[0..name.len], name); return plane; } pub fn process_renderer_event(self: *Self, msg: []const u8) !void { const Input = struct { kind: u8, codepoint: u21, shifted_codepoint: u21, text: []const u8, mods: u8, }; const MousePos = struct { col: i32, row: i32, xoffset: i32, yoffset: i32, }; const Winsize = struct { cell_width: u16, cell_height: u16, pixel_width: u16, pixel_height: u16, }; { var args: Input = undefined; if (try cbor.match(msg, .{ cbor.any, "I", cbor.extract(&args.kind), cbor.extract(&args.codepoint), cbor.extract(&args.shifted_codepoint), cbor.extract(&args.text), cbor.extract(&args.mods), })) { var buf: [300]u8 = undefined; const cbor_msg = fmtmsg(&buf, .{ "I", args.kind, args.codepoint, args.shifted_codepoint, args.text, args.mods, }); if (self.dispatch_input) |f| f(self.handler_ctx, cbor_msg); return; } } { var args: Winsize = undefined; if (try cbor.match(msg, .{ cbor.any, "Resize", cbor.extract(&args.cell_width), cbor.extract(&args.cell_height), cbor.extract(&args.pixel_width), cbor.extract(&args.pixel_height), })) { var drop_writer = DropWriter{}; self.vx.resize(self.allocator, drop_writer.writer().any(), .{ .rows = @intCast(args.cell_height), .cols = @intCast(args.cell_width), .x_pixel = @intCast(args.pixel_width), .y_pixel = @intCast(args.pixel_height), }) catch |err| std.debug.panic("resize failed with {s}", .{@errorName(err)}); self.vx.queueRefresh(); { var buf: [200]u8 = undefined; if (self.dispatch_event) |f| f(self.handler_ctx, fmtmsg(&buf, .{"resize"})); } return; } } { var args: MousePos = undefined; if (try cbor.match(msg, .{ cbor.any, "M", cbor.extract(&args.col), cbor.extract(&args.row), cbor.extract(&args.xoffset), cbor.extract(&args.yoffset), })) { var buf: [200]u8 = undefined; if (self.dispatch_mouse) |f| f( self.handler_ctx, @intCast(args.row), @intCast(args.col), fmtmsg(&buf, .{ "M", args.col, args.row, args.xoffset, args.yoffset, }), ); return; } } { var args: struct { pos: MousePos, button: struct { press: u8, id: u8, }, } = undefined; if (try cbor.match(msg, .{ cbor.any, "B", cbor.extract(&args.button.press), cbor.extract(&args.button.id), cbor.extract(&args.pos.col), cbor.extract(&args.pos.row), cbor.extract(&args.pos.xoffset), cbor.extract(&args.pos.yoffset), })) { var buf: [200]u8 = undefined; if (self.dispatch_mouse) |f| f( self.handler_ctx, @intCast(args.pos.row), @intCast(args.pos.col), fmtmsg(&buf, .{ "B", args.button.press, args.button.id, input.utils.button_id_string(@enumFromInt(args.button.id)), args.pos.col, args.pos.row, args.pos.xoffset, args.pos.yoffset, }), ); return; } } { var args: struct { pos: MousePos, button_id: u8, } = undefined; if (try cbor.match(msg, .{ cbor.any, "D", cbor.extract(&args.button_id), cbor.extract(&args.pos.col), cbor.extract(&args.pos.row), cbor.extract(&args.pos.xoffset), cbor.extract(&args.pos.yoffset), })) { var buf: [200]u8 = undefined; if (self.dispatch_mouse_drag) |f| f( self.handler_ctx, @intCast(args.pos.row), @intCast(args.pos.col), fmtmsg(&buf, .{ "D", input.event.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; } } { 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.update_window_title(); } 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); } else { self.allocator.free(title); } } pub fn set_terminal_style(self: *Self, style_: Style) void { 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 { _ = self; _ = color; //@panic("todo"); } pub fn set_terminal_working_directory(self: *Self, absolute_path: []const u8) void { _ = self; _ = absolute_path; // this is usually a no-op for GUI renderers // it is used by terminals to spawn new windows or splits in the same directory } pub const copy_to_windows_clipboard = @import("tuirenderer").copy_to_windows_clipboard; pub const request_windows_clipboard = @import("tuirenderer").request_windows_clipboard; pub fn request_mouse_cursor_text(self: *Self, push_or_pop: bool) void { _ = self; _ = push_or_pop; //@panic("todo"); } pub fn request_mouse_cursor_pointer(self: *Self, push_or_pop: bool) void { _ = self; _ = push_or_pop; //@panic("todo"); } pub fn request_mouse_cursor_default(self: *Self, push_or_pop: bool) void { _ = self; _ = push_or_pop; //@panic("todo"); } pub fn cursor_enable(self: *Self, y: c_int, x: c_int, shape: CursorShape) !void { _ = self; _ = y; _ = x; _ = shape; //@panic("todo"); } pub fn cursor_disable(self: *Self) void { _ = self; //@panic("todo"); }