diff --git a/src/core/context.zig b/src/core/context.zig index 51cebac..061b9ed 100644 --- a/src/core/context.zig +++ b/src/core/context.zig @@ -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 diff --git a/src/core/input.zig b/src/core/input.zig index 55b1a89..1ef4ab2 100644 --- a/src/core/input.zig +++ b/src/core/input.zig @@ -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; + } }; // ============================================================================= diff --git a/src/widgets/text_input.zig b/src/widgets/text_input.zig index cbe6044..72803f6 100644 --- a/src/widgets/text_input.zig +++ b/src/widgets/text_input.zig @@ -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 + 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) { - 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( cursor_x, inner.y,