From d16019d54ffd6f9232a6d775d3a0b92945e45061 Mon Sep 17 00:00:00 2001 From: reugenio Date: Sat, 27 Dec 2025 14:57:42 +0100 Subject: [PATCH] feat(table_core): RowEditBuffer + commit al abandonar fila (Excel-style) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - RowEditBuffer: acumula cambios de fila antes de commit - checkRowChangeAndCommit(): detecta cambio fila + commit automático - buildCommitInfo(), isGhostRow(), NEW_ROW_ID - VirtualAdvancedTableState: row_edit_buffer field - VirtualAdvancedTableResult: row_committed, row_changes[], etc. - Comportamiento: Tab entre celdas acumula, cambiar fila hace commit --- src/widgets/table_core.zig | 181 ++++++++++++++++++ src/widgets/virtual_advanced_table/state.zig | 8 + .../virtual_advanced_table.zig | 96 +++++++--- 3 files changed, 261 insertions(+), 24 deletions(-) diff --git a/src/widgets/table_core.zig b/src/widgets/table_core.zig index 6aa1f5b..17cd480 100644 --- a/src/widgets/table_core.zig +++ b/src/widgets/table_core.zig @@ -371,6 +371,187 @@ pub fn handleEditingKeyboard( return result; } +// ============================================================================= +// Edición de fila completa (commit al abandonar fila, estilo Excel) +// ============================================================================= + +/// Máximo de columnas soportadas para cambios pendientes +pub const MAX_PENDING_COLUMNS: usize = 32; + +/// Máximo tamaño de valor por celda +pub const MAX_CELL_VALUE_LEN: usize = 256; + +/// ID especial para filas nuevas (ghost row) +pub const NEW_ROW_ID: i64 = -1; + +/// Cambio pendiente en una columna +pub const PendingCellChange = struct { + /// Índice de columna + col: usize, + /// Valor nuevo (slice al buffer interno) + value: []const u8, +}; + +/// Buffer para acumular cambios de una fila antes de commit +/// Usado por los states de los widgets, procesado por funciones de table_core +pub const RowEditBuffer = struct { + /// ID de la fila siendo editada (NEW_ROW_ID si es ghost row) + row_id: i64 = NEW_ROW_ID, + + /// Índice de fila (para navegación) + row_index: usize = 0, + + /// Es una fila nueva (ghost row que el usuario está rellenando) + is_new_row: bool = false, + + /// Hay cambios pendientes + has_changes: bool = false, + + /// Buffers de valores por columna (almacenamiento fijo) + value_buffers: [MAX_PENDING_COLUMNS][MAX_CELL_VALUE_LEN]u8 = undefined, + + /// Longitudes de cada valor + value_lens: [MAX_PENDING_COLUMNS]usize = [_]usize{0} ** MAX_PENDING_COLUMNS, + + /// Flags: qué columnas tienen cambios + changed_cols: [MAX_PENDING_COLUMNS]bool = [_]bool{false} ** MAX_PENDING_COLUMNS, + + /// Número de columnas con cambios + change_count: usize = 0, + + /// Inicializa/resetea el buffer para una nueva fila + pub fn startEdit(self: *RowEditBuffer, row_id: i64, row_index: usize, is_new: bool) void { + self.row_id = row_id; + self.row_index = row_index; + self.is_new_row = is_new; + self.has_changes = false; + self.change_count = 0; + for (0..MAX_PENDING_COLUMNS) |i| { + self.changed_cols[i] = false; + self.value_lens[i] = 0; + } + } + + /// Añade un cambio pendiente para una columna + pub fn addChange(self: *RowEditBuffer, col: usize, value: []const u8) void { + if (col >= MAX_PENDING_COLUMNS) return; + + // Copiar valor al buffer interno + const len = @min(value.len, MAX_CELL_VALUE_LEN); + @memcpy(self.value_buffers[col][0..len], value[0..len]); + self.value_lens[col] = len; + + // Marcar como cambiado + if (!self.changed_cols[col]) { + self.changed_cols[col] = true; + self.change_count += 1; + } + + self.has_changes = true; + } + + /// Obtiene el valor pendiente de una columna (si hay cambio) + pub fn getPendingValue(self: *const RowEditBuffer, col: usize) ?[]const u8 { + if (col >= MAX_PENDING_COLUMNS) return null; + if (!self.changed_cols[col]) return null; + return self.value_buffers[col][0..self.value_lens[col]]; + } + + /// Limpia el buffer (después de commit o discard) + pub fn clear(self: *RowEditBuffer) void { + self.row_id = NEW_ROW_ID; + self.row_index = 0; + self.is_new_row = false; + self.has_changes = false; + self.change_count = 0; + for (0..MAX_PENDING_COLUMNS) |i| { + self.changed_cols[i] = false; + self.value_lens[i] = 0; + } + } +}; + +/// Información para hacer commit de los cambios de una fila +/// Retornada cuando el usuario abandona una fila editada +pub const RowCommitInfo = struct { + /// ID de la fila (NEW_ROW_ID si es INSERT) + row_id: i64, + + /// Es INSERT (nueva fila) o UPDATE (fila existente) + is_insert: bool, + + /// Lista de cambios (columna, valor) + changes: []const PendingCellChange, + + /// Número de cambios + change_count: usize, +}; + +/// Construye la info de commit desde un RowEditBuffer +/// El caller debe proveer el array para almacenar los cambios +pub fn buildCommitInfo( + buffer: *const RowEditBuffer, + changes_out: []PendingCellChange, +) ?RowCommitInfo { + if (!buffer.has_changes) return null; + + var count: usize = 0; + for (0..MAX_PENDING_COLUMNS) |col| { + if (buffer.changed_cols[col] and count < changes_out.len) { + changes_out[count] = .{ + .col = col, + .value = buffer.value_buffers[col][0..buffer.value_lens[col]], + }; + count += 1; + } + } + + return RowCommitInfo{ + .row_id = buffer.row_id, + .is_insert = buffer.is_new_row, + .changes = changes_out[0..count], + .change_count = count, + }; +} + +/// Verifica si hay que hacer commit antes de editar nueva celda. +/// Si la fila cambió y hay cambios pendientes, retorna commit info. +/// Siempre inicializa el buffer para la nueva fila. +/// +/// Uso típico en widget: +/// ``` +/// if (table_core.checkRowChangeAndCommit(&state.row_edit_buffer, new_id, new_idx, is_ghost, &changes)) |info| { +/// result.row_committed = true; +/// result.commit_info = info; +/// } +/// ``` +pub fn checkRowChangeAndCommit( + buffer: *RowEditBuffer, + new_row_id: i64, + new_row_index: usize, + is_new_row: bool, + changes_out: []PendingCellChange, +) ?RowCommitInfo { + // Si es la misma fila, no hacer nada + if (buffer.row_id == new_row_id) return null; + + // Si hay cambios pendientes en la fila anterior, construir commit + var commit_info: ?RowCommitInfo = null; + if (buffer.has_changes) { + commit_info = buildCommitInfo(buffer, changes_out); + } + + // Iniciar edición de la nueva fila + buffer.startEdit(new_row_id, new_row_index, is_new_row); + + return commit_info; +} + +/// Verifica si un row_id corresponde a la ghost row (fila nueva) +pub fn isGhostRow(row_id: i64) bool { + return row_id == NEW_ROW_ID; +} + // ============================================================================= // Utilidades // ============================================================================= diff --git a/src/widgets/virtual_advanced_table/state.zig b/src/widgets/virtual_advanced_table/state.zig index c5ee49f..83a2055 100644 --- a/src/widgets/virtual_advanced_table/state.zig +++ b/src/widgets/virtual_advanced_table/state.zig @@ -177,6 +177,13 @@ pub const VirtualAdvancedTableState = struct { /// Flag: celda requiere commit al terminar edición cell_value_changed: bool = false, + // ========================================================================= + // Buffer de cambios de fila (commit al abandonar fila, estilo Excel) + // ========================================================================= + + /// Buffer para acumular cambios de la fila actual antes de commit + row_edit_buffer: table_core.RowEditBuffer = .{}, + const Self = @This(); // ========================================================================= @@ -713,6 +720,7 @@ pub const VirtualAdvancedTableState = struct { self.edit_buffer_len = 0; self.edit_cursor = 0; self.cell_value_changed = false; + self.row_edit_buffer.clear(); } // ========================================================================= diff --git a/src/widgets/virtual_advanced_table/virtual_advanced_table.zig b/src/widgets/virtual_advanced_table/virtual_advanced_table.zig index d750431..9ba6bbd 100644 --- a/src/widgets/virtual_advanced_table/virtual_advanced_table.zig +++ b/src/widgets/virtual_advanced_table/virtual_advanced_table.zig @@ -85,28 +85,30 @@ pub const VirtualAdvancedTableResult = struct { clicked: bool = false, // ========================================================================= - // Edición CRUD Excel-style + // Edición CRUD Excel-style (commit al abandonar fila) // ========================================================================= - /// Una celda fue modificada (el usuario hizo commit con Enter/Tab) - cell_committed: bool = false, + /// Una fila fue completada (el usuario cambió de fila, tenía cambios pendientes) + /// Cuando es true, row_commit_id y row_changes contienen los datos + row_committed: bool = false, - /// La fila cambió de edición (auto-save requerido) - row_changed: bool = false, + /// ID de la fila que se hizo commit (NEW_ROW_ID = -1 para inserts) + 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) + /// Buffer estático que sobrevive el frame + row_changes: [table_core.MAX_PENDING_COLUMNS]table_core.PendingCellChange = undefined, + + /// Número de cambios en row_changes + row_changes_count: usize = 0, /// El usuario canceló edición (Escape 2x = descartar fila) row_discarded: bool = false, - /// Celda que fue editada (si cell_committed = true) - edited_cell: ?CellId = null, - - /// Nuevo valor de la celda (si cell_committed = true) - edited_value: ?[]const u8 = null, - - /// Fila anterior (si row_changed = true, para auto-save) - previous_row: ?usize = null, - - /// Navegación solicitada después de commit + /// Navegación solicitada después de edición navigate_direction: cell_editor.NavigateDirection = .none, /// Tab presionado sin edición activa (pasar focus al siguiente widget) @@ -114,6 +116,26 @@ pub const VirtualAdvancedTableResult = struct { /// Shift estaba presionado con Tab (para tab_out inverso) tab_shift: bool = false, + + // ========================================================================= + // Compatibilidad (DEPRECADO - usar row_committed) + // ========================================================================= + + /// @deprecated: Usar row_committed. Mantenido para compatibilidad. + cell_committed: bool = false, + /// @deprecated + row_changed: bool = false, + /// @deprecated + edited_cell: ?CellId = null, + /// @deprecated + edited_value: ?[]const u8 = null, + /// @deprecated + previous_row: ?usize = null, + + /// Obtiene los cambios como slice (helper para compatibilidad) + pub fn getRowChanges(self: *const VirtualAdvancedTableResult) []const table_core.PendingCellChange { + return self.row_changes[0..self.row_changes_count]; + } }; // ============================================================================= @@ -287,26 +309,25 @@ pub fn virtualAdvancedTableRect( const edited_cell = list_state.editing_cell.?; const new_value = list_state.getEditText(); - // Check if row changed (for auto-save) - if (list_state.last_edited_row) |last_row| { - if (edited_cell.row != last_row and list_state.row_dirty) { - result.row_changed = true; - result.previous_row = last_row; - } - } + // Añadir cambio al buffer de fila (NO commit inmediato) + // El commit real se hace cuando el usuario abandona la fila + if (list_state.hasValueChanged()) { + list_state.row_edit_buffer.addChange(edited_cell.col, new_value); - // Commit the edit - if (list_state.commitEdit()) { + // Compatibilidad: mantener flags antiguos result.cell_committed = true; result.edited_cell = edited_cell; result.edited_value = new_value; } + // Finalizar edición de celda (sin commit a BD) + _ = list_state.commitEdit(); result.navigate_direction = editor_result.navigate; } else if (editor_result.escaped) { const action = list_state.handleEscape(); if (action == .discard_row) { result.row_discarded = true; + list_state.row_edit_buffer.clear(); } } } @@ -373,6 +394,33 @@ pub fn virtualAdvancedTableRect( result.double_click_id = list_state.selected_id; } + // ========================================================================= + // Commit de fila al cambiar de selección + // ========================================================================= + // Si la selección cambió y hay cambios pendientes en otra fila, hacer commit + if (list_state.selection_changed and list_state.row_edit_buffer.has_changes) { + const new_row_id = list_state.selected_id orelse table_core.NEW_ROW_ID; + const new_row_idx = list_state.getSelectedRow() orelse 0; + const is_ghost = table_core.isGhostRow(new_row_id); + + // checkRowChangeAndCommit compara row_ids y hace commit si son diferentes + if (table_core.checkRowChangeAndCommit( + &list_state.row_edit_buffer, + new_row_id, + new_row_idx, + is_ghost, + &result.row_changes, + )) |commit_info| { + result.row_committed = true; + result.row_commit_id = commit_info.row_id; + result.row_commit_is_insert = commit_info.is_insert; + result.row_changes_count = commit_info.change_count; + + // Compatibilidad + result.row_changed = true; + } + } + return result; }