feat: Hybrid cursor blink - blinks while active, solid when idle

Implement user-friendly cursor behavior:
- Cursor blinks (500ms on/off) while there's user activity
- After 5 seconds of inactivity, cursor becomes solid (always visible)
- Any input (keyboard, mouse move, click, scroll) resets the timer

Changes:
- context.zig: Add last_input_time_ms tracking
- input.zig: Add hasActivity() and hasActivityWithMouse() methods
- input.zig: Track mouse_x_prev/mouse_y_prev for movement detection
- text_input.zig: Implement hybrid blink logic

This saves battery on laptops while maintaining natural cursor feedback.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
reugenio 2025-12-11 23:32:35 +01:00
parent 59935aeb2b
commit e98646b442
3 changed files with 69 additions and 11 deletions

View file

@ -95,6 +95,10 @@ pub const Context = struct {
/// Time delta since last frame in milliseconds /// Time delta since last frame in milliseconds
frame_delta_ms: u32 = 0, frame_delta_ms: u32 = 0,
/// Last time there was user input (keyboard or mouse activity)
/// Used for idle detection (e.g., cursor stops blinking after inactivity)
last_input_time_ms: u64 = 0,
const Self = @This(); const Self = @This();
/// Frame statistics for performance monitoring /// Frame statistics for performance monitoring
@ -186,6 +190,11 @@ pub const Context = struct {
// Focus system frame end (processes Tab navigation) // Focus system frame end (processes Tab navigation)
self.focus.endFrame(); self.focus.endFrame();
// Update last input time if there was activity this frame
if (self.input.hasActivityWithMouse() and self.current_time_ms > 0) {
self.last_input_time_ms = self.current_time_ms;
}
self.input.endFrame(); self.input.endFrame();
// Update final stats // Update final stats

View file

@ -172,6 +172,10 @@ pub const InputState = struct {
mouse_x: i32 = 0, mouse_x: i32 = 0,
mouse_y: i32 = 0, mouse_y: i32 = 0,
// Mouse position (previous frame, for detecting movement)
mouse_x_prev: i32 = 0,
mouse_y_prev: i32 = 0,
// Mouse buttons (current frame) // Mouse buttons (current frame)
mouse_down: [5]bool = .{ false, false, false, false, false }, mouse_down: [5]bool = .{ false, false, false, false, false },
@ -210,6 +214,8 @@ pub const InputState = struct {
pub fn endFrame(self: *Self) void { pub fn endFrame(self: *Self) void {
self.mouse_down_prev = self.mouse_down; self.mouse_down_prev = self.mouse_down;
self.keys_down_prev = self.keys_down; self.keys_down_prev = self.keys_down;
self.mouse_x_prev = self.mouse_x;
self.mouse_y_prev = self.mouse_y;
self.scroll_x = 0; self.scroll_x = 0;
self.scroll_y = 0; self.scroll_y = 0;
self.text_input_len = 0; self.text_input_len = 0;
@ -367,6 +373,36 @@ pub const InputState = struct {
} }
return null; return null;
} }
/// Check if there was any user activity this frame (keyboard or mouse)
/// Used for idle detection (cursor blinking, screensaver, etc.)
pub fn hasActivity(self: Self) bool {
// Key events this frame
if (self.key_event_count > 0) return true;
// Text input this frame
if (self.text_input_len > 0) return true;
// Mouse button changes
for (self.mouse_down, 0..) |down, i| {
if (down != self.mouse_down_prev[i]) return true;
}
// Scroll activity
if (self.scroll_x != 0 or self.scroll_y != 0) return true;
return false;
}
/// Check if there was any activity including mouse movement
pub fn hasActivityWithMouse(self: Self) bool {
if (self.hasActivity()) return true;
// Mouse movement
if (self.mouse_x != self.mouse_x_prev or self.mouse_y != self.mouse_y_prev) return true;
return false;
}
}; };
// ============================================================================= // =============================================================================

View file

@ -351,20 +351,33 @@ pub fn textInputRect(
ctx.pushCommand(Command.text(inner.x, text_y, display_text, display_color)); ctx.pushCommand(Command.text(inner.x, text_y, display_text, display_color));
} }
// Draw cursor if focused (blinking every 500ms) // Draw cursor if focused
// Hybrid behavior: blinks while active, solid after idle timeout
if (has_focus and !config.readonly) { if (has_focus and !config.readonly) {
// Cursor blinks: visible for 500ms, hidden for 500ms const char_width: u32 = 8;
const blink_period_ms: u64 = 500; const cursor_x = inner.x + @as(i32, @intCast(state.cursor * char_width));
const cursor_visible = if (ctx.current_time_ms > 0) const cursor_color = theme.foreground;
(ctx.current_time_ms / blink_period_ms) % 2 == 0
else // Determine if cursor should be visible
true; // Always visible if no timing available const cursor_visible = blk: {
// If no timing available, always show cursor
if (ctx.current_time_ms == 0) break :blk true;
// Check idle time (time since last user input)
const idle_timeout_ms: u64 = 5000; // 5 seconds
const idle_time = ctx.current_time_ms -| ctx.last_input_time_ms;
if (idle_time >= idle_timeout_ms) {
// Idle: cursor always visible (solid, no blink)
break :blk true;
} else {
// Active: cursor blinks (visible for 500ms, hidden for 500ms)
const blink_period_ms: u64 = 500;
break :blk (ctx.current_time_ms / blink_period_ms) % 2 == 0;
}
};
if (cursor_visible) { if (cursor_visible) {
const char_width: u32 = 8;
const cursor_x = inner.x + @as(i32, @intCast(state.cursor * char_width));
const cursor_color = theme.foreground;
ctx.pushCommand(Command.rect( ctx.pushCommand(Command.rect(
cursor_x, cursor_x,
inner.y, inner.y,