feat(table_core): Selection on focus Excel-style

- Añade selection_start/selection_end a CellEditState
- F2/Space selecciona todo el texto al entrar en edición
- Typing con selección reemplaza texto seleccionado
- Backspace/Delete borra selección si existe
- Flechas/Home/End limpian selección
- Dibuja highlight azul en ambas tablas (Advanced + Virtual)
This commit is contained in:
reugenio 2025-12-27 21:41:35 +01:00
parent 91570368cc
commit dc82340381
4 changed files with 178 additions and 25 deletions

View file

@ -561,12 +561,29 @@ fn drawEditingOverlay(
// Draw edit text // Draw edit text
const edit_text = table_state.getEditText(); const edit_text = table_state.getEditText();
const text_y = cell_y + @as(i32, @intCast((cell_h - 8) / 2)); const text_y = cell_y + @as(i32, @intCast((cell_h - 8) / 2));
// Draw selection highlight if exists (Excel-style)
const sel_start = table_state.cell_edit.selection_start;
const sel_end = table_state.cell_edit.selection_end;
if (sel_start != sel_end and edit_text.len > 0) {
const sel_min = @min(sel_start, sel_end);
const sel_max = @min(@max(sel_start, sel_end), edit_text.len);
if (sel_max > sel_min) {
const sel_x = col_x + 4 + @as(i32, @intCast(sel_min * 8));
const sel_width = @as(u32, @intCast((sel_max - sel_min) * 8));
ctx.pushCommand(Command.rect(sel_x, text_y, sel_width, 8, colors.cell_selection_bg));
}
}
// Draw text (on top of selection)
ctx.pushCommand(Command.text(col_x + 4, text_y, edit_text, colors.text_selected)); ctx.pushCommand(Command.text(col_x + 4, text_y, edit_text, colors.text_selected));
// Draw cursor // Draw cursor only if no selection
if (sel_start == sel_end) {
const cursor_x = col_x + 4 + @as(i32, @intCast(table_state.cell_edit.edit_cursor * 8)); const cursor_x = col_x + 4 + @as(i32, @intCast(table_state.cell_edit.edit_cursor * 8));
ctx.pushCommand(Command.rect(cursor_x, text_y, 1, 8, colors.text_selected)); ctx.pushCommand(Command.rect(cursor_x, text_y, 1, 8, colors.text_selected));
} }
}
// ============================================================================= // =============================================================================
// Keyboard Handling (Brain-in-Core pattern) // Keyboard Handling (Brain-in-Core pattern)
@ -881,7 +898,7 @@ fn handleEditingKeyboard(
else else
null; null;
// Usar table_core para procesamiento de teclado (DRY) // Usar table_core para procesamiento de teclado (DRY) con soporte selección
const kb_result = table_core.handleEditingKeyboard( const kb_result = table_core.handleEditingKeyboard(
ctx, ctx,
&table_state.cell_edit.edit_buffer, &table_state.cell_edit.edit_buffer,
@ -889,6 +906,8 @@ fn handleEditingKeyboard(
&table_state.cell_edit.edit_cursor, &table_state.cell_edit.edit_cursor,
&table_state.cell_edit.escape_count, &table_state.cell_edit.escape_count,
original_text, original_text,
&table_state.cell_edit.selection_start,
&table_state.cell_edit.selection_end,
); );
// Si no se procesó ningún evento, salir // Si no se procesó ningún evento, salir

View file

@ -293,6 +293,7 @@ pub const TableColors = struct {
// Cell editing // Cell editing
cell_editing_bg: Style.Color = Style.Color.rgb(60, 60, 80), cell_editing_bg: Style.Color = Style.Color.rgb(60, 60, 80),
cell_editing_border: Style.Color = Style.Color.primary, cell_editing_border: Style.Color = Style.Color.primary,
cell_selection_bg: Style.Color = Style.Color.rgb(0, 120, 215), // Azul para selección
// Borders // Borders
border: Style.Color = Style.Color.rgb(60, 60, 60), border: Style.Color = Style.Color.rgb(60, 60, 60),

View file

@ -48,6 +48,7 @@ pub const TableColors = struct {
cell_editing_bg: Style.Color = Style.Color.rgb(255, 255, 255), cell_editing_bg: Style.Color = Style.Color.rgb(255, 255, 255),
cell_editing_border: Style.Color = Style.Color.rgb(0, 120, 215), cell_editing_border: Style.Color = Style.Color.rgb(0, 120, 215),
cell_editing_text: Style.Color = Style.Color.rgb(0, 0, 0), cell_editing_text: Style.Color = Style.Color.rgb(0, 0, 0),
cell_selection_bg: Style.Color = Style.Color.rgb(0, 120, 215), // Azul para selección
// Header // Header
header_bg: Style.Color = Style.Color.rgb(45, 45, 50), header_bg: Style.Color = Style.Color.rgb(45, 45, 50),
@ -138,6 +139,11 @@ pub const CellEditState = struct {
/// Flag: el valor cambió respecto al original /// Flag: el valor cambió respecto al original
value_changed: bool = false, value_changed: bool = false,
/// Selección de texto (Excel-style: todo seleccionado al entrar con F2)
/// Si selection_start == selection_end, no hay selección (solo cursor)
selection_start: usize = 0,
selection_end: usize = 0,
const Self = @This(); const Self = @This();
/// Inicia edición de una celda /// Inicia edición de una celda
@ -155,15 +161,20 @@ pub const CellEditState = struct {
// Inicializar buffer de edición // Inicializar buffer de edición
if (initial_char) |c| { if (initial_char) |c| {
// Tecla alfanumérica: empezar con ese caracter // Tecla alfanumérica: empezar con ese caracter, sin selección
self.edit_buffer[0] = c; self.edit_buffer[0] = c;
self.edit_len = 1; self.edit_len = 1;
self.edit_cursor = 1; self.edit_cursor = 1;
self.selection_start = 0;
self.selection_end = 0;
} else { } else {
// F2/Space/DoubleClick: mostrar valor actual // F2/Space/DoubleClick: mostrar valor actual con TODO seleccionado (Excel-style)
@memcpy(self.edit_buffer[0..orig_len], current_value[0..orig_len]); @memcpy(self.edit_buffer[0..orig_len], current_value[0..orig_len]);
self.edit_len = orig_len; self.edit_len = orig_len;
self.edit_cursor = orig_len; self.edit_cursor = orig_len;
// Seleccionar todo el texto
self.selection_start = 0;
self.selection_end = orig_len;
} }
} }
@ -184,12 +195,25 @@ pub const CellEditState = struct {
return !std.mem.eql(u8, current, original); return !std.mem.eql(u8, current, original);
} }
/// Verifica si hay texto seleccionado
pub fn hasSelection(self: *const Self) bool {
return self.selection_start != self.selection_end;
}
/// Limpia la selección (pero mantiene el cursor)
pub fn clearSelection(self: *Self) void {
self.selection_start = 0;
self.selection_end = 0;
}
/// Revierte al valor original (Escape 1) /// Revierte al valor original (Escape 1)
pub fn revertToOriginal(self: *Self) void { pub fn revertToOriginal(self: *Self) void {
const orig = self.getOriginalValue(); const orig = self.getOriginalValue();
@memcpy(self.edit_buffer[0..orig.len], orig); @memcpy(self.edit_buffer[0..orig.len], orig);
self.edit_len = orig.len; self.edit_len = orig.len;
self.edit_cursor = orig.len; self.edit_cursor = orig.len;
// Limpiar selección al revertir
self.clearSelection();
} }
/// Finaliza edición /// Finaliza edición
@ -198,6 +222,7 @@ pub const CellEditState = struct {
self.edit_len = 0; self.edit_len = 0;
self.edit_cursor = 0; self.edit_cursor = 0;
self.escape_count = 0; self.escape_count = 0;
self.clearSelection();
} }
/// Resultado de handleEscape /// Resultado de handleEscape
@ -348,6 +373,8 @@ pub fn drawEditingOverlay(
height: u32, height: u32,
edit_text: []const u8, edit_text: []const u8,
cursor_pos: usize, cursor_pos: usize,
selection_start: usize,
selection_end: usize,
colors: *const TableColors, colors: *const TableColors,
) void { ) void {
// Fondo blanco // Fondo blanco
@ -359,13 +386,27 @@ pub fn drawEditingOverlay(
// Texto // Texto
const text_y = y + @as(i32, @intCast((height -| 16) / 2)); const text_y = y + @as(i32, @intCast((height -| 16) / 2));
const text_to_show = if (edit_text.len > 0) edit_text else ""; const text_to_show = if (edit_text.len > 0) edit_text else "";
// Dibujar selección si existe (Excel-style highlight)
if (selection_start != selection_end) {
const sel_min = @min(selection_start, selection_end);
const sel_max = @max(selection_start, selection_end);
const sel_x = x + 4 + @as(i32, @intCast(sel_min * 8)); // 8px por caracter (monospace)
const sel_width = @as(u32, @intCast((sel_max - sel_min) * 8));
// Color azul semitransparente para selección
ctx.pushCommand(Command.rect(sel_x, text_y, sel_width, 16, colors.cell_selection_bg));
}
// Texto (encima de la selección)
ctx.pushCommand(Command.text(x + 4, text_y, text_to_show, colors.cell_editing_text)); ctx.pushCommand(Command.text(x + 4, text_y, text_to_show, colors.cell_editing_text));
// Cursor parpadeante (simplificado: siempre visible) // Cursor parpadeante (simplificado: siempre visible)
// Calcular posición X del cursor basado en caracteres // Solo mostrar cursor si NO hay selección completa
if (selection_start == selection_end) {
const cursor_x = x + 4 + @as(i32, @intCast(cursor_pos * 8)); // Asumiendo fuente monospace 8px const cursor_x = x + 4 + @as(i32, @intCast(cursor_pos * 8)); // Asumiendo fuente monospace 8px
ctx.pushCommand(Command.rect(cursor_x, text_y, 2, 16, colors.cell_editing_border)); ctx.pushCommand(Command.rect(cursor_x, text_y, 2, 16, colors.cell_editing_border));
} }
}
/// Dibuja el texto de una celda /// Dibuja el texto de una celda
pub fn drawCellText( pub fn drawCellText(
@ -716,6 +757,7 @@ pub const EditKeyboardResult = struct {
/// 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
/// Soporta selección Excel-style: typing reemplaza selección
/// Retorna resultado con flags de navegación y si se procesó algún evento /// Retorna resultado con flags de navegación y si se procesó algún evento
pub fn handleEditingKeyboard( pub fn handleEditingKeyboard(
ctx: *Context, ctx: *Context,
@ -724,9 +766,41 @@ pub fn handleEditingKeyboard(
edit_cursor: *usize, edit_cursor: *usize,
escape_count: *u8, escape_count: *u8,
original_text: ?[]const u8, original_text: ?[]const u8,
selection_start: ?*usize,
selection_end: ?*usize,
) EditKeyboardResult { ) EditKeyboardResult {
var result = EditKeyboardResult{}; var result = EditKeyboardResult{};
// Helper para eliminar texto seleccionado
const deleteSelection = struct {
fn f(buf: []u8, len: *usize, cursor: *usize, sel_start: *usize, sel_end: *usize) bool {
if (sel_start.* == sel_end.*) return false;
const min_pos = @min(sel_start.*, sel_end.*);
const max_pos = @min(@max(sel_start.*, sel_end.*), len.*);
if (max_pos <= min_pos) return false;
// Mover caracteres después de la selección hacia atrás
const chars_to_delete = max_pos - min_pos;
var i: usize = min_pos;
while (i + chars_to_delete < len.*) : (i += 1) {
buf[i] = buf[i + chars_to_delete];
}
len.* -= chars_to_delete;
cursor.* = min_pos;
sel_start.* = 0;
sel_end.* = 0;
return true;
}
}.f;
// Helper para limpiar selección
const clearSelection = struct {
fn f(sel_start: ?*usize, sel_end: ?*usize) void {
if (sel_start) |s| s.* = 0;
if (sel_end) |e| e.* = 0;
}
}.f;
// Procesar eventos de tecla // Procesar eventos de tecla
for (ctx.input.getKeyEvents()) |event| { for (ctx.input.getKeyEvents()) |event| {
if (!event.pressed) continue; if (!event.pressed) continue;
@ -774,29 +848,41 @@ pub fn handleEditingKeyboard(
return result; return result;
}, },
.left => { .left => {
clearSelection(selection_start, selection_end);
if (edit_cursor.* > 0) edit_cursor.* -= 1; if (edit_cursor.* > 0) edit_cursor.* -= 1;
result.handled = true; result.handled = true;
// Reset escape count
escape_count.* = 0; escape_count.* = 0;
}, },
.right => { .right => {
clearSelection(selection_start, selection_end);
if (edit_cursor.* < edit_len.*) edit_cursor.* += 1; if (edit_cursor.* < edit_len.*) edit_cursor.* += 1;
result.handled = true; result.handled = true;
escape_count.* = 0; escape_count.* = 0;
}, },
.home => { .home => {
clearSelection(selection_start, selection_end);
edit_cursor.* = 0; edit_cursor.* = 0;
result.handled = true; result.handled = true;
escape_count.* = 0; escape_count.* = 0;
}, },
.end => { .end => {
clearSelection(selection_start, selection_end);
edit_cursor.* = edit_len.*; edit_cursor.* = edit_len.*;
result.handled = true; result.handled = true;
escape_count.* = 0; escape_count.* = 0;
}, },
.backspace => { .backspace => {
// Si hay selección, borrar selección
if (selection_start != null and selection_end != null) {
if (deleteSelection(edit_buffer, edit_len, edit_cursor, selection_start.?, selection_end.?)) {
result.text_changed = true;
result.handled = true;
escape_count.* = 0;
continue;
}
}
// Sin selección: borrar caracter antes del cursor
if (edit_cursor.* > 0 and edit_len.* > 0) { if (edit_cursor.* > 0 and edit_len.* > 0) {
// Shift characters left
const pos = edit_cursor.* - 1; const pos = edit_cursor.* - 1;
var i: usize = pos; var i: usize = pos;
while (i < edit_len.* - 1) : (i += 1) { while (i < edit_len.* - 1) : (i += 1) {
@ -810,6 +896,16 @@ pub fn handleEditingKeyboard(
escape_count.* = 0; escape_count.* = 0;
}, },
.delete => { .delete => {
// Si hay selección, borrar selección
if (selection_start != null and selection_end != null) {
if (deleteSelection(edit_buffer, edit_len, edit_cursor, selection_start.?, selection_end.?)) {
result.text_changed = true;
result.handled = true;
escape_count.* = 0;
continue;
}
}
// Sin selección: borrar caracter después del cursor
if (edit_cursor.* < edit_len.*) { if (edit_cursor.* < edit_len.*) {
var i: usize = edit_cursor.*; var i: usize = edit_cursor.*;
while (i < edit_len.* - 1) : (i += 1) { while (i < edit_len.* - 1) : (i += 1) {
@ -828,6 +924,13 @@ pub fn handleEditingKeyboard(
// Procesar texto ingresado (caracteres imprimibles) // Procesar texto ingresado (caracteres imprimibles)
const text_input = ctx.input.getTextInput(); const text_input = ctx.input.getTextInput();
if (text_input.len > 0) { if (text_input.len > 0) {
// Si hay selección, borrarla primero (comportamiento Excel/Word)
if (selection_start != null and selection_end != null) {
if (selection_start.?.* != selection_end.?.*) {
_ = deleteSelection(edit_buffer, edit_len, edit_cursor, selection_start.?, selection_end.?);
}
}
for (text_input) |ch| { for (text_input) |ch| {
if (edit_len.* < edit_buffer.len - 1) { if (edit_len.* < edit_buffer.len - 1) {
// Hacer espacio moviendo caracteres hacia la derecha // Hacer espacio moviendo caracteres hacia la derecha

View file

@ -81,6 +81,31 @@ pub fn drawCellEditor(
// Texto actual // Texto actual
const text = state.getEditText(); const text = state.getEditText();
const text_y = geom.y + @as(i32, @intCast((geom.h -| 16) / 2)); // Centrado vertical const text_y = geom.y + @as(i32, @intCast((geom.h -| 16) / 2)); // Centrado vertical
// Dibujar selección si existe (Excel-style: todo seleccionado al entrar con F2)
const sel_start = state.cell_edit.selection_start;
const sel_end = state.cell_edit.selection_end;
if (sel_start != sel_end and text.len > 0) {
const sel_min = @min(sel_start, sel_end);
const sel_max = @min(@max(sel_start, sel_end), text.len);
if (sel_max > sel_min) {
// Calcular posición X del inicio de selección
const sel_x_offset = ctx.measureTextToCursor(text, sel_min);
const sel_end_offset = ctx.measureTextToCursor(text, sel_max);
const sel_width = sel_end_offset - sel_x_offset;
if (sel_width > 0) {
ctx.pushCommand(Command.rect(
geom.x + padding + @as(i32, @intCast(sel_x_offset)),
text_y,
@intCast(sel_width),
16,
colors.selection,
));
}
}
}
// Texto (encima de la selección)
ctx.pushCommand(Command.text( ctx.pushCommand(Command.text(
geom.x + padding, geom.x + padding,
text_y, text_y,
@ -88,7 +113,9 @@ pub fn drawCellEditor(
colors.text, colors.text,
)); ));
// Cursor: posición calculada con measureTextToCursor (TTF-aware) // Cursor: solo si NO hay selección activa
const has_selection = sel_start != sel_end;
if (!has_selection) {
const cursor_offset = ctx.measureTextToCursor(text, state.cell_edit.edit_cursor); const cursor_offset = ctx.measureTextToCursor(text, state.cell_edit.edit_cursor);
const cursor_x = geom.x + padding + @as(i32, @intCast(cursor_offset)); const cursor_x = geom.x + padding + @as(i32, @intCast(cursor_offset));
@ -104,8 +131,9 @@ pub fn drawCellEditor(
colors.cursor, colors.cursor,
)); ));
} }
}
// Procesar input de teclado usando table_core // Procesar input de teclado usando table_core (con soporte selección)
const original_text = state.getOriginalValue(); const original_text = state.getOriginalValue();
const kb_result = table_core.handleEditingKeyboard( const kb_result = table_core.handleEditingKeyboard(
ctx, ctx,
@ -114,6 +142,8 @@ pub fn drawCellEditor(
&state.cell_edit.edit_cursor, &state.cell_edit.edit_cursor,
&state.cell_edit.escape_count, &state.cell_edit.escape_count,
if (original_text.len > 0) original_text else null, if (original_text.len > 0) original_text else null,
&state.cell_edit.selection_start,
&state.cell_edit.selection_end,
); );
// Mapear resultado de table_core a CellEditorResult // Mapear resultado de table_core a CellEditorResult