From 37e3b61acaf3f8138dfa405df8be1c9f6f7297d1 Mon Sep 17 00:00:00 2001 From: reugenio Date: Sat, 27 Dec 2025 16:45:47 +0100 Subject: [PATCH] refactor(states): Embed CellEditState in AdvancedTableState and VirtualAdvancedTableState FASE 2 del refactor de tablas: - AdvancedTableState: Embed cell_edit, delegate editing methods - VirtualAdvancedTableState: Embed cell_edit, replace editing_cell/edit_buffer - Update advanced_table.zig to use isEditing() and cell_edit.* - Update virtual_advanced_table.zig to use getEditingCell() - Update cell_editor.zig to use cell_edit.* Reduces code duplication, centralizes editing logic in table_core --- src/widgets/advanced_table/advanced_table.zig | 14 +-- src/widgets/advanced_table/state.zig | 102 ++++++++-------- .../virtual_advanced_table/cell_editor.zig | 10 +- src/widgets/virtual_advanced_table/state.zig | 110 ++++++------------ .../virtual_advanced_table.zig | 4 +- 5 files changed, 96 insertions(+), 144 deletions(-) diff --git a/src/widgets/advanced_table/advanced_table.zig b/src/widgets/advanced_table/advanced_table.zig index 7f413f8..5c7002b 100644 --- a/src/widgets/advanced_table/advanced_table.zig +++ b/src/widgets/advanced_table/advanced_table.zig @@ -175,7 +175,7 @@ pub fn advancedTableRect( // Handle keyboard if (has_focus) { - if (table_state.editing) { + if (table_state.isEditing()) { // Handle editing keyboard handleEditingKeyboard(ctx, table_state, table_schema, &result); @@ -444,7 +444,7 @@ fn drawRow( const time_diff = current_time -| table_state.last_click_time; const is_double_click = same_cell and time_diff < table_state.double_click_threshold_ms; - if (is_double_click and config.allow_edit and col.editable and !table_state.editing) { + if (is_double_click and config.allow_edit and col.editable and !table_state.isEditing()) { // Double-click: start editing if (table_state.getRow(row_idx)) |row| { const value = row.get(col.name); @@ -594,7 +594,7 @@ fn drawEditingOverlay( ctx.pushCommand(Command.text(col_x + 4, text_y, edit_text, colors.text_selected)); // Draw cursor - const cursor_x = col_x + 4 + @as(i32, @intCast(table_state.edit_cursor * 8)); + const cursor_x = col_x + 4 + @as(i32, @intCast(table_state.cell_edit.edit_cursor * 8)); ctx.pushCommand(Command.rect(cursor_x, text_y, 1, 8, colors.text_selected)); } @@ -933,10 +933,10 @@ fn handleEditingKeyboard( // Usar table_core para procesamiento de teclado (DRY) const kb_result = table_core.handleEditingKeyboard( ctx, - &table_state.edit_buffer, - &table_state.edit_len, - &table_state.edit_cursor, - &table_state.escape_count, + &table_state.cell_edit.edit_buffer, + &table_state.cell_edit.edit_len, + &table_state.cell_edit.edit_cursor, + &table_state.cell_edit.escape_count, original_text, ); diff --git a/src/widgets/advanced_table/state.zig b/src/widgets/advanced_table/state.zig index 20258e3..74c5f1d 100644 --- a/src/widgets/advanced_table/state.zig +++ b/src/widgets/advanced_table/state.zig @@ -90,27 +90,16 @@ pub const AdvancedTableState = struct { last_validation_message_len: usize = 0, // ========================================================================= - // Editing + // Editing (usa CellEditState de table_core para composición) // ========================================================================= - /// Is currently editing a cell - editing: bool = false, - - /// Edit buffer for current edit - edit_buffer: [MAX_EDIT_BUFFER]u8 = undefined, - - /// Length of text in edit buffer - edit_len: usize = 0, - - /// Cursor position in edit buffer - edit_cursor: usize = 0, + /// Estado de edición embebido (Fase 2 refactor) + cell_edit: table_core.CellEditState = .{}, /// Original value before editing (for revert on Escape) + /// NOTA: Mantenemos esto porque CellValue es más rico que buffer crudo original_value: ?CellValue = null, - /// Escape count (1 = revert, 2 = cancel) - escape_count: u8 = 0, - // ========================================================================= // Double-click detection // ========================================================================= @@ -303,9 +292,7 @@ pub const AdvancedTableState = struct { self.prev_selected_col = -1; // Clear editing - self.editing = false; - self.edit_len = 0; - self.escape_count = 0; + self.cell_edit.stopEditing(); // Clear sorting self.sort_column = -1; @@ -689,76 +676,81 @@ pub const AdvancedTableState = struct { } // ========================================================================= - // Editing + // Editing (delega a cell_edit embebido) // ========================================================================= /// Start editing current cell + /// Usa la celda seleccionada (selected_row, selected_col) pub fn startEditing(self: *AdvancedTableState, initial_value: []const u8) void { - self.editing = true; - self.escape_count = 0; - - // Copy initial value to edit buffer - const len = @min(initial_value.len, MAX_EDIT_BUFFER); - @memcpy(self.edit_buffer[0..len], initial_value[0..len]); - self.edit_len = len; - self.edit_cursor = len; + const row: usize = if (self.selected_row >= 0) @intCast(self.selected_row) else 0; + const col: usize = if (self.selected_col >= 0) @intCast(self.selected_col) else 0; + self.cell_edit.startEditing(row, col, initial_value, null); } /// Stop editing pub fn stopEditing(self: *AdvancedTableState) void { - self.editing = false; - self.edit_len = 0; - self.edit_cursor = 0; - self.escape_count = 0; + self.cell_edit.stopEditing(); self.original_value = null; } /// Get current edit text pub fn getEditText(self: *const AdvancedTableState) []const u8 { - return self.edit_buffer[0..self.edit_len]; + return self.cell_edit.getEditText(); + } + + /// Check if currently editing + pub fn isEditing(self: *const AdvancedTableState) bool { + return self.cell_edit.editing; } /// Insert text at cursor position pub fn insertText(self: *AdvancedTableState, text: []const u8) void { for (text) |c| { - if (self.edit_len < MAX_EDIT_BUFFER) { + if (self.cell_edit.edit_len < table_core.MAX_EDIT_BUFFER_SIZE) { // Shift text after cursor - var i = self.edit_len; - while (i > self.edit_cursor) : (i -= 1) { - self.edit_buffer[i] = self.edit_buffer[i - 1]; + var i = self.cell_edit.edit_len; + while (i > self.cell_edit.edit_cursor) : (i -= 1) { + self.cell_edit.edit_buffer[i] = self.cell_edit.edit_buffer[i - 1]; } - self.edit_buffer[self.edit_cursor] = c; - self.edit_len += 1; - self.edit_cursor += 1; + self.cell_edit.edit_buffer[self.cell_edit.edit_cursor] = c; + self.cell_edit.edit_len += 1; + self.cell_edit.edit_cursor += 1; } } } /// Delete character before cursor (backspace) pub fn deleteBackward(self: *AdvancedTableState) void { - if (self.edit_cursor > 0) { - // Shift text after cursor - var i = self.edit_cursor - 1; - while (i < self.edit_len - 1) : (i += 1) { - self.edit_buffer[i] = self.edit_buffer[i + 1]; + if (self.cell_edit.edit_cursor > 0) { + var i = self.cell_edit.edit_cursor - 1; + while (i < self.cell_edit.edit_len - 1) : (i += 1) { + self.cell_edit.edit_buffer[i] = self.cell_edit.edit_buffer[i + 1]; } - self.edit_len -= 1; - self.edit_cursor -= 1; + self.cell_edit.edit_len -= 1; + self.cell_edit.edit_cursor -= 1; } } /// Delete character at cursor (delete) pub fn deleteForward(self: *AdvancedTableState) void { - if (self.edit_cursor < self.edit_len) { - // Shift text after cursor - var i = self.edit_cursor; - while (i < self.edit_len - 1) : (i += 1) { - self.edit_buffer[i] = self.edit_buffer[i + 1]; + if (self.cell_edit.edit_cursor < self.cell_edit.edit_len) { + var i = self.cell_edit.edit_cursor; + while (i < self.cell_edit.edit_len - 1) : (i += 1) { + self.cell_edit.edit_buffer[i] = self.cell_edit.edit_buffer[i + 1]; } - self.edit_len -= 1; + self.cell_edit.edit_len -= 1; } } + /// Handle Escape key (revert or cancel) + pub fn handleEditEscape(self: *AdvancedTableState) table_core.CellEditState.EscapeAction { + const action = self.cell_edit.handleEscape(); + if (action == .cancelled) { + self.original_value = null; + } + return action; + } + // ========================================================================= // Snapshots (for Auto-CRUD) // ========================================================================= @@ -1037,10 +1029,10 @@ test "AdvancedTableState editing" { var state = AdvancedTableState.init(std.testing.allocator); defer state.deinit(); - try std.testing.expect(!state.editing); + try std.testing.expect(!state.isEditing()); state.startEditing("Hello"); - try std.testing.expect(state.editing); + try std.testing.expect(state.isEditing()); try std.testing.expectEqualStrings("Hello", state.getEditText()); state.insertText(" World"); @@ -1050,7 +1042,7 @@ test "AdvancedTableState editing" { try std.testing.expectEqualStrings("Hello Worl", state.getEditText()); state.stopEditing(); - try std.testing.expect(!state.editing); + try std.testing.expect(!state.isEditing()); } test "AdvancedTableState sorting" { diff --git a/src/widgets/virtual_advanced_table/cell_editor.zig b/src/widgets/virtual_advanced_table/cell_editor.zig index 7a9c459..b91aed7 100644 --- a/src/widgets/virtual_advanced_table/cell_editor.zig +++ b/src/widgets/virtual_advanced_table/cell_editor.zig @@ -89,7 +89,7 @@ pub fn drawCellEditor( )); // Cursor: posición calculada con measureTextToCursor (TTF-aware) - const cursor_offset = ctx.measureTextToCursor(text, state.edit_cursor); + const cursor_offset = ctx.measureTextToCursor(text, state.cell_edit.edit_cursor); const cursor_x = geom.x + padding + @as(i32, @intCast(cursor_offset)); // Visibilidad del cursor usando función compartida de Context @@ -109,10 +109,10 @@ pub fn drawCellEditor( const original_text = state.getOriginalValue(); const kb_result = table_core.handleEditingKeyboard( ctx, - &state.edit_buffer, - &state.edit_buffer_len, - &state.edit_cursor, - &state.escape_count, + &state.cell_edit.edit_buffer, + &state.cell_edit.edit_len, + &state.cell_edit.edit_cursor, + &state.cell_edit.escape_count, if (original_text.len > 0) original_text else null, ); diff --git a/src/widgets/virtual_advanced_table/state.zig b/src/widgets/virtual_advanced_table/state.zig index 83a2055..95d7e0d 100644 --- a/src/widgets/virtual_advanced_table/state.zig +++ b/src/widgets/virtual_advanced_table/state.zig @@ -147,18 +147,11 @@ pub const VirtualAdvancedTableState = struct { footer_display_len: usize = 0, // ========================================================================= - // Estado de edición CRUD Excel-style + // Estado de edición CRUD Excel-style (usa CellEditState de table_core) // ========================================================================= - /// Celda actualmente en edición (null = no editando) - editing_cell: ?CellId = null, - - /// Valor original de la celda (para Escape revertir) - original_value: [256]u8 = undefined, - original_value_len: usize = 0, - - /// Contador de Escapes (1 = revertir celda, 2 = descartar fila) - escape_count: u8 = 0, + /// Estado de edición embebido (Fase 2 refactor) + cell_edit: table_core.CellEditState = .{}, /// Fila actual tiene cambios sin guardar en BD row_dirty: bool = false, @@ -166,11 +159,6 @@ pub const VirtualAdvancedTableState = struct { /// Última fila editada (para detectar cambio de fila) last_edited_row: ?usize = null, - /// Buffer de edición (texto actual en el editor) - edit_buffer: [256]u8 = undefined, - edit_buffer_len: usize = 0, - edit_cursor: usize = 0, - /// Tiempo de última edición (para parpadeo cursor) last_edit_time_ms: u64 = 0, @@ -579,96 +567,76 @@ pub const VirtualAdvancedTableState = struct { } // ========================================================================= - // Métodos de edición CRUD Excel-style + // Métodos de edición CRUD Excel-style (delega a cell_edit embebido) // ========================================================================= /// Verifica si hay una celda en edición pub fn isEditing(self: *const Self) bool { - return self.editing_cell != null; + return self.cell_edit.editing; + } + + /// Obtiene la celda actualmente en edición + pub fn getEditingCell(self: *const Self) ?CellId { + if (!self.cell_edit.editing) return null; + return .{ .row = self.cell_edit.edit_row, .col = self.cell_edit.edit_col }; } /// Inicia edición de una celda /// initial_char: si viene de tecla alfanumérica, el caracter inicial (null = mostrar valor actual) pub fn startEditing(self: *Self, cell: CellId, current_value: []const u8, initial_char: ?u8, current_time_ms: u64) void { - // Guardar valor original (para Escape) - const len = @min(current_value.len, self.original_value.len); - @memcpy(self.original_value[0..len], current_value[0..len]); - self.original_value_len = len; - - // Inicializar buffer de edición - if (initial_char) |c| { - // Tecla alfanumérica: empezar con ese caracter - self.edit_buffer[0] = c; - self.edit_buffer_len = 1; - self.edit_cursor = 1; - } else { - // Doble-click/Space: mostrar valor actual - @memcpy(self.edit_buffer[0..len], current_value[0..len]); - self.edit_buffer_len = len; - self.edit_cursor = len; - } - - self.editing_cell = cell; - self.escape_count = 0; + self.cell_edit.startEditing(cell.row, cell.col, current_value, initial_char); self.last_edit_time_ms = current_time_ms; self.cell_value_changed = false; } /// Obtiene el texto actual del editor pub fn getEditText(self: *const Self) []const u8 { - return self.edit_buffer[0..self.edit_buffer_len]; + return self.cell_edit.getEditText(); } /// Establece el texto del editor pub fn setEditText(self: *Self, text: []const u8) void { - const len = @min(text.len, self.edit_buffer.len); - @memcpy(self.edit_buffer[0..len], text[0..len]); - self.edit_buffer_len = len; - self.edit_cursor = len; + const len = @min(text.len, table_core.MAX_EDIT_BUFFER_SIZE); + @memcpy(self.cell_edit.edit_buffer[0..len], text[0..len]); + self.cell_edit.edit_len = len; + self.cell_edit.edit_cursor = len; } /// Obtiene el valor original (antes de editar) pub fn getOriginalValue(self: *const Self) []const u8 { - return self.original_value[0..self.original_value_len]; + return self.cell_edit.getOriginalValue(); } /// Verifica si el valor ha cambiado respecto al original pub fn hasValueChanged(self: *const Self) bool { - const current = self.getEditText(); - const original = self.getOriginalValue(); - return !std.mem.eql(u8, current, original); + return self.cell_edit.hasChanged(); } /// Finaliza edición guardando cambios (retorna true si hubo cambios) pub fn commitEdit(self: *Self) bool { - if (self.editing_cell == null) return false; + if (!self.cell_edit.editing) return false; - const changed = self.hasValueChanged(); + const changed = self.cell_edit.hasChanged(); if (changed) { self.row_dirty = true; self.cell_value_changed = true; // Solo actualizar última fila editada si hubo cambios reales - self.last_edited_row = self.editing_cell.?.row; + self.last_edited_row = self.cell_edit.edit_row; } - self.editing_cell = null; - self.escape_count = 0; + self.cell_edit.stopEditing(); return changed; } /// Finaliza edición descartando cambios pub fn cancelEdit(self: *Self) void { - self.editing_cell = null; - self.escape_count = 0; + self.cell_edit.stopEditing(); self.cell_value_changed = false; } /// Revierte el texto de la celda al valor original (Escape 1) pub fn revertCellText(self: *Self) void { - const original = self.getOriginalValue(); - @memcpy(self.edit_buffer[0..original.len], original); - self.edit_buffer_len = original.len; - self.edit_cursor = original.len; + self.cell_edit.revertToOriginal(); } /// Maneja la tecla Escape (retorna acción a tomar) @@ -682,20 +650,15 @@ pub const VirtualAdvancedTableState = struct { }; pub fn handleEscape(self: *Self) EscapeAction { - if (self.editing_cell == null) return .none; - - self.escape_count += 1; - - if (self.escape_count == 1) { - // Escape 1: Revertir texto a valor original - self.revertCellText(); - return .reverted; - } else { - // Escape 2+: Descartar cambios de fila - self.cancelEdit(); - self.row_dirty = false; - return .discard_row; - } + const action = self.cell_edit.handleEscape(); + return switch (action) { + .reverted => .reverted, + .cancelled => blk: { + self.row_dirty = false; + break :blk .discard_row; + }, + .none => .none, + }; } /// Verifica si cambió de fila (para auto-save) @@ -713,12 +676,9 @@ pub const VirtualAdvancedTableState = struct { /// Resetea el estado de edición completamente pub fn resetEditState(self: *Self) void { - self.editing_cell = null; - self.escape_count = 0; + self.cell_edit.stopEditing(); self.row_dirty = false; self.last_edited_row = null; - 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 9ba6bbd..d5f6dd5 100644 --- a/src/widgets/virtual_advanced_table/virtual_advanced_table.zig +++ b/src/widgets/virtual_advanced_table/virtual_advanced_table.zig @@ -279,7 +279,7 @@ pub fn virtualAdvancedTableRect( // Draw CellEditor overlay if editing if (list_state.isEditing()) { - const editing = list_state.editing_cell.?; + const editing = list_state.getEditingCell().?; // Calculate cell geometry for the editing cell if (list_state.getCellGeometry( @@ -306,7 +306,7 @@ pub fn virtualAdvancedTableRect( // Handle editor results if (editor_result.committed) { - const edited_cell = list_state.editing_cell.?; + const edited_cell = list_state.getEditingCell().?; const new_value = list_state.getEditText(); // Añadir cambio al buffer de fila (NO commit inmediato)