refactor(table_core): Move Tab navigation logic to shared module (Norma #7 DRY)
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
This commit is contained in:
parent
c2f0fbb19d
commit
27b69cfcde
3 changed files with 271 additions and 104 deletions
|
|
@ -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;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
// Última fila
|
||||
if (wrap_to_start) {
|
||||
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();
|
||||
return .navigated;
|
||||
} else {
|
||||
self.moveDown(visible_rows);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// Primera fila
|
||||
if (wrap_to_end) {
|
||||
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);
|
||||
return .navigated;
|
||||
} else {
|
||||
self.moveUp();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return .tab_out;
|
||||
return pos.result;
|
||||
}
|
||||
|
||||
/// Obtiene la fila global seleccionada
|
||||
|
|
|
|||
Loading…
Reference in a new issue