feat(gui): add gui cursor blink rendering

This commit is contained in:
CJ van den Berg 2026-04-07 16:17:35 +02:00
parent 546cf1f6dc
commit 1c1886defc
Signed by: neurocyte
GPG key ID: 8EB1E1BB660E3FB9
3 changed files with 69 additions and 9 deletions

View file

@ -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,

View file

@ -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 {

View file

@ -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;