feat(virtual_advanced_table): Add CellEditor widget for inline editing

This commit is contained in:
reugenio 2025-12-26 14:48:56 +01:00
parent 66816bcbf1
commit 93836aef50
2 changed files with 237 additions and 0 deletions

View file

@ -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;
}

View file

@ -24,6 +24,7 @@ const text_input = @import("../text_input.zig");
pub const types = @import("types.zig"); pub const types = @import("types.zig");
pub const data_provider = @import("data_provider.zig"); pub const data_provider = @import("data_provider.zig");
pub const state_mod = @import("state.zig"); pub const state_mod = @import("state.zig");
pub const cell_editor = @import("cell_editor.zig");
// Tipos principales // Tipos principales
pub const RowData = types.RowData; pub const RowData = types.RowData;
@ -39,6 +40,9 @@ pub const CellId = types.CellId;
pub const CellGeometry = types.CellGeometry; pub const CellGeometry = types.CellGeometry;
pub const DataProvider = data_provider.DataProvider; 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; pub const VirtualAdvancedTableState = state_mod.VirtualAdvancedTableState;
/// Resultado de renderizar el VirtualAdvancedTable /// Resultado de renderizar el VirtualAdvancedTable
@ -909,4 +913,5 @@ test {
_ = @import("types.zig"); _ = @import("types.zig");
_ = @import("data_provider.zig"); _ = @import("data_provider.zig");
_ = @import("state.zig"); _ = @import("state.zig");
_ = @import("cell_editor.zig");
} }