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
|
/// 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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
|
||||||
|
|
@ -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 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 char_width: u32 = 8;
|
||||||
const cursor_x = inner.x + @as(i32, @intCast(state.cursor * char_width));
|
const cursor_x = inner.x + @as(i32, @intCast(state.cursor * char_width));
|
||||||
const cursor_color = theme.foreground;
|
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(
|
ctx.pushCommand(Command.rect(
|
||||||
cursor_x,
|
cursor_x,
|
||||||
inner.y,
|
inner.y,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue