feat(gui): add gui cursor rendering
This commit is contained in:
parent
b0d32f3581
commit
546cf1f6dc
5 changed files with 156 additions and 21 deletions
|
|
@ -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 = .{
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue