feat(virtual_table): Tab navigation + cursor fixes + cell editing
- 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
This commit is contained in:
parent
47fc5b28f7
commit
702c33c13a
5 changed files with 85 additions and 36 deletions
|
|
@ -111,7 +111,7 @@ pub const Context = struct {
|
|||
|
||||
/// Idle timeout for cursor blinking (ms). After this time without input,
|
||||
/// cursor becomes solid and no animation frames are needed.
|
||||
pub const CURSOR_IDLE_TIMEOUT_MS: u64 = 5000;
|
||||
pub const CURSOR_IDLE_TIMEOUT_MS: u64 = 20000;
|
||||
|
||||
/// Cursor blink period (ms). Cursor toggles visibility at this rate.
|
||||
/// 300ms = ~3.3 blinks/sec (faster for better editing feedback)
|
||||
|
|
@ -363,6 +363,26 @@ pub const Context = struct {
|
|||
return @intCast(@max(time_until_toggle, 16)); // Minimum 16ms to avoid busy loop
|
||||
}
|
||||
|
||||
/// Determina si el cursor debe ser visible basado en tiempo de actividad.
|
||||
/// Usa parpadeo mientras hay actividad reciente, sólido cuando está idle.
|
||||
/// @param last_activity_time_ms: Última vez que hubo actividad (edición, input)
|
||||
/// @return true si el cursor debe dibujarse visible
|
||||
pub fn isCursorVisible(self: Self, last_activity_time_ms: u64) bool {
|
||||
// Si no hay timing disponible, mostrar cursor siempre
|
||||
if (self.current_time_ms == 0) return true;
|
||||
|
||||
// Calcular tiempo idle desde última actividad
|
||||
const idle_time = self.current_time_ms -| last_activity_time_ms;
|
||||
|
||||
if (idle_time >= CURSOR_IDLE_TIMEOUT_MS) {
|
||||
// Idle: cursor siempre visible (sin parpadeo, ahorra batería)
|
||||
return true;
|
||||
} else {
|
||||
// Activo: cursor parpadea
|
||||
return (self.current_time_ms / CURSOR_BLINK_PERIOD_MS) % 2 == 0;
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// ID Management
|
||||
// =========================================================================
|
||||
|
|
|
|||
|
|
@ -376,22 +376,8 @@ pub fn textInputRect(
|
|||
const cursor_x = inner.x + @as(i32, @intCast(cursor_offset));
|
||||
const cursor_color = theme.foreground;
|
||||
|
||||
// Determine if cursor should be visible
|
||||
const cursor_visible = blk: {
|
||||
// If no timing available, always show cursor
|
||||
if (ctx.current_time_ms == 0) break :blk true;
|
||||
|
||||
// Check idle time (time since last user input)
|
||||
const idle_time = ctx.current_time_ms -| ctx.last_input_time_ms;
|
||||
|
||||
if (idle_time >= Context.CURSOR_IDLE_TIMEOUT_MS) {
|
||||
// Idle: cursor always visible (solid, no blink)
|
||||
break :blk true;
|
||||
} else {
|
||||
// Active: cursor blinks
|
||||
break :blk (ctx.current_time_ms / Context.CURSOR_BLINK_PERIOD_MS) % 2 == 0;
|
||||
}
|
||||
};
|
||||
// Visibilidad del cursor usando función compartida de Context
|
||||
const cursor_visible = ctx.isCursorVisible(ctx.last_input_time_ms);
|
||||
|
||||
if (cursor_visible) {
|
||||
ctx.pushCommand(Command.rect(
|
||||
|
|
|
|||
|
|
@ -81,24 +81,27 @@ pub fn drawCellEditor(
|
|||
|
||||
// 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,
|
||||
geom.y + padding,
|
||||
text_y,
|
||||
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
|
||||
// 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,
|
||||
geom.y + padding,
|
||||
1, // 1px de ancho
|
||||
geom.h - (padding * 2),
|
||||
text_y,
|
||||
2, // 2px de ancho (más visible)
|
||||
16, // Altura del texto
|
||||
colors.cursor,
|
||||
));
|
||||
}
|
||||
|
|
@ -106,6 +109,11 @@ pub fn drawCellEditor(
|
|||
// 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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -63,6 +63,9 @@ pub const VirtualAdvancedTableState = struct {
|
|||
/// Offset donde empieza la ventana actual
|
||||
window_start: usize = 0,
|
||||
|
||||
/// Forzar refetch en próximo frame (después de edición)
|
||||
needs_window_refresh: bool = false,
|
||||
|
||||
// =========================================================================
|
||||
// Estado de carga
|
||||
// =========================================================================
|
||||
|
|
@ -167,6 +170,9 @@ pub const VirtualAdvancedTableState = struct {
|
|||
edit_buffer_len: usize = 0,
|
||||
edit_cursor: usize = 0,
|
||||
|
||||
/// Tiempo de última edición (para parpadeo cursor)
|
||||
last_edit_time_ms: u64 = 0,
|
||||
|
||||
/// Flag: celda requiere commit al terminar edición
|
||||
cell_value_changed: bool = false,
|
||||
|
||||
|
|
@ -430,6 +436,12 @@ pub const VirtualAdvancedTableState = struct {
|
|||
}
|
||||
}
|
||||
|
||||
/// Invalida la ventana de datos para forzar refetch en próximo frame.
|
||||
/// Llamar después de modificar datos en el DataProvider.
|
||||
pub fn invalidateWindow(self: *Self) void {
|
||||
self.needs_window_refresh = true;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Navegación horizontal
|
||||
// =========================================================================
|
||||
|
|
@ -510,7 +522,7 @@ pub const VirtualAdvancedTableState = struct {
|
|||
|
||||
/// Inicia edición de una celda
|
||||
/// initial_char: si viene de tecla alfanumérica, el caracter inicial (null = mostrar valor actual)
|
||||
pub fn startEditing(self: *Self, cell: CellId, current_value: []const u8, initial_char: ?u8) void {
|
||||
pub fn startEditing(self: *Self, cell: CellId, current_value: []const u8, initial_char: ?u8, current_time_ms: u64) void {
|
||||
// Guardar valor original (para Escape)
|
||||
const len = @min(current_value.len, self.original_value.len);
|
||||
@memcpy(self.original_value[0..len], current_value[0..len]);
|
||||
|
|
@ -531,6 +543,7 @@ pub const VirtualAdvancedTableState = struct {
|
|||
|
||||
self.editing_cell = cell;
|
||||
self.escape_count = 0;
|
||||
self.last_edit_time_ms = current_time_ms;
|
||||
self.cell_value_changed = false;
|
||||
}
|
||||
|
||||
|
|
@ -567,11 +580,10 @@ pub const VirtualAdvancedTableState = struct {
|
|||
if (changed) {
|
||||
self.row_dirty = true;
|
||||
self.cell_value_changed = true;
|
||||
// Solo actualizar última fila editada si hubo cambios reales
|
||||
self.last_edited_row = self.editing_cell.?.row;
|
||||
}
|
||||
|
||||
// Actualizar última fila editada
|
||||
self.last_edited_row = self.editing_cell.?.row;
|
||||
|
||||
self.editing_cell = null;
|
||||
self.escape_count = 0;
|
||||
return changed;
|
||||
|
|
|
|||
|
|
@ -107,6 +107,12 @@ pub const VirtualAdvancedTableResult = struct {
|
|||
|
||||
/// 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_out: bool = false,
|
||||
|
||||
/// Shift estaba presionado con Tab (para tab_out inverso)
|
||||
tab_shift: bool = false,
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
|
|
@ -263,12 +269,16 @@ pub fn virtualAdvancedTableRect(
|
|||
header_h,
|
||||
filter_bar_h,
|
||||
)) |geom| {
|
||||
// Draw cell editor
|
||||
// Draw cell editor con colores del panel
|
||||
const editor_result = cell_editor.drawCellEditor(
|
||||
ctx,
|
||||
list_state,
|
||||
geom,
|
||||
cell_editor.CellEditorColors{},
|
||||
cell_editor.CellEditorColors{
|
||||
.background = colors.background,
|
||||
.text = colors.text,
|
||||
.cursor = colors.text, // Cursor mismo color que texto
|
||||
},
|
||||
);
|
||||
|
||||
// Handle editor results
|
||||
|
|
@ -369,7 +379,13 @@ pub fn virtualAdvancedTableRect(
|
|||
// Helper: Check if refetch needed
|
||||
// =============================================================================
|
||||
|
||||
fn needsRefetch(list_state: *const VirtualAdvancedTableState, visible_rows: usize, buffer_size: usize) bool {
|
||||
fn needsRefetch(list_state: *VirtualAdvancedTableState, visible_rows: usize, buffer_size: usize) bool {
|
||||
// Manual invalidation (después de editar datos)
|
||||
if (list_state.needs_window_refresh) {
|
||||
list_state.needs_window_refresh = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// First load
|
||||
if (list_state.current_window.len == 0) return true;
|
||||
|
||||
|
|
@ -762,10 +778,11 @@ fn drawRows(
|
|||
|
||||
// Draw active cell indicator BEFORE text
|
||||
if (is_active_cell) {
|
||||
// Convertir colores del config a TableColors para table_core
|
||||
// Usar colores contrastantes para el indicador
|
||||
const tc_colors = table_core.TableColors{
|
||||
.selected_cell = colors.row_selected, // Usar color de selección pero más claro
|
||||
.selected_cell_unfocus = colors.row_selected_unfocus,
|
||||
// Blanco/cyan brillante para máximo contraste
|
||||
.selected_cell = Style.Color.rgb(100, 200, 255),
|
||||
.selected_cell_unfocus = Style.Color.rgb(150, 150, 160),
|
||||
.border = colors.border,
|
||||
};
|
||||
table_core.drawCellActiveIndicator(
|
||||
|
|
@ -984,6 +1001,7 @@ fn handleKeyboard(
|
|||
}
|
||||
|
||||
// F2 o Space: iniciar edición de celda activa
|
||||
// Tab: pasar focus al siguiente widget (solo si NO estamos editando)
|
||||
for (ctx.input.getKeyEvents()) |event| {
|
||||
if (!event.pressed) continue;
|
||||
|
||||
|
|
@ -997,6 +1015,11 @@ fn handleKeyboard(
|
|||
result.edited_cell = cell;
|
||||
}
|
||||
},
|
||||
.tab => {
|
||||
// Tab sin edición activa: indica que el panel debe mover focus
|
||||
result.tab_out = true;
|
||||
result.tab_shift = event.modifiers.shift;
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
|
@ -1006,7 +1029,7 @@ fn handleKeyboard(
|
|||
if (char_input.len > 0 and !list_state.isEditing()) {
|
||||
if (list_state.getActiveCell()) |cell| {
|
||||
// Iniciar edición con el primer caracter
|
||||
list_state.startEditing(cell, "", char_input[0]);
|
||||
list_state.startEditing(cell, "", char_input[0], ctx.current_time_ms);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue