From 27b69cfcde5723bce00724db6bac975906ac5c6a Mon Sep 17 00:00:00 2001 From: reugenio Date: Sat, 27 Dec 2025 01:49:45 +0100 Subject: [PATCH] refactor(table_core): Move Tab navigation logic to shared module (Norma #7 DRY) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ANTES: calculateNextCell/calculatePrevCell duplicados en: - AdvancedTableState - VirtualAdvancedTableState AHORA: Lógica común en table_core.zig: - calculateNextCell() - calcula siguiente celda (Tab) - calculatePrevCell() - calcula celda anterior (Shift+Tab) - toggleSort() - alterna ordenación de columna - TabNavigateResult, CellPosition, SortDirection, SortToggleResult Ambos widgets usan table_core, adaptando el resultado a su modelo: - AdvancedTable: selected_row/selected_col (índices) - VirtualAdvancedTable: selected_id + active_col (ID + columna) Tests añadidos para calculateNextCell, calculatePrevCell, toggleSort --- src/widgets/advanced_table/state.zig | 73 ++---- src/widgets/table_core.zig | 220 +++++++++++++++++++ src/widgets/virtual_advanced_table/state.zig | 82 +++---- 3 files changed, 271 insertions(+), 104 deletions(-) diff --git a/src/widgets/advanced_table/state.zig b/src/widgets/advanced_table/state.zig index 68cade0..20258e3 100644 --- a/src/widgets/advanced_table/state.zig +++ b/src/widgets/advanced_table/state.zig @@ -5,6 +5,7 @@ const std = @import("std"); const types = @import("types.zig"); const schema_mod = @import("schema.zig"); +const table_core = @import("../table_core.zig"); pub const CellValue = types.CellValue; pub const RowState = types.RowState; @@ -827,86 +828,48 @@ pub const AdvancedTableState = struct { // ========================================================================= // Navegación Tab Excel-style (con wrap) + // Usa table_core para la lógica común (Norma #7 DRY) // ========================================================================= - /// Resultado de navegación Tab - pub const TabNavigateResult = enum { - /// Navegó a otra celda dentro del widget - navigated, - /// Salió del widget (Tab en última celda o Shift+Tab en primera) - tab_out, - }; + /// Re-exporta TabNavigateResult desde table_core para compatibilidad + pub const TabNavigateResult = table_core.TabNavigateResult; /// Navega a siguiente celda (Tab) /// Si está en última columna, va a primera columna de siguiente fila. /// Si está en última fila, hace wrap a primera fila o retorna tab_out. pub fn tabToNextCell(self: *AdvancedTableState, num_cols: usize, wrap_to_start: bool) TabNavigateResult { - if (num_cols == 0) return .tab_out; - const row_count = self.getRowCount(); - if (row_count == 0) return .tab_out; - const current_col: usize = if (self.selected_col >= 0) @intCast(self.selected_col) else 0; const current_row: usize = if (self.selected_row >= 0) @intCast(self.selected_row) else 0; - if (current_col + 1 < num_cols) { - // Siguiente columna en misma fila - self.selected_col = @intCast(current_col + 1); - return .navigated; + // Usar función de table_core + const pos = table_core.calculateNextCell(current_row, current_col, num_cols, row_count, wrap_to_start); + + if (pos.result == .navigated) { + self.selected_row = @intCast(pos.row); + self.selected_col = @intCast(pos.col); } - // Última columna: ir a primera columna de siguiente fila - self.selected_col = 0; - - if (current_row + 1 < row_count) { - // Hay siguiente fila - self.selected_row = @intCast(current_row + 1); - return .navigated; - } - - // Última fila - if (wrap_to_start) { - self.selected_row = 0; - return .navigated; - } - - return .tab_out; + return pos.result; } /// Navega a celda anterior (Shift+Tab) /// Si está en primera columna, va a última columna de fila anterior. /// Si está en primera fila, hace wrap a última fila o retorna tab_out. pub fn tabToPrevCell(self: *AdvancedTableState, num_cols: usize, wrap_to_end: bool) TabNavigateResult { - if (num_cols == 0) return .tab_out; - const row_count = self.getRowCount(); - if (row_count == 0) return .tab_out; - const current_col: usize = if (self.selected_col >= 0) @intCast(self.selected_col) else 0; const current_row: usize = if (self.selected_row >= 0) @intCast(self.selected_row) else 0; - if (current_col > 0) { - // Columna anterior en misma fila - self.selected_col = @intCast(current_col - 1); - return .navigated; + // Usar función de table_core + const pos = table_core.calculatePrevCell(current_row, current_col, num_cols, row_count, wrap_to_end); + + if (pos.result == .navigated) { + self.selected_row = @intCast(pos.row); + self.selected_col = @intCast(pos.col); } - // Primera columna: ir a última columna de fila anterior - self.selected_col = @intCast(num_cols - 1); - - if (current_row > 0) { - // Hay fila anterior - self.selected_row = @intCast(current_row - 1); - return .navigated; - } - - // Primera fila - if (wrap_to_end) { - self.selected_row = @intCast(row_count - 1); - return .navigated; - } - - return .tab_out; + return pos.result; } // ========================================================================= diff --git a/src/widgets/table_core.zig b/src/widgets/table_core.zig index 4dc380a..32bb5a3 100644 --- a/src/widgets/table_core.zig +++ b/src/widgets/table_core.zig @@ -382,6 +382,155 @@ pub fn startsWithIgnoreCase(haystack: []const u8, needle: []const u8) bool { return true; } +// ============================================================================= +// Navegación Tab Excel-style (compartida por AdvancedTable y VirtualAdvancedTable) +// ============================================================================= + +/// Resultado de navegación Tab +pub const TabNavigateResult = enum { + /// Navegó a otra celda dentro del widget + navigated, + /// Salió del widget (Tab en última celda o Shift+Tab en primera) + tab_out, +}; + +/// Resultado del cálculo de nueva posición de celda +pub const CellPosition = struct { + row: usize, + col: usize, + result: TabNavigateResult, +}; + +/// Calcula la siguiente celda después de Tab +/// Parámetros genéricos para que funcione con ambos tipos de tabla. +pub fn calculateNextCell( + current_row: usize, + current_col: usize, + num_cols: usize, + num_rows: usize, + wrap_to_start: bool, +) CellPosition { + if (num_cols == 0 or num_rows == 0) { + return .{ .row = current_row, .col = current_col, .result = .tab_out }; + } + + var new_row = current_row; + var new_col = current_col; + + if (current_col + 1 < num_cols) { + // Siguiente columna en misma fila + new_col = current_col + 1; + return .{ .row = new_row, .col = new_col, .result = .navigated }; + } + + // Última columna: ir a primera columna de siguiente fila + new_col = 0; + + if (current_row + 1 < num_rows) { + // Hay siguiente fila + new_row = current_row + 1; + return .{ .row = new_row, .col = new_col, .result = .navigated }; + } + + // Última fila + if (wrap_to_start) { + new_row = 0; + return .{ .row = new_row, .col = new_col, .result = .navigated }; + } + + return .{ .row = current_row, .col = current_col, .result = .tab_out }; +} + +/// Calcula la celda anterior después de Shift+Tab +pub fn calculatePrevCell( + current_row: usize, + current_col: usize, + num_cols: usize, + num_rows: usize, + wrap_to_end: bool, +) CellPosition { + if (num_cols == 0 or num_rows == 0) { + return .{ .row = current_row, .col = current_col, .result = .tab_out }; + } + + var new_row = current_row; + var new_col = current_col; + + if (current_col > 0) { + // Columna anterior en misma fila + new_col = current_col - 1; + return .{ .row = new_row, .col = new_col, .result = .navigated }; + } + + // Primera columna: ir a última columna de fila anterior + new_col = num_cols - 1; + + if (current_row > 0) { + // Hay fila anterior + new_row = current_row - 1; + return .{ .row = new_row, .col = new_col, .result = .navigated }; + } + + // Primera fila + if (wrap_to_end) { + new_row = num_rows - 1; + return .{ .row = new_row, .col = new_col, .result = .navigated }; + } + + return .{ .row = current_row, .col = current_col, .result = .tab_out }; +} + +// ============================================================================= +// Ordenación (compartida) +// ============================================================================= + +/// Dirección de ordenación +pub const SortDirection = enum { + none, + ascending, + descending, + + /// Alterna la dirección: none → asc → desc → none + pub fn toggle(self: SortDirection) SortDirection { + return switch (self) { + .none => .ascending, + .ascending => .descending, + .descending => .none, + }; + } +}; + +/// Resultado de toggle de ordenación en columna +pub const SortToggleResult = struct { + /// Nueva columna de ordenación (null si se desactivó) + column: ?usize, + /// Nueva dirección + direction: SortDirection, +}; + +/// Calcula el nuevo estado de ordenación al hacer click en una columna +pub fn toggleSort( + current_column: ?usize, + current_direction: SortDirection, + clicked_column: usize, +) SortToggleResult { + if (current_column) |col| { + if (col == clicked_column) { + // Misma columna: ciclar dirección + const new_dir = current_direction.toggle(); + return .{ + .column = if (new_dir == .none) null else clicked_column, + .direction = new_dir, + }; + } + } + // Columna diferente o sin ordenación: empezar ascendente + return .{ + .column = clicked_column, + .direction = .ascending, + }; +} + // ============================================================================= // Tests // ============================================================================= @@ -420,3 +569,74 @@ test "detectDoubleClick" { const third = detectDoubleClick(&state, 1400, 0, 0); try std.testing.expect(!third); } + +test "calculateNextCell - basic navigation" { + // Tabla 3x4 (3 columnas, 4 filas) + // Celda (0,0) -> (0,1) + const r1 = calculateNextCell(0, 0, 3, 4, false); + try std.testing.expectEqual(@as(usize, 0), r1.row); + try std.testing.expectEqual(@as(usize, 1), r1.col); + try std.testing.expectEqual(TabNavigateResult.navigated, r1.result); + + // Última columna -> primera columna de siguiente fila + const r2 = calculateNextCell(0, 2, 3, 4, false); + try std.testing.expectEqual(@as(usize, 1), r2.row); + try std.testing.expectEqual(@as(usize, 0), r2.col); + try std.testing.expectEqual(TabNavigateResult.navigated, r2.result); + + // Última celda sin wrap -> tab_out + const r3 = calculateNextCell(3, 2, 3, 4, false); + try std.testing.expectEqual(TabNavigateResult.tab_out, r3.result); + + // Última celda con wrap -> primera celda + const r4 = calculateNextCell(3, 2, 3, 4, true); + try std.testing.expectEqual(@as(usize, 0), r4.row); + try std.testing.expectEqual(@as(usize, 0), r4.col); + try std.testing.expectEqual(TabNavigateResult.navigated, r4.result); +} + +test "calculatePrevCell - basic navigation" { + // Celda (0,2) -> (0,1) + const r1 = calculatePrevCell(0, 2, 3, 4, false); + try std.testing.expectEqual(@as(usize, 0), r1.row); + try std.testing.expectEqual(@as(usize, 1), r1.col); + try std.testing.expectEqual(TabNavigateResult.navigated, r1.result); + + // Primera columna -> última columna de fila anterior + const r2 = calculatePrevCell(1, 0, 3, 4, false); + try std.testing.expectEqual(@as(usize, 0), r2.row); + try std.testing.expectEqual(@as(usize, 2), r2.col); + try std.testing.expectEqual(TabNavigateResult.navigated, r2.result); + + // Primera celda sin wrap -> tab_out + const r3 = calculatePrevCell(0, 0, 3, 4, false); + try std.testing.expectEqual(TabNavigateResult.tab_out, r3.result); + + // Primera celda con wrap -> última celda + const r4 = calculatePrevCell(0, 0, 3, 4, true); + try std.testing.expectEqual(@as(usize, 3), r4.row); + try std.testing.expectEqual(@as(usize, 2), r4.col); + try std.testing.expectEqual(TabNavigateResult.navigated, r4.result); +} + +test "toggleSort" { + // Sin ordenación -> ascendente en columna 2 + const r1 = toggleSort(null, .none, 2); + try std.testing.expectEqual(@as(?usize, 2), r1.column); + try std.testing.expectEqual(SortDirection.ascending, r1.direction); + + // Ascendente en columna 2 -> descendente + const r2 = toggleSort(2, .ascending, 2); + try std.testing.expectEqual(@as(?usize, 2), r2.column); + try std.testing.expectEqual(SortDirection.descending, r2.direction); + + // Descendente -> none (columna null) + const r3 = toggleSort(2, .descending, 2); + try std.testing.expectEqual(@as(?usize, null), r3.column); + try std.testing.expectEqual(SortDirection.none, r3.direction); + + // Click en columna diferente -> ascendente en nueva columna + const r4 = toggleSort(2, .ascending, 5); + try std.testing.expectEqual(@as(?usize, 5), r4.column); + try std.testing.expectEqual(SortDirection.ascending, r4.direction); +} diff --git a/src/widgets/virtual_advanced_table/state.zig b/src/widgets/virtual_advanced_table/state.zig index bb09ce3..c5ee49f 100644 --- a/src/widgets/virtual_advanced_table/state.zig +++ b/src/widgets/virtual_advanced_table/state.zig @@ -4,6 +4,7 @@ const std = @import("std"); const types = @import("types.zig"); +const table_core = @import("../table_core.zig"); const RowData = types.RowData; const CountInfo = types.CountInfo; const SortDirection = types.SortDirection; @@ -494,78 +495,61 @@ pub const VirtualAdvancedTableState = struct { // ========================================================================= // Navegación Tab Excel-style (con wrap) + // Usa table_core para la lógica común (Norma #7 DRY) // ========================================================================= - /// Resultado de navegación Tab - pub const TabNavigateResult = enum { - /// Navegó a otra celda dentro del widget - navigated, - /// Salió del widget (Tab en última celda o Shift+Tab en primera) - tab_out, - }; + /// Re-exporta TabNavigateResult desde table_core para compatibilidad + pub const TabNavigateResult = table_core.TabNavigateResult; /// Navega a siguiente celda (Tab) /// Si está en última columna, va a primera columna de siguiente fila. /// Si está en última fila, hace wrap a primera fila o retorna tab_out. pub fn tabToNextCell(self: *Self, num_cols: usize, visible_rows: usize, wrap_to_start: bool) TabNavigateResult { - if (num_cols == 0) return .tab_out; + // Obtener fila actual + const current_row = self.getSelectedRow() orelse 0; + const total_rows = self.current_window.len + self.window_start; - if (self.active_col + 1 < num_cols) { - // Siguiente columna en misma fila - self.active_col += 1; - return .navigated; - } + // Usar función de table_core + const pos = table_core.calculateNextCell(current_row, self.active_col, num_cols, total_rows, wrap_to_start); - // Última columna: ir a primera columna de siguiente fila - self.active_col = 0; - - if (self.findSelectedInWindow()) |window_idx| { - if (window_idx + 1 < self.current_window.len) { - // Hay siguiente fila - self.moveDown(visible_rows); - return .navigated; + if (pos.result == .navigated) { + self.active_col = pos.col; + // Navegar a la nueva fila si cambió + if (pos.row != current_row) { + if (pos.row == 0) { + self.goToStart(); + } else { + self.moveDown(visible_rows); + } } } - // Última fila - if (wrap_to_start) { - self.goToStart(); - return .navigated; - } - - return .tab_out; + return pos.result; } /// Navega a celda anterior (Shift+Tab) /// Si está en primera columna, va a última columna de fila anterior. /// Si está en primera fila, hace wrap a última fila o retorna tab_out. pub fn tabToPrevCell(self: *Self, num_cols: usize, visible_rows: usize, total_rows: usize, wrap_to_end: bool) TabNavigateResult { - if (num_cols == 0) return .tab_out; + // Obtener fila actual + const current_row = self.getSelectedRow() orelse 0; - if (self.active_col > 0) { - // Columna anterior en misma fila - self.active_col -= 1; - return .navigated; - } + // Usar función de table_core + const pos = table_core.calculatePrevCell(current_row, self.active_col, num_cols, total_rows, wrap_to_end); - // Primera columna: ir a última columna de fila anterior - self.active_col = num_cols - 1; - - if (self.findSelectedInWindow()) |window_idx| { - if (window_idx > 0) { - // Hay fila anterior - self.moveUp(); - return .navigated; + if (pos.result == .navigated) { + self.active_col = pos.col; + // Navegar a la nueva fila si cambió + if (pos.row != current_row) { + if (pos.row == total_rows - 1) { + self.goToEnd(visible_rows, total_rows); + } else { + self.moveUp(); + } } } - // Primera fila - if (wrap_to_end) { - self.goToEnd(visible_rows, total_rows); - return .navigated; - } - - return .tab_out; + return pos.result; } /// Obtiene la fila global seleccionada