- Tab/Shift+Tab with wrap navigation in editing mode - tab_out/tab_shift result fields for application handling - Cursor position, blinking, background color fixes - markRowSaved() to clear dirty state after save - last_edited_row only set when actual changes made
240 lines
7.7 KiB
Zig
240 lines
7.7 KiB
Zig
//! 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();
|
|
const text_y = geom.y + @as(i32, @intCast((geom.h -| 16) / 2)); // Centrado vertical
|
|
ctx.pushCommand(Command.text(
|
|
geom.x + padding,
|
|
text_y,
|
|
text,
|
|
colors.text,
|
|
));
|
|
|
|
// Cursor: posición calculada con measureTextToCursor (TTF-aware)
|
|
const cursor_offset = ctx.measureTextToCursor(text, state.edit_cursor);
|
|
const cursor_x = geom.x + padding + @as(i32, @intCast(cursor_offset));
|
|
|
|
// Visibilidad del cursor usando función compartida de Context
|
|
const cursor_visible = ctx.isCursorVisible(state.last_edit_time_ms);
|
|
|
|
if (cursor_visible) {
|
|
ctx.pushCommand(Command.rect(
|
|
cursor_x,
|
|
text_y,
|
|
2, // 2px de ancho (más visible)
|
|
16, // Altura del texto
|
|
colors.cursor,
|
|
));
|
|
}
|
|
|
|
// Procesar input de teclado
|
|
result = handleCellEditorInput(ctx, state);
|
|
|
|
// Actualizar tiempo de última edición si hubo cambios
|
|
if (result.text_changed) {
|
|
state.last_edit_time_ms = ctx.current_time_ms;
|
|
}
|
|
|
|
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;
|
|
}
|