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:
parent
59935aeb2b
commit
e98646b442
3 changed files with 69 additions and 11 deletions
|
|
@ -95,6 +95,10 @@ pub const Context = struct {
|
|||
/// Time delta since last frame in milliseconds
|
||||
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();
|
||||
|
||||
/// Frame statistics for performance monitoring
|
||||
|
|
@ -186,6 +190,11 @@ pub const Context = struct {
|
|||
// Focus system frame end (processes Tab navigation)
|
||||
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();
|
||||
|
||||
// Update final stats
|
||||
|
|
|
|||
|
|
@ -172,6 +172,10 @@ pub const InputState = struct {
|
|||
mouse_x: 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_down: [5]bool = .{ false, false, false, false, false },
|
||||
|
||||
|
|
@ -210,6 +214,8 @@ pub const InputState = struct {
|
|||
pub fn endFrame(self: *Self) void {
|
||||
self.mouse_down_prev = self.mouse_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_y = 0;
|
||||
self.text_input_len = 0;
|
||||
|
|
@ -367,6 +373,36 @@ pub const InputState = struct {
|
|||
}
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
|
|
|
|||
|
|
@ -351,20 +351,33 @@ pub fn textInputRect(
|
|||
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) {
|
||||
// Cursor blinks: visible for 500ms, hidden for 500ms
|
||||
const blink_period_ms: u64 = 500;
|
||||
const cursor_visible = if (ctx.current_time_ms > 0)
|
||||
(ctx.current_time_ms / blink_period_ms) % 2 == 0
|
||||
else
|
||||
true; // Always visible if no timing available
|
||||
|
||||
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;
|
||||
|
||||
// Determine if cursor should be visible
|
||||
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) {
|
||||
ctx.pushCommand(Command.rect(
|
||||
cursor_x,
|
||||
inner.y,
|
||||
|
|
|
|||
Loading…
Reference in a new issue