feat(gui): M2 - sokol_gfx GPU backend initialised from wio window
Implements milestone 2 of the wio+sokol_gfx cross-platform GUI renderer: - build.zig.zon: add wio and sokol-zig as lazy path dependencies - build.zig: wire -Drenderer=gui - creates modules for gpu, app, and the stub rasterizer; links sokol_clib against system GL/X11/Xi/Xcursor/ALSA (requires: libgl-dev libx11-dev libxi-dev libxcursor-dev libasound2-dev) - src/gui/gpu/builtin.glsl.zig: hand-crafted GLCORE ShaderDesc with a full-screen-quad vertex stage and a cell-grid fragment stage; Y-flip via row_count*cell_size_y; FsParams as 4 INT uniforms; RGBA32UI cell texture and R8 glyph-atlas texture with NONFILTERING samplers - src/gui/gpu/gpu.zig: sokol_gfx backend mirroring the d3d11 backend; TRIANGLE_STRIP pipeline, CPU-side glyph atlas shadow, per-frame cell texture update via sg.updateImage - src/gui/rasterizer/stub.zig: blank-glyph stub rasterizer for M2 testing - src/gui/wio/app.zig: wio event loop + thespian bridge; creates a core OpenGL 3.3 context, calls sg.setup/gpu.init, sends RDR messages for WindowCreated/Resize/keyboard/mouse events, renders on screen_pending - src/gui/wio/input.zig: wio Button → Flow codepoint/mouse-button mapping - src/renderer/gui/renderer.zig: standard renderer interface consumed by tui.zig; dispatches RDR events, delegates font/cursor/clipboard to app
This commit is contained in:
parent
9c66b19650
commit
39b482b2e0
8 changed files with 1634 additions and 3 deletions
72
build.zig
72
build.zig
|
|
@ -72,7 +72,10 @@ fn build_development(
|
||||||
_: bool, // all_targets
|
_: bool, // all_targets
|
||||||
test_filters: []const []const u8,
|
test_filters: []const []const u8,
|
||||||
) void {
|
) void {
|
||||||
const target = b.standardTargetOptions(.{ .default_target = .{ .abi = if (builtin.os.tag == .linux and !tracy_enabled) .musl else null } });
|
// The gui renderer links system GL/X11 libraries which are not available
|
||||||
|
// via the musl sysroot, so use the native ABI when building it.
|
||||||
|
const force_musl = builtin.os.tag == .linux and !tracy_enabled and renderer != .gui;
|
||||||
|
const target = b.standardTargetOptions(.{ .default_target = .{ .abi = if (force_musl) .musl else null } });
|
||||||
const optimize = b.standardOptimizeOption(.{});
|
const optimize = b.standardOptimizeOption(.{});
|
||||||
|
|
||||||
return build_exe(
|
return build_exe(
|
||||||
|
|
@ -530,8 +533,71 @@ pub fn build_exe(
|
||||||
break :blk mod;
|
break :blk mod;
|
||||||
},
|
},
|
||||||
.gui => {
|
.gui => {
|
||||||
std.log.err("-Drenderer=gui is not yet implemented", .{});
|
const wio_dep = b.lazyDependency("wio", .{
|
||||||
std.process.exit(0xff);
|
.target = target,
|
||||||
|
.optimize = optimize_deps,
|
||||||
|
.enable_opengl = true,
|
||||||
|
}) orelse break :blk tui_renderer_mod;
|
||||||
|
const sokol_dep = b.lazyDependency("sokol", .{
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize_deps,
|
||||||
|
.gl = true,
|
||||||
|
// Requires system packages: libgl-dev libx11-dev libxi-dev
|
||||||
|
// libxcursor-dev libasound2-dev (Debian/Ubuntu)
|
||||||
|
}) orelse break :blk tui_renderer_mod;
|
||||||
|
|
||||||
|
const wio_mod = wio_dep.module("wio");
|
||||||
|
const sokol_mod = sokol_dep.module("sokol");
|
||||||
|
|
||||||
|
const gui_xy_mod = b.createModule(.{ .root_source_file = b.path("src/gui/xy.zig") });
|
||||||
|
const gui_cell_mod = b.createModule(.{ .root_source_file = b.path("src/gui/Cell.zig") });
|
||||||
|
const gui_glyph_cache_mod = b.createModule(.{ .root_source_file = b.path("src/gui/GlyphIndexCache.zig") });
|
||||||
|
const gui_xterm_mod = b.createModule(.{ .root_source_file = b.path("src/gui/xterm.zig") });
|
||||||
|
|
||||||
|
const stub_rasterizer_mod = b.createModule(.{
|
||||||
|
.root_source_file = b.path("src/gui/rasterizer/stub.zig"),
|
||||||
|
.imports = &.{
|
||||||
|
.{ .name = "xy", .module = gui_xy_mod },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const gpu_mod = b.createModule(.{
|
||||||
|
.root_source_file = b.path("src/gui/gpu/gpu.zig"),
|
||||||
|
.imports = &.{
|
||||||
|
.{ .name = "sokol", .module = sokol_mod },
|
||||||
|
.{ .name = "rasterizer", .module = stub_rasterizer_mod },
|
||||||
|
.{ .name = "xy", .module = gui_xy_mod },
|
||||||
|
.{ .name = "Cell", .module = gui_cell_mod },
|
||||||
|
.{ .name = "GlyphIndexCache", .module = gui_glyph_cache_mod },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const app_mod = b.createModule(.{
|
||||||
|
.root_source_file = b.path("src/gui/wio/app.zig"),
|
||||||
|
.imports = &.{
|
||||||
|
.{ .name = "wio", .module = wio_mod },
|
||||||
|
.{ .name = "sokol", .module = sokol_mod },
|
||||||
|
.{ .name = "gpu", .module = gpu_mod },
|
||||||
|
.{ .name = "thespian", .module = thespian_mod },
|
||||||
|
.{ .name = "cbor", .module = cbor_mod },
|
||||||
|
.{ .name = "vaxis", .module = vaxis_mod },
|
||||||
|
.{ .name = "xterm", .module = gui_xterm_mod },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const mod = b.createModule(.{
|
||||||
|
.root_source_file = b.path("src/renderer/gui/renderer.zig"),
|
||||||
|
.imports = &.{
|
||||||
|
.{ .name = "theme", .module = themes_dep.module("theme") },
|
||||||
|
.{ .name = "cbor", .module = cbor_mod },
|
||||||
|
.{ .name = "thespian", .module = thespian_mod },
|
||||||
|
.{ .name = "input", .module = input_mod },
|
||||||
|
.{ .name = "app", .module = app_mod },
|
||||||
|
.{ .name = "tuirenderer", .module = tui_renderer_mod },
|
||||||
|
.{ .name = "vaxis", .module = vaxis_mod },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
break :blk mod;
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,14 @@
|
||||||
.hash = "zigwin32-25.0.28-preview-AAAAAICM5AMResOGQnQ85mfe60TTOQeMtt7GRATUOKoP",
|
.hash = "zigwin32-25.0.28-preview-AAAAAICM5AMResOGQnQ85mfe60TTOQeMtt7GRATUOKoP",
|
||||||
.lazy = true,
|
.lazy = true,
|
||||||
},
|
},
|
||||||
|
.wio = .{
|
||||||
|
.path = "../flow-deps/wio",
|
||||||
|
.lazy = true,
|
||||||
|
},
|
||||||
|
.sokol = .{
|
||||||
|
.path = "../flow-deps/sokol-zig",
|
||||||
|
.lazy = true,
|
||||||
|
},
|
||||||
.diffz = .{
|
.diffz = .{
|
||||||
.url = "git+https://github.com/ziglibs/diffz.git#fbdf690b87db6b1142bbce6d4906f90b09ce60bb",
|
.url = "git+https://github.com/ziglibs/diffz.git#fbdf690b87db6b1142bbce6d4906f90b09ce60bb",
|
||||||
.hash = "diffz-0.0.1-G2tlIezMAQBwGNGDs7Hn_N25dWSjEzgR_FAx9GFAvCuZ",
|
.hash = "diffz-0.0.1-G2tlIezMAQBwGNGDs7Hn_N25dWSjEzgR_FAx9GFAvCuZ",
|
||||||
|
|
|
||||||
134
src/gui/gpu/builtin.glsl.zig
Normal file
134
src/gui/gpu/builtin.glsl.zig
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
// Hand-crafted sokol_gfx ShaderDesc for the builtin grid shader.
|
||||||
|
// Targets the GLCORE backend only (OpenGL 4.10 core profile).
|
||||||
|
//
|
||||||
|
// Vertex stage: full-screen quad from gl_VertexID (no vertex buffer needed)
|
||||||
|
// Fragment stage: cell-grid renderer — reads a RGBA32UI cell texture and an
|
||||||
|
// R8 glyph-atlas texture and blends fg over bg per pixel.
|
||||||
|
|
||||||
|
const sg = @import("sokol").gfx;
|
||||||
|
|
||||||
|
// Uniform block slot 0, fragment stage.
|
||||||
|
// Individual GLSL uniforms (GLCORE uses glUniform* calls, not UBOs).
|
||||||
|
pub const FsParams = extern struct {
|
||||||
|
cell_size_x: i32,
|
||||||
|
cell_size_y: i32,
|
||||||
|
col_count: i32,
|
||||||
|
row_count: i32,
|
||||||
|
};
|
||||||
|
|
||||||
|
const vs_src =
|
||||||
|
\\#version 410
|
||||||
|
\\void main() {
|
||||||
|
\\ int id = gl_VertexID;
|
||||||
|
\\ float x = 2.0 * (float(id & 1) - 0.5);
|
||||||
|
\\ float y = -(float(id >> 1) - 0.5) * 2.0;
|
||||||
|
\\ gl_Position = vec4(x, y, 0.0, 1.0);
|
||||||
|
\\}
|
||||||
|
;
|
||||||
|
|
||||||
|
const fs_src =
|
||||||
|
\\#version 410
|
||||||
|
\\uniform int cell_size_x;
|
||||||
|
\\uniform int cell_size_y;
|
||||||
|
\\uniform int col_count;
|
||||||
|
\\uniform int row_count;
|
||||||
|
\\uniform sampler2D glyph_tex_glyph_smp;
|
||||||
|
\\uniform usampler2D cell_tex_cell_smp;
|
||||||
|
\\out vec4 frag_color;
|
||||||
|
\\
|
||||||
|
\\vec4 unpack_rgba(uint packed) {
|
||||||
|
\\ return vec4(
|
||||||
|
\\ float((packed >> 24u) & 255u) / 255.0,
|
||||||
|
\\ float((packed >> 16u) & 255u) / 255.0,
|
||||||
|
\\ float((packed >> 8u) & 255u) / 255.0,
|
||||||
|
\\ float( packed & 255u) / 255.0
|
||||||
|
\\ );
|
||||||
|
\\}
|
||||||
|
\\
|
||||||
|
\\void main() {
|
||||||
|
\\ // Convert gl_FragCoord (bottom-left origin) to top-left origin.
|
||||||
|
\\ // row_count * cell_size_y >= viewport height (we ceil the division),
|
||||||
|
\\ // so this formula maps the top screen pixel to row 0.
|
||||||
|
\\ int px = int(gl_FragCoord.x);
|
||||||
|
\\ int py = row_count * cell_size_y - 1 - int(gl_FragCoord.y);
|
||||||
|
\\ int col = px / cell_size_x;
|
||||||
|
\\ int row = py / cell_size_y;
|
||||||
|
\\
|
||||||
|
\\ if (col >= col_count || row >= row_count || row < 0 || col < 0) {
|
||||||
|
\\ frag_color = vec4(0.0, 0.0, 0.0, 1.0);
|
||||||
|
\\ return;
|
||||||
|
\\ }
|
||||||
|
\\
|
||||||
|
\\ // Fetch cell: texel = (glyph_index, bg_packed, fg_packed, 0)
|
||||||
|
\\ uvec4 cell = texelFetch(cell_tex_cell_smp, ivec2(col, row), 0);
|
||||||
|
\\ vec4 bg = unpack_rgba(cell.g);
|
||||||
|
\\ vec4 fg = unpack_rgba(cell.b);
|
||||||
|
\\
|
||||||
|
\\ // Pixel coordinates within the cell
|
||||||
|
\\ int cell_px_x = px % cell_size_x;
|
||||||
|
\\ int cell_px_y = py % cell_size_y;
|
||||||
|
\\
|
||||||
|
\\ // Glyph atlas lookup
|
||||||
|
\\ ivec2 atlas_size = textureSize(glyph_tex_glyph_smp, 0);
|
||||||
|
\\ int cells_per_row = atlas_size.x / cell_size_x;
|
||||||
|
\\ int gi = int(cell.r);
|
||||||
|
\\ int gc = gi % cells_per_row;
|
||||||
|
\\ int gr = gi / cells_per_row;
|
||||||
|
\\ ivec2 atlas_coord = ivec2(gc * cell_size_x + cell_px_x,
|
||||||
|
\\ gr * cell_size_y + cell_px_y);
|
||||||
|
\\ float glyph_alpha = texelFetch(glyph_tex_glyph_smp, atlas_coord, 0).r;
|
||||||
|
\\
|
||||||
|
\\ // Blend fg over bg
|
||||||
|
\\ vec3 color = mix(bg.rgb, fg.rgb, fg.a * glyph_alpha);
|
||||||
|
\\ frag_color = vec4(color, 1.0);
|
||||||
|
\\}
|
||||||
|
;
|
||||||
|
|
||||||
|
pub fn shaderDesc(backend: sg.Backend) sg.ShaderDesc {
|
||||||
|
var desc: sg.ShaderDesc = .{};
|
||||||
|
switch (backend) {
|
||||||
|
.GLCORE => {
|
||||||
|
desc.vertex_func.source = vs_src;
|
||||||
|
desc.fragment_func.source = fs_src;
|
||||||
|
|
||||||
|
// Fragment uniform block: 4 individual INT uniforms
|
||||||
|
desc.uniform_blocks[0].stage = .FRAGMENT;
|
||||||
|
desc.uniform_blocks[0].size = @sizeOf(FsParams);
|
||||||
|
desc.uniform_blocks[0].layout = .NATIVE;
|
||||||
|
desc.uniform_blocks[0].glsl_uniforms[0] = .{ .type = .INT, .glsl_name = "cell_size_x" };
|
||||||
|
desc.uniform_blocks[0].glsl_uniforms[1] = .{ .type = .INT, .glsl_name = "cell_size_y" };
|
||||||
|
desc.uniform_blocks[0].glsl_uniforms[2] = .{ .type = .INT, .glsl_name = "col_count" };
|
||||||
|
desc.uniform_blocks[0].glsl_uniforms[3] = .{ .type = .INT, .glsl_name = "row_count" };
|
||||||
|
|
||||||
|
// Glyph atlas texture: R8 → sample_type = FLOAT
|
||||||
|
desc.views[0].texture = .{
|
||||||
|
.stage = .FRAGMENT,
|
||||||
|
.image_type = ._2D,
|
||||||
|
.sample_type = .FLOAT,
|
||||||
|
};
|
||||||
|
desc.samplers[0] = .{ .stage = .FRAGMENT, .sampler_type = .NONFILTERING };
|
||||||
|
desc.texture_sampler_pairs[0] = .{
|
||||||
|
.stage = .FRAGMENT,
|
||||||
|
.view_slot = 0,
|
||||||
|
.sampler_slot = 0,
|
||||||
|
.glsl_name = "glyph_tex_glyph_smp",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Cell texture: RGBA32UI → sample_type = UINT
|
||||||
|
desc.views[1].texture = .{
|
||||||
|
.stage = .FRAGMENT,
|
||||||
|
.image_type = ._2D,
|
||||||
|
.sample_type = .UINT,
|
||||||
|
};
|
||||||
|
desc.samplers[1] = .{ .stage = .FRAGMENT, .sampler_type = .NONFILTERING };
|
||||||
|
desc.texture_sampler_pairs[1] = .{
|
||||||
|
.stage = .FRAGMENT,
|
||||||
|
.view_slot = 1,
|
||||||
|
.sampler_slot = 1,
|
||||||
|
.glsl_name = "cell_tex_cell_smp",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
return desc;
|
||||||
|
}
|
||||||
432
src/gui/gpu/gpu.zig
Normal file
432
src/gui/gpu/gpu.zig
Normal file
|
|
@ -0,0 +1,432 @@
|
||||||
|
// sokol_gfx GPU backend for the wio-based renderer.
|
||||||
|
//
|
||||||
|
// Threading: all public functions must be called from the wio/GL thread
|
||||||
|
// (the thread that called sg.setup()).
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const sg = @import("sokol").gfx;
|
||||||
|
const Rasterizer = @import("rasterizer");
|
||||||
|
const GlyphIndexCache = @import("GlyphIndexCache");
|
||||||
|
const gui_cell = @import("Cell");
|
||||||
|
const XY = @import("xy").XY;
|
||||||
|
const builtin_shader = @import("builtin.glsl.zig");
|
||||||
|
|
||||||
|
pub const Font = Rasterizer.Font;
|
||||||
|
pub const Cell = gui_cell.Cell;
|
||||||
|
pub const Color = gui_cell.Rgba8;
|
||||||
|
const Rgba8 = gui_cell.Rgba8;
|
||||||
|
|
||||||
|
const log = std.log.scoped(.gpu);
|
||||||
|
|
||||||
|
// Maximum glyph atlas dimension (matching D3D11_REQ_TEXTURE2D_U_OR_V_DIMENSION)
|
||||||
|
const max_atlas_dim: u16 = 16384;
|
||||||
|
|
||||||
|
fn getAtlasCellCount(cell_size: XY(u16)) XY(u16) {
|
||||||
|
return .{
|
||||||
|
.x = @intCast(@divTrunc(max_atlas_dim, cell_size.x)),
|
||||||
|
.y = @intCast(@divTrunc(max_atlas_dim, cell_size.y)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shader cell layout for the RGBA32UI cell texture.
|
||||||
|
// Each texel encodes one terminal cell:
|
||||||
|
// .r = glyph_index (u32)
|
||||||
|
// .g = bg packed (Rgba8 bit-cast to u32: r<<24|g<<16|b<<8|a)
|
||||||
|
// .b = fg packed (same)
|
||||||
|
// .a = 0 (reserved)
|
||||||
|
const ShaderCell = extern struct {
|
||||||
|
glyph_index: u32,
|
||||||
|
bg: u32,
|
||||||
|
fg: u32,
|
||||||
|
_pad: u32 = 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const global = struct {
|
||||||
|
var init_called: bool = false;
|
||||||
|
var rasterizer: Rasterizer = undefined;
|
||||||
|
var pip: sg.Pipeline = .{};
|
||||||
|
var glyph_sampler: sg.Sampler = .{};
|
||||||
|
var cell_sampler: sg.Sampler = .{};
|
||||||
|
var glyph_cache_arena: std.heap.ArenaAllocator = undefined;
|
||||||
|
var background: Rgba8 = .{ .r = 19, .g = 19, .b = 19, .a = 255 };
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn init(allocator: std.mem.Allocator) !void {
|
||||||
|
std.debug.assert(!global.init_called);
|
||||||
|
global.init_called = true;
|
||||||
|
|
||||||
|
global.rasterizer = try Rasterizer.init(allocator);
|
||||||
|
global.glyph_cache_arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
|
||||||
|
|
||||||
|
// Build shader + pipeline
|
||||||
|
const shd = sg.makeShader(builtin_shader.shaderDesc(sg.queryBackend()));
|
||||||
|
|
||||||
|
var pip_desc: sg.PipelineDesc = .{ .shader = shd };
|
||||||
|
pip_desc.primitive_type = .TRIANGLE_STRIP;
|
||||||
|
pip_desc.color_count = 1;
|
||||||
|
global.pip = sg.makePipeline(pip_desc);
|
||||||
|
|
||||||
|
// Nearest-neighbour samplers (no filtering)
|
||||||
|
global.glyph_sampler = sg.makeSampler(.{
|
||||||
|
.min_filter = .NEAREST,
|
||||||
|
.mag_filter = .NEAREST,
|
||||||
|
.mipmap_filter = .NEAREST,
|
||||||
|
.wrap_u = .CLAMP_TO_EDGE,
|
||||||
|
.wrap_v = .CLAMP_TO_EDGE,
|
||||||
|
});
|
||||||
|
global.cell_sampler = sg.makeSampler(.{
|
||||||
|
.min_filter = .NEAREST,
|
||||||
|
.mag_filter = .NEAREST,
|
||||||
|
.mipmap_filter = .NEAREST,
|
||||||
|
.wrap_u = .CLAMP_TO_EDGE,
|
||||||
|
.wrap_v = .CLAMP_TO_EDGE,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit() void {
|
||||||
|
std.debug.assert(global.init_called);
|
||||||
|
global.init_called = false;
|
||||||
|
sg.destroyPipeline(global.pip);
|
||||||
|
sg.destroySampler(global.glyph_sampler);
|
||||||
|
sg.destroySampler(global.cell_sampler);
|
||||||
|
global.glyph_cache_arena.deinit();
|
||||||
|
global.rasterizer.deinit();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn loadFont(name: []const u8, size_px: u16) !Font {
|
||||||
|
return global.rasterizer.loadFont(name, size_px);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setBackground(color: Rgba8) void {
|
||||||
|
global.background = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── WindowState ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
pub const WindowState = struct {
|
||||||
|
// GL window size in pixels
|
||||||
|
size: XY(u32) = .{ .x = 0, .y = 0 },
|
||||||
|
|
||||||
|
// Glyph atlas (R8 2D texture + view)
|
||||||
|
glyph_image: sg.Image = .{},
|
||||||
|
glyph_view: sg.View = .{},
|
||||||
|
glyph_image_size: XY(u16) = .{ .x = 0, .y = 0 },
|
||||||
|
|
||||||
|
// Cell grid (RGBA32UI 2D texture + view), updated each frame
|
||||||
|
cell_image: sg.Image = .{},
|
||||||
|
cell_view: sg.View = .{},
|
||||||
|
cell_image_size: XY(u16) = .{ .x = 0, .y = 0 },
|
||||||
|
cell_buf: std.ArrayListUnmanaged(ShaderCell) = .{},
|
||||||
|
|
||||||
|
// Glyph index cache
|
||||||
|
glyph_cache_cell_size: ?XY(u16) = null,
|
||||||
|
glyph_index_cache: ?GlyphIndexCache = null,
|
||||||
|
|
||||||
|
pub fn init() WindowState {
|
||||||
|
std.debug.assert(global.init_called);
|
||||||
|
return .{};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(state: *WindowState) void {
|
||||||
|
if (state.glyph_view.id != 0) sg.destroyView(state.glyph_view);
|
||||||
|
if (state.glyph_image.id != 0) sg.destroyImage(state.glyph_image);
|
||||||
|
if (state.cell_view.id != 0) sg.destroyView(state.cell_view);
|
||||||
|
if (state.cell_image.id != 0) sg.destroyImage(state.cell_image);
|
||||||
|
state.cell_buf.deinit(global.glyph_cache_arena.allocator());
|
||||||
|
if (state.glyph_index_cache) |*c| {
|
||||||
|
c.deinit(global.glyph_cache_arena.allocator());
|
||||||
|
}
|
||||||
|
state.* = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the glyph atlas image is (at least) the requested pixel size.
|
||||||
|
// Returns true if the image was retained, false if (re)created.
|
||||||
|
fn updateGlyphImage(state: *WindowState, pixel_size: XY(u16)) bool {
|
||||||
|
if (state.glyph_image_size.eql(pixel_size)) return true;
|
||||||
|
|
||||||
|
if (state.glyph_view.id != 0) sg.destroyView(state.glyph_view);
|
||||||
|
if (state.glyph_image.id != 0) sg.destroyImage(state.glyph_image);
|
||||||
|
|
||||||
|
state.glyph_image = sg.makeImage(.{
|
||||||
|
.width = pixel_size.x,
|
||||||
|
.height = pixel_size.y,
|
||||||
|
.pixel_format = .R8,
|
||||||
|
.usage = .{ .dynamic_update = true },
|
||||||
|
});
|
||||||
|
state.glyph_view = sg.makeView(.{
|
||||||
|
.texture = .{ .image = state.glyph_image },
|
||||||
|
});
|
||||||
|
state.glyph_image_size = pixel_size;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the cell texture is the requested size.
|
||||||
|
fn updateCellImage(state: *WindowState, allocator: std.mem.Allocator, cols: u16, rows: u16) void {
|
||||||
|
const needed: u32 = @as(u32, cols) * @as(u32, rows);
|
||||||
|
const sz: XY(u16) = .{ .x = cols, .y = rows };
|
||||||
|
|
||||||
|
if (!state.cell_image_size.eql(sz)) {
|
||||||
|
if (state.cell_view.id != 0) sg.destroyView(state.cell_view);
|
||||||
|
if (state.cell_image.id != 0) sg.destroyImage(state.cell_image);
|
||||||
|
|
||||||
|
state.cell_image = sg.makeImage(.{
|
||||||
|
.width = cols,
|
||||||
|
.height = rows,
|
||||||
|
.pixel_format = .RGBA32UI,
|
||||||
|
.usage = .{ .dynamic_update = true },
|
||||||
|
});
|
||||||
|
state.cell_view = sg.makeView(.{
|
||||||
|
.texture = .{ .image = state.cell_image },
|
||||||
|
});
|
||||||
|
state.cell_image_size = sz;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.cell_buf.items.len < needed) {
|
||||||
|
state.cell_buf.resize(allocator, needed) catch |e| oom(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn generateGlyph(
|
||||||
|
state: *WindowState,
|
||||||
|
font: Font,
|
||||||
|
codepoint: u21,
|
||||||
|
kind: Rasterizer.GlyphKind,
|
||||||
|
) u32 {
|
||||||
|
const atlas_cell_count = getAtlasCellCount(font.cell_size);
|
||||||
|
const atlas_total: u32 = @as(u32, atlas_cell_count.x) * @as(u32, atlas_cell_count.y);
|
||||||
|
const atlas_pixel_size: XY(u16) = .{
|
||||||
|
.x = atlas_cell_count.x * font.cell_size.x,
|
||||||
|
.y = atlas_cell_count.y * font.cell_size.y,
|
||||||
|
};
|
||||||
|
|
||||||
|
const atlas_retained = state.updateGlyphImage(atlas_pixel_size);
|
||||||
|
|
||||||
|
const cache_valid = if (state.glyph_cache_cell_size) |s| s.eql(font.cell_size) else false;
|
||||||
|
state.glyph_cache_cell_size = font.cell_size;
|
||||||
|
|
||||||
|
if (!atlas_retained or !cache_valid) {
|
||||||
|
if (state.glyph_index_cache) |*c| {
|
||||||
|
c.deinit(global.glyph_cache_arena.allocator());
|
||||||
|
_ = global.glyph_cache_arena.reset(.retain_capacity);
|
||||||
|
state.glyph_index_cache = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cache = blk: {
|
||||||
|
if (state.glyph_index_cache) |*c| break :blk c;
|
||||||
|
state.glyph_index_cache = GlyphIndexCache.init(
|
||||||
|
global.glyph_cache_arena.allocator(),
|
||||||
|
atlas_total,
|
||||||
|
) catch |e| oom(e);
|
||||||
|
break :blk &(state.glyph_index_cache.?);
|
||||||
|
};
|
||||||
|
|
||||||
|
const right_half: bool = switch (kind) {
|
||||||
|
.single, .left => false,
|
||||||
|
.right => true,
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (cache.reserve(
|
||||||
|
global.glyph_cache_arena.allocator(),
|
||||||
|
codepoint,
|
||||||
|
right_half,
|
||||||
|
) catch |e| oom(e)) {
|
||||||
|
.newly_reserved => |reserved| {
|
||||||
|
// Rasterize into a staging buffer then upload the relevant
|
||||||
|
// portion to the atlas.
|
||||||
|
const staging_w: u32 = @as(u32, font.cell_size.x) * 2;
|
||||||
|
const staging_h: u32 = font.cell_size.y;
|
||||||
|
var staging_buf = global.glyph_cache_arena.allocator().alloc(
|
||||||
|
u8,
|
||||||
|
staging_w * staging_h,
|
||||||
|
) catch |e| oom(e);
|
||||||
|
defer global.glyph_cache_arena.allocator().free(staging_buf);
|
||||||
|
@memset(staging_buf, 0);
|
||||||
|
|
||||||
|
global.rasterizer.render(font, codepoint, kind, staging_buf);
|
||||||
|
|
||||||
|
// Atlas cell position for this glyph index
|
||||||
|
const atlas_col: u16 = @intCast(reserved.index % atlas_cell_count.x);
|
||||||
|
const atlas_row: u16 = @intCast(reserved.index / atlas_cell_count.x);
|
||||||
|
const atlas_x: u16 = atlas_col * font.cell_size.x;
|
||||||
|
const atlas_y: u16 = atlas_row * font.cell_size.y;
|
||||||
|
|
||||||
|
// Source region in the staging buffer
|
||||||
|
const src_x: u16 = if (right_half) font.cell_size.x else 0;
|
||||||
|
const glyph_w: u16 = font.cell_size.x;
|
||||||
|
const glyph_h: u16 = font.cell_size.y;
|
||||||
|
|
||||||
|
// Build a sub-region buffer for sokol updateImage
|
||||||
|
var region_buf = global.glyph_cache_arena.allocator().alloc(
|
||||||
|
u8,
|
||||||
|
@as(u32, glyph_w) * @as(u32, glyph_h),
|
||||||
|
) catch |e| oom(e);
|
||||||
|
defer global.glyph_cache_arena.allocator().free(region_buf);
|
||||||
|
|
||||||
|
for (0..glyph_h) |row_i| {
|
||||||
|
const src_off = row_i * staging_w + src_x;
|
||||||
|
const dst_off = row_i * glyph_w;
|
||||||
|
@memcpy(region_buf[dst_off .. dst_off + glyph_w], staging_buf[src_off .. src_off + glyph_w]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload to atlas via a temporary full-atlas image data struct.
|
||||||
|
// sokol's updateImage uploads the whole mip0 slice. We use a
|
||||||
|
// trick: create a temporary single-cell-sized image, upload it,
|
||||||
|
// then… actually, sokol doesn't expose sub-rect uploads.
|
||||||
|
//
|
||||||
|
// Workaround: upload the WHOLE atlas with only the new cell
|
||||||
|
// updated. For M2 this is acceptable; a smarter approach
|
||||||
|
// (ping-pong or persistent mapped buffer) can be added later.
|
||||||
|
uploadGlyphAtlasCell(state, atlas_x, atlas_y, glyph_w, glyph_h, region_buf);
|
||||||
|
|
||||||
|
return reserved.index;
|
||||||
|
},
|
||||||
|
.already_reserved => |index| return index,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Upload one cell's pixels into the glyph atlas.
|
||||||
|
// Because sokol only supports full-image updates via sg.updateImage, we
|
||||||
|
// maintain a CPU-side copy of the atlas and re-upload it each time.
|
||||||
|
// (M2 budget approach — acceptable for the stub rasterizer that always
|
||||||
|
// produces blank glyphs anyway.)
|
||||||
|
var atlas_cpu: ?[]u8 = null;
|
||||||
|
var atlas_cpu_size: XY(u16) = .{ .x = 0, .y = 0 };
|
||||||
|
|
||||||
|
fn uploadGlyphAtlasCell(
|
||||||
|
state: *const WindowState,
|
||||||
|
x: u16,
|
||||||
|
y: u16,
|
||||||
|
w: u16,
|
||||||
|
h: u16,
|
||||||
|
pixels: []const u8,
|
||||||
|
) void {
|
||||||
|
const asz = state.glyph_image_size;
|
||||||
|
const total: usize = @as(usize, asz.x) * @as(usize, asz.y);
|
||||||
|
|
||||||
|
// Resize cpu shadow if needed
|
||||||
|
if (!atlas_cpu_size.eql(asz)) {
|
||||||
|
if (atlas_cpu) |old| std.heap.page_allocator.free(old);
|
||||||
|
atlas_cpu = std.heap.page_allocator.alloc(u8, total) catch |e| oom(e);
|
||||||
|
@memset(atlas_cpu.?, 0);
|
||||||
|
atlas_cpu_size = asz;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blit the cell into the cpu shadow
|
||||||
|
const buf = atlas_cpu.?;
|
||||||
|
for (0..h) |row_i| {
|
||||||
|
const src_off = row_i * w;
|
||||||
|
const dst_off = (@as(usize, y) + row_i) * asz.x + x;
|
||||||
|
@memcpy(buf[dst_off .. dst_off + w], pixels[src_off .. src_off + w]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-upload the full atlas
|
||||||
|
var img_data: sg.ImageData = .{};
|
||||||
|
img_data.mip_levels[0] = .{ .ptr = buf.ptr, .size = total };
|
||||||
|
sg.updateImage(state.glyph_image, img_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn paint(
|
||||||
|
state: *WindowState,
|
||||||
|
client_size: XY(u32),
|
||||||
|
font: Font,
|
||||||
|
row_count: u16,
|
||||||
|
col_count: u16,
|
||||||
|
top: u16,
|
||||||
|
cells: []const Cell,
|
||||||
|
) void {
|
||||||
|
const shader_col_count: u16 = @intCast(@divTrunc(client_size.x + font.cell_size.x - 1, font.cell_size.x));
|
||||||
|
const shader_row_count: u16 = @intCast(@divTrunc(client_size.y + font.cell_size.y - 1, font.cell_size.y));
|
||||||
|
|
||||||
|
const copy_col_count: u16 = @min(col_count, shader_col_count);
|
||||||
|
const blank_glyph_index = state.generateGlyph(font, ' ', .single);
|
||||||
|
|
||||||
|
const alloc = global.glyph_cache_arena.allocator();
|
||||||
|
state.updateCellImage(alloc, shader_col_count, shader_row_count);
|
||||||
|
|
||||||
|
const shader_cells = state.cell_buf.items[0 .. @as(u32, shader_col_count) * @as(u32, shader_row_count)];
|
||||||
|
|
||||||
|
for (0..shader_row_count) |row_i| {
|
||||||
|
const src_row = blk: {
|
||||||
|
const r = top + @as(u16, @intCast(row_i));
|
||||||
|
break :blk if (r < row_count) r else 0;
|
||||||
|
};
|
||||||
|
const src_row_offset = @as(usize, src_row) * col_count;
|
||||||
|
const dst_row_offset = @as(usize, row_i) * shader_col_count;
|
||||||
|
const copy_len = if (row_i < row_count) copy_col_count else 0;
|
||||||
|
|
||||||
|
for (0..copy_len) |ci| {
|
||||||
|
const src = cells[src_row_offset + ci];
|
||||||
|
shader_cells[dst_row_offset + ci] = .{
|
||||||
|
.glyph_index = src.glyph_index,
|
||||||
|
.bg = @bitCast(src.background),
|
||||||
|
.fg = @bitCast(src.foreground),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
for (copy_len..shader_col_count) |ci| {
|
||||||
|
shader_cells[dst_row_offset + ci] = .{
|
||||||
|
.glyph_index = blank_glyph_index,
|
||||||
|
.bg = @bitCast(global.background),
|
||||||
|
.fg = @bitCast(global.background),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload cell texture
|
||||||
|
var cell_data: sg.ImageData = .{};
|
||||||
|
const cell_bytes = std.mem.sliceAsBytes(shader_cells);
|
||||||
|
cell_data.mip_levels[0] = .{ .ptr = cell_bytes.ptr, .size = cell_bytes.len };
|
||||||
|
sg.updateImage(state.cell_image, cell_data);
|
||||||
|
|
||||||
|
// Render pass
|
||||||
|
var pass_action: sg.PassAction = .{};
|
||||||
|
pass_action.colors[0] = .{
|
||||||
|
.load_action = .CLEAR,
|
||||||
|
.clear_value = .{
|
||||||
|
.r = @as(f32, @floatFromInt(global.background.r)) / 255.0,
|
||||||
|
.g = @as(f32, @floatFromInt(global.background.g)) / 255.0,
|
||||||
|
.b = @as(f32, @floatFromInt(global.background.b)) / 255.0,
|
||||||
|
.a = 1.0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
sg.beginPass(.{
|
||||||
|
.swapchain = .{
|
||||||
|
.width = @intCast(client_size.x),
|
||||||
|
.height = @intCast(client_size.y),
|
||||||
|
.sample_count = 1,
|
||||||
|
.color_format = .RGBA8,
|
||||||
|
.depth_format = .NONE,
|
||||||
|
.gl = .{ .framebuffer = 0 },
|
||||||
|
},
|
||||||
|
.action = pass_action,
|
||||||
|
});
|
||||||
|
sg.applyPipeline(global.pip);
|
||||||
|
|
||||||
|
var bindings: sg.Bindings = .{};
|
||||||
|
bindings.views[0] = state.glyph_view;
|
||||||
|
bindings.views[1] = state.cell_view;
|
||||||
|
bindings.samplers[0] = global.glyph_sampler;
|
||||||
|
bindings.samplers[1] = global.cell_sampler;
|
||||||
|
sg.applyBindings(bindings);
|
||||||
|
|
||||||
|
const fs_params = builtin_shader.FsParams{
|
||||||
|
.cell_size_x = font.cell_size.x,
|
||||||
|
.cell_size_y = font.cell_size.y,
|
||||||
|
.col_count = shader_col_count,
|
||||||
|
.row_count = shader_row_count,
|
||||||
|
};
|
||||||
|
sg.applyUniforms(0, .{
|
||||||
|
.ptr = &fs_params,
|
||||||
|
.size = @sizeOf(builtin_shader.FsParams),
|
||||||
|
});
|
||||||
|
|
||||||
|
sg.draw(0, 4, 1);
|
||||||
|
sg.endPass();
|
||||||
|
// Note: caller (app.zig) calls sg.commit() and window.swapBuffers()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn oom(e: error{OutOfMemory}) noreturn {
|
||||||
|
@panic(@errorName(e));
|
||||||
|
}
|
||||||
49
src/gui/rasterizer/stub.zig
Normal file
49
src/gui/rasterizer/stub.zig
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
// Stub rasterizer for M2 testing — renders blank (zeroed) glyphs.
|
||||||
|
const std = @import("std");
|
||||||
|
const XY = @import("xy").XY;
|
||||||
|
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
pub const Font = struct {
|
||||||
|
cell_size: XY(u16),
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Fonts = struct {};
|
||||||
|
|
||||||
|
pub const GlyphKind = enum {
|
||||||
|
single,
|
||||||
|
left,
|
||||||
|
right,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn init(allocator: std.mem.Allocator) !Self {
|
||||||
|
_ = allocator;
|
||||||
|
return .{};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Self) void {
|
||||||
|
_ = self;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn loadFont(self: *Self, name: []const u8, size_px: u16) !Font {
|
||||||
|
_ = self;
|
||||||
|
_ = name;
|
||||||
|
const cell_h = @max(size_px, 4);
|
||||||
|
const cell_w = @max(cell_h / 2, 2);
|
||||||
|
return Font{ .cell_size = .{ .x = cell_w, .y = cell_h } };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render(
|
||||||
|
self: *const Self,
|
||||||
|
font: Font,
|
||||||
|
codepoint: u21,
|
||||||
|
kind: GlyphKind,
|
||||||
|
staging_buf: []u8,
|
||||||
|
) void {
|
||||||
|
_ = self;
|
||||||
|
_ = font;
|
||||||
|
_ = codepoint;
|
||||||
|
_ = kind;
|
||||||
|
// Blank glyph — caller has already zeroed staging_buf.
|
||||||
|
_ = staging_buf;
|
||||||
|
}
|
||||||
358
src/gui/wio/app.zig
Normal file
358
src/gui/wio/app.zig
Normal file
|
|
@ -0,0 +1,358 @@
|
||||||
|
// wio event-loop + sokol_gfx rendering for the GUI renderer.
|
||||||
|
//
|
||||||
|
// Threading model:
|
||||||
|
// - start() is called from the tui/actor thread; it clones the caller's
|
||||||
|
// thespian PID and spawns the wio loop on a new thread.
|
||||||
|
// - The wio thread owns the GL context and all sokol/GPU state.
|
||||||
|
// - requestRender() / updateScreen() can be called from any thread; they
|
||||||
|
// post data to shared state protected by a mutex and wake the wio thread.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const wio = @import("wio");
|
||||||
|
const sg = @import("sokol").gfx;
|
||||||
|
const slog = @import("sokol").log;
|
||||||
|
const gpu = @import("gpu");
|
||||||
|
const thespian = @import("thespian");
|
||||||
|
const cbor = @import("cbor");
|
||||||
|
const vaxis = @import("vaxis");
|
||||||
|
|
||||||
|
const input_translate = @import("input.zig");
|
||||||
|
|
||||||
|
const log = std.log.scoped(.wio_app);
|
||||||
|
|
||||||
|
// ── Shared state (protected by screen_mutex) ──────────────────────────────
|
||||||
|
|
||||||
|
const ScreenSnapshot = struct {
|
||||||
|
cells: []gpu.Cell,
|
||||||
|
width: u16,
|
||||||
|
height: u16,
|
||||||
|
font: gpu.Font,
|
||||||
|
};
|
||||||
|
|
||||||
|
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .{};
|
||||||
|
var screen_mutex: std.Thread.Mutex = .{};
|
||||||
|
var screen_pending: std.atomic.Value(bool) = .init(false);
|
||||||
|
var screen_snap: ?ScreenSnapshot = null;
|
||||||
|
var tui_pid: thespian.pid = undefined;
|
||||||
|
var font_size_px: u16 = 16;
|
||||||
|
var font_name_buf: [256]u8 = undefined;
|
||||||
|
var font_name_len: usize = 0;
|
||||||
|
|
||||||
|
// ── Public API (called from tui thread) ───────────────────────────────────
|
||||||
|
|
||||||
|
pub fn start() !std.Thread {
|
||||||
|
tui_pid = thespian.self_pid().clone();
|
||||||
|
font_name_len = 0;
|
||||||
|
return std.Thread.spawn(.{}, wioLoop, .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stop() void {
|
||||||
|
// The wio thread will stop when the window's .close event arrives.
|
||||||
|
// We can't easily interrupt wio.wait() from outside without cancelWait.
|
||||||
|
wio.cancelWait();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called from the tui thread to push a new screen to the GPU thread.
|
||||||
|
pub fn updateScreen(vx_screen: *const vaxis.Screen) void {
|
||||||
|
const allocator = gpa.allocator();
|
||||||
|
const cell_count: usize = @as(usize, vx_screen.width) * @as(usize, vx_screen.height);
|
||||||
|
|
||||||
|
const new_cells = allocator.alloc(gpu.Cell, cell_count) catch return;
|
||||||
|
const new_font = getFont();
|
||||||
|
|
||||||
|
// Convert vaxis cells → gpu.Cell (glyph + colours)
|
||||||
|
// Glyph indices are filled in on the GPU thread; here we just store 0.
|
||||||
|
for (vx_screen.buf[0..cell_count], new_cells) |*vc, *gc| {
|
||||||
|
gc.* = .{
|
||||||
|
.glyph_index = 0,
|
||||||
|
.background = colorFromVaxis(vc.style.bg),
|
||||||
|
.foreground = colorFromVaxis(vc.style.fg),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
screen_mutex.lock();
|
||||||
|
defer screen_mutex.unlock();
|
||||||
|
|
||||||
|
// Free the previous snapshot
|
||||||
|
if (screen_snap) |old| allocator.free(old.cells);
|
||||||
|
screen_snap = .{
|
||||||
|
.cells = new_cells,
|
||||||
|
.width = vx_screen.width,
|
||||||
|
.height = vx_screen.height,
|
||||||
|
.font = new_font,
|
||||||
|
};
|
||||||
|
|
||||||
|
screen_pending.store(true, .release);
|
||||||
|
wio.cancelWait();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn requestRender() void {
|
||||||
|
screen_pending.store(true, .release);
|
||||||
|
wio.cancelWait();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setFontSize(size_px: f32) void {
|
||||||
|
font_size_px = @intFromFloat(@max(4, size_px));
|
||||||
|
requestRender();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn adjustFontSize(delta: f32) void {
|
||||||
|
const new: f32 = @as(f32, @floatFromInt(font_size_px)) + delta;
|
||||||
|
setFontSize(new);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setFontFace(name: []const u8) void {
|
||||||
|
const copy_len = @min(name.len, font_name_buf.len);
|
||||||
|
@memcpy(font_name_buf[0..copy_len], name[0..copy_len]);
|
||||||
|
font_name_len = copy_len;
|
||||||
|
requestRender();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Internal helpers ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
fn getFont() gpu.Font {
|
||||||
|
const name = if (font_name_len > 0) font_name_buf[0..font_name_len] else "monospace";
|
||||||
|
return gpu.loadFont(name, font_size_px) catch gpu.Font{ .cell_size = .{ .x = 8, .y = 16 } };
|
||||||
|
}
|
||||||
|
|
||||||
|
fn colorFromVaxis(color: vaxis.Cell.Color) gpu.Color {
|
||||||
|
return switch (color) {
|
||||||
|
.default => gpu.Color.initRgb(0, 0, 0),
|
||||||
|
.index => |idx| blk: {
|
||||||
|
const xterm = @import("xterm");
|
||||||
|
const rgb24 = xterm.colors[idx];
|
||||||
|
break :blk gpu.Color.initRgb(
|
||||||
|
@truncate(rgb24 >> 16),
|
||||||
|
@truncate(rgb24 >> 8),
|
||||||
|
@truncate(rgb24),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
.rgb => |rgb| gpu.Color.initRgb(rgb[0], rgb[1], rgb[2]),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── wio main loop (runs on dedicated thread) ──────────────────────────────
|
||||||
|
|
||||||
|
fn wioLoop() void {
|
||||||
|
const allocator = gpa.allocator();
|
||||||
|
|
||||||
|
wio.init(allocator, .{}) catch |e| {
|
||||||
|
log.err("wio.init failed: {s}", .{@errorName(e)});
|
||||||
|
tui_pid.send(.{"quit"}) catch {};
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
defer wio.deinit();
|
||||||
|
|
||||||
|
var window = wio.createWindow(.{
|
||||||
|
.title = "flow",
|
||||||
|
.size = .{ .width = 1280, .height = 720 },
|
||||||
|
.scale = 1.0,
|
||||||
|
.opengl = .{
|
||||||
|
.major_version = 3,
|
||||||
|
.minor_version = 3,
|
||||||
|
.profile = .core,
|
||||||
|
.forward_compatible = true,
|
||||||
|
},
|
||||||
|
}) catch |e| {
|
||||||
|
log.err("wio.createWindow failed: {s}", .{@errorName(e)});
|
||||||
|
tui_pid.send(.{"quit"}) catch {};
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
defer window.destroy();
|
||||||
|
|
||||||
|
window.makeContextCurrent();
|
||||||
|
|
||||||
|
sg.setup(.{
|
||||||
|
.logger = .{ .func = slog.func },
|
||||||
|
});
|
||||||
|
defer sg.shutdown();
|
||||||
|
|
||||||
|
gpu.init(allocator) catch |e| {
|
||||||
|
log.err("gpu.init failed: {s}", .{@errorName(e)});
|
||||||
|
tui_pid.send(.{"quit"}) catch {};
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
defer gpu.deinit();
|
||||||
|
|
||||||
|
var state = gpu.WindowState.init();
|
||||||
|
defer state.deinit();
|
||||||
|
|
||||||
|
// Current window size in pixels (updated by size_physical events)
|
||||||
|
var win_size: wio.Size = .{ .width = 1280, .height = 720 };
|
||||||
|
// Cell grid dimensions (updated on resize)
|
||||||
|
var cell_width: u16 = 80;
|
||||||
|
var cell_height: u16 = 24;
|
||||||
|
|
||||||
|
// Notify the tui that the window is ready
|
||||||
|
sendResize(win_size, &state, &cell_width, &cell_height);
|
||||||
|
tui_pid.send(.{ "RDR", "WindowCreated", @as(usize, 0) }) catch {};
|
||||||
|
|
||||||
|
var held_buttons = input_translate.ButtonSet{};
|
||||||
|
var mouse_pos: wio.Position = .{ .x = 0, .y = 0 };
|
||||||
|
var running = true;
|
||||||
|
|
||||||
|
while (running) {
|
||||||
|
wio.wait(.{});
|
||||||
|
|
||||||
|
while (window.getEvent()) |event| {
|
||||||
|
switch (event) {
|
||||||
|
.close => {
|
||||||
|
running = false;
|
||||||
|
},
|
||||||
|
.size_physical => |sz| {
|
||||||
|
win_size = sz;
|
||||||
|
sendResize(sz, &state, &cell_width, &cell_height);
|
||||||
|
},
|
||||||
|
.button_press => |btn| {
|
||||||
|
held_buttons.press(btn);
|
||||||
|
const mods = input_translate.Mods.fromButtons(held_buttons);
|
||||||
|
if (input_translate.mouseButtonId(btn)) |mb_id| {
|
||||||
|
const col: i32 = @intCast(mouse_pos.x);
|
||||||
|
const row: i32 = @intCast(mouse_pos.y);
|
||||||
|
const font = getFont();
|
||||||
|
const col_cell: i32 = @intCast(@divTrunc(col, font.cell_size.x));
|
||||||
|
const row_cell: i32 = @intCast(@divTrunc(row, font.cell_size.y));
|
||||||
|
const xoff: i32 = @intCast(@mod(col, font.cell_size.x));
|
||||||
|
const yoff: i32 = @intCast(@mod(row, font.cell_size.y));
|
||||||
|
tui_pid.send(.{
|
||||||
|
"RDR", "B",
|
||||||
|
@as(u8, 1), // press
|
||||||
|
mb_id,
|
||||||
|
col_cell,
|
||||||
|
row_cell,
|
||||||
|
xoff,
|
||||||
|
yoff,
|
||||||
|
}) catch {};
|
||||||
|
} else {
|
||||||
|
const cp = input_translate.codepointFromButton(btn, mods);
|
||||||
|
sendKey(1, cp, cp, mods);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.button_repeat => |btn| {
|
||||||
|
const mods = input_translate.Mods.fromButtons(held_buttons);
|
||||||
|
if (input_translate.mouseButtonId(btn) == null) {
|
||||||
|
const cp = input_translate.codepointFromButton(btn, mods);
|
||||||
|
sendKey(2, cp, cp, mods);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.button_release => |btn| {
|
||||||
|
held_buttons.release(btn);
|
||||||
|
const mods = input_translate.Mods.fromButtons(held_buttons);
|
||||||
|
if (input_translate.mouseButtonId(btn)) |mb_id| {
|
||||||
|
const col: i32 = @intCast(mouse_pos.x);
|
||||||
|
const row: i32 = @intCast(mouse_pos.y);
|
||||||
|
const font = getFont();
|
||||||
|
const col_cell: i32 = @intCast(@divTrunc(col, font.cell_size.x));
|
||||||
|
const row_cell: i32 = @intCast(@divTrunc(row, font.cell_size.y));
|
||||||
|
const xoff: i32 = @intCast(@mod(col, font.cell_size.x));
|
||||||
|
const yoff: i32 = @intCast(@mod(row, font.cell_size.y));
|
||||||
|
tui_pid.send(.{
|
||||||
|
"RDR", "B",
|
||||||
|
@as(u8, 0), // release
|
||||||
|
mb_id,
|
||||||
|
col_cell,
|
||||||
|
row_cell,
|
||||||
|
xoff,
|
||||||
|
yoff,
|
||||||
|
}) catch {};
|
||||||
|
} else {
|
||||||
|
const cp = input_translate.codepointFromButton(btn, mods);
|
||||||
|
sendKey(3, cp, cp, mods);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.char => |cp| {
|
||||||
|
const mods = input_translate.Mods.fromButtons(held_buttons);
|
||||||
|
sendKey(1, cp, cp, mods);
|
||||||
|
},
|
||||||
|
.mouse => |pos| {
|
||||||
|
mouse_pos = pos;
|
||||||
|
const font = getFont();
|
||||||
|
const col_cell: i32 = @intCast(@divTrunc(@as(i32, @intCast(pos.x)), font.cell_size.x));
|
||||||
|
const row_cell: i32 = @intCast(@divTrunc(@as(i32, @intCast(pos.y)), font.cell_size.y));
|
||||||
|
const xoff: i32 = @intCast(@mod(@as(i32, @intCast(pos.x)), font.cell_size.x));
|
||||||
|
const yoff: i32 = @intCast(@mod(@as(i32, @intCast(pos.y)), font.cell_size.y));
|
||||||
|
tui_pid.send(.{
|
||||||
|
"RDR", "M",
|
||||||
|
col_cell, row_cell,
|
||||||
|
xoff, yoff,
|
||||||
|
}) catch {};
|
||||||
|
},
|
||||||
|
.scroll_vertical => |dy| {
|
||||||
|
const btn_id: u8 = if (dy < 0) 64 else 65; // up / down scroll
|
||||||
|
const font = getFont();
|
||||||
|
const col_cell: i32 = @intCast(@divTrunc(@as(i32, @intCast(mouse_pos.x)), font.cell_size.x));
|
||||||
|
const row_cell: i32 = @intCast(@divTrunc(@as(i32, @intCast(mouse_pos.y)), font.cell_size.y));
|
||||||
|
tui_pid.send(.{ "RDR", "B", @as(u8, 1), btn_id, col_cell, row_cell, @as(i32, 0), @as(i32, 0) }) catch {};
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Paint if the tui pushed new screen data
|
||||||
|
if (screen_pending.swap(false, .acq_rel)) {
|
||||||
|
screen_mutex.lock();
|
||||||
|
const snap = screen_snap;
|
||||||
|
screen_mutex.unlock();
|
||||||
|
|
||||||
|
if (snap) |s| {
|
||||||
|
state.size = .{ .x = win_size.width, .y = win_size.height };
|
||||||
|
const font = s.font;
|
||||||
|
|
||||||
|
// Regenerate glyph indices using the GPU state
|
||||||
|
const cells_with_glyphs = allocator.alloc(gpu.Cell, s.cells.len) catch continue;
|
||||||
|
defer allocator.free(cells_with_glyphs);
|
||||||
|
@memcpy(cells_with_glyphs, s.cells);
|
||||||
|
|
||||||
|
for (cells_with_glyphs) |*cell| {
|
||||||
|
// TODO: carry codepoint/width from the vaxis screen snapshot.
|
||||||
|
cell.glyph_index = state.generateGlyph(font, ' ', .single);
|
||||||
|
}
|
||||||
|
|
||||||
|
gpu.paint(
|
||||||
|
&state,
|
||||||
|
.{ .x = win_size.width, .y = win_size.height },
|
||||||
|
font,
|
||||||
|
s.height,
|
||||||
|
s.width,
|
||||||
|
0,
|
||||||
|
cells_with_glyphs,
|
||||||
|
);
|
||||||
|
sg.commit();
|
||||||
|
window.swapBuffers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tui_pid.send(.{"quit"}) catch {};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sendResize(
|
||||||
|
sz: wio.Size,
|
||||||
|
state: *gpu.WindowState,
|
||||||
|
cell_width: *u16,
|
||||||
|
cell_height: *u16,
|
||||||
|
) void {
|
||||||
|
const font = getFont();
|
||||||
|
cell_width.* = @intCast(@divTrunc(sz.width, font.cell_size.x));
|
||||||
|
cell_height.* = @intCast(@divTrunc(sz.height, font.cell_size.y));
|
||||||
|
state.size = .{ .x = sz.width, .y = sz.height };
|
||||||
|
tui_pid.send(.{
|
||||||
|
"RDR", "Resize",
|
||||||
|
cell_width.*, cell_height.*,
|
||||||
|
@as(u16, @intCast(sz.width)), @as(u16, @intCast(sz.height)),
|
||||||
|
}) catch {};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sendKey(kind: u8, codepoint: u21, shifted_codepoint: u21, mods: input_translate.Mods) void {
|
||||||
|
var text_buf: [4]u8 = undefined;
|
||||||
|
const text_len = if (codepoint >= 0x20 and codepoint < 0x7f)
|
||||||
|
std.unicode.utf8Encode(codepoint, &text_buf) catch 0
|
||||||
|
else
|
||||||
|
0;
|
||||||
|
tui_pid.send(.{
|
||||||
|
"RDR", "I",
|
||||||
|
kind, @as(u21, codepoint),
|
||||||
|
@as(u21, shifted_codepoint), text_buf[0..text_len],
|
||||||
|
@as(u8, @bitCast(mods)),
|
||||||
|
}) catch {};
|
||||||
|
}
|
||||||
167
src/gui/wio/input.zig
Normal file
167
src/gui/wio/input.zig
Normal file
|
|
@ -0,0 +1,167 @@
|
||||||
|
// Translate wio button events into the Flow keyboard/mouse event format
|
||||||
|
// that is forwarded to the tui thread via cbor messages.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const wio = @import("wio");
|
||||||
|
|
||||||
|
// Modifiers bitmask (matches vaxis.Key.Modifiers packed struct layout used
|
||||||
|
// by the rest of Flow's input handling).
|
||||||
|
pub const Mods = packed struct(u8) {
|
||||||
|
shift: bool = false,
|
||||||
|
alt: bool = false,
|
||||||
|
ctrl: bool = false,
|
||||||
|
super: bool = false,
|
||||||
|
hyper: bool = false,
|
||||||
|
meta: bool = false,
|
||||||
|
_pad: u2 = 0,
|
||||||
|
|
||||||
|
pub fn fromButtons(pressed: ButtonSet) Mods {
|
||||||
|
return .{
|
||||||
|
.shift = pressed.has(.left_shift) or pressed.has(.right_shift),
|
||||||
|
.alt = pressed.has(.left_alt) or pressed.has(.right_alt),
|
||||||
|
.ctrl = pressed.has(.left_control) or pressed.has(.right_control),
|
||||||
|
.super = pressed.has(.left_gui) or pressed.has(.right_gui),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Simple set of currently held buttons (for modifier tracking)
|
||||||
|
pub const ButtonSet = struct {
|
||||||
|
bits: std.bit_set.IntegerBitSet(256) = .initEmpty(),
|
||||||
|
|
||||||
|
pub fn press(self: *ButtonSet, b: wio.Button) void {
|
||||||
|
self.bits.set(@intFromEnum(b));
|
||||||
|
}
|
||||||
|
pub fn release(self: *ButtonSet, b: wio.Button) void {
|
||||||
|
self.bits.unset(@intFromEnum(b));
|
||||||
|
}
|
||||||
|
pub fn has(self: ButtonSet, b: wio.Button) bool {
|
||||||
|
return self.bits.isSet(@intFromEnum(b));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Translate a wio.Button that is a keyboard key into a Unicode codepoint
|
||||||
|
// (or 0 for non-printable keys) and a Flow key kind (press=1, repeat=2,
|
||||||
|
// release=3).
|
||||||
|
pub const KeyEvent = struct {
|
||||||
|
kind: u8,
|
||||||
|
codepoint: u21,
|
||||||
|
shifted_codepoint: u21,
|
||||||
|
text: []const u8,
|
||||||
|
mods: u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Map a wio.Button to the primary codepoint for that key
|
||||||
|
pub fn codepointFromButton(b: wio.Button, mods: Mods) u21 {
|
||||||
|
return switch (b) {
|
||||||
|
.a => if (mods.ctrl) 0x01 else if (mods.shift) 'A' else 'a',
|
||||||
|
.b => if (mods.ctrl) 0x02 else if (mods.shift) 'B' else 'b',
|
||||||
|
.c => if (mods.ctrl) 0x03 else if (mods.shift) 'C' else 'c',
|
||||||
|
.d => if (mods.ctrl) 0x04 else if (mods.shift) 'D' else 'd',
|
||||||
|
.e => if (mods.ctrl) 0x05 else if (mods.shift) 'E' else 'e',
|
||||||
|
.f => if (mods.ctrl) 0x06 else if (mods.shift) 'F' else 'f',
|
||||||
|
.g => if (mods.ctrl) 0x07 else if (mods.shift) 'G' else 'g',
|
||||||
|
.h => if (mods.ctrl) 0x08 else if (mods.shift) 'H' else 'h',
|
||||||
|
.i => if (mods.ctrl) 0x09 else if (mods.shift) 'I' else 'i',
|
||||||
|
.j => if (mods.ctrl) 0x0A else if (mods.shift) 'J' else 'j',
|
||||||
|
.k => if (mods.ctrl) 0x0B else if (mods.shift) 'K' else 'k',
|
||||||
|
.l => if (mods.ctrl) 0x0C else if (mods.shift) 'L' else 'l',
|
||||||
|
.m => if (mods.ctrl) 0x0D else if (mods.shift) 'M' else 'm',
|
||||||
|
.n => if (mods.ctrl) 0x0E else if (mods.shift) 'N' else 'n',
|
||||||
|
.o => if (mods.ctrl) 0x0F else if (mods.shift) 'O' else 'o',
|
||||||
|
.p => if (mods.ctrl) 0x10 else if (mods.shift) 'P' else 'p',
|
||||||
|
.q => if (mods.ctrl) 0x11 else if (mods.shift) 'Q' else 'q',
|
||||||
|
.r => if (mods.ctrl) 0x12 else if (mods.shift) 'R' else 'r',
|
||||||
|
.s => if (mods.ctrl) 0x13 else if (mods.shift) 'S' else 's',
|
||||||
|
.t => if (mods.ctrl) 0x14 else if (mods.shift) 'T' else 't',
|
||||||
|
.u => if (mods.ctrl) 0x15 else if (mods.shift) 'U' else 'u',
|
||||||
|
.v => if (mods.ctrl) 0x16 else if (mods.shift) 'V' else 'v',
|
||||||
|
.w => if (mods.ctrl) 0x17 else if (mods.shift) 'W' else 'w',
|
||||||
|
.x => if (mods.ctrl) 0x18 else if (mods.shift) 'X' else 'x',
|
||||||
|
.y => if (mods.ctrl) 0x19 else if (mods.shift) 'Y' else 'y',
|
||||||
|
.z => if (mods.ctrl) 0x1A else if (mods.shift) 'Z' else 'z',
|
||||||
|
.@"0" => if (mods.shift) ')' else '0',
|
||||||
|
.@"1" => if (mods.shift) '!' else '1',
|
||||||
|
.@"2" => if (mods.shift) '@' else '2',
|
||||||
|
.@"3" => if (mods.shift) '#' else '3',
|
||||||
|
.@"4" => if (mods.shift) '$' else '4',
|
||||||
|
.@"5" => if (mods.shift) '%' else '5',
|
||||||
|
.@"6" => if (mods.shift) '^' else '6',
|
||||||
|
.@"7" => if (mods.shift) '&' else '7',
|
||||||
|
.@"8" => if (mods.shift) '*' else '8',
|
||||||
|
.@"9" => if (mods.shift) '(' else '9',
|
||||||
|
.space => ' ',
|
||||||
|
.enter => '\r',
|
||||||
|
.tab => '\t',
|
||||||
|
.backspace => 0x7f,
|
||||||
|
.escape => 0x1b,
|
||||||
|
.minus => if (mods.shift) '_' else '-',
|
||||||
|
.equals => if (mods.shift) '+' else '=',
|
||||||
|
.left_bracket => if (mods.shift) '{' else '[',
|
||||||
|
.right_bracket => if (mods.shift) '}' else ']',
|
||||||
|
.backslash => if (mods.shift) '|' else '\\',
|
||||||
|
.semicolon => if (mods.shift) ':' else ';',
|
||||||
|
.apostrophe => if (mods.shift) '"' else '\'',
|
||||||
|
.grave => if (mods.shift) '~' else '`',
|
||||||
|
.comma => if (mods.shift) '<' else ',',
|
||||||
|
.dot => if (mods.shift) '>' else '.',
|
||||||
|
.slash => if (mods.shift) '?' else '/',
|
||||||
|
// Navigation keys map to special Unicode private-use codepoints
|
||||||
|
// that Flow's input layer understands (matching kitty protocol).
|
||||||
|
.up => 0xF700,
|
||||||
|
.down => 0xF701,
|
||||||
|
.left => 0xF702,
|
||||||
|
.right => 0xF703,
|
||||||
|
.home => 0xF704,
|
||||||
|
.end => 0xF705,
|
||||||
|
.page_up => 0xF706,
|
||||||
|
.page_down => 0xF707,
|
||||||
|
.insert => 0xF708,
|
||||||
|
.delete => 0xF709,
|
||||||
|
.f1 => 0xF710,
|
||||||
|
.f2 => 0xF711,
|
||||||
|
.f3 => 0xF712,
|
||||||
|
.f4 => 0xF713,
|
||||||
|
.f5 => 0xF714,
|
||||||
|
.f6 => 0xF715,
|
||||||
|
.f7 => 0xF716,
|
||||||
|
.f8 => 0xF717,
|
||||||
|
.f9 => 0xF718,
|
||||||
|
.f10 => 0xF719,
|
||||||
|
.f11 => 0xF71A,
|
||||||
|
.f12 => 0xF71B,
|
||||||
|
.kp_0 => '0',
|
||||||
|
.kp_1 => '1',
|
||||||
|
.kp_2 => '2',
|
||||||
|
.kp_3 => '3',
|
||||||
|
.kp_4 => '4',
|
||||||
|
.kp_5 => '5',
|
||||||
|
.kp_6 => '6',
|
||||||
|
.kp_7 => '7',
|
||||||
|
.kp_8 => '8',
|
||||||
|
.kp_9 => '9',
|
||||||
|
.kp_dot => '.',
|
||||||
|
.kp_slash => '/',
|
||||||
|
.kp_star => '*',
|
||||||
|
.kp_minus => '-',
|
||||||
|
.kp_plus => '+',
|
||||||
|
.kp_enter => '\r',
|
||||||
|
.kp_equals => '=',
|
||||||
|
else => 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const mouse_button_left: u8 = 0;
|
||||||
|
pub const mouse_button_right: u8 = 1;
|
||||||
|
pub const mouse_button_middle: u8 = 2;
|
||||||
|
|
||||||
|
pub fn mouseButtonId(b: wio.Button) ?u8 {
|
||||||
|
return switch (b) {
|
||||||
|
.mouse_left => mouse_button_left,
|
||||||
|
.mouse_right => mouse_button_right,
|
||||||
|
.mouse_middle => mouse_button_middle,
|
||||||
|
.mouse_back => 3,
|
||||||
|
.mouse_forward => 4,
|
||||||
|
else => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
417
src/renderer/gui/renderer.zig
Normal file
417
src/renderer/gui/renderer.zig
Normal file
|
|
@ -0,0 +1,417 @@
|
||||||
|
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 MouseCursorShape = vaxis.Mouse.Shape;
|
||||||
|
|
||||||
|
const GraphemeCache = @import("tuirenderer").GraphemeCache;
|
||||||
|
pub const Plane = @import("tuirenderer").Plane;
|
||||||
|
pub const Layer = @import("tuirenderer").Layer;
|
||||||
|
const input = @import("input");
|
||||||
|
const app = @import("app");
|
||||||
|
|
||||||
|
pub const Cell = @import("tuirenderer").Cell;
|
||||||
|
pub const StyleBits = @import("tuirenderer").style;
|
||||||
|
pub const style = StyleBits;
|
||||||
|
pub const styles = @import("tuirenderer").styles;
|
||||||
|
|
||||||
|
pub const Error = error{
|
||||||
|
UnexpectedRendererEvent,
|
||||||
|
OutOfMemory,
|
||||||
|
IntegerTooLarge,
|
||||||
|
IntegerTooSmall,
|
||||||
|
InvalidType,
|
||||||
|
TooShort,
|
||||||
|
Utf8CannotEncodeSurrogateHalf,
|
||||||
|
CodepointTooLarge,
|
||||||
|
VaxisResizeError,
|
||||||
|
InvalidFloatType,
|
||||||
|
InvalidArrayType,
|
||||||
|
InvalidPIntType,
|
||||||
|
JsonIncompatibleType,
|
||||||
|
NotAnObject,
|
||||||
|
BadArrayAllocExtract,
|
||||||
|
InvalidMapType,
|
||||||
|
InvalidUnion,
|
||||||
|
WriteFailed,
|
||||||
|
} || std.Thread.SpawnError;
|
||||||
|
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
vx: vaxis.Vaxis,
|
||||||
|
cache_storage: GraphemeCache.Storage = .{},
|
||||||
|
event_buffer: std.Io.Writer.Allocating,
|
||||||
|
|
||||||
|
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: i32, x: i32, cbor_msg: []const u8) void = null,
|
||||||
|
dispatch_mouse_drag: ?*const fn (ctx: *anyopaque, y: i32, x: i32, cbor_msg: []const u8) void = null,
|
||||||
|
dispatch_event: ?*const fn (ctx: *anyopaque, cbor_msg: []const u8) void = null,
|
||||||
|
|
||||||
|
thread: ?std.Thread = null,
|
||||||
|
window_ready: bool = false,
|
||||||
|
|
||||||
|
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,
|
||||||
|
) Error!Self {
|
||||||
|
std.debug.assert(!global.init_called);
|
||||||
|
global.init_called = true;
|
||||||
|
_ = no_alternate;
|
||||||
|
|
||||||
|
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),
|
||||||
|
.event_buffer = .init(allocator),
|
||||||
|
.handler_ctx = handler_ctx,
|
||||||
|
.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: std.Io.Writer.Discarding = .init(&.{});
|
||||||
|
self.vx.deinit(self.allocator, &drop.writer);
|
||||||
|
self.event_buffer.deinit();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run(self: *Self) Error!void {
|
||||||
|
if (self.thread) |_| return;
|
||||||
|
// Do a dummy resize to fully initialise vaxis internal state
|
||||||
|
var drop: std.Io.Writer.Discarding = .init(&.{});
|
||||||
|
self.vx.resize(
|
||||||
|
self.allocator,
|
||||||
|
&drop.writer,
|
||||||
|
.{ .rows = 25, .cols = 80, .x_pixel = 0, .y_pixel = 0 },
|
||||||
|
) catch return error.VaxisResizeError;
|
||||||
|
self.thread = try app.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fmtmsg(self: *Self, value: anytype) std.Io.Writer.Error![]const u8 {
|
||||||
|
self.event_buffer.clearRetainingCapacity();
|
||||||
|
try cbor.writeValue(&self.event_buffer.writer, value);
|
||||||
|
return self.event_buffer.written();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render(self: *Self) error{}!void {
|
||||||
|
if (!self.window_ready) return;
|
||||||
|
app.updateScreen(&self.vx.screen);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sigwinch(self: *Self) !void {
|
||||||
|
_ = self;
|
||||||
|
// TODO: implement
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stop(self: *Self) void {
|
||||||
|
app.stop();
|
||||||
|
if (self.thread) |thread| {
|
||||||
|
thread.join();
|
||||||
|
self.thread = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stdplane(self: *Self) Plane {
|
||||||
|
const name = "root";
|
||||||
|
var plane: Plane = .{
|
||||||
|
.window = self.vx.window(),
|
||||||
|
.cache = self.cache_storage.cache(),
|
||||||
|
.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) Error!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),
|
||||||
|
})) {
|
||||||
|
const cbor_msg = try self.fmtmsg(.{
|
||||||
|
"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: std.Io.Writer.Discarding = .init(&.{});
|
||||||
|
self.vx.resize(self.allocator, &drop.writer, .{
|
||||||
|
.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();
|
||||||
|
if (self.dispatch_event) |f| f(self.handler_ctx, try self.fmtmsg(.{"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),
|
||||||
|
})) {
|
||||||
|
if (self.dispatch_mouse) |f| f(
|
||||||
|
self.handler_ctx,
|
||||||
|
@intCast(args.row),
|
||||||
|
@intCast(args.col),
|
||||||
|
try self.fmtmsg(.{
|
||||||
|
"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),
|
||||||
|
})) {
|
||||||
|
if (self.dispatch_mouse) |f| f(
|
||||||
|
self.handler_ctx,
|
||||||
|
@intCast(args.pos.row),
|
||||||
|
@intCast(args.pos.col),
|
||||||
|
try self.fmtmsg(.{
|
||||||
|
"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 hwnd: usize = undefined;
|
||||||
|
if (try cbor.match(msg, .{
|
||||||
|
cbor.any,
|
||||||
|
"WindowCreated",
|
||||||
|
cbor.extract(&hwnd),
|
||||||
|
})) {
|
||||||
|
self.window_ready = true;
|
||||||
|
self.dispatch_initialized(self.handler_ctx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return error.UnexpectedRendererEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_sgr_pixel_mode_support(self: *Self, enable: bool) void {
|
||||||
|
_ = self;
|
||||||
|
_ = enable;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_terminal_title(self: *Self, text: []const u8) void {
|
||||||
|
_ = self;
|
||||||
|
_ = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_terminal_style(self: *Self, style_: Style) void {
|
||||||
|
_ = self;
|
||||||
|
_ = style_;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn adjust_fontsize(self: *Self, amount: f32) void {
|
||||||
|
_ = self;
|
||||||
|
app.adjustFontSize(amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_fontsize(self: *Self, fontsize: f32) void {
|
||||||
|
_ = self;
|
||||||
|
app.setFontSize(fontsize);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset_fontsize(self: *Self) void {
|
||||||
|
_ = self;
|
||||||
|
app.setFontSize(16);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_fontface(self: *Self, fontface: []const u8) void {
|
||||||
|
_ = self;
|
||||||
|
app.setFontFace(fontface);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset_fontface(self: *Self) void {
|
||||||
|
_ = self;
|
||||||
|
app.setFontFace("monospace");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_fontfaces(self: *Self) void {
|
||||||
|
_ = self;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_terminal_cursor_color(self: *Self, color: Color) void {
|
||||||
|
_ = self;
|
||||||
|
_ = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_terminal_secondary_cursor_color(self: *Self, color: Color) void {
|
||||||
|
_ = self;
|
||||||
|
_ = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_terminal_working_directory(self: *Self, absolute_path: []const u8) void {
|
||||||
|
_ = self;
|
||||||
|
_ = absolute_path;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn copy_to_windows_clipboard(self: *Self, text: []const u8) void {
|
||||||
|
_ = self;
|
||||||
|
_ = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn request_windows_clipboard(self: *Self) void {
|
||||||
|
_ = self;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn request_mouse_cursor(self: *Self, shape: MouseCursorShape, push_or_pop: bool) void {
|
||||||
|
_ = self;
|
||||||
|
_ = shape;
|
||||||
|
_ = push_or_pop;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn request_mouse_cursor_text(self: *Self, push_or_pop: bool) void {
|
||||||
|
_ = self;
|
||||||
|
_ = push_or_pop;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn request_mouse_cursor_pointer(self: *Self, push_or_pop: bool) void {
|
||||||
|
_ = self;
|
||||||
|
_ = push_or_pop;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn request_mouse_cursor_default(self: *Self, push_or_pop: bool) void {
|
||||||
|
_ = self;
|
||||||
|
_ = push_or_pop;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cursor_enable(self: *Self, y: i32, x: i32, shape: CursorShape) !void {
|
||||||
|
_ = self;
|
||||||
|
_ = y;
|
||||||
|
_ = x;
|
||||||
|
_ = shape;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cursor_disable(self: *Self) void {
|
||||||
|
_ = self;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_all_multi_cursors(self: *Self) !void {
|
||||||
|
_ = self;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn show_multi_cursor_yx(self: *Self, y: i32, x: i32) !void {
|
||||||
|
_ = self;
|
||||||
|
_ = y;
|
||||||
|
_ = x;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn copy_to_system_clipboard(self: *Self, text: []const u8) void {
|
||||||
|
_ = self;
|
||||||
|
_ = text;
|
||||||
|
// TODO: implement
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn request_system_clipboard(self: *Self) void {
|
||||||
|
_ = self;
|
||||||
|
// TODO: implement
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue