From f3cdb213cf8963d86ee9858ecf5e7cfbe22a6498 Mon Sep 17 00:00:00 2001 From: reugenio Date: Thu, 11 Dec 2025 23:41:56 +0100 Subject: [PATCH] fix: Add table clipping + cursor animation API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Table clipping: - Add clip region around table content area - Prevents cell text from drawing outside table bounds - Header and scrollbar render outside clip region Cursor animation API: - Add CURSOR_IDLE_TIMEOUT_MS (5s) and CURSOR_BLINK_PERIOD_MS (500ms) constants - Add needsCursorAnimation() to check if cursor should blink - Add getAnimationTimeout() for dynamic event loop timeout - Update TextInput to use constants from Context The application can now query ctx.getAnimationTimeout() to determine if a short timeout is needed for cursor animation, or if it can wait indefinitely for events. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/core/context.zig | 28 ++++++++++++++++++++++++++++ src/widgets/table/table.zig | 11 ++++++++++- src/widgets/text_input.zig | 8 +++----- 3 files changed, 41 insertions(+), 6 deletions(-) diff --git a/src/core/context.zig b/src/core/context.zig index 061b9ed..610e052 100644 --- a/src/core/context.zig +++ b/src/core/context.zig @@ -99,6 +99,13 @@ pub const Context = struct { /// Used for idle detection (e.g., cursor stops blinking after inactivity) last_input_time_ms: u64 = 0, + /// Idle timeout for cursor blinking (ms). After this time without input, + /// cursor becomes solid and no animation frames are needed. + pub const CURSOR_IDLE_TIMEOUT_MS: u64 = 5000; + + /// Cursor blink period (ms). Cursor toggles visibility at this rate. + pub const CURSOR_BLINK_PERIOD_MS: u64 = 500; + const Self = @This(); /// Frame statistics for performance monitoring @@ -288,6 +295,27 @@ pub const Context = struct { return self.frame_delta_ms; } + /// Check if cursor animation is needed (for event loop timeout decisions). + /// Returns true if we're within the active period where cursor should blink. + /// The application should use a short timeout (e.g., 500ms) when this returns true, + /// and can use infinite timeout when false. + pub fn needsCursorAnimation(self: Self) bool { + if (self.current_time_ms == 0) return false; + const idle_time = self.current_time_ms -| self.last_input_time_ms; + return idle_time < CURSOR_IDLE_TIMEOUT_MS; + } + + /// Get recommended event loop timeout in milliseconds. + /// Returns the time until next cursor blink toggle, or null for infinite wait. + pub fn getAnimationTimeout(self: Self) ?u32 { + if (!self.needsCursorAnimation()) return null; + + // Calculate time until next blink toggle + const time_in_period = self.current_time_ms % CURSOR_BLINK_PERIOD_MS; + const time_until_toggle = CURSOR_BLINK_PERIOD_MS - time_in_period; + return @intCast(@max(time_until_toggle, 16)); // Minimum 16ms to avoid busy loop + } + // ========================================================================= // ID Management // ========================================================================= diff --git a/src/widgets/table/table.zig b/src/widgets/table/table.zig index 21e109b..7af2832 100644 --- a/src/widgets/table/table.zig +++ b/src/widgets/table/table.zig @@ -13,6 +13,7 @@ const std = @import("std"); const Context = @import("../../core/context.zig").Context; +const Command = @import("../../core/command.zig"); const Layout = @import("../../core/layout.zig"); // Re-export types @@ -152,6 +153,11 @@ pub fn tableRectFull( } } + // Begin clipping to table content area (excludes header) + // This prevents cell text from drawing outside the table bounds + const content_y = bounds.y + @as(i32, @intCast(header_h)); + ctx.pushCommand(Command.clip(bounds.x, content_y, bounds.w, content_h)); + // Calculate visible row range const first_visible = table_state.scroll_row; const last_visible = @min(first_visible + visible_rows, table_state.row_count); @@ -189,7 +195,10 @@ pub fn tableRectFull( if (row_result.cell_edited) result.cell_edited = true; } - // Draw scrollbar if needed + // End clipping + ctx.pushCommand(Command.clipEnd()); + + // Draw scrollbar if needed (outside clip region) if (table_state.row_count > visible_rows) { render.drawScrollbar(ctx, bounds, table_state, visible_rows, config, colors); } diff --git a/src/widgets/text_input.zig b/src/widgets/text_input.zig index 72803f6..917d2fb 100644 --- a/src/widgets/text_input.zig +++ b/src/widgets/text_input.zig @@ -364,16 +364,14 @@ pub fn textInputRect( 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) { + if (idle_time >= Context.CURSOR_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; + // Active: cursor blinks + break :blk (ctx.current_time_ms / Context.CURSOR_BLINK_PERIOD_MS) % 2 == 0; } };