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
642 lines
22 KiB
Zig
642 lines
22 KiB
Zig
//! Table Core - Funciones compartidas para renderizado de tablas
|
|
//!
|
|
//! Este módulo contiene la lógica común de renderizado utilizada por:
|
|
//! - AdvancedTable (datos en memoria)
|
|
//! - VirtualAdvancedTable (datos paginados desde DataProvider)
|
|
//!
|
|
//! Principio: Una sola implementación de UI, dos estrategias de datos.
|
|
|
|
const std = @import("std");
|
|
const Context = @import("../core/context.zig").Context;
|
|
const Command = @import("../core/command.zig");
|
|
const Layout = @import("../core/layout.zig");
|
|
const Style = @import("../core/style.zig");
|
|
|
|
// =============================================================================
|
|
// Tipos comunes
|
|
// =============================================================================
|
|
|
|
/// Colores para renderizado de tabla
|
|
pub const TableColors = struct {
|
|
// Fondos
|
|
background: Style.Color = Style.Color.rgb(30, 30, 35),
|
|
row_normal: Style.Color = Style.Color.rgb(35, 35, 40),
|
|
row_alternate: Style.Color = Style.Color.rgb(40, 40, 45),
|
|
row_hover: Style.Color = Style.Color.rgb(50, 50, 60),
|
|
selected_row: Style.Color = Style.Color.rgb(0, 90, 180),
|
|
selected_row_unfocus: Style.Color = Style.Color.rgb(60, 60, 70),
|
|
|
|
// Celda activa
|
|
selected_cell: Style.Color = Style.Color.rgb(100, 150, 255),
|
|
selected_cell_unfocus: Style.Color = Style.Color.rgb(80, 80, 90),
|
|
|
|
// Edición
|
|
cell_editing_bg: Style.Color = Style.Color.rgb(255, 255, 255),
|
|
cell_editing_border: Style.Color = Style.Color.rgb(0, 120, 215),
|
|
cell_editing_text: Style.Color = Style.Color.rgb(0, 0, 0),
|
|
|
|
// Header
|
|
header_bg: Style.Color = Style.Color.rgb(45, 45, 50),
|
|
header_fg: Style.Color = Style.Color.rgb(200, 200, 200),
|
|
|
|
// Texto
|
|
text_normal: Style.Color = Style.Color.rgb(220, 220, 220),
|
|
text_selected: Style.Color = Style.Color.rgb(255, 255, 255),
|
|
text_placeholder: Style.Color = Style.Color.rgb(128, 128, 128),
|
|
|
|
// Bordes
|
|
border: Style.Color = Style.Color.rgb(60, 60, 65),
|
|
focus_ring: Style.Color = Style.Color.rgb(0, 120, 215),
|
|
};
|
|
|
|
/// Información de una celda para renderizado
|
|
pub const CellRenderInfo = struct {
|
|
/// Texto a mostrar
|
|
text: []const u8,
|
|
/// Posición X de la celda
|
|
x: i32,
|
|
/// Ancho de la celda
|
|
width: u32,
|
|
/// Es la celda actualmente seleccionada
|
|
is_selected: bool = false,
|
|
/// Es editable
|
|
is_editable: bool = true,
|
|
/// Alineación del texto (0=left, 1=center, 2=right)
|
|
text_align: u2 = 0,
|
|
};
|
|
|
|
/// Estado de edición para renderizado
|
|
pub const EditState = struct {
|
|
/// Está en modo edición
|
|
editing: bool = false,
|
|
/// Fila en edición
|
|
edit_row: i32 = -1,
|
|
/// Columna en edición
|
|
edit_col: i32 = -1,
|
|
/// Buffer de texto actual
|
|
edit_text: []const u8 = "",
|
|
/// Posición del cursor
|
|
edit_cursor: usize = 0,
|
|
};
|
|
|
|
/// Estado de doble-click
|
|
pub const DoubleClickState = struct {
|
|
last_click_time: u64 = 0,
|
|
last_click_row: i64 = -1,
|
|
last_click_col: i32 = -1,
|
|
threshold_ms: u64 = 400,
|
|
};
|
|
|
|
/// Resultado de procesar click en celda
|
|
pub const CellClickResult = struct {
|
|
/// Hubo click
|
|
clicked: bool = false,
|
|
/// Fue doble-click
|
|
double_click: bool = false,
|
|
/// Fila clickeada
|
|
row: usize = 0,
|
|
/// Columna clickeada
|
|
col: usize = 0,
|
|
};
|
|
|
|
// =============================================================================
|
|
// Funciones de renderizado
|
|
// =============================================================================
|
|
|
|
/// Dibuja el indicador de celda activa (fondo + borde)
|
|
/// Llamar ANTES de dibujar el texto de la celda
|
|
pub fn drawCellActiveIndicator(
|
|
ctx: *Context,
|
|
x: i32,
|
|
y: i32,
|
|
width: u32,
|
|
height: u32,
|
|
row_bg: Style.Color,
|
|
colors: *const TableColors,
|
|
has_focus: bool,
|
|
) void {
|
|
if (has_focus) {
|
|
// Con focus: fondo más visible + borde doble
|
|
const tinted_bg = blendColor(row_bg, colors.selected_cell, 0.35);
|
|
ctx.pushCommand(Command.rect(x, y, width, height, tinted_bg));
|
|
ctx.pushCommand(Command.rectOutline(x, y, width, height, colors.selected_cell));
|
|
ctx.pushCommand(Command.rectOutline(x + 1, y + 1, width -| 2, height -| 2, colors.selected_cell));
|
|
} else {
|
|
// Sin focus: indicación más sutil
|
|
const tinted_bg = blendColor(row_bg, colors.selected_cell_unfocus, 0.15);
|
|
ctx.pushCommand(Command.rect(x, y, width, height, tinted_bg));
|
|
ctx.pushCommand(Command.rectOutline(x, y, width, height, colors.border));
|
|
}
|
|
}
|
|
|
|
/// Dibuja el overlay de edición de celda
|
|
pub fn drawEditingOverlay(
|
|
ctx: *Context,
|
|
x: i32,
|
|
y: i32,
|
|
width: u32,
|
|
height: u32,
|
|
edit_text: []const u8,
|
|
cursor_pos: usize,
|
|
colors: *const TableColors,
|
|
) void {
|
|
// Fondo blanco
|
|
ctx.pushCommand(Command.rect(x, y, width, height, colors.cell_editing_bg));
|
|
|
|
// Borde azul
|
|
ctx.pushCommand(Command.rectOutline(x, y, width, height, colors.cell_editing_border));
|
|
|
|
// Texto
|
|
const text_y = y + @as(i32, @intCast((height -| 16) / 2));
|
|
const text_to_show = if (edit_text.len > 0) edit_text else "";
|
|
ctx.pushCommand(Command.text(x + 4, text_y, text_to_show, colors.cell_editing_text));
|
|
|
|
// Cursor parpadeante (simplificado: siempre visible)
|
|
// Calcular posición X del cursor basado en caracteres
|
|
const cursor_x = x + 4 + @as(i32, @intCast(cursor_pos * 8)); // Asumiendo fuente monospace 8px
|
|
ctx.pushCommand(Command.rect(cursor_x, text_y, 2, 16, colors.cell_editing_border));
|
|
}
|
|
|
|
/// Dibuja el texto de una celda
|
|
pub fn drawCellText(
|
|
ctx: *Context,
|
|
x: i32,
|
|
y: i32,
|
|
width: u32,
|
|
height: u32,
|
|
text: []const u8,
|
|
color: Style.Color,
|
|
text_align: u2,
|
|
) void {
|
|
const text_y = y + @as(i32, @intCast((height -| 16) / 2));
|
|
|
|
const text_x = switch (text_align) {
|
|
0 => x + 4, // Left
|
|
1 => x + @as(i32, @intCast(width / 2)) - @as(i32, @intCast(text.len * 4)), // Center (aprox)
|
|
2 => x + @as(i32, @intCast(width)) - @as(i32, @intCast(text.len * 8 + 4)), // Right
|
|
3 => x + 4, // Default left
|
|
};
|
|
|
|
ctx.pushCommand(Command.text(text_x, text_y, text, color));
|
|
}
|
|
|
|
/// Detecta si un click es doble-click
|
|
pub fn detectDoubleClick(
|
|
state: *DoubleClickState,
|
|
current_time: u64,
|
|
row: i64,
|
|
col: i32,
|
|
) bool {
|
|
const same_cell = state.last_click_row == row and state.last_click_col == col;
|
|
const time_diff = current_time -| state.last_click_time;
|
|
const is_double = same_cell and time_diff < state.threshold_ms;
|
|
|
|
if (is_double) {
|
|
// Reset para no detectar triple-click
|
|
state.last_click_time = 0;
|
|
state.last_click_row = -1;
|
|
state.last_click_col = -1;
|
|
} else {
|
|
// Guardar para próximo click
|
|
state.last_click_time = current_time;
|
|
state.last_click_row = row;
|
|
state.last_click_col = col;
|
|
}
|
|
|
|
return is_double;
|
|
}
|
|
|
|
// =============================================================================
|
|
// Manejo de teclado para edición
|
|
// =============================================================================
|
|
|
|
/// Resultado de procesar teclado en modo edición
|
|
pub const EditKeyboardResult = struct {
|
|
/// Se confirmó la edición (Enter)
|
|
committed: bool = false,
|
|
/// Se canceló la edición (Escape)
|
|
cancelled: bool = false,
|
|
/// Se revirtió al valor original (primer Escape)
|
|
reverted: bool = false,
|
|
/// Se debe navegar a siguiente celda (Tab)
|
|
navigate_next: bool = false,
|
|
/// Se debe navegar a celda anterior (Shift+Tab)
|
|
navigate_prev: bool = false,
|
|
/// El buffer de edición cambió
|
|
text_changed: bool = false,
|
|
};
|
|
|
|
/// Procesa teclado en modo edición
|
|
/// Modifica edit_buffer, edit_len, edit_cursor según las teclas
|
|
pub fn handleEditingKeyboard(
|
|
ctx: *Context,
|
|
edit_buffer: []u8,
|
|
edit_len: *usize,
|
|
edit_cursor: *usize,
|
|
escape_count: *u8,
|
|
original_text: ?[]const u8,
|
|
) EditKeyboardResult {
|
|
var result = EditKeyboardResult{};
|
|
|
|
// Escape: cancelar o revertir
|
|
if (ctx.input.keyPressed(.escape)) {
|
|
escape_count.* += 1;
|
|
if (escape_count.* >= 2 or original_text == null) {
|
|
result.cancelled = true;
|
|
} else {
|
|
// Revertir al valor original
|
|
if (original_text) |orig| {
|
|
const len = @min(orig.len, edit_buffer.len);
|
|
@memcpy(edit_buffer[0..len], orig[0..len]);
|
|
edit_len.* = len;
|
|
edit_cursor.* = len;
|
|
result.reverted = true;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// Reset escape count en cualquier otra tecla
|
|
escape_count.* = 0;
|
|
|
|
// Enter: confirmar
|
|
if (ctx.input.keyPressed(.enter)) {
|
|
result.committed = true;
|
|
return result;
|
|
}
|
|
|
|
// Tab: confirmar y navegar
|
|
if (ctx.input.keyPressed(.tab)) {
|
|
result.committed = true;
|
|
if (ctx.input.modifiers.shift) {
|
|
result.navigate_prev = true;
|
|
} else {
|
|
result.navigate_next = true;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// Movimiento del cursor
|
|
if (ctx.input.keyPressed(.left)) {
|
|
if (edit_cursor.* > 0) edit_cursor.* -= 1;
|
|
return result;
|
|
}
|
|
if (ctx.input.keyPressed(.right)) {
|
|
if (edit_cursor.* < edit_len.*) edit_cursor.* += 1;
|
|
return result;
|
|
}
|
|
if (ctx.input.keyPressed(.home)) {
|
|
edit_cursor.* = 0;
|
|
return result;
|
|
}
|
|
if (ctx.input.keyPressed(.end)) {
|
|
edit_cursor.* = edit_len.*;
|
|
return result;
|
|
}
|
|
|
|
// Backspace
|
|
if (ctx.input.keyPressed(.backspace)) {
|
|
if (edit_cursor.* > 0) {
|
|
// Shift characters left
|
|
var i: usize = edit_cursor.* - 1;
|
|
while (i < edit_len.* - 1) : (i += 1) {
|
|
edit_buffer[i] = edit_buffer[i + 1];
|
|
}
|
|
edit_len.* -= 1;
|
|
edit_cursor.* -= 1;
|
|
result.text_changed = true;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// Delete
|
|
if (ctx.input.keyPressed(.delete)) {
|
|
if (edit_cursor.* < edit_len.*) {
|
|
var i: usize = edit_cursor.*;
|
|
while (i < edit_len.* - 1) : (i += 1) {
|
|
edit_buffer[i] = edit_buffer[i + 1];
|
|
}
|
|
edit_len.* -= 1;
|
|
result.text_changed = true;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// Character input
|
|
if (ctx.input.text_input_len > 0) {
|
|
const text = ctx.input.text_input[0..ctx.input.text_input_len];
|
|
for (text) |ch| {
|
|
if (ch >= 32 and ch < 127) {
|
|
if (edit_len.* < edit_buffer.len - 1) {
|
|
// Shift characters right
|
|
var i: usize = edit_len.*;
|
|
while (i > edit_cursor.*) : (i -= 1) {
|
|
edit_buffer[i] = edit_buffer[i - 1];
|
|
}
|
|
edit_buffer[edit_cursor.*] = ch;
|
|
edit_len.* += 1;
|
|
edit_cursor.* += 1;
|
|
result.text_changed = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// =============================================================================
|
|
// Utilidades
|
|
// =============================================================================
|
|
|
|
/// Mezcla dos colores con un factor alpha
|
|
pub fn blendColor(base: Style.Color, overlay: Style.Color, alpha: f32) Style.Color {
|
|
const inv_alpha = 1.0 - alpha;
|
|
|
|
return Style.Color.rgba(
|
|
@intFromFloat(@as(f32, @floatFromInt(base.r)) * inv_alpha + @as(f32, @floatFromInt(overlay.r)) * alpha),
|
|
@intFromFloat(@as(f32, @floatFromInt(base.g)) * inv_alpha + @as(f32, @floatFromInt(overlay.g)) * alpha),
|
|
@intFromFloat(@as(f32, @floatFromInt(base.b)) * inv_alpha + @as(f32, @floatFromInt(overlay.b)) * alpha),
|
|
base.a,
|
|
);
|
|
}
|
|
|
|
/// Compara strings case-insensitive para búsqueda incremental
|
|
pub fn startsWithIgnoreCase(haystack: []const u8, needle: []const u8) bool {
|
|
if (needle.len > haystack.len) return false;
|
|
if (needle.len == 0) return true;
|
|
|
|
for (needle, 0..) |needle_char, i| {
|
|
const haystack_char = haystack[i];
|
|
const needle_lower = if (needle_char >= 'A' and needle_char <= 'Z')
|
|
needle_char + 32
|
|
else
|
|
needle_char;
|
|
const haystack_lower = if (haystack_char >= 'A' and haystack_char <= 'Z')
|
|
haystack_char + 32
|
|
else
|
|
haystack_char;
|
|
|
|
if (needle_lower != haystack_lower) return false;
|
|
}
|
|
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
|
|
// =============================================================================
|
|
|
|
test "blendColor" {
|
|
const white = Style.Color.rgb(255, 255, 255);
|
|
const black = Style.Color.rgb(0, 0, 0);
|
|
|
|
const gray = blendColor(white, black, 0.5);
|
|
try std.testing.expectEqual(@as(u8, 127), gray.r);
|
|
try std.testing.expectEqual(@as(u8, 127), gray.g);
|
|
try std.testing.expectEqual(@as(u8, 127), gray.b);
|
|
}
|
|
|
|
test "startsWithIgnoreCase" {
|
|
try std.testing.expect(startsWithIgnoreCase("Hello World", "Hello"));
|
|
try std.testing.expect(startsWithIgnoreCase("Hello World", "hello"));
|
|
try std.testing.expect(startsWithIgnoreCase("hello world", "HELLO"));
|
|
try std.testing.expect(startsWithIgnoreCase("anything", ""));
|
|
try std.testing.expect(!startsWithIgnoreCase("Hello", "World"));
|
|
try std.testing.expect(!startsWithIgnoreCase("Hi", "Hello World"));
|
|
}
|
|
|
|
test "detectDoubleClick" {
|
|
var state = DoubleClickState{};
|
|
|
|
// Primer click
|
|
const first = detectDoubleClick(&state, 1000, 0, 0);
|
|
try std.testing.expect(!first);
|
|
|
|
// Segundo click rápido en misma celda = doble click
|
|
const second = detectDoubleClick(&state, 1200, 0, 0);
|
|
try std.testing.expect(second);
|
|
|
|
// Tercer click (estado reseteado)
|
|
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);
|
|
}
|