feat(table_core): RowEditBuffer + commit al abandonar fila (Excel-style)

- 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
This commit is contained in:
reugenio 2025-12-27 14:57:42 +01:00
parent 0026dbff2a
commit d16019d54f
3 changed files with 261 additions and 24 deletions

View file

@ -371,6 +371,187 @@ pub fn handleEditingKeyboard(
return result; 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 // Utilidades
// ============================================================================= // =============================================================================

View file

@ -177,6 +177,13 @@ pub const VirtualAdvancedTableState = struct {
/// Flag: celda requiere commit al terminar edición /// Flag: celda requiere commit al terminar edición
cell_value_changed: bool = false, 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(); const Self = @This();
// ========================================================================= // =========================================================================
@ -713,6 +720,7 @@ pub const VirtualAdvancedTableState = struct {
self.edit_buffer_len = 0; self.edit_buffer_len = 0;
self.edit_cursor = 0; self.edit_cursor = 0;
self.cell_value_changed = false; self.cell_value_changed = false;
self.row_edit_buffer.clear();
} }
// ========================================================================= // =========================================================================

View file

@ -85,28 +85,30 @@ pub const VirtualAdvancedTableResult = struct {
clicked: bool = false, 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) /// Una fila fue completada (el usuario cambió de fila, tenía cambios pendientes)
cell_committed: bool = false, /// 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) /// ID de la fila que se hizo commit (NEW_ROW_ID = -1 para inserts)
row_changed: bool = false, 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) /// El usuario canceló edición (Escape 2x = descartar fila)
row_discarded: bool = false, row_discarded: bool = false,
/// Celda que fue editada (si cell_committed = true) /// Navegación solicitada después de edición
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
navigate_direction: cell_editor.NavigateDirection = .none, navigate_direction: cell_editor.NavigateDirection = .none,
/// Tab presionado sin edición activa (pasar focus al siguiente widget) /// 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) /// Shift estaba presionado con Tab (para tab_out inverso)
tab_shift: bool = false, 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 edited_cell = list_state.editing_cell.?;
const new_value = list_state.getEditText(); const new_value = list_state.getEditText();
// Check if row changed (for auto-save) // Añadir cambio al buffer de fila (NO commit inmediato)
if (list_state.last_edited_row) |last_row| { // El commit real se hace cuando el usuario abandona la fila
if (edited_cell.row != last_row and list_state.row_dirty) { if (list_state.hasValueChanged()) {
result.row_changed = true; list_state.row_edit_buffer.addChange(edited_cell.col, new_value);
result.previous_row = last_row;
}
}
// Commit the edit // Compatibilidad: mantener flags antiguos
if (list_state.commitEdit()) {
result.cell_committed = true; result.cell_committed = true;
result.edited_cell = edited_cell; result.edited_cell = edited_cell;
result.edited_value = new_value; result.edited_value = new_value;
} }
// Finalizar edición de celda (sin commit a BD)
_ = list_state.commitEdit();
result.navigate_direction = editor_result.navigate; result.navigate_direction = editor_result.navigate;
} else if (editor_result.escaped) { } else if (editor_result.escaped) {
const action = list_state.handleEscape(); const action = list_state.handleEscape();
if (action == .discard_row) { if (action == .discard_row) {
result.row_discarded = true; 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; 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; return result;
} }