diff --git a/src/core/context.zig b/src/core/context.zig index f588a79..a3c817a 100644 --- a/src/core/context.zig +++ b/src/core/context.zig @@ -111,7 +111,7 @@ pub const Context = struct { /// 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; + pub const CURSOR_IDLE_TIMEOUT_MS: u64 = 20000; /// Cursor blink period (ms). Cursor toggles visibility at this rate. /// 300ms = ~3.3 blinks/sec (faster for better editing feedback) @@ -363,6 +363,26 @@ pub const Context = struct { return @intCast(@max(time_until_toggle, 16)); // Minimum 16ms to avoid busy loop } + /// Determina si el cursor debe ser visible basado en tiempo de actividad. + /// Usa parpadeo mientras hay actividad reciente, sólido cuando está idle. + /// @param last_activity_time_ms: Última vez que hubo actividad (edición, input) + /// @return true si el cursor debe dibujarse visible + pub fn isCursorVisible(self: Self, last_activity_time_ms: u64) bool { + // Si no hay timing disponible, mostrar cursor siempre + if (self.current_time_ms == 0) return true; + + // Calcular tiempo idle desde última actividad + const idle_time = self.current_time_ms -| last_activity_time_ms; + + if (idle_time >= CURSOR_IDLE_TIMEOUT_MS) { + // Idle: cursor siempre visible (sin parpadeo, ahorra batería) + return true; + } else { + // Activo: cursor parpadea + return (self.current_time_ms / CURSOR_BLINK_PERIOD_MS) % 2 == 0; + } + } + // ========================================================================= // ID Management // ========================================================================= diff --git a/src/widgets/text_input.zig b/src/widgets/text_input.zig index afb5573..bcad615 100644 --- a/src/widgets/text_input.zig +++ b/src/widgets/text_input.zig @@ -376,22 +376,8 @@ pub fn textInputRect( const cursor_x = inner.x + @as(i32, @intCast(cursor_offset)); 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_time = ctx.current_time_ms -| ctx.last_input_time_ms; - - if (idle_time >= Context.CURSOR_IDLE_TIMEOUT_MS) { - // Idle: cursor always visible (solid, no blink) - break :blk true; - } else { - // Active: cursor blinks - break :blk (ctx.current_time_ms / Context.CURSOR_BLINK_PERIOD_MS) % 2 == 0; - } - }; + // Visibilidad del cursor usando función compartida de Context + const cursor_visible = ctx.isCursorVisible(ctx.last_input_time_ms); if (cursor_visible) { ctx.pushCommand(Command.rect( diff --git a/src/widgets/virtual_advanced_table/cell_editor.zig b/src/widgets/virtual_advanced_table/cell_editor.zig index c907132..c8fa16f 100644 --- a/src/widgets/virtual_advanced_table/cell_editor.zig +++ b/src/widgets/virtual_advanced_table/cell_editor.zig @@ -81,24 +81,27 @@ pub fn drawCellEditor( // Texto actual const text = state.getEditText(); + const text_y = geom.y + @as(i32, @intCast((geom.h -| 16) / 2)); // Centrado vertical ctx.pushCommand(Command.text( geom.x + padding, - geom.y + padding, + text_y, text, colors.text, )); - // Cursor (línea vertical parpadeante) - // Calcular posición X del cursor basado en edit_cursor - const cursor_x = geom.x + padding + @as(i32, @intCast(state.edit_cursor * 7)); // ~7px por caracter (monospace) - const cursor_visible = (state.frame_count / 30) % 2 == 0; // Parpadeo cada 30 frames + // Cursor: posición calculada con measureTextToCursor (TTF-aware) + const cursor_offset = ctx.measureTextToCursor(text, state.edit_cursor); + const cursor_x = geom.x + padding + @as(i32, @intCast(cursor_offset)); + + // Visibilidad del cursor usando función compartida de Context + const cursor_visible = ctx.isCursorVisible(state.last_edit_time_ms); if (cursor_visible) { ctx.pushCommand(Command.rect( cursor_x, - geom.y + padding, - 1, // 1px de ancho - geom.h - (padding * 2), + text_y, + 2, // 2px de ancho (más visible) + 16, // Altura del texto colors.cursor, )); } @@ -106,6 +109,11 @@ pub fn drawCellEditor( // Procesar input de teclado result = handleCellEditorInput(ctx, state); + // Actualizar tiempo de última edición si hubo cambios + if (result.text_changed) { + state.last_edit_time_ms = ctx.current_time_ms; + } + return result; } diff --git a/src/widgets/virtual_advanced_table/state.zig b/src/widgets/virtual_advanced_table/state.zig index 6381e64..755cc80 100644 --- a/src/widgets/virtual_advanced_table/state.zig +++ b/src/widgets/virtual_advanced_table/state.zig @@ -63,6 +63,9 @@ pub const VirtualAdvancedTableState = struct { /// Offset donde empieza la ventana actual window_start: usize = 0, + /// Forzar refetch en próximo frame (después de edición) + needs_window_refresh: bool = false, + // ========================================================================= // Estado de carga // ========================================================================= @@ -167,6 +170,9 @@ pub const VirtualAdvancedTableState = struct { edit_buffer_len: usize = 0, edit_cursor: usize = 0, + /// Tiempo de última edición (para parpadeo cursor) + last_edit_time_ms: u64 = 0, + /// Flag: celda requiere commit al terminar edición cell_value_changed: bool = false, @@ -430,6 +436,12 @@ pub const VirtualAdvancedTableState = struct { } } + /// Invalida la ventana de datos para forzar refetch en próximo frame. + /// Llamar después de modificar datos en el DataProvider. + pub fn invalidateWindow(self: *Self) void { + self.needs_window_refresh = true; + } + // ========================================================================= // Navegación horizontal // ========================================================================= @@ -510,7 +522,7 @@ pub const VirtualAdvancedTableState = struct { /// Inicia edición de una celda /// initial_char: si viene de tecla alfanumérica, el caracter inicial (null = mostrar valor actual) - pub fn startEditing(self: *Self, cell: CellId, current_value: []const u8, initial_char: ?u8) void { + pub fn startEditing(self: *Self, cell: CellId, current_value: []const u8, initial_char: ?u8, current_time_ms: u64) void { // Guardar valor original (para Escape) const len = @min(current_value.len, self.original_value.len); @memcpy(self.original_value[0..len], current_value[0..len]); @@ -531,6 +543,7 @@ pub const VirtualAdvancedTableState = struct { self.editing_cell = cell; self.escape_count = 0; + self.last_edit_time_ms = current_time_ms; self.cell_value_changed = false; } @@ -567,11 +580,10 @@ pub const VirtualAdvancedTableState = struct { if (changed) { self.row_dirty = true; self.cell_value_changed = true; + // Solo actualizar última fila editada si hubo cambios reales + self.last_edited_row = self.editing_cell.?.row; } - // Actualizar última fila editada - self.last_edited_row = self.editing_cell.?.row; - self.editing_cell = null; self.escape_count = 0; return changed; diff --git a/src/widgets/virtual_advanced_table/virtual_advanced_table.zig b/src/widgets/virtual_advanced_table/virtual_advanced_table.zig index 7a6f643..bbb4225 100644 --- a/src/widgets/virtual_advanced_table/virtual_advanced_table.zig +++ b/src/widgets/virtual_advanced_table/virtual_advanced_table.zig @@ -107,6 +107,12 @@ pub const VirtualAdvancedTableResult = struct { /// Navegación solicitada después de commit navigate_direction: cell_editor.CellEditorResult.NavigateDirection = .none, + + /// Tab presionado sin edición activa (pasar focus al siguiente widget) + tab_out: bool = false, + + /// Shift estaba presionado con Tab (para tab_out inverso) + tab_shift: bool = false, }; // ============================================================================= @@ -263,12 +269,16 @@ pub fn virtualAdvancedTableRect( header_h, filter_bar_h, )) |geom| { - // Draw cell editor + // Draw cell editor con colores del panel const editor_result = cell_editor.drawCellEditor( ctx, list_state, geom, - cell_editor.CellEditorColors{}, + cell_editor.CellEditorColors{ + .background = colors.background, + .text = colors.text, + .cursor = colors.text, // Cursor mismo color que texto + }, ); // Handle editor results @@ -369,7 +379,13 @@ pub fn virtualAdvancedTableRect( // Helper: Check if refetch needed // ============================================================================= -fn needsRefetch(list_state: *const VirtualAdvancedTableState, visible_rows: usize, buffer_size: usize) bool { +fn needsRefetch(list_state: *VirtualAdvancedTableState, visible_rows: usize, buffer_size: usize) bool { + // Manual invalidation (después de editar datos) + if (list_state.needs_window_refresh) { + list_state.needs_window_refresh = false; + return true; + } + // First load if (list_state.current_window.len == 0) return true; @@ -762,10 +778,11 @@ fn drawRows( // Draw active cell indicator BEFORE text if (is_active_cell) { - // Convertir colores del config a TableColors para table_core + // Usar colores contrastantes para el indicador const tc_colors = table_core.TableColors{ - .selected_cell = colors.row_selected, // Usar color de selección pero más claro - .selected_cell_unfocus = colors.row_selected_unfocus, + // Blanco/cyan brillante para máximo contraste + .selected_cell = Style.Color.rgb(100, 200, 255), + .selected_cell_unfocus = Style.Color.rgb(150, 150, 160), .border = colors.border, }; table_core.drawCellActiveIndicator( @@ -984,6 +1001,7 @@ fn handleKeyboard( } // F2 o Space: iniciar edición de celda activa + // Tab: pasar focus al siguiente widget (solo si NO estamos editando) for (ctx.input.getKeyEvents()) |event| { if (!event.pressed) continue; @@ -997,6 +1015,11 @@ fn handleKeyboard( result.edited_cell = cell; } }, + .tab => { + // Tab sin edición activa: indica que el panel debe mover focus + result.tab_out = true; + result.tab_shift = event.modifiers.shift; + }, else => {}, } } @@ -1006,7 +1029,7 @@ fn handleKeyboard( if (char_input.len > 0 and !list_state.isEditing()) { if (list_state.getActiveCell()) |cell| { // Iniciar edición con el primer caracter - list_state.startEditing(cell, "", char_input[0]); + list_state.startEditing(cell, "", char_input[0], ctx.current_time_ms); } } }