diff --git a/src/gui/gpu/builtin.glsl.zig b/src/gui/gpu/builtin.glsl.zig index 88fb7501..eb1b3873 100644 --- a/src/gui/gpu/builtin.glsl.zig +++ b/src/gui/gpu/builtin.glsl.zig @@ -15,6 +15,14 @@ pub const FsParams = extern struct { col_count: i32, row_count: i32, viewport_height: i32, + // Primary cursor (position + appearance) + cursor_col: i32, + cursor_row: i32, + cursor_shape: i32, // 0=block, 1=beam, 2=underline + cursor_vis: i32, // 0=hidden, 1=visible + cursor_color: [4]f32, // RGBA normalized [0,1] + // Secondary cursor colour (positions encoded in ShaderCell._pad) + sec_cursor_color: [4]f32, // RGBA normalized [0,1] }; const vs_src = @@ -34,6 +42,12 @@ const fs_src = \\uniform int col_count; \\uniform int row_count; \\uniform int viewport_height; + \\uniform int cursor_col; + \\uniform int cursor_row; + \\uniform int cursor_shape; + \\uniform int cursor_vis; + \\uniform vec4 cursor_color; + \\uniform vec4 sec_cursor_color; \\uniform sampler2D glyph_tex_glyph_smp; \\uniform usampler2D cell_tex_cell_smp; \\out vec4 frag_color; @@ -59,7 +73,7 @@ const fs_src = \\ return; \\ } \\ - \\ // Fetch cell: texel = (glyph_index, bg_packed, fg_packed, 0) + \\ // Fetch cell: texel = (glyph_index, bg_packed, fg_packed, cursor_flag) \\ uvec4 cell = texelFetch(cell_tex_cell_smp, ivec2(col, row), 0); \\ vec4 bg = unpack_rgba(cell.g); \\ vec4 fg = unpack_rgba(cell.b); @@ -78,9 +92,31 @@ const fs_src = \\ 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); + \\ // Cursor detection + \\ bool is_primary = (cursor_vis != 0) && (col == cursor_col) && (row == cursor_row); + \\ bool is_secondary = (cell.a != 0u); + \\ + \\ vec3 final_bg = bg.rgb; + \\ vec3 final_fg = fg.rgb; + \\ + \\ if (is_primary || is_secondary) { + \\ vec4 cur = is_primary ? cursor_color : sec_cursor_color; + \\ int shape = cursor_shape; + \\ + \\ if (shape == 1) { + \\ // Beam: 2px vertical bar at left edge of cell + \\ if (cell_px_x < 2) { frag_color = vec4(cur.rgb, 1.0); return; } + \\ } else if (shape == 2) { + \\ // Underline: 2px horizontal bar at bottom of cell + \\ if (cell_px_y >= cell_size_y - 2) { frag_color = vec4(cur.rgb, 1.0); return; } + \\ } else { + \\ // Block: cursor colour as bg, inverted for glyph contrast + \\ final_bg = cur.rgb; + \\ final_fg = vec3(1.0) - cur.rgb; + \\ } + \\ } + \\ + \\ frag_color = vec4(mix(final_bg, final_fg, fg.a * glyph_alpha), 1.0); \\} ; @@ -91,7 +127,7 @@ pub fn shaderDesc(backend: sg.Backend) sg.ShaderDesc { desc.vertex_func.source = vs_src; desc.fragment_func.source = fs_src; - // Fragment uniform block: 4 individual INT uniforms + // Fragment uniform block: individual uniforms (GLCORE uses glUniform* calls) desc.uniform_blocks[0].stage = .FRAGMENT; desc.uniform_blocks[0].size = @sizeOf(FsParams); desc.uniform_blocks[0].layout = .NATIVE; @@ -100,6 +136,12 @@ pub fn shaderDesc(backend: sg.Backend) sg.ShaderDesc { 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" }; desc.uniform_blocks[0].glsl_uniforms[4] = .{ .type = .INT, .glsl_name = "viewport_height" }; + desc.uniform_blocks[0].glsl_uniforms[5] = .{ .type = .INT, .glsl_name = "cursor_col" }; + desc.uniform_blocks[0].glsl_uniforms[6] = .{ .type = .INT, .glsl_name = "cursor_row" }; + desc.uniform_blocks[0].glsl_uniforms[7] = .{ .type = .INT, .glsl_name = "cursor_shape" }; + desc.uniform_blocks[0].glsl_uniforms[8] = .{ .type = .INT, .glsl_name = "cursor_vis" }; + desc.uniform_blocks[0].glsl_uniforms[9] = .{ .type = .FLOAT4, .glsl_name = "cursor_color" }; + desc.uniform_blocks[0].glsl_uniforms[10] = .{ .type = .FLOAT4, .glsl_name = "sec_cursor_color" }; // Glyph atlas texture: R8 → sample_type = FLOAT desc.views[0].texture = .{ diff --git a/src/gui/gpu/gpu.zig b/src/gui/gpu/gpu.zig index d741adbc..7c75b181 100644 --- a/src/gui/gpu/gpu.zig +++ b/src/gui/gpu/gpu.zig @@ -18,6 +18,16 @@ pub const Cell = gui_cell.Cell; pub const Color = gui_cell.Rgba8; const Rgba8 = gui_cell.Rgba8; +pub const CursorShape = enum(i32) { block = 0, beam = 1, underline = 2 }; + +pub const CursorInfo = struct { + vis: bool = false, + row: u16 = 0, + col: u16 = 0, + shape: CursorShape = .block, + color: Color = Color.initRgb(255, 255, 255), +}; + const log = std.log.scoped(.gpu); // Maximum glyph atlas dimension. 4096 is universally supported and gives @@ -350,6 +360,8 @@ pub fn paint( col_count: u16, top: u16, cells: []const Cell, + cursor: CursorInfo, + secondary_cursors: []const CursorInfo, ) void { const shader_col_count: u16 = @intCast(@divTrunc(client_size.x, font.cell_size.x)); const shader_row_count: u16 = @intCast(@divTrunc(client_size.y, font.cell_size.y)); @@ -388,6 +400,13 @@ pub fn paint( } } + // Mark secondary cursor cells in the _pad field (read by fragment shader). + for (secondary_cursors) |sc| { + if (!sc.vis) continue; + if (sc.row >= shader_row_count or sc.col >= shader_col_count) continue; + shader_cells[@as(usize, sc.row) * shader_col_count + sc.col]._pad = 1; + } + // Upload glyph atlas to GPU if any new glyphs were rasterized this frame. if (state.glyph_atlas_dirty) flushGlyphAtlas(state); @@ -429,12 +448,23 @@ pub fn paint( bindings.samplers[1] = global.cell_sampler; sg.applyBindings(bindings); + const sec_color: Color = if (secondary_cursors.len > 0) + secondary_cursors[0].color + else + Color.initRgb(255, 255, 255); + 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, .viewport_height = @intCast(client_size.y), + .cursor_col = cursor.col, + .cursor_row = cursor.row, + .cursor_shape = @intFromEnum(cursor.shape), + .cursor_vis = if (cursor.vis) 1 else 0, + .cursor_color = colorToVec4(cursor.color), + .sec_cursor_color = colorToVec4(sec_color), }; sg.applyUniforms(0, .{ .ptr = &fs_params, @@ -446,6 +476,15 @@ pub fn paint( // Note: caller (app.zig) calls sg.commit() and window.swapBuffers() } +fn colorToVec4(c: Color) [4]f32 { + return .{ + @as(f32, @floatFromInt(c.r)) / 255.0, + @as(f32, @floatFromInt(c.g)) / 255.0, + @as(f32, @floatFromInt(c.b)) / 255.0, + @as(f32, @floatFromInt(c.a)) / 255.0, + }; +} + fn oom(e: error{OutOfMemory}) noreturn { @panic(@errorName(e)); } diff --git a/src/gui/wio/app.zig b/src/gui/wio/app.zig index 0b1e06bd..9d1bb98a 100644 --- a/src/gui/wio/app.zig +++ b/src/gui/wio/app.zig @@ -22,6 +22,12 @@ const gui_config = @import("gui_config"); const log = std.log.scoped(.wio_app); +// Re-export cursor types so renderer.zig (which imports 'app' but not 'gpu') +// can use them without a direct dependency on the gpu module. +pub const CursorInfo = gpu.CursorInfo; +pub const CursorShape = gpu.CursorShape; +pub const GpuColor = gpu.Color; + // ── Shared state (protected by screen_mutex) ────────────────────────────── const ScreenSnapshot = struct { @@ -31,6 +37,9 @@ const ScreenSnapshot = struct { widths: []u8, width: u16, height: u16, + // Cursor state (set by renderer thread, consumed by wio thread) + cursor: gpu.CursorInfo, + secondary_cursors: []gpu.CursorInfo, // heap-allocated, freed with snapshot }; var gpa: std.heap.GeneralPurposeAllocator(.{}) = .{}; @@ -92,7 +101,11 @@ pub fn stop() void { } /// Called from the tui thread to push a new screen to the GPU thread. -pub fn updateScreen(vx_screen: *const vaxis.Screen) void { +pub fn updateScreen( + vx_screen: *const vaxis.Screen, + cursor: gpu.CursorInfo, + secondary_cursors: []const gpu.CursorInfo, +) void { const allocator = gpa.allocator(); const cell_count: usize = @as(usize, vx_screen.width) * @as(usize, vx_screen.height); @@ -106,6 +119,13 @@ pub fn updateScreen(vx_screen: *const vaxis.Screen) void { allocator.free(new_codepoints); return; }; + const new_sec = allocator.alloc(gpu.CursorInfo, secondary_cursors.len) catch { + allocator.free(new_cells); + allocator.free(new_codepoints); + allocator.free(new_widths); + return; + }; + @memcpy(new_sec, secondary_cursors); // Convert vaxis cells → gpu.Cell (colours only; glyph indices filled on GPU thread). for (vx_screen.buf[0..cell_count], new_cells, new_codepoints, new_widths) |*vc, *gc, *cp, *wt| { @@ -131,6 +151,7 @@ pub fn updateScreen(vx_screen: *const vaxis.Screen) void { allocator.free(old.cells); allocator.free(old.codepoints); allocator.free(old.widths); + allocator.free(old.secondary_cursors); } screen_snap = .{ .cells = new_cells, @@ -138,6 +159,8 @@ pub fn updateScreen(vx_screen: *const vaxis.Screen) void { .widths = new_widths, .width = vx_screen.width, .height = vx_screen.height, + .cursor = cursor, + .secondary_cursors = new_sec, }; screen_pending.store(true, .release); @@ -561,6 +584,7 @@ fn wioLoop() void { allocator.free(s.cells); allocator.free(s.codepoints); allocator.free(s.widths); + allocator.free(s.secondary_cursors); } state.size = .{ .x = win_size.width, .y = win_size.height }; @@ -595,6 +619,8 @@ fn wioLoop() void { s.width, 0, cells_with_glyphs, + s.cursor, + s.secondary_cursors, ); sg.commit(); window.swapBuffers(); diff --git a/src/renderer/gui/renderer.zig b/src/renderer/gui/renderer.zig index 49c4a73c..7d0e53f4 100644 --- a/src/renderer/gui/renderer.zig +++ b/src/renderer/gui/renderer.zig @@ -56,6 +56,11 @@ dispatch_event: ?*const fn (ctx: *anyopaque, cbor_msg: []const u8) void = null, thread: ?std.Thread = null, window_ready: bool = false, +cursor_info: app.CursorInfo = .{}, +cursor_color: app.GpuColor = app.GpuColor.initRgb(255, 255, 255), +secondary_cursors: std.ArrayListUnmanaged(app.CursorInfo) = .{}, +secondary_color: app.GpuColor = app.GpuColor.initRgb(255, 255, 255), + const global = struct { var init_called: bool = false; }; @@ -92,6 +97,7 @@ pub fn init( .dispatch_initialized = dispatch_initialized, }; result.vx.caps.unicode = .unicode; + result.vx.caps.multi_cursor = true; result.vx.screen.width_method = .unicode; return result; } @@ -101,6 +107,7 @@ pub fn deinit(self: *Self) void { var drop: std.Io.Writer.Discarding = .init(&.{}); self.vx.deinit(self.allocator, &drop.writer); self.event_buffer.deinit(); + self.secondary_cursors.deinit(self.allocator); } pub fn run(self: *Self) Error!void { @@ -123,7 +130,7 @@ fn fmtmsg(self: *Self, value: anytype) std.Io.Writer.Error![]const u8 { pub fn render(self: *Self) error{}!void { if (!self.window_ready) return; - app.updateScreen(&self.vx.screen); + app.updateScreen(&self.vx.screen, self.cursor_info, self.secondary_cursors.items); } pub fn sigwinch(self: *Self) !void { @@ -414,13 +421,11 @@ pub fn get_fontfaces(self: *Self) void { } pub fn set_terminal_cursor_color(self: *Self, color: Color) void { - _ = self; - _ = color; + self.cursor_color = themeColorToGpu(color); } pub fn set_terminal_secondary_cursor_color(self: *Self, color: Color) void { - _ = self; - _ = color; + self.secondary_color = themeColorToGpu(color); } pub fn set_terminal_working_directory(self: *Self, absolute_path: []const u8) void { @@ -462,24 +467,48 @@ pub fn request_mouse_cursor_default(self: *Self, push_or_pop: bool) void { } pub fn cursor_enable(self: *Self, y: i32, x: i32, shape: CursorShape) !void { - _ = self; - _ = y; - _ = x; - _ = shape; + self.cursor_info = .{ + .vis = true, + .row = if (y < 0) 0 else @intCast(y), + .col = if (x < 0) 0 else @intCast(x), + .shape = vaxisCursorShape(shape), + .color = self.cursor_color, + }; } pub fn cursor_disable(self: *Self) void { - _ = self; + self.cursor_info.vis = false; } pub fn clear_all_multi_cursors(self: *Self) !void { - _ = self; + self.secondary_cursors.clearRetainingCapacity(); } pub fn show_multi_cursor_yx(self: *Self, y: i32, x: i32) !void { - _ = self; - _ = y; - _ = x; + try self.secondary_cursors.append(self.allocator, .{ + .vis = true, + .row = if (y < 0) 0 else @intCast(y), + .col = if (x < 0) 0 else @intCast(x), + .shape = self.cursor_info.shape, + .color = self.secondary_color, + }); +} + +fn themeColorToGpu(color: Color) app.GpuColor { + return .{ + .r = @truncate(color.color >> 16), + .g = @truncate(color.color >> 8), + .b = @truncate(color.color), + .a = color.alpha, + }; +} + +fn vaxisCursorShape(shape: CursorShape) app.CursorShape { + return switch (shape) { + .default, .block, .block_blink => .block, + .beam, .beam_blink => .beam, + .underline, .underline_blink => .underline, + }; } pub fn copy_to_system_clipboard(self: *Self, text: []const u8) void { diff --git a/src/tui/tui.zig b/src/tui/tui.zig index 502ae979..bc92fa2b 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -2190,7 +2190,6 @@ pub fn is_cursor_beam() bool { } pub fn has_native_cursor() bool { - if (build_options.gui) return false; return current().config_.enable_terminal_cursor; }