refactor(table_core): Add CellEditState + NavigationState for composition
- Delete obsolete 'table_core (conflicted).zig' - Add memory ownership protocol documentation - Add CellEditState: embeddable edit state with buffer, cursor, escape handling - Add NavigationState: embeddable nav state with active_col, scroll, Tab methods - Maintains backward compatibility with existing EditState
This commit is contained in:
parent
d16019d54f
commit
6819919060
2 changed files with 201 additions and 423 deletions
|
|
@ -1,422 +0,0 @@
|
|||
//! 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: i32 = -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: i32,
|
||||
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;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 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);
|
||||
}
|
||||
|
|
@ -5,6 +5,20 @@
|
|||
//! - VirtualAdvancedTable (datos paginados desde DataProvider)
|
||||
//!
|
||||
//! Principio: Una sola implementación de UI, dos estrategias de datos.
|
||||
//!
|
||||
//! ## Protocolo de Propiedad de Memoria
|
||||
//!
|
||||
//! 1. **Strings de celda:** El DataSource retorna punteros a memoria estable.
|
||||
//! El widget NO libera estos strings. Son válidos hasta el próximo fetch.
|
||||
//!
|
||||
//! 2. **Buffers de edición:** El widget mantiene edit_buffer[256] propio.
|
||||
//! Los cambios se copian al DataSource solo en commit.
|
||||
//!
|
||||
//! 3. **Rendering:** Todos los strings pasados a ctx.pushCommand() deben ser
|
||||
//! estables durante todo el frame. Usar buffers persistentes, NO stack.
|
||||
//!
|
||||
//! 4. **getValueInto pattern:** Cuando se necesita formatear valores,
|
||||
//! el caller provee el buffer destino para evitar memory ownership issues.
|
||||
|
||||
const std = @import("std");
|
||||
const Context = @import("../core/context.zig").Context;
|
||||
|
|
@ -65,7 +79,8 @@ pub const CellRenderInfo = struct {
|
|||
text_align: u2 = 0,
|
||||
};
|
||||
|
||||
/// Estado de edición para renderizado
|
||||
/// Estado de edición para renderizado (info para draw)
|
||||
/// NOTA: Para estado embebible en widgets, usar CellEditState
|
||||
pub const EditState = struct {
|
||||
/// Está en modo edición
|
||||
editing: bool = false,
|
||||
|
|
@ -79,6 +94,191 @@ pub const EditState = struct {
|
|||
edit_cursor: usize = 0,
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// Estados embebibles (para composición en AdvancedTableState/VirtualAdvancedTableState)
|
||||
// =============================================================================
|
||||
|
||||
/// Tamaño máximo del buffer de edición
|
||||
pub const MAX_EDIT_BUFFER_SIZE: usize = 256;
|
||||
|
||||
/// Estado completo de edición de celda
|
||||
/// Diseñado para ser embebido en AdvancedTableState y VirtualAdvancedTableState
|
||||
pub const CellEditState = struct {
|
||||
/// Está en modo edición
|
||||
editing: bool = false,
|
||||
|
||||
/// Celda en edición (fila, columna)
|
||||
edit_row: usize = 0,
|
||||
edit_col: usize = 0,
|
||||
|
||||
/// Buffer de texto actual
|
||||
edit_buffer: [MAX_EDIT_BUFFER_SIZE]u8 = undefined,
|
||||
edit_len: usize = 0,
|
||||
|
||||
/// Posición del cursor
|
||||
edit_cursor: usize = 0,
|
||||
|
||||
/// Valor original (para revertir con Escape)
|
||||
original_buffer: [MAX_EDIT_BUFFER_SIZE]u8 = undefined,
|
||||
original_len: usize = 0,
|
||||
|
||||
/// Contador de Escapes (1=revertir, 2=cancelar)
|
||||
escape_count: u8 = 0,
|
||||
|
||||
/// Flag: el valor cambió respecto al original
|
||||
value_changed: bool = false,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
/// Inicia edición de una celda
|
||||
pub fn startEditing(self: *Self, row: usize, col: usize, current_value: []const u8, initial_char: ?u8) void {
|
||||
self.editing = true;
|
||||
self.edit_row = row;
|
||||
self.edit_col = col;
|
||||
self.escape_count = 0;
|
||||
self.value_changed = false;
|
||||
|
||||
// Guardar valor original
|
||||
const orig_len = @min(current_value.len, MAX_EDIT_BUFFER_SIZE);
|
||||
@memcpy(self.original_buffer[0..orig_len], current_value[0..orig_len]);
|
||||
self.original_len = orig_len;
|
||||
|
||||
// Inicializar buffer de edición
|
||||
if (initial_char) |c| {
|
||||
// Tecla alfanumérica: empezar con ese caracter
|
||||
self.edit_buffer[0] = c;
|
||||
self.edit_len = 1;
|
||||
self.edit_cursor = 1;
|
||||
} else {
|
||||
// F2/Space/DoubleClick: mostrar valor actual
|
||||
@memcpy(self.edit_buffer[0..orig_len], current_value[0..orig_len]);
|
||||
self.edit_len = orig_len;
|
||||
self.edit_cursor = orig_len;
|
||||
}
|
||||
}
|
||||
|
||||
/// Obtiene el texto actual del editor
|
||||
pub fn getEditText(self: *const Self) []const u8 {
|
||||
return self.edit_buffer[0..self.edit_len];
|
||||
}
|
||||
|
||||
/// Obtiene el valor original
|
||||
pub fn getOriginalValue(self: *const Self) []const u8 {
|
||||
return self.original_buffer[0..self.original_len];
|
||||
}
|
||||
|
||||
/// Verifica si el valor cambió
|
||||
pub fn hasChanged(self: *const Self) bool {
|
||||
const current = self.getEditText();
|
||||
const original = self.getOriginalValue();
|
||||
return !std.mem.eql(u8, current, original);
|
||||
}
|
||||
|
||||
/// Revierte al valor original (Escape 1)
|
||||
pub fn revertToOriginal(self: *Self) void {
|
||||
const orig = self.getOriginalValue();
|
||||
@memcpy(self.edit_buffer[0..orig.len], orig);
|
||||
self.edit_len = orig.len;
|
||||
self.edit_cursor = orig.len;
|
||||
}
|
||||
|
||||
/// Finaliza edición
|
||||
pub fn stopEditing(self: *Self) void {
|
||||
self.editing = false;
|
||||
self.edit_len = 0;
|
||||
self.edit_cursor = 0;
|
||||
self.escape_count = 0;
|
||||
}
|
||||
|
||||
/// Resultado de handleEscape
|
||||
pub const EscapeAction = enum { reverted, cancelled, none };
|
||||
|
||||
/// Maneja Escape (retorna acción a tomar)
|
||||
pub fn handleEscape(self: *Self) EscapeAction {
|
||||
if (!self.editing) return .none;
|
||||
|
||||
self.escape_count += 1;
|
||||
if (self.escape_count == 1) {
|
||||
self.revertToOriginal();
|
||||
return .reverted;
|
||||
} else {
|
||||
self.stopEditing();
|
||||
return .cancelled;
|
||||
}
|
||||
}
|
||||
|
||||
/// Convierte a EditState para funciones de renderizado
|
||||
pub fn toEditState(self: *const Self) EditState {
|
||||
return .{
|
||||
.editing = self.editing,
|
||||
.edit_row = @intCast(self.edit_row),
|
||||
.edit_col = @intCast(self.edit_col),
|
||||
.edit_text = self.getEditText(),
|
||||
.edit_cursor = self.edit_cursor,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/// Estado de navegación compartido
|
||||
/// Diseñado para ser embebido en AdvancedTableState y VirtualAdvancedTableState
|
||||
pub const NavigationState = struct {
|
||||
/// Columna activa (para Tab navigation)
|
||||
active_col: usize = 0,
|
||||
|
||||
/// Scroll vertical (en filas)
|
||||
scroll_row: usize = 0,
|
||||
|
||||
/// Scroll horizontal (en pixels)
|
||||
scroll_x: i32 = 0,
|
||||
|
||||
/// El widget tiene focus
|
||||
has_focus: bool = false,
|
||||
|
||||
/// Double-click state
|
||||
double_click: DoubleClickState = .{},
|
||||
|
||||
const Self = @This();
|
||||
|
||||
/// Navega a siguiente celda (Tab)
|
||||
/// Retorna nueva posición y si navegó o salió del widget
|
||||
pub fn tabToNextCell(self: *Self, current_row: usize, num_cols: usize, num_rows: usize, wrap: bool) struct { row: usize, col: usize, result: TabNavigateResult } {
|
||||
const pos = calculateNextCell(current_row, self.active_col, num_cols, num_rows, wrap);
|
||||
if (pos.result == .navigated) {
|
||||
self.active_col = pos.col;
|
||||
}
|
||||
return .{ .row = pos.row, .col = pos.col, .result = pos.result };
|
||||
}
|
||||
|
||||
/// Navega a celda anterior (Shift+Tab)
|
||||
pub fn tabToPrevCell(self: *Self, current_row: usize, num_cols: usize, num_rows: usize, wrap: bool) struct { row: usize, col: usize, result: TabNavigateResult } {
|
||||
const pos = calculatePrevCell(current_row, self.active_col, num_cols, num_rows, wrap);
|
||||
if (pos.result == .navigated) {
|
||||
self.active_col = pos.col;
|
||||
}
|
||||
return .{ .row = pos.row, .col = pos.col, .result = pos.result };
|
||||
}
|
||||
|
||||
/// Mueve a columna anterior
|
||||
pub fn moveToPrevCol(self: *Self) void {
|
||||
if (self.active_col > 0) self.active_col -= 1;
|
||||
}
|
||||
|
||||
/// Mueve a columna siguiente
|
||||
pub fn moveToNextCol(self: *Self, max_cols: usize) void {
|
||||
if (self.active_col + 1 < max_cols) self.active_col += 1;
|
||||
}
|
||||
|
||||
/// Va a primera columna
|
||||
pub fn goToFirstCol(self: *Self) void {
|
||||
self.active_col = 0;
|
||||
}
|
||||
|
||||
/// Va a última columna
|
||||
pub fn goToLastCol(self: *Self, max_cols: usize) void {
|
||||
if (max_cols > 0) self.active_col = max_cols - 1;
|
||||
}
|
||||
};
|
||||
|
||||
/// Estado de doble-click
|
||||
pub const DoubleClickState = struct {
|
||||
last_click_time: u64 = 0,
|
||||
|
|
|
|||
Loading…
Reference in a new issue