From 454803fe038059ec20fbbd9b1281dfa651fe032c Mon Sep 17 00:00:00 2001 From: reugenio Date: Sun, 28 Dec 2025 01:59:24 +0100 Subject: [PATCH] feat(advanced_table): Unify Excel-style editing with VirtualAdvancedTable - Add row_edit_buffer to AdvancedTableState - Connect edit_buffer to rendering via drawRowsWithDataSource - Use DRY planTabNavigation for Tab navigation + auto-commit - Add Excel-style result fields: row_committed, row_changes, etc. - Both widgets now share identical commit-on-row-change behavior --- src/widgets/advanced_table/advanced_table.zig | 87 ++++++++++++------- src/widgets/advanced_table/state.zig | 33 +++++++ 2 files changed, 91 insertions(+), 29 deletions(-) diff --git a/src/widgets/advanced_table/advanced_table.zig b/src/widgets/advanced_table/advanced_table.zig index b1463b6..2a108dc 100644 --- a/src/widgets/advanced_table/advanced_table.zig +++ b/src/widgets/advanced_table/advanced_table.zig @@ -193,6 +193,7 @@ pub fn advancedTableRect( .apply_state_colors = true, .draw_row_borders = true, .alternating_rows = config.alternating_rows, + .edit_buffer = &table_state.row_edit_buffer, }, &cell_buffer); // End clipping @@ -704,39 +705,67 @@ fn handleKeyboard( } // ========================================================================= - // Tab navigation (usando lógica del Core + config local) + // Tab navigation con commit Excel-style (DRY: lógica en table_core) // ========================================================================= if (events.tab_out and config.handle_tab) { - if (events.tab_shift) { - // Shift+Tab: move left, wrap to previous row - if (table_state.selected_col > 0) { - table_state.selectCell( - @intCast(@max(0, table_state.selected_row)), - @intCast(table_state.selected_col - 1), - ); - result.selection_changed = true; - } else if (table_state.selected_row > 0 and config.wrap_navigation) { - table_state.selectCell( - @intCast(table_state.selected_row - 1), - col_count - 1, - ); - result.selection_changed = true; + // Wrapper para obtener row_id por índice (en AdvancedTable, usamos índice como ID) + const RowIdGetter = struct { + total: usize, + + pub fn getRowId(self: @This(), row: usize) i64 { + // Ghost row está al final + if (row >= self.total) return table_core.NEW_ROW_ID; + return @intCast(row); } - } else { - // Tab: move right, wrap to next row - if (table_state.selected_col < @as(i32, @intCast(col_count)) - 1) { - table_state.selectCell( - @intCast(@max(0, table_state.selected_row)), - @intCast(table_state.selected_col + 1), - ); + }; + + const getter = RowIdGetter{ .total = row_count }; + const current_row: usize = @intCast(@max(0, table_state.selected_row)); + const current_col: usize = @intCast(@max(0, table_state.selected_col)); + const forward = !events.tab_shift; + // AdvancedTable: usar count de filas existentes (no tiene ghost row como VirtualAdvancedTable) + const num_rows = row_count; + + const plan = table_core.planTabNavigation( + &table_state.row_edit_buffer, + current_row, + current_col, + col_count, + num_rows, + forward, + config.wrap_navigation, + getter, + &result.row_changes, + ); + + // Ejecutar el plan + switch (plan.action) { + .move, .move_with_commit => { + table_state.selectCell(plan.new_row, plan.new_col); result.selection_changed = true; - } else if (table_state.selected_row < @as(i32, @intCast(row_count)) - 1 and config.wrap_navigation) { - table_state.selectCell( - @intCast(table_state.selected_row + 1), - 0, - ); - result.selection_changed = true; - } + + if (plan.action == .move_with_commit) { + if (plan.commit_info) |info| { + result.row_committed = true; + result.row_commit_id = info.row_id; + result.row_commit_is_insert = info.is_insert; + result.row_changes_count = info.change_count; + } + } + }, + .exit, .exit_with_commit => { + result.tab_out = true; + result.tab_shift = events.tab_shift; + + if (plan.action == .exit_with_commit) { + if (plan.commit_info) |info| { + result.row_committed = true; + result.row_commit_id = info.row_id; + result.row_commit_is_insert = info.is_insert; + result.row_changes_count = info.change_count; + } + } + }, } } diff --git a/src/widgets/advanced_table/state.zig b/src/widgets/advanced_table/state.zig index c615c63..b9a5679 100644 --- a/src/widgets/advanced_table/state.zig +++ b/src/widgets/advanced_table/state.zig @@ -96,6 +96,9 @@ pub const AdvancedTableState = struct { /// Estado de edición embebido (Fase 2 refactor) cell_edit: table_core.CellEditState = .{}, + /// Buffer de edición de fila Excel-style (acumula cambios antes de commit) + row_edit_buffer: table_core.RowEditBuffer = .{}, + /// Original value before editing (for revert on Escape) /// NOTA: Mantenemos esto porque CellValue es más rico que buffer crudo original_value: ?CellValue = null, @@ -1016,6 +1019,36 @@ pub const AdvancedTableResult = struct { // Focus clicked: bool = false, + + // ========================================================================= + // Edición CRUD Excel-style (simétrico con VirtualAdvancedTableResult) + // ========================================================================= + + /// Una fila fue completada (el usuario cambió de fila, tenía cambios pendientes) + row_committed: bool = false, + + /// ID de la fila que se hizo commit (índice en AdvancedTable) + row_commit_id: i64 = table_core.NEW_ROW_ID, + + /// Es un INSERT (ghost row) o UPDATE (fila existente) + row_commit_is_insert: bool = false, + + /// Cambios de la fila (válidos si row_committed = true) + row_changes: [table_core.MAX_PENDING_COLUMNS]table_core.PendingCellChange = undefined, + + /// Número de cambios en row_changes + row_changes_count: usize = 0, + + /// Tab presionado para salir del widget + tab_out: bool = false, + + /// Shift estaba presionado con Tab + tab_shift: bool = false, + + /// Obtiene los cambios como slice + pub fn getRowChanges(self: *const AdvancedTableResult) []const table_core.PendingCellChange { + return self.row_changes[0..self.row_changes_count]; + } }; // =============================================================================