flow/src/renderer/win32/renderer.zig
CJ van den Berg 6d7a316abb
fix(win32 gui): remove warning about set_terminal_cursor_color implementation
It won't be implemented until we support terminal cursor rendering.
2025-01-08 12:22:12 +01:00

412 lines
12 KiB
Zig

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");
}