From 93836aef508f5803ff08bf3abe2e2066031beb3f Mon Sep 17 00:00:00 2001 From: reugenio Date: Fri, 26 Dec 2025 14:48:56 +0100 Subject: [PATCH] feat(virtual_advanced_table): Add CellEditor widget for inline editing --- .../virtual_advanced_table/cell_editor.zig | 232 ++++++++++++++++++ .../virtual_advanced_table.zig | 5 + 2 files changed, 237 insertions(+) create mode 100644 src/widgets/virtual_advanced_table/cell_editor.zig diff --git a/src/widgets/virtual_advanced_table/cell_editor.zig b/src/widgets/virtual_advanced_table/cell_editor.zig new file mode 100644 index 0000000..c907132 --- /dev/null +++ b/src/widgets/virtual_advanced_table/cell_editor.zig @@ -0,0 +1,232 @@ +//! CellEditor - Editor de celda overlay para edición inline +//! +//! Dibuja un campo de texto sobre una celda para edición estilo Excel. +//! Utiliza el edit_buffer del VirtualAdvancedTableState. + +const std = @import("std"); +const Context = @import("../../core/context.zig").Context; +const Command = @import("../../core/command.zig"); +const Style = @import("../../core/style.zig"); +const Input = @import("../../core/input.zig"); +const types = @import("types.zig"); +const state_mod = @import("state.zig"); + +const CellGeometry = types.CellGeometry; +const VirtualAdvancedTableState = state_mod.VirtualAdvancedTableState; + +/// Colores del editor de celda +pub const CellEditorColors = struct { + background: Style.Color = Style.Color.rgb(255, 255, 255), + border: Style.Color = Style.Color.rgb(0, 120, 215), // Azul Windows + text: Style.Color = Style.Color.rgb(0, 0, 0), + cursor: Style.Color = Style.Color.rgb(0, 0, 0), + selection: Style.Color = Style.Color.rgb(173, 214, 255), +}; + +/// Resultado del procesamiento del CellEditor +pub const CellEditorResult = struct { + /// El usuario presionó Enter o Tab (commit) + committed: bool = false, + + /// El usuario presionó Escape + escaped: bool = false, + + /// El texto cambió + text_changed: bool = false, + + /// Navegación solicitada después de commit + navigate: NavigateDirection = .none, + + pub const NavigateDirection = enum { + none, + next_cell, // Tab + prev_cell, // Shift+Tab + next_row, // Enter o ↓ + prev_row, // ↑ + }; +}; + +/// Dibuja el editor de celda overlay +/// Retorna resultado con acciones del usuario +pub fn drawCellEditor( + ctx: *Context, + state: *VirtualAdvancedTableState, + geom: CellGeometry, + colors: CellEditorColors, +) CellEditorResult { + var result = CellEditorResult{}; + + if (!state.isEditing()) return result; + + // Padding interno + const padding: i32 = 2; + + // Fondo blanco + ctx.pushCommand(Command.rect( + geom.x, + geom.y, + geom.w, + geom.h, + colors.background, + )); + + // Borde azul (indica edición activa) + ctx.pushCommand(Command.rectOutline( + geom.x, + geom.y, + geom.w, + geom.h, + colors.border, + )); + + // Texto actual + const text = state.getEditText(); + ctx.pushCommand(Command.text( + geom.x + padding, + geom.y + padding, + text, + colors.text, + )); + + // Cursor (línea vertical parpadeante) + // Calcular posición X del cursor basado en edit_cursor + const cursor_x = geom.x + padding + @as(i32, @intCast(state.edit_cursor * 7)); // ~7px por caracter (monospace) + const cursor_visible = (state.frame_count / 30) % 2 == 0; // Parpadeo cada 30 frames + + if (cursor_visible) { + ctx.pushCommand(Command.rect( + cursor_x, + geom.y + padding, + 1, // 1px de ancho + geom.h - (padding * 2), + colors.cursor, + )); + } + + // Procesar input de teclado + result = handleCellEditorInput(ctx, state); + + return result; +} + +/// Procesa input de teclado para el editor +fn handleCellEditorInput(ctx: *Context, state: *VirtualAdvancedTableState) CellEditorResult { + var result = CellEditorResult{}; + + // Procesar eventos de tecla + for (ctx.input.getKeyEvents()) |event| { + if (!event.pressed) continue; + + switch (event.key) { + .escape => { + result.escaped = true; + return result; + }, + .enter => { + result.committed = true; + result.navigate = .next_row; + return result; + }, + .tab => { + result.committed = true; + if (event.modifiers.shift) { + result.navigate = .prev_cell; + } else { + result.navigate = .next_cell; + } + return result; + }, + .up => { + result.committed = true; + result.navigate = .prev_row; + return result; + }, + .down => { + result.committed = true; + result.navigate = .next_row; + return result; + }, + .left => { + // Mover cursor a la izquierda + if (state.edit_cursor > 0) { + state.edit_cursor -= 1; + } + }, + .right => { + // Mover cursor a la derecha + if (state.edit_cursor < state.edit_buffer_len) { + state.edit_cursor += 1; + } + }, + .home => { + state.edit_cursor = 0; + }, + .end => { + state.edit_cursor = state.edit_buffer_len; + }, + .backspace => { + if (state.edit_cursor > 0 and state.edit_buffer_len > 0) { + // Eliminar caracter antes del cursor + const pos = state.edit_cursor - 1; + // Mover caracteres hacia la izquierda + std.mem.copyForwards( + u8, + state.edit_buffer[pos .. state.edit_buffer_len - 1], + state.edit_buffer[pos + 1 .. state.edit_buffer_len], + ); + state.edit_buffer_len -= 1; + state.edit_cursor -= 1; + result.text_changed = true; + } + }, + .delete => { + if (state.edit_cursor < state.edit_buffer_len) { + // Eliminar caracter en el cursor + std.mem.copyForwards( + u8, + state.edit_buffer[state.edit_cursor .. state.edit_buffer_len - 1], + state.edit_buffer[state.edit_cursor + 1 .. state.edit_buffer_len], + ); + state.edit_buffer_len -= 1; + result.text_changed = true; + } + }, + else => {}, + } + } + + // Procesar texto ingresado (caracteres imprimibles) + const text_input = ctx.input.getTextInput(); + if (text_input.len > 0) { + // Insertar caracteres en la posición del cursor + for (text_input) |c| { + if (state.edit_buffer_len < state.edit_buffer.len - 1) { + // Hacer espacio moviendo caracteres hacia la derecha + if (state.edit_cursor < state.edit_buffer_len) { + var i = state.edit_buffer_len; + while (i > state.edit_cursor) : (i -= 1) { + state.edit_buffer[i] = state.edit_buffer[i - 1]; + } + } + state.edit_buffer[state.edit_cursor] = c; + state.edit_buffer_len += 1; + state.edit_cursor += 1; + result.text_changed = true; + } + } + } + + return result; +} + +// ============================================================================= +// Tests +// ============================================================================= + +test "CellEditor types" { + const colors = CellEditorColors{}; + _ = colors; + + const result = CellEditorResult{}; + _ = result; +} diff --git a/src/widgets/virtual_advanced_table/virtual_advanced_table.zig b/src/widgets/virtual_advanced_table/virtual_advanced_table.zig index 1f18b21..4ef2d24 100644 --- a/src/widgets/virtual_advanced_table/virtual_advanced_table.zig +++ b/src/widgets/virtual_advanced_table/virtual_advanced_table.zig @@ -24,6 +24,7 @@ const text_input = @import("../text_input.zig"); pub const types = @import("types.zig"); pub const data_provider = @import("data_provider.zig"); pub const state_mod = @import("state.zig"); +pub const cell_editor = @import("cell_editor.zig"); // Tipos principales pub const RowData = types.RowData; @@ -39,6 +40,9 @@ pub const CellId = types.CellId; pub const CellGeometry = types.CellGeometry; pub const DataProvider = data_provider.DataProvider; +pub const CellEditorColors = cell_editor.CellEditorColors; +pub const CellEditorResult = cell_editor.CellEditorResult; +pub const drawCellEditor = cell_editor.drawCellEditor; pub const VirtualAdvancedTableState = state_mod.VirtualAdvancedTableState; /// Resultado de renderizar el VirtualAdvancedTable @@ -909,4 +913,5 @@ test { _ = @import("types.zig"); _ = @import("data_provider.zig"); _ = @import("state.zig"); + _ = @import("cell_editor.zig"); }