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)
|
//! - VirtualAdvancedTable (datos paginados desde DataProvider)
|
||||||
//!
|
//!
|
||||||
//! Principio: Una sola implementación de UI, dos estrategias de datos.
|
//! 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 std = @import("std");
|
||||||
const Context = @import("../core/context.zig").Context;
|
const Context = @import("../core/context.zig").Context;
|
||||||
|
|
@ -65,7 +79,8 @@ pub const CellRenderInfo = struct {
|
||||||
text_align: u2 = 0,
|
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 {
|
pub const EditState = struct {
|
||||||
/// Está en modo edición
|
/// Está en modo edición
|
||||||
editing: bool = false,
|
editing: bool = false,
|
||||||
|
|
@ -79,6 +94,191 @@ pub const EditState = struct {
|
||||||
edit_cursor: usize = 0,
|
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
|
/// Estado de doble-click
|
||||||
pub const DoubleClickState = struct {
|
pub const DoubleClickState = struct {
|
||||||
last_click_time: u64 = 0,
|
last_click_time: u64 = 0,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue