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
This commit is contained in:
reugenio 2025-12-27 16:45:47 +01:00
parent 6819919060
commit 37e3b61aca
5 changed files with 96 additions and 144 deletions

View file

@ -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,
);

View file

@ -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" {

View file

@ -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,
);

View file

@ -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();
}

View file

@ -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)