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:
parent
91570368cc
commit
dc82340381
4 changed files with 178 additions and 25 deletions
|
|
@ -561,12 +561,29 @@ fn drawEditingOverlay(
|
|||
// Draw edit text
|
||||
const edit_text = table_state.getEditText();
|
||||
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));
|
||||
|
||||
// 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));
|
||||
ctx.pushCommand(Command.rect(cursor_x, text_y, 1, 8, colors.text_selected));
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Keyboard Handling (Brain-in-Core pattern)
|
||||
|
|
@ -881,7 +898,7 @@ fn handleEditingKeyboard(
|
|||
else
|
||||
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(
|
||||
ctx,
|
||||
&table_state.cell_edit.edit_buffer,
|
||||
|
|
@ -889,6 +906,8 @@ fn handleEditingKeyboard(
|
|||
&table_state.cell_edit.edit_cursor,
|
||||
&table_state.cell_edit.escape_count,
|
||||
original_text,
|
||||
&table_state.cell_edit.selection_start,
|
||||
&table_state.cell_edit.selection_end,
|
||||
);
|
||||
|
||||
// Si no se procesó ningún evento, salir
|
||||
|
|
|
|||
|
|
@ -293,6 +293,7 @@ pub const TableColors = struct {
|
|||
// Cell editing
|
||||
cell_editing_bg: Style.Color = Style.Color.rgb(60, 60, 80),
|
||||
cell_editing_border: Style.Color = Style.Color.primary,
|
||||
cell_selection_bg: Style.Color = Style.Color.rgb(0, 120, 215), // Azul para selección
|
||||
|
||||
// Borders
|
||||
border: Style.Color = Style.Color.rgb(60, 60, 60),
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ pub const TableColors = struct {
|
|||
cell_editing_bg: Style.Color = Style.Color.rgb(255, 255, 255),
|
||||
cell_editing_border: Style.Color = Style.Color.rgb(0, 120, 215),
|
||||
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_bg: Style.Color = Style.Color.rgb(45, 45, 50),
|
||||
|
|
@ -138,6 +139,11 @@ pub const CellEditState = struct {
|
|||
/// Flag: el valor cambió respecto al original
|
||||
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();
|
||||
|
||||
/// Inicia edición de una celda
|
||||
|
|
@ -155,15 +161,20 @@ pub const CellEditState = struct {
|
|||
|
||||
// Inicializar buffer de edición
|
||||
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_len = 1;
|
||||
self.edit_cursor = 1;
|
||||
self.selection_start = 0;
|
||||
self.selection_end = 0;
|
||||
} 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]);
|
||||
self.edit_len = 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);
|
||||
}
|
||||
|
||||
/// 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)
|
||||
pub fn revertToOriginal(self: *Self) void {
|
||||
const orig = self.getOriginalValue();
|
||||
@memcpy(self.edit_buffer[0..orig.len], orig);
|
||||
self.edit_len = orig.len;
|
||||
self.edit_cursor = orig.len;
|
||||
// Limpiar selección al revertir
|
||||
self.clearSelection();
|
||||
}
|
||||
|
||||
/// Finaliza edición
|
||||
|
|
@ -198,6 +222,7 @@ pub const CellEditState = struct {
|
|||
self.edit_len = 0;
|
||||
self.edit_cursor = 0;
|
||||
self.escape_count = 0;
|
||||
self.clearSelection();
|
||||
}
|
||||
|
||||
/// Resultado de handleEscape
|
||||
|
|
@ -348,6 +373,8 @@ pub fn drawEditingOverlay(
|
|||
height: u32,
|
||||
edit_text: []const u8,
|
||||
cursor_pos: usize,
|
||||
selection_start: usize,
|
||||
selection_end: usize,
|
||||
colors: *const TableColors,
|
||||
) void {
|
||||
// Fondo blanco
|
||||
|
|
@ -359,13 +386,27 @@ pub fn drawEditingOverlay(
|
|||
// Texto
|
||||
const text_y = y + @as(i32, @intCast((height -| 16) / 2));
|
||||
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));
|
||||
|
||||
// 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
|
||||
ctx.pushCommand(Command.rect(cursor_x, text_y, 2, 16, colors.cell_editing_border));
|
||||
}
|
||||
}
|
||||
|
||||
/// Dibuja el texto de una celda
|
||||
pub fn drawCellText(
|
||||
|
|
@ -716,6 +757,7 @@ pub const EditKeyboardResult = struct {
|
|||
|
||||
/// Procesa teclado en modo edición
|
||||
/// 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
|
||||
pub fn handleEditingKeyboard(
|
||||
ctx: *Context,
|
||||
|
|
@ -724,9 +766,41 @@ pub fn handleEditingKeyboard(
|
|||
edit_cursor: *usize,
|
||||
escape_count: *u8,
|
||||
original_text: ?[]const u8,
|
||||
selection_start: ?*usize,
|
||||
selection_end: ?*usize,
|
||||
) 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
|
||||
for (ctx.input.getKeyEvents()) |event| {
|
||||
if (!event.pressed) continue;
|
||||
|
|
@ -774,29 +848,41 @@ pub fn handleEditingKeyboard(
|
|||
return result;
|
||||
},
|
||||
.left => {
|
||||
clearSelection(selection_start, selection_end);
|
||||
if (edit_cursor.* > 0) edit_cursor.* -= 1;
|
||||
result.handled = true;
|
||||
// Reset escape count
|
||||
escape_count.* = 0;
|
||||
},
|
||||
.right => {
|
||||
clearSelection(selection_start, selection_end);
|
||||
if (edit_cursor.* < edit_len.*) edit_cursor.* += 1;
|
||||
result.handled = true;
|
||||
escape_count.* = 0;
|
||||
},
|
||||
.home => {
|
||||
clearSelection(selection_start, selection_end);
|
||||
edit_cursor.* = 0;
|
||||
result.handled = true;
|
||||
escape_count.* = 0;
|
||||
},
|
||||
.end => {
|
||||
clearSelection(selection_start, selection_end);
|
||||
edit_cursor.* = edit_len.*;
|
||||
result.handled = true;
|
||||
escape_count.* = 0;
|
||||
},
|
||||
.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) {
|
||||
// Shift characters left
|
||||
const pos = edit_cursor.* - 1;
|
||||
var i: usize = pos;
|
||||
while (i < edit_len.* - 1) : (i += 1) {
|
||||
|
|
@ -810,6 +896,16 @@ pub fn handleEditingKeyboard(
|
|||
escape_count.* = 0;
|
||||
},
|
||||
.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.*) {
|
||||
var i: usize = edit_cursor.*;
|
||||
while (i < edit_len.* - 1) : (i += 1) {
|
||||
|
|
@ -828,6 +924,13 @@ pub fn handleEditingKeyboard(
|
|||
// Procesar texto ingresado (caracteres imprimibles)
|
||||
const text_input = ctx.input.getTextInput();
|
||||
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| {
|
||||
if (edit_len.* < edit_buffer.len - 1) {
|
||||
// Hacer espacio moviendo caracteres hacia la derecha
|
||||
|
|
|
|||
|
|
@ -81,6 +81,31 @@ pub fn drawCellEditor(
|
|||
// Texto actual
|
||||
const text = state.getEditText();
|
||||
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(
|
||||
geom.x + padding,
|
||||
text_y,
|
||||
|
|
@ -88,7 +113,9 @@ pub fn drawCellEditor(
|
|||
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_x = geom.x + padding + @as(i32, @intCast(cursor_offset));
|
||||
|
||||
|
|
@ -104,8 +131,9 @@ pub fn drawCellEditor(
|
|||
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 kb_result = table_core.handleEditingKeyboard(
|
||||
ctx,
|
||||
|
|
@ -114,6 +142,8 @@ pub fn drawCellEditor(
|
|||
&state.cell_edit.edit_cursor,
|
||||
&state.cell_edit.escape_count,
|
||||
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
|
||||
|
|
|
|||
Loading…
Reference in a new issue