Compare commits
6 commits
e8b4c98d4a
...
d16019d54f
| Author | SHA1 | Date | |
|---|---|---|---|
| d16019d54f | |||
| 0026dbff2a | |||
| 60c3f9d456 | |||
| 91969cb728 | |||
| 9b2cf2a3dd | |||
| 49cf12f7b9 |
6 changed files with 567 additions and 391 deletions
|
|
@ -241,19 +241,31 @@ pub fn toggleSort(current_column, current_direction, clicked_column) SortToggleR
|
||||||
### Edición de Celda
|
### Edición de Celda
|
||||||
|
|
||||||
```zig
|
```zig
|
||||||
pub const EditKeyboardResult = struct {
|
/// Dirección de navegación después de commit
|
||||||
committed: bool, // Enter presionado
|
pub const NavigateDirection = enum {
|
||||||
cancelled: bool, // Escape 2x
|
none, // Sin navegación
|
||||||
reverted: bool, // Escape 1x (revertir a original)
|
next_cell, // Tab → siguiente celda
|
||||||
navigate_next: bool, // Tab
|
prev_cell, // Shift+Tab → celda anterior
|
||||||
navigate_prev: bool, // Shift+Tab
|
next_row, // Enter/↓ → siguiente fila
|
||||||
text_changed: bool, // Texto modificado
|
prev_row, // ↑ → fila anterior
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Procesa teclado en modo edición
|
pub const EditKeyboardResult = struct {
|
||||||
|
committed: bool, // Enter/Tab/flechas presionados
|
||||||
|
cancelled: bool, // Escape 2x
|
||||||
|
reverted: bool, // Escape 1x (revertir a original)
|
||||||
|
navigate: NavigateDirection, // Dirección de navegación post-commit
|
||||||
|
text_changed: bool, // Texto modificado
|
||||||
|
handled: bool, // IMPORTANTE: evento fue procesado
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Procesa teclado en modo edición (DRY - un solo lugar)
|
||||||
pub fn handleEditingKeyboard(ctx, edit_buffer, edit_len, edit_cursor, escape_count, original_text) EditKeyboardResult
|
pub fn handleEditingKeyboard(ctx, edit_buffer, edit_len, edit_cursor, escape_count, original_text) EditKeyboardResult
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> **IMPORTANTE:** El campo `handled` indica si table_core procesó el evento de teclado.
|
||||||
|
> Esto es crítico para evitar doble procesamiento de Tab (ver "Lecciones Aprendidas").
|
||||||
|
|
||||||
### Renderizado
|
### Renderizado
|
||||||
|
|
||||||
```zig
|
```zig
|
||||||
|
|
@ -353,10 +365,55 @@ Tests incluidos:
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Lecciones Aprendidas
|
||||||
|
|
||||||
|
### Bug: Color de fila alternando al presionar Tab (2025-12-27)
|
||||||
|
|
||||||
|
**Síntoma:** Al navegar entre celdas con Tab, el color de la fila seleccionada alternaba
|
||||||
|
entre azul (con focus) y marrón/gris (sin focus), independientemente de si se modificaba
|
||||||
|
el contenido.
|
||||||
|
|
||||||
|
**Causa raíz:** Tab se procesaba DOS veces:
|
||||||
|
1. Por VirtualAdvancedTable/CellEditor → navegación entre celdas
|
||||||
|
2. Por main.zig de la aplicación → cambio de focus entre widgets (`ctx.handleTabKey()`)
|
||||||
|
|
||||||
|
Esto causaba que el focus del widget alternara incorrectamente cada frame.
|
||||||
|
|
||||||
|
**Solución (3 partes):**
|
||||||
|
|
||||||
|
1. **table_core.zig:** Añadir campo `handled` a `EditKeyboardResult` para indicar que el
|
||||||
|
evento fue procesado.
|
||||||
|
|
||||||
|
2. **VirtualAdvancedTable:** Verificar `navigate_direction != .none` antes de procesar
|
||||||
|
Tab como `tab_out`. Si la tabla ya procesó Tab para navegación interna, no marcar
|
||||||
|
`tab_out = true`.
|
||||||
|
|
||||||
|
3. **Aplicación (main.zig):** No llamar `ctx.handleTabKey()` cuando el panel activo
|
||||||
|
maneja Tab internamente (ej: cuando `active_tab == .configuracion`).
|
||||||
|
|
||||||
|
**Regla general:**
|
||||||
|
> Cuando un widget procesa teclado internamente en `draw()`, la aplicación debe
|
||||||
|
> respetar esa decisión y NO procesar el mismo evento a nivel global.
|
||||||
|
|
||||||
|
**Código clave:**
|
||||||
|
```zig
|
||||||
|
// VirtualAdvancedTable - handleKeyboard()
|
||||||
|
.tab => {
|
||||||
|
// IMPORTANTE: Solo si CellEditor no procesó Tab
|
||||||
|
if (result.navigate_direction == .none) {
|
||||||
|
result.tab_out = true;
|
||||||
|
result.tab_shift = event.modifiers.shift;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Historial de Cambios
|
## Historial de Cambios
|
||||||
|
|
||||||
| Fecha | Cambio |
|
| Fecha | Cambio |
|
||||||
|-------|--------|
|
|-------|--------|
|
||||||
|
| 2025-12-27 | Fix bug colores alternando + campo `handled` en EditKeyboardResult |
|
||||||
| 2025-12-27 | Refactorización DRY: lógica común movida a table_core.zig |
|
| 2025-12-27 | Refactorización DRY: lógica común movida a table_core.zig |
|
||||||
| 2025-12-26 | table_core.zig creado con funciones de renderizado compartidas |
|
| 2025-12-26 | table_core.zig creado con funciones de renderizado compartidas |
|
||||||
| 2025-12-17 | VirtualAdvancedTable añadido para tablas grandes |
|
| 2025-12-17 | VirtualAdvancedTable añadido para tablas grandes |
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ const Context = @import("../../core/context.zig").Context;
|
||||||
const Command = @import("../../core/command.zig");
|
const Command = @import("../../core/command.zig");
|
||||||
const Layout = @import("../../core/layout.zig");
|
const Layout = @import("../../core/layout.zig");
|
||||||
const Style = @import("../../core/style.zig");
|
const Style = @import("../../core/style.zig");
|
||||||
|
const table_core = @import("../table_core.zig");
|
||||||
|
|
||||||
// Re-export types
|
// Re-export types
|
||||||
pub const types = @import("types.zig");
|
pub const types = @import("types.zig");
|
||||||
|
|
@ -46,6 +47,9 @@ pub const state = @import("state.zig");
|
||||||
pub const AdvancedTableState = state.AdvancedTableState;
|
pub const AdvancedTableState = state.AdvancedTableState;
|
||||||
pub const AdvancedTableResult = state.AdvancedTableResult;
|
pub const AdvancedTableResult = state.AdvancedTableResult;
|
||||||
|
|
||||||
|
// Re-export table_core types
|
||||||
|
pub const NavigateDirection = table_core.NavigateDirection;
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Public API
|
// Public API
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
@ -919,155 +923,94 @@ fn handleEditingKeyboard(
|
||||||
) void {
|
) void {
|
||||||
const config = table_schema.config;
|
const config = table_schema.config;
|
||||||
|
|
||||||
// Escape: cancel editing (1st = revert, 2nd = exit without save)
|
// Obtener texto original para revert
|
||||||
if (ctx.input.keyPressed(.escape)) {
|
var orig_format_buf: [128]u8 = undefined;
|
||||||
table_state.escape_count += 1;
|
const original_text: ?[]const u8 = if (table_state.original_value) |orig|
|
||||||
if (table_state.escape_count >= 2 or table_state.original_value == null) {
|
orig.format(&orig_format_buf)
|
||||||
// Exit without saving
|
else
|
||||||
table_state.stopEditing();
|
null;
|
||||||
result.edit_ended = true;
|
|
||||||
} else {
|
|
||||||
// Revert to original value
|
|
||||||
if (table_state.original_value) |orig| {
|
|
||||||
var format_buf: [128]u8 = undefined;
|
|
||||||
const text = orig.format(&format_buf);
|
|
||||||
table_state.startEditing(text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset escape count on any other key
|
// Usar table_core para procesamiento de teclado (DRY)
|
||||||
table_state.escape_count = 0;
|
const kb_result = table_core.handleEditingKeyboard(
|
||||||
|
ctx,
|
||||||
|
&table_state.edit_buffer,
|
||||||
|
&table_state.edit_len,
|
||||||
|
&table_state.edit_cursor,
|
||||||
|
&table_state.escape_count,
|
||||||
|
original_text,
|
||||||
|
);
|
||||||
|
|
||||||
// Enter: confirm editing
|
// Si no se procesó ningún evento, salir
|
||||||
if (ctx.input.keyPressed(.enter)) {
|
if (!kb_result.handled) return;
|
||||||
commitEdit(table_state, table_schema, result);
|
|
||||||
|
// Escape canceló la edición
|
||||||
|
if (kb_result.cancelled) {
|
||||||
table_state.stopEditing();
|
table_state.stopEditing();
|
||||||
result.edit_ended = true;
|
result.edit_ended = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tab: confirm and move to next cell
|
// Commit (Enter, Tab, flechas) y navegación
|
||||||
if (ctx.input.keyPressed(.tab) and config.handle_tab) {
|
if (kb_result.committed) {
|
||||||
commitEdit(table_state, table_schema, result);
|
commitEdit(table_state, table_schema, result);
|
||||||
table_state.stopEditing();
|
table_state.stopEditing();
|
||||||
result.edit_ended = true;
|
result.edit_ended = true;
|
||||||
|
|
||||||
// Move to next/prev cell
|
// Procesar navegación después de commit
|
||||||
const shift = ctx.input.modifiers.shift;
|
switch (kb_result.navigate) {
|
||||||
const col_count = table_schema.columns.len;
|
.next_cell, .prev_cell => {
|
||||||
const row_count = table_state.getRowCount();
|
if (!config.handle_tab) return;
|
||||||
|
|
||||||
if (shift) {
|
const col_count = table_schema.columns.len;
|
||||||
// Shift+Tab: move left
|
const row_count = table_state.getRowCount();
|
||||||
if (table_state.selected_col > 0) {
|
|
||||||
table_state.selectCell(
|
|
||||||
@intCast(@max(0, table_state.selected_row)),
|
|
||||||
@intCast(table_state.selected_col - 1),
|
|
||||||
);
|
|
||||||
} else if (table_state.selected_row > 0 and config.wrap_navigation) {
|
|
||||||
table_state.selectCell(
|
|
||||||
@intCast(table_state.selected_row - 1),
|
|
||||||
col_count - 1,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Tab: move right
|
|
||||||
if (table_state.selected_col < @as(i32, @intCast(col_count)) - 1) {
|
|
||||||
table_state.selectCell(
|
|
||||||
@intCast(@max(0, table_state.selected_row)),
|
|
||||||
@intCast(table_state.selected_col + 1),
|
|
||||||
);
|
|
||||||
} else if (table_state.selected_row < @as(i32, @intCast(row_count)) - 1 and config.wrap_navigation) {
|
|
||||||
table_state.selectCell(
|
|
||||||
@intCast(table_state.selected_row + 1),
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto-start editing in new cell if editable
|
if (kb_result.navigate == .prev_cell) {
|
||||||
const new_col: usize = @intCast(@max(0, table_state.selected_col));
|
// Shift+Tab: move left
|
||||||
if (new_col < table_schema.columns.len and table_schema.columns[new_col].editable) {
|
if (table_state.selected_col > 0) {
|
||||||
if (table_state.getRow(@intCast(@max(0, table_state.selected_row)))) |row| {
|
table_state.selectCell(
|
||||||
const value = row.get(table_schema.columns[new_col].name);
|
@intCast(@max(0, table_state.selected_row)),
|
||||||
var format_buf: [128]u8 = undefined;
|
@intCast(table_state.selected_col - 1),
|
||||||
const text = value.format(&format_buf);
|
);
|
||||||
table_state.startEditing(text);
|
} else if (table_state.selected_row > 0 and config.wrap_navigation) {
|
||||||
result.edit_started = true;
|
table_state.selectCell(
|
||||||
}
|
@intCast(table_state.selected_row - 1),
|
||||||
}
|
col_count - 1,
|
||||||
|
);
|
||||||
result.selection_changed = true;
|
}
|
||||||
return;
|
} else {
|
||||||
}
|
// Tab: move right
|
||||||
|
if (table_state.selected_col < @as(i32, @intCast(col_count)) - 1) {
|
||||||
// Cursor movement within edit buffer
|
table_state.selectCell(
|
||||||
if (ctx.input.keyPressed(.left)) {
|
@intCast(@max(0, table_state.selected_row)),
|
||||||
if (table_state.edit_cursor > 0) {
|
@intCast(table_state.selected_col + 1),
|
||||||
table_state.edit_cursor -= 1;
|
);
|
||||||
}
|
} else if (table_state.selected_row < @as(i32, @intCast(row_count)) - 1 and config.wrap_navigation) {
|
||||||
return;
|
table_state.selectCell(
|
||||||
}
|
@intCast(table_state.selected_row + 1),
|
||||||
if (ctx.input.keyPressed(.right)) {
|
0,
|
||||||
if (table_state.edit_cursor < table_state.edit_len) {
|
);
|
||||||
table_state.edit_cursor += 1;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (ctx.input.keyPressed(.home)) {
|
|
||||||
table_state.edit_cursor = 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (ctx.input.keyPressed(.end)) {
|
|
||||||
table_state.edit_cursor = table_state.edit_len;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Backspace: delete char before cursor
|
|
||||||
if (ctx.input.keyPressed(.backspace)) {
|
|
||||||
if (table_state.edit_cursor > 0) {
|
|
||||||
// Shift characters left
|
|
||||||
var i: usize = table_state.edit_cursor - 1;
|
|
||||||
while (i < table_state.edit_len - 1) : (i += 1) {
|
|
||||||
table_state.edit_buffer[i] = table_state.edit_buffer[i + 1];
|
|
||||||
}
|
|
||||||
table_state.edit_len -= 1;
|
|
||||||
table_state.edit_cursor -= 1;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete: delete char at cursor
|
|
||||||
if (ctx.input.keyPressed(.delete)) {
|
|
||||||
if (table_state.edit_cursor < table_state.edit_len) {
|
|
||||||
// Shift characters left
|
|
||||||
var i: usize = table_state.edit_cursor;
|
|
||||||
while (i < table_state.edit_len - 1) : (i += 1) {
|
|
||||||
table_state.edit_buffer[i] = table_state.edit_buffer[i + 1];
|
|
||||||
}
|
|
||||||
table_state.edit_len -= 1;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 (table_state.edit_len < types.MAX_EDIT_BUFFER - 1) {
|
|
||||||
// Shift characters right
|
|
||||||
var i: usize = table_state.edit_len;
|
|
||||||
while (i > table_state.edit_cursor) : (i -= 1) {
|
|
||||||
table_state.edit_buffer[i] = table_state.edit_buffer[i - 1];
|
|
||||||
}
|
}
|
||||||
table_state.edit_buffer[table_state.edit_cursor] = ch;
|
|
||||||
table_state.edit_len += 1;
|
|
||||||
table_state.edit_cursor += 1;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// Auto-start editing in new cell if editable
|
||||||
|
const new_col: usize = @intCast(@max(0, table_state.selected_col));
|
||||||
|
if (new_col < table_schema.columns.len and table_schema.columns[new_col].editable) {
|
||||||
|
if (table_state.getRow(@intCast(@max(0, table_state.selected_row)))) |row| {
|
||||||
|
const value = row.get(table_schema.columns[new_col].name);
|
||||||
|
var format_buf: [128]u8 = undefined;
|
||||||
|
const text = value.format(&format_buf);
|
||||||
|
table_state.startEditing(text);
|
||||||
|
result.edit_started = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.selection_changed = true;
|
||||||
|
},
|
||||||
|
.next_row, .prev_row => {
|
||||||
|
// Enter o flechas arriba/abajo: solo commit, sin navegación adicional aquí
|
||||||
|
// (La navegación entre filas se maneja en otro lugar si es necesario)
|
||||||
|
},
|
||||||
|
.none => {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -210,24 +210,34 @@ pub fn detectDoubleClick(
|
||||||
// Manejo de teclado para edición
|
// Manejo de teclado para edición
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
|
/// Dirección de navegación después de edición
|
||||||
|
pub const NavigateDirection = enum {
|
||||||
|
none,
|
||||||
|
next_cell, // Tab
|
||||||
|
prev_cell, // Shift+Tab
|
||||||
|
next_row, // Enter o ↓
|
||||||
|
prev_row, // ↑
|
||||||
|
};
|
||||||
|
|
||||||
/// Resultado de procesar teclado en modo edición
|
/// Resultado de procesar teclado en modo edición
|
||||||
pub const EditKeyboardResult = struct {
|
pub const EditKeyboardResult = struct {
|
||||||
/// Se confirmó la edición (Enter)
|
/// Se confirmó la edición (Enter, Tab, flechas)
|
||||||
committed: bool = false,
|
committed: bool = false,
|
||||||
/// Se canceló la edición (Escape)
|
/// Se canceló la edición (Escape 2x)
|
||||||
cancelled: bool = false,
|
cancelled: bool = false,
|
||||||
/// Se revirtió al valor original (primer Escape)
|
/// Se revirtió al valor original (Escape 1x)
|
||||||
reverted: bool = false,
|
reverted: bool = false,
|
||||||
/// Se debe navegar a siguiente celda (Tab)
|
/// Dirección de navegación después de commit
|
||||||
navigate_next: bool = false,
|
navigate: NavigateDirection = .none,
|
||||||
/// Se debe navegar a celda anterior (Shift+Tab)
|
|
||||||
navigate_prev: bool = false,
|
|
||||||
/// El buffer de edición cambió
|
/// El buffer de edición cambió
|
||||||
text_changed: bool = false,
|
text_changed: bool = false,
|
||||||
|
/// Indica que se procesó un evento de teclado (para evitar doble procesamiento)
|
||||||
|
handled: bool = false,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Procesa teclado en modo edición
|
/// Procesa teclado en modo edición
|
||||||
/// Modifica edit_buffer, edit_len, edit_cursor según las teclas
|
/// Modifica edit_buffer, edit_len, edit_cursor según las teclas
|
||||||
|
/// Retorna resultado con flags de navegación y si se procesó algún evento
|
||||||
pub fn handleEditingKeyboard(
|
pub fn handleEditingKeyboard(
|
||||||
ctx: *Context,
|
ctx: *Context,
|
||||||
edit_buffer: []u8,
|
edit_buffer: []u8,
|
||||||
|
|
@ -238,113 +248,310 @@ pub fn handleEditingKeyboard(
|
||||||
) EditKeyboardResult {
|
) EditKeyboardResult {
|
||||||
var result = EditKeyboardResult{};
|
var result = EditKeyboardResult{};
|
||||||
|
|
||||||
// Escape: cancelar o revertir
|
// Procesar eventos de tecla
|
||||||
if (ctx.input.keyPressed(.escape)) {
|
for (ctx.input.getKeyEvents()) |event| {
|
||||||
escape_count.* += 1;
|
if (!event.pressed) continue;
|
||||||
if (escape_count.* >= 2 or original_text == null) {
|
|
||||||
result.cancelled = true;
|
switch (event.key) {
|
||||||
} else {
|
.escape => {
|
||||||
// Revertir al valor original
|
escape_count.* += 1;
|
||||||
if (original_text) |orig| {
|
if (escape_count.* >= 2 or original_text == null) {
|
||||||
const len = @min(orig.len, edit_buffer.len);
|
result.cancelled = true;
|
||||||
@memcpy(edit_buffer[0..len], orig[0..len]);
|
} else {
|
||||||
edit_len.* = len;
|
// Revertir al valor original
|
||||||
edit_cursor.* = len;
|
if (original_text) |orig| {
|
||||||
result.reverted = true;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.handled = true;
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
.enter => {
|
||||||
|
result.committed = true;
|
||||||
|
result.navigate = .next_row;
|
||||||
|
result.handled = true;
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
.tab => {
|
||||||
|
result.committed = true;
|
||||||
|
result.navigate = if (event.modifiers.shift) .prev_cell else .next_cell;
|
||||||
|
result.handled = true;
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
.up => {
|
||||||
|
result.committed = true;
|
||||||
|
result.navigate = .prev_row;
|
||||||
|
result.handled = true;
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
.down => {
|
||||||
|
result.committed = true;
|
||||||
|
result.navigate = .next_row;
|
||||||
|
result.handled = true;
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
.left => {
|
||||||
|
if (edit_cursor.* > 0) edit_cursor.* -= 1;
|
||||||
|
result.handled = true;
|
||||||
|
// Reset escape count
|
||||||
|
escape_count.* = 0;
|
||||||
|
},
|
||||||
|
.right => {
|
||||||
|
if (edit_cursor.* < edit_len.*) edit_cursor.* += 1;
|
||||||
|
result.handled = true;
|
||||||
|
escape_count.* = 0;
|
||||||
|
},
|
||||||
|
.home => {
|
||||||
|
edit_cursor.* = 0;
|
||||||
|
result.handled = true;
|
||||||
|
escape_count.* = 0;
|
||||||
|
},
|
||||||
|
.end => {
|
||||||
|
edit_cursor.* = edit_len.*;
|
||||||
|
result.handled = true;
|
||||||
|
escape_count.* = 0;
|
||||||
|
},
|
||||||
|
.backspace => {
|
||||||
|
if (edit_cursor.* > 0 and edit_len.* > 0) {
|
||||||
|
// Shift characters left
|
||||||
|
const pos = edit_cursor.* - 1;
|
||||||
|
var i: usize = pos;
|
||||||
|
while (i < edit_len.* - 1) : (i += 1) {
|
||||||
|
edit_buffer[i] = edit_buffer[i + 1];
|
||||||
|
}
|
||||||
|
edit_len.* -= 1;
|
||||||
|
edit_cursor.* -= 1;
|
||||||
|
result.text_changed = true;
|
||||||
|
}
|
||||||
|
result.handled = true;
|
||||||
|
escape_count.* = 0;
|
||||||
|
},
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
result.handled = true;
|
||||||
|
escape_count.* = 0;
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
}
|
}
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset escape count en cualquier otra tecla
|
// Procesar texto ingresado (caracteres imprimibles)
|
||||||
escape_count.* = 0;
|
const text_input = ctx.input.getTextInput();
|
||||||
|
if (text_input.len > 0) {
|
||||||
// Enter: confirmar
|
for (text_input) |ch| {
|
||||||
if (ctx.input.keyPressed(.enter)) {
|
if (edit_len.* < edit_buffer.len - 1) {
|
||||||
result.committed = true;
|
// Hacer espacio moviendo caracteres hacia la derecha
|
||||||
return result;
|
if (edit_cursor.* < edit_len.*) {
|
||||||
}
|
var i = edit_len.*;
|
||||||
|
|
||||||
// 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) {
|
while (i > edit_cursor.*) : (i -= 1) {
|
||||||
edit_buffer[i] = edit_buffer[i - 1];
|
edit_buffer[i] = edit_buffer[i - 1];
|
||||||
}
|
}
|
||||||
edit_buffer[edit_cursor.*] = ch;
|
|
||||||
edit_len.* += 1;
|
|
||||||
edit_cursor.* += 1;
|
|
||||||
result.text_changed = true;
|
|
||||||
}
|
}
|
||||||
|
edit_buffer[edit_cursor.*] = ch;
|
||||||
|
edit_len.* += 1;
|
||||||
|
edit_cursor.* += 1;
|
||||||
|
result.text_changed = true;
|
||||||
|
result.handled = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
escape_count.* = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Edición de fila completa (commit al abandonar fila, estilo Excel)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/// Máximo de columnas soportadas para cambios pendientes
|
||||||
|
pub const MAX_PENDING_COLUMNS: usize = 32;
|
||||||
|
|
||||||
|
/// Máximo tamaño de valor por celda
|
||||||
|
pub const MAX_CELL_VALUE_LEN: usize = 256;
|
||||||
|
|
||||||
|
/// ID especial para filas nuevas (ghost row)
|
||||||
|
pub const NEW_ROW_ID: i64 = -1;
|
||||||
|
|
||||||
|
/// Cambio pendiente en una columna
|
||||||
|
pub const PendingCellChange = struct {
|
||||||
|
/// Índice de columna
|
||||||
|
col: usize,
|
||||||
|
/// Valor nuevo (slice al buffer interno)
|
||||||
|
value: []const u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Buffer para acumular cambios de una fila antes de commit
|
||||||
|
/// Usado por los states de los widgets, procesado por funciones de table_core
|
||||||
|
pub const RowEditBuffer = struct {
|
||||||
|
/// ID de la fila siendo editada (NEW_ROW_ID si es ghost row)
|
||||||
|
row_id: i64 = NEW_ROW_ID,
|
||||||
|
|
||||||
|
/// Índice de fila (para navegación)
|
||||||
|
row_index: usize = 0,
|
||||||
|
|
||||||
|
/// Es una fila nueva (ghost row que el usuario está rellenando)
|
||||||
|
is_new_row: bool = false,
|
||||||
|
|
||||||
|
/// Hay cambios pendientes
|
||||||
|
has_changes: bool = false,
|
||||||
|
|
||||||
|
/// Buffers de valores por columna (almacenamiento fijo)
|
||||||
|
value_buffers: [MAX_PENDING_COLUMNS][MAX_CELL_VALUE_LEN]u8 = undefined,
|
||||||
|
|
||||||
|
/// Longitudes de cada valor
|
||||||
|
value_lens: [MAX_PENDING_COLUMNS]usize = [_]usize{0} ** MAX_PENDING_COLUMNS,
|
||||||
|
|
||||||
|
/// Flags: qué columnas tienen cambios
|
||||||
|
changed_cols: [MAX_PENDING_COLUMNS]bool = [_]bool{false} ** MAX_PENDING_COLUMNS,
|
||||||
|
|
||||||
|
/// Número de columnas con cambios
|
||||||
|
change_count: usize = 0,
|
||||||
|
|
||||||
|
/// Inicializa/resetea el buffer para una nueva fila
|
||||||
|
pub fn startEdit(self: *RowEditBuffer, row_id: i64, row_index: usize, is_new: bool) void {
|
||||||
|
self.row_id = row_id;
|
||||||
|
self.row_index = row_index;
|
||||||
|
self.is_new_row = is_new;
|
||||||
|
self.has_changes = false;
|
||||||
|
self.change_count = 0;
|
||||||
|
for (0..MAX_PENDING_COLUMNS) |i| {
|
||||||
|
self.changed_cols[i] = false;
|
||||||
|
self.value_lens[i] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Añade un cambio pendiente para una columna
|
||||||
|
pub fn addChange(self: *RowEditBuffer, col: usize, value: []const u8) void {
|
||||||
|
if (col >= MAX_PENDING_COLUMNS) return;
|
||||||
|
|
||||||
|
// Copiar valor al buffer interno
|
||||||
|
const len = @min(value.len, MAX_CELL_VALUE_LEN);
|
||||||
|
@memcpy(self.value_buffers[col][0..len], value[0..len]);
|
||||||
|
self.value_lens[col] = len;
|
||||||
|
|
||||||
|
// Marcar como cambiado
|
||||||
|
if (!self.changed_cols[col]) {
|
||||||
|
self.changed_cols[col] = true;
|
||||||
|
self.change_count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.has_changes = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Obtiene el valor pendiente de una columna (si hay cambio)
|
||||||
|
pub fn getPendingValue(self: *const RowEditBuffer, col: usize) ?[]const u8 {
|
||||||
|
if (col >= MAX_PENDING_COLUMNS) return null;
|
||||||
|
if (!self.changed_cols[col]) return null;
|
||||||
|
return self.value_buffers[col][0..self.value_lens[col]];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Limpia el buffer (después de commit o discard)
|
||||||
|
pub fn clear(self: *RowEditBuffer) void {
|
||||||
|
self.row_id = NEW_ROW_ID;
|
||||||
|
self.row_index = 0;
|
||||||
|
self.is_new_row = false;
|
||||||
|
self.has_changes = false;
|
||||||
|
self.change_count = 0;
|
||||||
|
for (0..MAX_PENDING_COLUMNS) |i| {
|
||||||
|
self.changed_cols[i] = false;
|
||||||
|
self.value_lens[i] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Información para hacer commit de los cambios de una fila
|
||||||
|
/// Retornada cuando el usuario abandona una fila editada
|
||||||
|
pub const RowCommitInfo = struct {
|
||||||
|
/// ID de la fila (NEW_ROW_ID si es INSERT)
|
||||||
|
row_id: i64,
|
||||||
|
|
||||||
|
/// Es INSERT (nueva fila) o UPDATE (fila existente)
|
||||||
|
is_insert: bool,
|
||||||
|
|
||||||
|
/// Lista de cambios (columna, valor)
|
||||||
|
changes: []const PendingCellChange,
|
||||||
|
|
||||||
|
/// Número de cambios
|
||||||
|
change_count: usize,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Construye la info de commit desde un RowEditBuffer
|
||||||
|
/// El caller debe proveer el array para almacenar los cambios
|
||||||
|
pub fn buildCommitInfo(
|
||||||
|
buffer: *const RowEditBuffer,
|
||||||
|
changes_out: []PendingCellChange,
|
||||||
|
) ?RowCommitInfo {
|
||||||
|
if (!buffer.has_changes) return null;
|
||||||
|
|
||||||
|
var count: usize = 0;
|
||||||
|
for (0..MAX_PENDING_COLUMNS) |col| {
|
||||||
|
if (buffer.changed_cols[col] and count < changes_out.len) {
|
||||||
|
changes_out[count] = .{
|
||||||
|
.col = col,
|
||||||
|
.value = buffer.value_buffers[col][0..buffer.value_lens[col]],
|
||||||
|
};
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return RowCommitInfo{
|
||||||
|
.row_id = buffer.row_id,
|
||||||
|
.is_insert = buffer.is_new_row,
|
||||||
|
.changes = changes_out[0..count],
|
||||||
|
.change_count = count,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verifica si hay que hacer commit antes de editar nueva celda.
|
||||||
|
/// Si la fila cambió y hay cambios pendientes, retorna commit info.
|
||||||
|
/// Siempre inicializa el buffer para la nueva fila.
|
||||||
|
///
|
||||||
|
/// Uso típico en widget:
|
||||||
|
/// ```
|
||||||
|
/// if (table_core.checkRowChangeAndCommit(&state.row_edit_buffer, new_id, new_idx, is_ghost, &changes)) |info| {
|
||||||
|
/// result.row_committed = true;
|
||||||
|
/// result.commit_info = info;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub fn checkRowChangeAndCommit(
|
||||||
|
buffer: *RowEditBuffer,
|
||||||
|
new_row_id: i64,
|
||||||
|
new_row_index: usize,
|
||||||
|
is_new_row: bool,
|
||||||
|
changes_out: []PendingCellChange,
|
||||||
|
) ?RowCommitInfo {
|
||||||
|
// Si es la misma fila, no hacer nada
|
||||||
|
if (buffer.row_id == new_row_id) return null;
|
||||||
|
|
||||||
|
// Si hay cambios pendientes en la fila anterior, construir commit
|
||||||
|
var commit_info: ?RowCommitInfo = null;
|
||||||
|
if (buffer.has_changes) {
|
||||||
|
commit_info = buildCommitInfo(buffer, changes_out);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iniciar edición de la nueva fila
|
||||||
|
buffer.startEdit(new_row_id, new_row_index, is_new_row);
|
||||||
|
|
||||||
|
return commit_info;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verifica si un row_id corresponde a la ghost row (fila nueva)
|
||||||
|
pub fn isGhostRow(row_id: i64) bool {
|
||||||
|
return row_id == NEW_ROW_ID;
|
||||||
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Utilidades
|
// Utilidades
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,22 @@
|
||||||
//! CellEditor - Editor de celda overlay para edición inline
|
//! CellEditor - Editor de celda overlay para edición inline
|
||||||
//!
|
//!
|
||||||
//! Dibuja un campo de texto sobre una celda para edición estilo Excel.
|
//! Dibuja un campo de texto sobre una celda para edición estilo Excel.
|
||||||
//! Utiliza el edit_buffer del VirtualAdvancedTableState.
|
//! Utiliza table_core.handleEditingKeyboard() para procesar teclado.
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const Context = @import("../../core/context.zig").Context;
|
const Context = @import("../../core/context.zig").Context;
|
||||||
const Command = @import("../../core/command.zig");
|
const Command = @import("../../core/command.zig");
|
||||||
const Style = @import("../../core/style.zig");
|
const Style = @import("../../core/style.zig");
|
||||||
const Input = @import("../../core/input.zig");
|
|
||||||
const types = @import("types.zig");
|
const types = @import("types.zig");
|
||||||
const state_mod = @import("state.zig");
|
const state_mod = @import("state.zig");
|
||||||
|
const table_core = @import("../table_core.zig");
|
||||||
|
|
||||||
const CellGeometry = types.CellGeometry;
|
const CellGeometry = types.CellGeometry;
|
||||||
const VirtualAdvancedTableState = state_mod.VirtualAdvancedTableState;
|
const VirtualAdvancedTableState = state_mod.VirtualAdvancedTableState;
|
||||||
|
|
||||||
|
// Re-exportar NavigateDirection desde table_core para compatibilidad
|
||||||
|
pub const NavigateDirection = table_core.NavigateDirection;
|
||||||
|
|
||||||
/// Colores del editor de celda
|
/// Colores del editor de celda
|
||||||
pub const CellEditorColors = struct {
|
pub const CellEditorColors = struct {
|
||||||
background: Style.Color = Style.Color.rgb(255, 255, 255),
|
background: Style.Color = Style.Color.rgb(255, 255, 255),
|
||||||
|
|
@ -24,8 +27,9 @@ pub const CellEditorColors = struct {
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Resultado del procesamiento del CellEditor
|
/// Resultado del procesamiento del CellEditor
|
||||||
|
/// Usa table_core.NavigateDirection para navegación
|
||||||
pub const CellEditorResult = struct {
|
pub const CellEditorResult = struct {
|
||||||
/// El usuario presionó Enter o Tab (commit)
|
/// El usuario presionó Enter, Tab, o flechas (commit)
|
||||||
committed: bool = false,
|
committed: bool = false,
|
||||||
|
|
||||||
/// El usuario presionó Escape
|
/// El usuario presionó Escape
|
||||||
|
|
@ -37,13 +41,8 @@ pub const CellEditorResult = struct {
|
||||||
/// Navegación solicitada después de commit
|
/// Navegación solicitada después de commit
|
||||||
navigate: NavigateDirection = .none,
|
navigate: NavigateDirection = .none,
|
||||||
|
|
||||||
pub const NavigateDirection = enum {
|
/// Se procesó algún evento de teclado
|
||||||
none,
|
handled: bool = false,
|
||||||
next_cell, // Tab
|
|
||||||
prev_cell, // Shift+Tab
|
|
||||||
next_row, // Enter o ↓
|
|
||||||
prev_row, // ↑
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Dibuja el editor de celda overlay
|
/// Dibuja el editor de celda overlay
|
||||||
|
|
@ -106,8 +105,28 @@ pub fn drawCellEditor(
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Procesar input de teclado
|
// Procesar input de teclado usando table_core
|
||||||
result = handleCellEditorInput(ctx, state);
|
const original_text = state.getOriginalValue();
|
||||||
|
const kb_result = table_core.handleEditingKeyboard(
|
||||||
|
ctx,
|
||||||
|
&state.edit_buffer,
|
||||||
|
&state.edit_buffer_len,
|
||||||
|
&state.edit_cursor,
|
||||||
|
&state.escape_count,
|
||||||
|
if (original_text.len > 0) original_text else null,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Mapear resultado de table_core a CellEditorResult
|
||||||
|
result.committed = kb_result.committed;
|
||||||
|
result.escaped = kb_result.cancelled;
|
||||||
|
result.text_changed = kb_result.text_changed;
|
||||||
|
result.navigate = kb_result.navigate;
|
||||||
|
result.handled = kb_result.handled;
|
||||||
|
|
||||||
|
// Escape con revert no es "escaped" sino solo revertir texto
|
||||||
|
if (kb_result.reverted) {
|
||||||
|
result.escaped = false; // No cerrar editor, solo revertir
|
||||||
|
}
|
||||||
|
|
||||||
// Actualizar tiempo de última edición si hubo cambios
|
// Actualizar tiempo de última edición si hubo cambios
|
||||||
if (result.text_changed) {
|
if (result.text_changed) {
|
||||||
|
|
@ -117,116 +136,6 @@ pub fn drawCellEditor(
|
||||||
return result;
|
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
|
// Tests
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
|
||||||
|
|
@ -177,6 +177,13 @@ pub const VirtualAdvancedTableState = struct {
|
||||||
/// Flag: celda requiere commit al terminar edición
|
/// Flag: celda requiere commit al terminar edición
|
||||||
cell_value_changed: bool = false,
|
cell_value_changed: bool = false,
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Buffer de cambios de fila (commit al abandonar fila, estilo Excel)
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/// Buffer para acumular cambios de la fila actual antes de commit
|
||||||
|
row_edit_buffer: table_core.RowEditBuffer = .{},
|
||||||
|
|
||||||
const Self = @This();
|
const Self = @This();
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
@ -713,6 +720,7 @@ pub const VirtualAdvancedTableState = struct {
|
||||||
self.edit_buffer_len = 0;
|
self.edit_buffer_len = 0;
|
||||||
self.edit_cursor = 0;
|
self.edit_cursor = 0;
|
||||||
self.cell_value_changed = false;
|
self.cell_value_changed = false;
|
||||||
|
self.row_edit_buffer.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ 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 CellEditorColors = cell_editor.CellEditorColors;
|
||||||
pub const CellEditorResult = cell_editor.CellEditorResult;
|
pub const CellEditorResult = cell_editor.CellEditorResult;
|
||||||
|
pub const NavigateDirection = cell_editor.NavigateDirection;
|
||||||
pub const drawCellEditor = cell_editor.drawCellEditor;
|
pub const drawCellEditor = cell_editor.drawCellEditor;
|
||||||
pub const VirtualAdvancedTableState = state_mod.VirtualAdvancedTableState;
|
pub const VirtualAdvancedTableState = state_mod.VirtualAdvancedTableState;
|
||||||
|
|
||||||
|
|
@ -84,35 +85,57 @@ pub const VirtualAdvancedTableResult = struct {
|
||||||
clicked: bool = false,
|
clicked: bool = false,
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// Edición CRUD Excel-style
|
// Edición CRUD Excel-style (commit al abandonar fila)
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
||||||
/// Una celda fue modificada (el usuario hizo commit con Enter/Tab)
|
/// Una fila fue completada (el usuario cambió de fila, tenía cambios pendientes)
|
||||||
cell_committed: bool = false,
|
/// Cuando es true, row_commit_id y row_changes contienen los datos
|
||||||
|
row_committed: bool = false,
|
||||||
|
|
||||||
/// La fila cambió de edición (auto-save requerido)
|
/// ID de la fila que se hizo commit (NEW_ROW_ID = -1 para inserts)
|
||||||
row_changed: bool = false,
|
row_commit_id: i64 = table_core.NEW_ROW_ID,
|
||||||
|
|
||||||
|
/// Es un INSERT (ghost row) o UPDATE (fila existente)
|
||||||
|
row_commit_is_insert: bool = false,
|
||||||
|
|
||||||
|
/// Cambios de la fila (válidos si row_committed = true)
|
||||||
|
/// Buffer estático que sobrevive el frame
|
||||||
|
row_changes: [table_core.MAX_PENDING_COLUMNS]table_core.PendingCellChange = undefined,
|
||||||
|
|
||||||
|
/// Número de cambios en row_changes
|
||||||
|
row_changes_count: usize = 0,
|
||||||
|
|
||||||
/// El usuario canceló edición (Escape 2x = descartar fila)
|
/// El usuario canceló edición (Escape 2x = descartar fila)
|
||||||
row_discarded: bool = false,
|
row_discarded: bool = false,
|
||||||
|
|
||||||
/// Celda que fue editada (si cell_committed = true)
|
/// Navegación solicitada después de edición
|
||||||
edited_cell: ?CellId = null,
|
navigate_direction: cell_editor.NavigateDirection = .none,
|
||||||
|
|
||||||
/// Nuevo valor de la celda (si cell_committed = true)
|
|
||||||
edited_value: ?[]const u8 = null,
|
|
||||||
|
|
||||||
/// Fila anterior (si row_changed = true, para auto-save)
|
|
||||||
previous_row: ?usize = null,
|
|
||||||
|
|
||||||
/// Navegación solicitada después de commit
|
|
||||||
navigate_direction: cell_editor.CellEditorResult.NavigateDirection = .none,
|
|
||||||
|
|
||||||
/// Tab presionado sin edición activa (pasar focus al siguiente widget)
|
/// Tab presionado sin edición activa (pasar focus al siguiente widget)
|
||||||
tab_out: bool = false,
|
tab_out: bool = false,
|
||||||
|
|
||||||
/// Shift estaba presionado con Tab (para tab_out inverso)
|
/// Shift estaba presionado con Tab (para tab_out inverso)
|
||||||
tab_shift: bool = false,
|
tab_shift: bool = false,
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Compatibilidad (DEPRECADO - usar row_committed)
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/// @deprecated: Usar row_committed. Mantenido para compatibilidad.
|
||||||
|
cell_committed: bool = false,
|
||||||
|
/// @deprecated
|
||||||
|
row_changed: bool = false,
|
||||||
|
/// @deprecated
|
||||||
|
edited_cell: ?CellId = null,
|
||||||
|
/// @deprecated
|
||||||
|
edited_value: ?[]const u8 = null,
|
||||||
|
/// @deprecated
|
||||||
|
previous_row: ?usize = null,
|
||||||
|
|
||||||
|
/// Obtiene los cambios como slice (helper para compatibilidad)
|
||||||
|
pub fn getRowChanges(self: *const VirtualAdvancedTableResult) []const table_core.PendingCellChange {
|
||||||
|
return self.row_changes[0..self.row_changes_count];
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
@ -286,26 +309,25 @@ pub fn virtualAdvancedTableRect(
|
||||||
const edited_cell = list_state.editing_cell.?;
|
const edited_cell = list_state.editing_cell.?;
|
||||||
const new_value = list_state.getEditText();
|
const new_value = list_state.getEditText();
|
||||||
|
|
||||||
// Check if row changed (for auto-save)
|
// Añadir cambio al buffer de fila (NO commit inmediato)
|
||||||
if (list_state.last_edited_row) |last_row| {
|
// El commit real se hace cuando el usuario abandona la fila
|
||||||
if (edited_cell.row != last_row and list_state.row_dirty) {
|
if (list_state.hasValueChanged()) {
|
||||||
result.row_changed = true;
|
list_state.row_edit_buffer.addChange(edited_cell.col, new_value);
|
||||||
result.previous_row = last_row;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Commit the edit
|
// Compatibilidad: mantener flags antiguos
|
||||||
if (list_state.commitEdit()) {
|
|
||||||
result.cell_committed = true;
|
result.cell_committed = true;
|
||||||
result.edited_cell = edited_cell;
|
result.edited_cell = edited_cell;
|
||||||
result.edited_value = new_value;
|
result.edited_value = new_value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Finalizar edición de celda (sin commit a BD)
|
||||||
|
_ = list_state.commitEdit();
|
||||||
result.navigate_direction = editor_result.navigate;
|
result.navigate_direction = editor_result.navigate;
|
||||||
} else if (editor_result.escaped) {
|
} else if (editor_result.escaped) {
|
||||||
const action = list_state.handleEscape();
|
const action = list_state.handleEscape();
|
||||||
if (action == .discard_row) {
|
if (action == .discard_row) {
|
||||||
result.row_discarded = true;
|
result.row_discarded = true;
|
||||||
|
list_state.row_edit_buffer.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -372,6 +394,33 @@ pub fn virtualAdvancedTableRect(
|
||||||
result.double_click_id = list_state.selected_id;
|
result.double_click_id = list_state.selected_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Commit de fila al cambiar de selección
|
||||||
|
// =========================================================================
|
||||||
|
// Si la selección cambió y hay cambios pendientes en otra fila, hacer commit
|
||||||
|
if (list_state.selection_changed and list_state.row_edit_buffer.has_changes) {
|
||||||
|
const new_row_id = list_state.selected_id orelse table_core.NEW_ROW_ID;
|
||||||
|
const new_row_idx = list_state.getSelectedRow() orelse 0;
|
||||||
|
const is_ghost = table_core.isGhostRow(new_row_id);
|
||||||
|
|
||||||
|
// checkRowChangeAndCommit compara row_ids y hace commit si son diferentes
|
||||||
|
if (table_core.checkRowChangeAndCommit(
|
||||||
|
&list_state.row_edit_buffer,
|
||||||
|
new_row_id,
|
||||||
|
new_row_idx,
|
||||||
|
is_ghost,
|
||||||
|
&result.row_changes,
|
||||||
|
)) |commit_info| {
|
||||||
|
result.row_committed = true;
|
||||||
|
result.row_commit_id = commit_info.row_id;
|
||||||
|
result.row_commit_is_insert = commit_info.is_insert;
|
||||||
|
result.row_changes_count = commit_info.change_count;
|
||||||
|
|
||||||
|
// Compatibilidad
|
||||||
|
result.row_changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1017,8 +1066,11 @@ fn handleKeyboard(
|
||||||
},
|
},
|
||||||
.tab => {
|
.tab => {
|
||||||
// Tab sin edición activa: indica que el panel debe mover focus
|
// Tab sin edición activa: indica que el panel debe mover focus
|
||||||
result.tab_out = true;
|
// IMPORTANTE: Solo si CellEditor no procesó Tab (evita doble procesamiento)
|
||||||
result.tab_shift = event.modifiers.shift;
|
if (result.navigate_direction == .none) {
|
||||||
|
result.tab_out = true;
|
||||||
|
result.tab_shift = event.modifiers.shift;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
else => {},
|
else => {},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue