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
This commit is contained in:
reugenio 2025-12-28 01:59:24 +01:00
parent 2a92c7530c
commit 454803fe03
2 changed files with 91 additions and 29 deletions

View file

@ -193,6 +193,7 @@ pub fn advancedTableRect(
.apply_state_colors = true, .apply_state_colors = true,
.draw_row_borders = true, .draw_row_borders = true,
.alternating_rows = config.alternating_rows, .alternating_rows = config.alternating_rows,
.edit_buffer = &table_state.row_edit_buffer,
}, &cell_buffer); }, &cell_buffer);
// End clipping // End clipping
@ -704,40 +705,68 @@ 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_out and config.handle_tab) {
if (events.tab_shift) { // Wrapper para obtener row_id por índice (en AdvancedTable, usamos índice como ID)
// Shift+Tab: move left, wrap to previous row const RowIdGetter = struct {
if (table_state.selected_col > 0) { total: usize,
table_state.selectCell(
@intCast(@max(0, table_state.selected_row)), pub fn getRowId(self: @This(), row: usize) i64 {
@intCast(table_state.selected_col - 1), // Ghost row está al final
); if (row >= self.total) return table_core.NEW_ROW_ID;
result.selection_changed = true; return @intCast(row);
} 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;
} }
} else { };
// Tab: move right, wrap to next row
if (table_state.selected_col < @as(i32, @intCast(col_count)) - 1) { const getter = RowIdGetter{ .total = row_count };
table_state.selectCell( const current_row: usize = @intCast(@max(0, table_state.selected_row));
@intCast(@max(0, table_state.selected_row)), const current_col: usize = @intCast(@max(0, table_state.selected_col));
@intCast(table_state.selected_col + 1), const forward = !events.tab_shift;
); // AdvancedTable: usar count de filas existentes (no tiene ghost row como VirtualAdvancedTable)
result.selection_changed = true; const num_rows = row_count;
} else if (table_state.selected_row < @as(i32, @intCast(row_count)) - 1 and config.wrap_navigation) {
table_state.selectCell( const plan = table_core.planTabNavigation(
@intCast(table_state.selected_row + 1), &table_state.row_edit_buffer,
0, 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; 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;
}
}
},
}
} }
// ========================================================================= // =========================================================================

View file

@ -96,6 +96,9 @@ pub const AdvancedTableState = struct {
/// Estado de edición embebido (Fase 2 refactor) /// Estado de edición embebido (Fase 2 refactor)
cell_edit: table_core.CellEditState = .{}, 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) /// Original value before editing (for revert on Escape)
/// NOTA: Mantenemos esto porque CellValue es más rico que buffer crudo /// NOTA: Mantenemos esto porque CellValue es más rico que buffer crudo
original_value: ?CellValue = null, original_value: ?CellValue = null,
@ -1016,6 +1019,36 @@ pub const AdvancedTableResult = struct {
// Focus // Focus
clicked: bool = false, 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];
}
}; };
// ============================================================================= // =============================================================================