diff --git a/src/widgets/table_core.zig b/src/widgets/table_core.zig index ffc73ac..6c3fe8b 100644 --- a/src/widgets/table_core.zig +++ b/src/widgets/table_core.zig @@ -850,6 +850,290 @@ pub fn handleEditingKeyboard( return result; } +// ============================================================================= +// BRAIN-IN-CORE: Procesamiento Unificado de Eventos de Tabla (FASE C) +// ============================================================================= +// +// Arquitectura "Brain-in-Core" (diseñado por Gemini): +// - TODA la lógica de decisión vive aquí +// - Los widgets solo pasan eventos y reaccionan a los resultados +// - Cualquier nueva tabla (CloudTable, etc.) hereda esta potencia automáticamente + +/// Resultado completo del procesamiento de eventos de tabla. +/// Contiene flags para TODAS las acciones posibles. +pub const TableEventResult = struct { + // ========================================================================= + // Navegación básica (flechas, PageUp/Down) + // ========================================================================= + move_up: bool = false, + move_down: bool = false, + move_left: bool = false, // Sin Ctrl: cambiar columna + move_right: bool = false, // Sin Ctrl: cambiar columna + page_up: bool = false, + page_down: bool = false, + + // ========================================================================= + // Navegación a extremos + // ========================================================================= + go_to_first_col: bool = false, // Home sin Ctrl + go_to_last_col: bool = false, // End sin Ctrl + go_to_first_row: bool = false, // Ctrl+Home: primera fila de datos + go_to_last_row: bool = false, // Ctrl+End: última fila de datos + + // ========================================================================= + // Scroll horizontal (Ctrl+Left/Right) + // ========================================================================= + scroll_left: bool = false, + scroll_right: bool = false, + + // ========================================================================= + // CRUD (Ctrl+N, Ctrl+B, Ctrl+Delete) + // ========================================================================= + insert_row: bool = false, // Ctrl+N: insertar nueva fila + delete_row: bool = false, // Ctrl+Delete o Ctrl+B: eliminar fila + + // ========================================================================= + // Ordenación (Ctrl+Shift+1..9) + // ========================================================================= + sort_by_column: ?usize = null, // Índice de columna (0-based) + + // ========================================================================= + // Edición (F2, Space, tecla alfanumérica) + // ========================================================================= + start_editing: bool = false, // Iniciar edición de celda activa + initial_char: ?u8 = null, // Caracter inicial (si fue tecla alfa) + + // ========================================================================= + // Tab navigation + // ========================================================================= + tab_out: bool = false, // Tab presionado (pasar focus a otro widget) + tab_shift: bool = false, // Fue Shift+Tab (dirección inversa) + + // ========================================================================= + // Flag general + // ========================================================================= + handled: bool = false, // Se procesó algún evento +}; + +/// Procesa TODOS los eventos de teclado de una tabla. +/// Esta es la función maestra "Brain-in-Core" que centraliza toda la lógica. +/// +/// Parámetros: +/// - ctx: Contexto de renderizado (acceso a input) +/// - is_editing: Si hay una celda en modo edición (ignora navegación) +/// +/// El widget debe reaccionar a los flags retornados y actualizar su estado. +/// +/// Ejemplo de uso en widget: +/// ```zig +/// const events = table_core.processTableEvents(ctx, list_state.isEditing()); +/// if (events.move_up) list_state.moveUp(); +/// if (events.move_down) list_state.moveDown(visible_rows); +/// if (events.go_to_first_row) list_state.goToStart(); +/// if (events.insert_row) result.insert_row = true; +/// // ... etc +/// ``` +pub fn processTableEvents(ctx: *Context, is_editing: bool) TableEventResult { + var result = TableEventResult{}; + + // Si hay edición activa, el CellEditor maneja las teclas + // Solo procesamos Tab para salir del widget + if (is_editing) { + for (ctx.input.getKeyEvents()) |event| { + if (!event.pressed) continue; + if (event.key == .tab) { + result.tab_out = true; + result.tab_shift = event.modifiers.shift; + result.handled = true; + return result; + } + } + return result; + } + + // ========================================================================= + // 1. Navegación con navKeyPressed (soporta key repeat) + // ========================================================================= + if (ctx.input.navKeyPressed()) |key| { + const ctrl = ctx.input.modifiers.ctrl; + + switch (key) { + .up => { + result.move_up = true; + result.handled = true; + }, + .down => { + result.move_down = true; + result.handled = true; + }, + .left => { + if (ctrl) { + result.scroll_left = true; + } else { + result.move_left = true; + } + result.handled = true; + }, + .right => { + if (ctrl) { + result.scroll_right = true; + } else { + result.move_right = true; + } + result.handled = true; + }, + .page_up => { + result.page_up = true; + result.handled = true; + }, + .page_down => { + result.page_down = true; + result.handled = true; + }, + .home => { + if (ctrl) { + result.go_to_first_row = true; + result.go_to_first_col = true; + } else { + result.go_to_first_col = true; + } + result.handled = true; + }, + .end => { + if (ctrl) { + result.go_to_last_row = true; + result.go_to_last_col = true; + } else { + result.go_to_last_col = true; + } + result.handled = true; + }, + else => {}, + } + } + + // ========================================================================= + // 2. Atajos con Ctrl y teclas especiales (getKeyEvents) + // ========================================================================= + for (ctx.input.getKeyEvents()) |event| { + if (!event.pressed) continue; + + // F2 o Space: iniciar edición + if (event.key == .f2 or event.key == .space) { + result.start_editing = true; + result.handled = true; + return result; + } + + // Tab: pasar focus al siguiente widget + if (event.key == .tab) { + result.tab_out = true; + result.tab_shift = event.modifiers.shift; + result.handled = true; + return result; + } + + // Atajos con Ctrl + if (event.modifiers.ctrl) { + switch (event.key) { + .n => { + // Ctrl+N: insertar nueva fila + result.insert_row = true; + result.handled = true; + return result; + }, + .b, .delete => { + // Ctrl+B o Ctrl+Delete: eliminar fila + result.delete_row = true; + result.handled = true; + return result; + }, + // Ctrl+Shift+1..9: ordenar por columna + .@"1" => { + if (event.modifiers.shift) { + result.sort_by_column = 0; + result.handled = true; + return result; + } + }, + .@"2" => { + if (event.modifiers.shift) { + result.sort_by_column = 1; + result.handled = true; + return result; + } + }, + .@"3" => { + if (event.modifiers.shift) { + result.sort_by_column = 2; + result.handled = true; + return result; + } + }, + .@"4" => { + if (event.modifiers.shift) { + result.sort_by_column = 3; + result.handled = true; + return result; + } + }, + .@"5" => { + if (event.modifiers.shift) { + result.sort_by_column = 4; + result.handled = true; + return result; + } + }, + .@"6" => { + if (event.modifiers.shift) { + result.sort_by_column = 5; + result.handled = true; + return result; + } + }, + .@"7" => { + if (event.modifiers.shift) { + result.sort_by_column = 6; + result.handled = true; + return result; + } + }, + .@"8" => { + if (event.modifiers.shift) { + result.sort_by_column = 7; + result.handled = true; + return result; + } + }, + .@"9" => { + if (event.modifiers.shift) { + result.sort_by_column = 8; + result.handled = true; + return result; + } + }, + else => {}, + } + } + } + + // ========================================================================= + // 3. Teclas alfanuméricas: iniciar edición con ese caracter + // ========================================================================= + const char_input = ctx.input.getTextInput(); + if (char_input.len > 0) { + result.start_editing = true; + result.initial_char = char_input[0]; + result.handled = true; + } + + return result; +} + +// Alias para compatibilidad (DEPRECADO - usar processTableEvents) +pub const TableKeyboardResult = TableEventResult; +pub const handleTableKeyboard = processTableEvents; + // ============================================================================= // Edición de fila completa (commit al abandonar fila, estilo Excel) // ============================================================================= diff --git a/src/widgets/virtual_advanced_table/virtual_advanced_table.zig b/src/widgets/virtual_advanced_table/virtual_advanced_table.zig index 59bbfed..81e6feb 100644 --- a/src/widgets/virtual_advanced_table/virtual_advanced_table.zig +++ b/src/widgets/virtual_advanced_table/virtual_advanced_table.zig @@ -67,6 +67,8 @@ pub const VirtualAdvancedTableResult = struct { sort_requested: bool = false, sort_column: ?[]const u8 = null, sort_direction: SortDirection = .none, + /// Índice de columna para ordenar (Ctrl+Shift+1..9, 0-based) + sort_column_index: ?usize = null, /// El filtro de texto cambió filter_changed: bool = false, @@ -110,6 +112,12 @@ pub const VirtualAdvancedTableResult = struct { /// El usuario canceló edición (Escape 2x = descartar fila) row_discarded: bool = false, + /// Ctrl+N: el usuario solicitó insertar nueva fila + insert_row_requested: bool = false, + + /// Ctrl+Delete o Ctrl+B: el usuario solicitó eliminar fila actual + delete_row_requested: bool = false, + /// Navegación solicitada después de edición navigate_direction: cell_editor.NavigateDirection = .none, @@ -969,8 +977,11 @@ fn drawScrollbarH( } // ============================================================================= -// Handle: Keyboard +// Handle: Keyboard (Brain-in-Core pattern) // ============================================================================= +// +// Arquitectura: TODA la lógica de decisión está en table_core.processTableEvents() +// Este handler solo traduce los flags a acciones sobre el state local. fn handleKeyboard( ctx: *Context, @@ -984,87 +995,75 @@ fn handleKeyboard( ) void { _ = provider; - // Si hay edición activa, el CellEditor maneja las teclas - if (list_state.isEditing()) return; - const h_scroll_step: i32 = 40; // Pixels per arrow key press - // Usar navKeyPressed() para soportar key repeat (tecla mantenida pulsada) - if (ctx.input.navKeyPressed()) |key| { - switch (key) { - .up => list_state.moveUp(), - .down => list_state.moveDown(visible_rows), - .left => { - // Con Ctrl: scroll horizontal - // Sin Ctrl: cambiar columna activa - if (ctx.input.modifiers.ctrl) { - list_state.scrollLeft(h_scroll_step); - } else { - list_state.moveToPrevCol(); - } - }, - .right => { - if (ctx.input.modifiers.ctrl) { - list_state.scrollRight(h_scroll_step, max_scroll_x); - } else { - list_state.moveToNextCol(num_columns); - } - }, - .page_up => list_state.pageUp(visible_rows), - .page_down => list_state.pageDown(visible_rows, total_rows), - .home => { - if (ctx.input.modifiers.ctrl) { - list_state.goToStart(); - list_state.goToFirstCol(); - } else { - list_state.goToFirstCol(); - } - }, - .end => { - if (ctx.input.modifiers.ctrl) { - list_state.goToEnd(visible_rows, total_rows); - list_state.goToLastCol(num_columns); - } else { - list_state.goToLastCol(num_columns); - } - }, - else => {}, + // ========================================================================= + // BRAIN-IN-CORE: Delegar toda la lógica de decisión al Core + // ========================================================================= + const events = table_core.processTableEvents(ctx, list_state.isEditing()); + + if (!events.handled) return; + + // ========================================================================= + // Aplicar acciones de 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); + + // ========================================================================= + // Propagar acciones CRUD al result (el panel las manejará) + // ========================================================================= + if (events.insert_row) { + result.insert_row_requested = true; + } + if (events.delete_row) { + result.delete_row_requested = true; + } + + // ========================================================================= + // Ordenación + // ========================================================================= + if (events.sort_by_column) |col| { + if (col < num_columns) { + result.sort_requested = true; + result.sort_column_index = col; } } - // 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; - - switch (event.key) { - .f2, .space => { - // Iniciar edición de celda activa - if (list_state.getActiveCell()) |cell| { - // El panel debe proveer el valor actual via callback - // Por ahora iniciamos con texto vacío - el panel debería llamar startEditing - result.cell_committed = false; // Flag especial: indica que se solicitó edición - result.edited_cell = cell; - } - }, - .tab => { - // Tab sin edición activa: indica que el panel debe mover focus - // IMPORTANTE: Solo si CellEditor no procesó Tab (evita doble procesamiento) - if (result.navigate_direction == .none) { - result.tab_out = true; - result.tab_shift = event.modifiers.shift; - } - }, - else => {}, - } - } - - // Teclas alfanuméricas: iniciar edición con ese caracter - const char_input = ctx.input.getTextInput(); - if (char_input.len > 0 and !list_state.isEditing()) { + // ========================================================================= + // Inicio de edición + // ========================================================================= + if (events.start_editing) { if (list_state.getActiveCell()) |cell| { - // Iniciar edición con el primer caracter - list_state.startEditing(cell, "", char_input[0], ctx.current_time_ms); + if (events.initial_char) |ch| { + // Tecla alfanumérica: iniciar con ese caracter + list_state.startEditing(cell, "", ch, ctx.current_time_ms); + } else { + // F2/Space: señalar al panel que debe iniciar edición + result.cell_committed = false; + result.edited_cell = cell; + } + } + } + + // ========================================================================= + // Tab navigation + // ========================================================================= + if (events.tab_out) { + // Solo si CellEditor no procesó Tab (evita doble procesamiento) + if (result.navigate_direction == .none) { + result.tab_out = true; + result.tab_shift = events.tab_shift; } } }