From 1c1886defca5bd7bce495960ffe0d3e34de5c746 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 7 Apr 2026 16:17:35 +0200 Subject: [PATCH] feat(gui): add gui cursor blink rendering --- src/renderer/gui/renderer.zig | 61 +++++++++++++++++++++++++++++++-- src/renderer/vaxis/renderer.zig | 5 +-- src/tui/tui.zig | 12 ++++--- 3 files changed, 69 insertions(+), 9 deletions(-) diff --git a/src/renderer/gui/renderer.zig b/src/renderer/gui/renderer.zig index 7d0e53f4..2cc582f2 100644 --- a/src/renderer/gui/renderer.zig +++ b/src/renderer/gui/renderer.zig @@ -61,6 +61,15 @@ 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), +cursor_blink: bool = false, +blink_on: bool = true, +blink_epoch: i64 = 0, +blink_period_us: i64 = 500_000, +blink_idle_us: i64 = 15_000_000, +blink_last_change: i64 = 0, +prev_cursor: app.CursorInfo = .{}, +prev_cursor_blink: bool = false, + const global = struct { var init_called: bool = false; }; @@ -128,9 +137,47 @@ fn fmtmsg(self: *Self, value: anytype) std.Io.Writer.Error![]const u8 { return self.event_buffer.written(); } -pub fn render(self: *Self) error{}!void { - if (!self.window_ready) return; - app.updateScreen(&self.vx.screen, self.cursor_info, self.secondary_cursors.items); +pub fn render(self: *Self) error{}!bool { + if (!self.window_ready) return false; + + var cursor = self.cursor_info; + + // Detect changes since the last rendered frame. Reset blink epoch and idle + // timer on any meaningful change so the cursor snaps to visible immediately. + if (cursor.vis != self.prev_cursor.vis or + cursor.row != self.prev_cursor.row or + cursor.col != self.prev_cursor.col or + cursor.shape != self.prev_cursor.shape or + self.cursor_blink != self.prev_cursor_blink) + { + const now = std.time.microTimestamp(); + if (cursor.vis) { + self.blink_epoch = now; + self.blink_on = true; + } + self.blink_last_change = now; + } + self.prev_cursor = cursor; + self.prev_cursor_blink = self.cursor_blink; + + // Apply blink unless the cursor has been idle for too long. + if (cursor.vis and self.cursor_blink) { + const now = std.time.microTimestamp(); + const idle = now - self.blink_last_change; + if (idle < self.blink_idle_us) { + const elapsed = @mod(now - self.blink_epoch, self.blink_period_us * 2); + self.blink_on = elapsed < self.blink_period_us; + cursor.vis = self.blink_on; + } else { + cursor.vis = true; // freeze visible after idle timeout + } + } + + app.updateScreen(&self.vx.screen, cursor, self.secondary_cursors.items); + + if (!self.cursor_info.vis or !self.cursor_blink) return false; + const idle = std.time.microTimestamp() - self.blink_last_change; + return idle < self.blink_idle_us; } pub fn sigwinch(self: *Self) !void { @@ -467,6 +514,7 @@ 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.cursor_blink = isBlink(shape); self.cursor_info = .{ .vis = true, .row = if (y < 0) 0 else @intCast(y), @@ -503,6 +551,13 @@ fn themeColorToGpu(color: Color) app.GpuColor { }; } +fn isBlink(shape: CursorShape) bool { + return switch (shape) { + .default, .block_blink, .beam_blink, .underline_blink => true, + else => false, + }; +} + fn vaxisCursorShape(shape: CursorShape) app.CursorShape { return switch (shape) { .default, .block, .block_blink => .block, diff --git a/src/renderer/vaxis/renderer.zig b/src/renderer/vaxis/renderer.zig index 95025892..30710f43 100644 --- a/src/renderer/vaxis/renderer.zig +++ b/src/renderer/vaxis/renderer.zig @@ -219,10 +219,11 @@ pub fn run(self: *Self) Error!void { try self.loop.start(); } -pub fn render(self: *Self) !void { - if (in_panic.load(.acquire)) return; +pub fn render(self: *Self) !bool { + if (in_panic.load(.acquire)) return false; try self.vx.render(self.tty.writer()); try self.tty.writer().flush(); + return false; } pub fn sigwinch(self: *Self) !void { diff --git a/src/tui/tui.zig b/src/tui/tui.zig index bc92fa2b..d457819d 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -681,13 +681,17 @@ fn render(self: *Self) void { top_layer_.draw(self.rdr_.stdplane()); } - { + const renderer_more = ret: { const frame = tracy.initZone(@src(), .{ .name = renderer.log_name ++ " render" }); defer frame.deinit(); - self.rdr_.render() catch |e| self.logger.err("render", e); + const m = self.rdr_.render() catch |e| blk: { + self.logger.err("render", e); + break :blk false; + }; tracy.frameMark(); self.unrendered_input_events_count = 0; - } + break :ret m; + }; self.top_layer_reset(); self.idle_frame_count = if (self.unrendered_input_events_count > 0) @@ -695,7 +699,7 @@ fn render(self: *Self) void { else self.idle_frame_count + 1; - if (more or self.idle_frame_count < idle_frames or self.no_sleep) { + if (more or renderer_more or self.idle_frame_count < idle_frames or self.no_sleep) { if (!self.frame_clock_running) { self.frame_clock.start() catch {}; self.frame_clock_running = true;