feat(table_core): Brain-in-Core - processTableEvents() unifica toda lógica de teclado

Arquitectura diseñada por Gemini:
- TODA la lógica de decisión en table_core.zig
- Widgets solo pasan eventos y reaccionan a flags
- Cualquier tabla nueva hereda automáticamente

Cambios:
- TableEventResult: struct con TODOS los flags de acciones
- processTableEvents(): función maestra que procesa teclado
- Soporta: navegación, CRUD (Ctrl+N/B/Del), ordenación (Ctrl+Shift+1-9), edición
- VirtualAdvancedTable refactorizado al patrón Brain-in-Core
- Nuevos campos result: insert_row_requested, delete_row_requested, sort_column_index
This commit is contained in:
reugenio 2025-12-27 21:07:08 +01:00
parent 7642ffe7f7
commit aed811a102
2 changed files with 359 additions and 76 deletions

View file

@ -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)
// =============================================================================

View file

@ -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;
}
}
}