perf: Smart Cursor - widgets request cursor blink explicitly

Implements "Active Request" pattern for cursor animation:
- Add requested_cursor_blink flag to Context (reset each frame)
- Add requestCursorBlink() function for widgets to call
- needsCursorAnimation() now returns false if no widget requested it
- TextInput calls requestCursorBlink() when focused and editable
- CellEditor calls requestCursorBlink() when editing

This eliminates unnecessary redraws when no cursor is visible
(e.g., in Config tab with tables but no active text input).

Also raised CURSOR_BLINK_PERIOD_MS from 300ms to 600ms (GTK/Linux standard).

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
R.Eugenio 2026-01-07 11:59:24 +01:00
parent f366a30b66
commit dacc3eb57d
3 changed files with 27 additions and 1 deletions

View file

@ -127,6 +127,11 @@ pub const Context = struct {
/// Main loop should check this and request another frame if true. /// Main loop should check this and request another frame if true.
needs_animation_frame: bool = false, needs_animation_frame: bool = false,
/// Flag set by widgets that have an active cursor (TextInput, CellEditor).
/// Reset each frame in beginFrame(). Only when true does needsCursorAnimation() return true.
/// This prevents unnecessary redraws when no text field is being edited.
requested_cursor_blink: bool = false,
/// Optional text measurement function (set by application with TTF font) /// Optional text measurement function (set by application with TTF font)
/// Returns pixel width of text. If null, falls back to char_width * len. /// Returns pixel width of text. If null, falls back to char_width * len.
text_measure_fn: ?*const fn ([]const u8) u32 = null, text_measure_fn: ?*const fn ([]const u8) u32 = null,
@ -244,6 +249,9 @@ pub const Context = struct {
// Reset animation request (set by widgets during draw) // Reset animation request (set by widgets during draw)
self.needs_animation_frame = false; self.needs_animation_frame = false;
// Reset cursor blink request (set by TextInput/CellEditor during draw)
self.requested_cursor_blink = false;
self.frame += 1; self.frame += 1;
} }
@ -393,14 +401,24 @@ pub const Context = struct {
/// Check if cursor animation is needed (for event loop timeout decisions). /// Check if cursor animation is needed (for event loop timeout decisions).
/// Returns true if we're within the active period where cursor should blink. /// 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, /// Only returns true if a widget (TextInput/CellEditor) requested cursor blink this frame.
/// The application should use a short timeout (e.g., 600ms) when this returns true,
/// and can use infinite timeout when false. /// and can use infinite timeout when false.
pub fn needsCursorAnimation(self: Self) bool { pub fn needsCursorAnimation(self: Self) bool {
if (self.current_time_ms == 0) return false; if (self.current_time_ms == 0) return false;
// Smart cursor: only blink if a widget actually requested it this frame
if (!self.requested_cursor_blink) return false;
const idle_time = self.current_time_ms -| self.last_input_time_ms; const idle_time = self.current_time_ms -| self.last_input_time_ms;
return idle_time < CURSOR_IDLE_TIMEOUT_MS; return idle_time < CURSOR_IDLE_TIMEOUT_MS;
} }
/// Request cursor blink animation for this frame.
/// Called by widgets with active text cursors (TextInput, CellEditor).
/// Must be called each frame while cursor is active (immediate-mode pattern).
pub fn requestCursorBlink(self: *Self) void {
self.requested_cursor_blink = true;
}
/// Get recommended event loop timeout in milliseconds. /// Get recommended event loop timeout in milliseconds.
/// Returns the time until next cursor blink toggle, or null for infinite wait. /// Returns the time until next cursor blink toggle, or null for infinite wait.
pub fn getAnimationTimeout(self: Self) ?u32 { pub fn getAnimationTimeout(self: Self) ?u32 {

View file

@ -284,6 +284,11 @@ pub fn textInputRect(
// Sync state.focused for backwards compatibility // Sync state.focused for backwards compatibility
state.focused = has_focus; state.focused = has_focus;
// Smart cursor: request cursor blink only when we have an active editable cursor
if (has_focus and !config.readonly) {
ctx.requestCursorBlink();
}
// Theme colors (Z-Design: usar theme dinámico, con overrides del panel) // Theme colors (Z-Design: usar theme dinámico, con overrides del panel)
const theme = Style.currentTheme().*; const theme = Style.currentTheme().*;
// Use override colors if provided, otherwise use theme defaults // Use override colors if provided, otherwise use theme defaults

View file

@ -57,6 +57,9 @@ pub fn drawCellEditor(
if (!state.isEditing()) return result; if (!state.isEditing()) return result;
// Smart cursor: request cursor blink while editing cell
ctx.requestCursorBlink();
// Padding interno // Padding interno
const padding: i32 = 2; const padding: i32 = 2;