zcatgui/src/widgets/virtual_advanced_table/input.zig
R.Eugenio bb2d6a7be1 fix: Cell editor bugs - buffer corrupción + desincronización
Bug 1: text_input bytes corruptos al editar celda con texto pre-seleccionado
- Causa: slice getTextInput() se corrompía tras deleteSelection
- Fix: Copiar a buffer local antes de modificar edit_buffer

Bug 2: Editor permanecía visible al hacer clic en otra fila
- Causa: Falta commit implícito al abandonar fila
- Fix: handleMouseClick detecta clic en fila diferente → commitEdit()

Diagnóstico: Gemini | Implementación: Claude

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-29 14:31:49 +01:00

246 lines
8.9 KiB
Zig

//! VirtualAdvancedTable - Funciones de Input
//!
//! Manejo de teclado y mouse extraído del archivo principal.
const std = @import("std");
const Context = @import("../../core/context.zig").Context;
const Layout = @import("../../core/layout.zig");
const table_core = @import("../table_core/table_core.zig");
const types = @import("types.zig");
const state_mod = @import("state.zig");
const data_provider = @import("data_provider.zig");
pub const VirtualAdvancedTableState = state_mod.VirtualAdvancedTableState;
pub const VirtualAdvancedTableConfig = types.VirtualAdvancedTableConfig;
pub const DataProvider = data_provider.DataProvider;
pub const VirtualAdvancedTableResult = @import("virtual_advanced_table.zig").VirtualAdvancedTableResult;
// =============================================================================
// Helper: Check if refetch needed
// =============================================================================
pub fn needsRefetch(list_state: *VirtualAdvancedTableState, visible_rows: usize, buffer_size: usize) bool {
// Manual invalidation
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;
// Check if scroll is outside current window
const scroll = list_state.nav.scroll_row;
const window_end = list_state.window_start + list_state.current_window.len;
// Refetch if scroll is near edges of window
const margin = visible_rows;
if (scroll < list_state.window_start + margin and list_state.window_start > 0) return true;
if (scroll + visible_rows + margin > window_end) return true;
_ = buffer_size;
return false;
}
// =============================================================================
// Auto-scroll horizontal helper
// =============================================================================
pub fn ensureColumnVisible(
list_state: *VirtualAdvancedTableState,
columns: []const types.ColumnDef,
visible_width: u32,
max_scroll_x: i32,
) void {
const active_col = list_state.nav.active_col;
if (active_col >= columns.len) return;
// Calcular posición X de la columna activa
var col_start: i32 = 0;
for (columns, 0..) |col, i| {
if (i == active_col) break;
col_start += @as(i32, @intCast(col.width));
}
const col_end = col_start + @as(i32, @intCast(columns[active_col].width));
// Posición visible
const visible_start = list_state.nav.scroll_x;
const visible_end = visible_start + @as(i32, @intCast(visible_width));
// Ajustar scroll
if (col_start < visible_start) {
list_state.nav.scroll_x = col_start;
} else if (col_end > visible_end) {
list_state.nav.scroll_x = col_end - @as(i32, @intCast(visible_width));
}
// Clamp
if (list_state.nav.scroll_x < 0) list_state.nav.scroll_x = 0;
if (list_state.nav.scroll_x > max_scroll_x) list_state.nav.scroll_x = max_scroll_x;
}
// =============================================================================
// Handle: Keyboard (Brain-in-Core pattern)
// =============================================================================
pub fn handleKeyboard(
ctx: *Context,
list_state: *VirtualAdvancedTableState,
provider: DataProvider,
visible_rows: usize,
total_rows: usize,
max_scroll_x: i32,
columns: []const types.ColumnDef,
visible_width: u32,
result: *VirtualAdvancedTableResult,
) void {
_ = provider;
const h_scroll_step: i32 = 40;
const num_columns = columns.len;
// Delegar al Core
const events = table_core.processTableEvents(ctx, list_state.isEditing());
if (!events.handled) return;
const prev_col = list_state.nav.active_col;
// Aplicar navegación
if (events.move_up) list_state.moveUp();
if (events.move_down) list_state.moveDown(visible_rows);
if (events.move_left) list_state.moveToPrevCol();
if (events.move_right) list_state.moveToNextCol(num_columns);
if (events.page_up) list_state.pageUp(visible_rows);
if (events.page_down) list_state.pageDown(visible_rows, total_rows);
if (events.go_to_first_col) list_state.goToFirstCol();
if (events.go_to_last_col) list_state.goToLastCol(num_columns);
if (events.go_to_first_row) list_state.goToStart();
if (events.go_to_last_row) list_state.goToEnd(visible_rows, total_rows);
if (events.scroll_left) list_state.scrollLeft(h_scroll_step);
if (events.scroll_right) list_state.scrollRight(h_scroll_step, max_scroll_x);
// Auto-scroll horizontal
if (list_state.nav.active_col != prev_col) {
ensureColumnVisible(list_state, columns, visible_width, max_scroll_x);
}
// Ctrl+N
if (events.insert_row) {
result.insert_row_requested = true;
list_state.enterInsertionMode();
}
// Ctrl+Delete/B
if (events.delete_row) {
result.delete_row_requested = true;
if (list_state.selected_id) |id| {
result.deleted_row_id = id;
}
}
// Ordenación
if (events.sort_by_column) |col| {
if (col < num_columns) {
result.sort_requested = true;
result.sort_column_index = col;
}
}
// Edición
if (events.start_editing) {
if (list_state.getActiveCell()) |cell| {
if (events.initial_char) |ch| {
list_state.startEditing(cell, "", ch, ctx.current_time_ms);
} else {
result.cell_committed = false;
result.edited_cell = cell;
}
}
}
// Tab sin edición
if (events.tab_out and !list_state.isEditing()) {
result.tab_out = true;
result.tab_shift = events.tab_shift;
}
}
// =============================================================================
// Handle: Mouse Click
// =============================================================================
pub fn handleMouseClick(
ctx: *Context,
bounds: Layout.Rect,
filter_bar_h: u32,
header_h: u32,
config: VirtualAdvancedTableConfig,
list_state: *VirtualAdvancedTableState,
result: *VirtualAdvancedTableResult,
) void {
const mouse = ctx.input.mousePos();
const content_y = bounds.y + @as(i32, @intCast(filter_bar_h)) + @as(i32, @intCast(header_h));
if (mouse.y >= content_y) {
const relative_y = mouse.y - content_y;
const screen_row = @as(usize, @intCast(relative_y)) / config.row_height;
const window_offset = list_state.nav.scroll_row -| list_state.window_start;
const data_idx = window_offset + screen_row;
if (data_idx < list_state.current_window.len) {
const global_row = list_state.nav.scroll_row + screen_row;
// FIX Bug 2: Si estamos editando y clic en otra fila, hacer commit implícito
if (list_state.isEditing()) {
const editing_row = list_state.cell_edit.edit_row;
if (global_row != editing_row) {
// Commit implícito: clic fuera de la fila en edición
if (list_state.commitEdit()) {
result.cell_committed = true;
result.row_committed = true;
} else {
list_state.cancelEdit();
}
}
}
// Detect column
var clicked_col: usize = 0;
const relative_x = mouse.x - bounds.x + list_state.nav.scroll_x;
var col_start: i32 = 0;
for (config.columns, 0..) |col, col_idx| {
const col_end = col_start + @as(i32, @intCast(col.width));
if (relative_x >= col_start and relative_x < col_end) {
clicked_col = col_idx;
break;
}
col_start = col_end;
}
// Double-click detection
var dc_state = list_state.nav.double_click;
const is_double_click = table_core.detectDoubleClick(
&dc_state,
ctx.current_time_ms,
@intCast(global_row),
@intCast(clicked_col),
);
list_state.nav.double_click = dc_state;
if (is_double_click and !list_state.isEditing()) {
const cell = types.CellId{ .row = global_row, .col = clicked_col };
result.edited_cell = cell;
result.double_clicked = true;
result.double_click_id = list_state.current_window[data_idx].id;
} else {
list_state.selectById(list_state.current_window[data_idx].id);
list_state.nav.active_col = clicked_col;
}
}
}
}