refactor(tables): Add table_core.zig with shared rendering functions
- New module: table_core.zig with common table rendering logic - drawCellActiveIndicator(): visual indicator for selected cell - detectDoubleClick(): timing-based double-click detection - handleEditingKeyboard(): common keyboard handling for editing - blendColor(), startsWithIgnoreCase(): utilities VirtualAdvancedTable now uses table_core: - Active cell indicator in drawRows (visible highlight on selected cell) - Double-click detection in handleMouseClick - Added state fields: last_click_time, last_click_row, last_click_col AdvancedTable changes: - Improved cell active indicator (alpha 0.35, double border) - Added double-click fields to state - Space starts editing with empty value - Alphanumeric keys start editing in editable cells
This commit is contained in:
parent
65f6782d24
commit
47fc5b28f7
7 changed files with 1063 additions and 63 deletions
|
|
@ -403,12 +403,17 @@ fn drawRow(
|
|||
const is_selected_cell = is_selected_row and table_state.selected_col == @as(i32, @intCast(col_idx));
|
||||
const cell_clicked = cell_rect.contains(mouse.x, mouse.y) and ctx.input.mousePressed(.left);
|
||||
|
||||
// Cell indicator for selected cell (outline instead of solid fill)
|
||||
if (is_selected_cell) {
|
||||
// Subtle background tint
|
||||
ctx.pushCommand(Command.rect(col_x, bounds.y, col.width, config.row_height, blendColor(row_bg, colors.selected_cell, 0.15)));
|
||||
// Border outline
|
||||
// Cell indicator for selected cell - más visible que antes
|
||||
if (is_selected_cell and has_focus) {
|
||||
// Fondo con tinte más visible (0.35 en lugar de 0.15)
|
||||
ctx.pushCommand(Command.rect(col_x, bounds.y, col.width, config.row_height, blendColor(row_bg, colors.selected_cell, 0.35)));
|
||||
// Borde doble para mayor visibilidad
|
||||
ctx.pushCommand(Command.rectOutline(col_x, bounds.y, col.width, config.row_height, colors.selected_cell));
|
||||
ctx.pushCommand(Command.rectOutline(col_x + 1, bounds.y + 1, col.width -| 2, config.row_height -| 2, colors.selected_cell));
|
||||
} else if (is_selected_cell) {
|
||||
// Sin focus: indicación más sutil
|
||||
ctx.pushCommand(Command.rect(col_x, bounds.y, col.width, config.row_height, blendColor(row_bg, colors.selected_cell, 0.15)));
|
||||
ctx.pushCommand(Command.rectOutline(col_x, bounds.y, col.width, config.row_height, colors.border));
|
||||
}
|
||||
|
||||
// Get cell value
|
||||
|
|
@ -427,14 +432,41 @@ fn drawRow(
|
|||
ctx.pushCommand(Command.text(col_x + 4, text_y, text, text_color));
|
||||
}
|
||||
|
||||
// Handle cell click
|
||||
// Handle cell click and double-click
|
||||
if (cell_clicked) {
|
||||
const current_time = ctx.current_time_ms;
|
||||
const same_cell = table_state.last_click_row == @as(i32, @intCast(row_idx)) and
|
||||
table_state.last_click_col == @as(i32, @intCast(col_idx));
|
||||
const time_diff = current_time -| table_state.last_click_time;
|
||||
const is_double_click = same_cell and time_diff < table_state.double_click_threshold_ms;
|
||||
|
||||
if (is_double_click and config.allow_edit and col.editable and !table_state.editing) {
|
||||
// Double-click: start editing
|
||||
if (table_state.getRow(row_idx)) |row| {
|
||||
const value = row.get(col.name);
|
||||
var format_buf: [128]u8 = undefined;
|
||||
const edit_text = value.format(&format_buf);
|
||||
table_state.startEditing(edit_text);
|
||||
table_state.original_value = value;
|
||||
result.edit_started = true;
|
||||
}
|
||||
// Reset click tracking
|
||||
table_state.last_click_time = 0;
|
||||
table_state.last_click_row = -1;
|
||||
table_state.last_click_col = -1;
|
||||
} else {
|
||||
// Single click: select cell
|
||||
if (!is_selected_cell) {
|
||||
table_state.selectCell(row_idx, col_idx);
|
||||
result.selection_changed = true;
|
||||
result.selected_row = row_idx;
|
||||
result.selected_col = col_idx;
|
||||
}
|
||||
// Update click tracking for potential double-click
|
||||
table_state.last_click_time = current_time;
|
||||
table_state.last_click_row = @intCast(row_idx);
|
||||
table_state.last_click_col = @intCast(col_idx);
|
||||
}
|
||||
}
|
||||
|
||||
col_x += @as(i32, @intCast(col.width));
|
||||
|
|
@ -727,23 +759,33 @@ fn handleKeyboard(
|
|||
}
|
||||
}
|
||||
|
||||
// Start editing with F2 or Enter
|
||||
if (config.allow_edit and (ctx.input.keyPressed(.f2) or ctx.input.keyPressed(.enter))) {
|
||||
// Start editing with F2, Enter, or Space (empty)
|
||||
if (config.allow_edit) {
|
||||
const start_edit_key = ctx.input.keyPressed(.f2) or ctx.input.keyPressed(.enter);
|
||||
const start_empty = ctx.input.keyPressed(.space);
|
||||
|
||||
if (start_edit_key or start_empty) {
|
||||
if (table_state.selected_row >= 0 and table_state.selected_col >= 0) {
|
||||
const col_idx: usize = @intCast(table_state.selected_col);
|
||||
if (col_idx < table_schema.columns.len and table_schema.columns[col_idx].editable) {
|
||||
// Get current value
|
||||
if (table_state.getRow(@intCast(table_state.selected_row))) |row| {
|
||||
const value = row.get(table_schema.columns[col_idx].name);
|
||||
if (start_empty) {
|
||||
// Espacio: empezar vacío
|
||||
table_state.startEditing("");
|
||||
} else {
|
||||
// F2/Enter: empezar con valor actual
|
||||
var format_buf: [128]u8 = undefined;
|
||||
const text = value.format(&format_buf);
|
||||
table_state.startEditing(text);
|
||||
}
|
||||
table_state.original_value = value;
|
||||
result.edit_started = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Row operations (if allowed)
|
||||
if (config.allow_row_operations) {
|
||||
|
|
@ -785,16 +827,40 @@ fn handleKeyboard(
|
|||
result.selection_changed = true;
|
||||
}
|
||||
|
||||
// Incremental search (type-to-search)
|
||||
// Only when not editing and no modifiers pressed
|
||||
// Type-to-edit or incremental search
|
||||
// Behavior: If current cell is editable → start editing with typed char
|
||||
// If not editable → incremental search in first column
|
||||
if (!ctx.input.modifiers.ctrl and !ctx.input.modifiers.alt) {
|
||||
if (ctx.input.text_input_len > 0) {
|
||||
const text = ctx.input.text_input[0..ctx.input.text_input_len];
|
||||
|
||||
// Check if current cell is editable
|
||||
const current_cell_editable = blk: {
|
||||
if (!config.allow_edit) break :blk false;
|
||||
if (table_state.selected_row < 0 or table_state.selected_col < 0) break :blk false;
|
||||
const col_idx: usize = @intCast(table_state.selected_col);
|
||||
if (col_idx >= table_schema.columns.len) break :blk false;
|
||||
break :blk table_schema.columns[col_idx].editable;
|
||||
};
|
||||
|
||||
if (current_cell_editable) {
|
||||
// Start editing with first typed character
|
||||
if (text.len > 0 and text[0] >= 32 and text[0] < 127) {
|
||||
if (table_state.getRow(@intCast(table_state.selected_row))) |row| {
|
||||
const col_idx: usize = @intCast(table_state.selected_col);
|
||||
const value = row.get(table_schema.columns[col_idx].name);
|
||||
// Start with the typed text (replaces content)
|
||||
table_state.startEditing(text);
|
||||
table_state.original_value = value;
|
||||
result.edit_started = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Incremental search (type-to-search) in first column
|
||||
for (text) |char| {
|
||||
if (char >= 32 and char < 127) { // Printable ASCII
|
||||
if (char >= 32 and char < 127) {
|
||||
const search_term = table_state.addSearchChar(char, ctx.current_time_ms);
|
||||
|
||||
// Search for matching row in first column
|
||||
if (search_term.len > 0 and table_schema.columns.len > 0) {
|
||||
const first_col_name = table_schema.columns[0].name;
|
||||
const start_row: usize = if (table_state.selected_row >= 0)
|
||||
|
|
@ -842,6 +908,7 @@ fn handleKeyboard(
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handleEditingKeyboard(
|
||||
|
|
|
|||
|
|
@ -110,6 +110,22 @@ pub const AdvancedTableState = struct {
|
|||
/// Escape count (1 = revert, 2 = cancel)
|
||||
escape_count: u8 = 0,
|
||||
|
||||
// =========================================================================
|
||||
// Double-click detection
|
||||
// =========================================================================
|
||||
|
||||
/// Time of last click (ms)
|
||||
last_click_time: u64 = 0,
|
||||
|
||||
/// Row of last click
|
||||
last_click_row: i32 = -1,
|
||||
|
||||
/// Column of last click
|
||||
last_click_col: i32 = -1,
|
||||
|
||||
/// Double-click threshold in ms
|
||||
double_click_threshold_ms: u64 = 400,
|
||||
|
||||
// =========================================================================
|
||||
// Sorting
|
||||
// =========================================================================
|
||||
|
|
|
|||
422
src/widgets/table_core (conflicted).zig
Normal file
422
src/widgets/table_core (conflicted).zig
Normal file
|
|
@ -0,0 +1,422 @@
|
|||
//! Table Core - Funciones compartidas para renderizado de tablas
|
||||
//!
|
||||
//! Este módulo contiene la lógica común de renderizado utilizada por:
|
||||
//! - AdvancedTable (datos en memoria)
|
||||
//! - VirtualAdvancedTable (datos paginados desde DataProvider)
|
||||
//!
|
||||
//! Principio: Una sola implementación de UI, dos estrategias de datos.
|
||||
|
||||
const std = @import("std");
|
||||
const Context = @import("../core/context.zig").Context;
|
||||
const Command = @import("../core/command.zig");
|
||||
const Layout = @import("../core/layout.zig");
|
||||
const Style = @import("../core/style.zig");
|
||||
|
||||
// =============================================================================
|
||||
// Tipos comunes
|
||||
// =============================================================================
|
||||
|
||||
/// Colores para renderizado de tabla
|
||||
pub const TableColors = struct {
|
||||
// Fondos
|
||||
background: Style.Color = Style.Color.rgb(30, 30, 35),
|
||||
row_normal: Style.Color = Style.Color.rgb(35, 35, 40),
|
||||
row_alternate: Style.Color = Style.Color.rgb(40, 40, 45),
|
||||
row_hover: Style.Color = Style.Color.rgb(50, 50, 60),
|
||||
selected_row: Style.Color = Style.Color.rgb(0, 90, 180),
|
||||
selected_row_unfocus: Style.Color = Style.Color.rgb(60, 60, 70),
|
||||
|
||||
// Celda activa
|
||||
selected_cell: Style.Color = Style.Color.rgb(100, 150, 255),
|
||||
selected_cell_unfocus: Style.Color = Style.Color.rgb(80, 80, 90),
|
||||
|
||||
// Edición
|
||||
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),
|
||||
|
||||
// Header
|
||||
header_bg: Style.Color = Style.Color.rgb(45, 45, 50),
|
||||
header_fg: Style.Color = Style.Color.rgb(200, 200, 200),
|
||||
|
||||
// Texto
|
||||
text_normal: Style.Color = Style.Color.rgb(220, 220, 220),
|
||||
text_selected: Style.Color = Style.Color.rgb(255, 255, 255),
|
||||
text_placeholder: Style.Color = Style.Color.rgb(128, 128, 128),
|
||||
|
||||
// Bordes
|
||||
border: Style.Color = Style.Color.rgb(60, 60, 65),
|
||||
focus_ring: Style.Color = Style.Color.rgb(0, 120, 215),
|
||||
};
|
||||
|
||||
/// Información de una celda para renderizado
|
||||
pub const CellRenderInfo = struct {
|
||||
/// Texto a mostrar
|
||||
text: []const u8,
|
||||
/// Posición X de la celda
|
||||
x: i32,
|
||||
/// Ancho de la celda
|
||||
width: u32,
|
||||
/// Es la celda actualmente seleccionada
|
||||
is_selected: bool = false,
|
||||
/// Es editable
|
||||
is_editable: bool = true,
|
||||
/// Alineación del texto (0=left, 1=center, 2=right)
|
||||
text_align: u2 = 0,
|
||||
};
|
||||
|
||||
/// Estado de edición para renderizado
|
||||
pub const EditState = struct {
|
||||
/// Está en modo edición
|
||||
editing: bool = false,
|
||||
/// Fila en edición
|
||||
edit_row: i32 = -1,
|
||||
/// Columna en edición
|
||||
edit_col: i32 = -1,
|
||||
/// Buffer de texto actual
|
||||
edit_text: []const u8 = "",
|
||||
/// Posición del cursor
|
||||
edit_cursor: usize = 0,
|
||||
};
|
||||
|
||||
/// Estado de doble-click
|
||||
pub const DoubleClickState = struct {
|
||||
last_click_time: u64 = 0,
|
||||
last_click_row: i32 = -1,
|
||||
last_click_col: i32 = -1,
|
||||
threshold_ms: u64 = 400,
|
||||
};
|
||||
|
||||
/// Resultado de procesar click en celda
|
||||
pub const CellClickResult = struct {
|
||||
/// Hubo click
|
||||
clicked: bool = false,
|
||||
/// Fue doble-click
|
||||
double_click: bool = false,
|
||||
/// Fila clickeada
|
||||
row: usize = 0,
|
||||
/// Columna clickeada
|
||||
col: usize = 0,
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// Funciones de renderizado
|
||||
// =============================================================================
|
||||
|
||||
/// Dibuja el indicador de celda activa (fondo + borde)
|
||||
/// Llamar ANTES de dibujar el texto de la celda
|
||||
pub fn drawCellActiveIndicator(
|
||||
ctx: *Context,
|
||||
x: i32,
|
||||
y: i32,
|
||||
width: u32,
|
||||
height: u32,
|
||||
row_bg: Style.Color,
|
||||
colors: *const TableColors,
|
||||
has_focus: bool,
|
||||
) void {
|
||||
if (has_focus) {
|
||||
// Con focus: fondo más visible + borde doble
|
||||
const tinted_bg = blendColor(row_bg, colors.selected_cell, 0.35);
|
||||
ctx.pushCommand(Command.rect(x, y, width, height, tinted_bg));
|
||||
ctx.pushCommand(Command.rectOutline(x, y, width, height, colors.selected_cell));
|
||||
ctx.pushCommand(Command.rectOutline(x + 1, y + 1, width -| 2, height -| 2, colors.selected_cell));
|
||||
} else {
|
||||
// Sin focus: indicación más sutil
|
||||
const tinted_bg = blendColor(row_bg, colors.selected_cell_unfocus, 0.15);
|
||||
ctx.pushCommand(Command.rect(x, y, width, height, tinted_bg));
|
||||
ctx.pushCommand(Command.rectOutline(x, y, width, height, colors.border));
|
||||
}
|
||||
}
|
||||
|
||||
/// Dibuja el overlay de edición de celda
|
||||
pub fn drawEditingOverlay(
|
||||
ctx: *Context,
|
||||
x: i32,
|
||||
y: i32,
|
||||
width: u32,
|
||||
height: u32,
|
||||
edit_text: []const u8,
|
||||
cursor_pos: usize,
|
||||
colors: *const TableColors,
|
||||
) void {
|
||||
// Fondo blanco
|
||||
ctx.pushCommand(Command.rect(x, y, width, height, colors.cell_editing_bg));
|
||||
|
||||
// Borde azul
|
||||
ctx.pushCommand(Command.rectOutline(x, y, width, height, colors.cell_editing_border));
|
||||
|
||||
// Texto
|
||||
const text_y = y + @as(i32, @intCast((height -| 16) / 2));
|
||||
const text_to_show = if (edit_text.len > 0) edit_text else "";
|
||||
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
|
||||
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(
|
||||
ctx: *Context,
|
||||
x: i32,
|
||||
y: i32,
|
||||
width: u32,
|
||||
height: u32,
|
||||
text: []const u8,
|
||||
color: Style.Color,
|
||||
text_align: u2,
|
||||
) void {
|
||||
const text_y = y + @as(i32, @intCast((height -| 16) / 2));
|
||||
|
||||
const text_x = switch (text_align) {
|
||||
0 => x + 4, // Left
|
||||
1 => x + @as(i32, @intCast(width / 2)) - @as(i32, @intCast(text.len * 4)), // Center (aprox)
|
||||
2 => x + @as(i32, @intCast(width)) - @as(i32, @intCast(text.len * 8 + 4)), // Right
|
||||
3 => x + 4, // Default left
|
||||
};
|
||||
|
||||
ctx.pushCommand(Command.text(text_x, text_y, text, color));
|
||||
}
|
||||
|
||||
/// Detecta si un click es doble-click
|
||||
pub fn detectDoubleClick(
|
||||
state: *DoubleClickState,
|
||||
current_time: u64,
|
||||
row: i32,
|
||||
col: i32,
|
||||
) bool {
|
||||
const same_cell = state.last_click_row == row and state.last_click_col == col;
|
||||
const time_diff = current_time -| state.last_click_time;
|
||||
const is_double = same_cell and time_diff < state.threshold_ms;
|
||||
|
||||
if (is_double) {
|
||||
// Reset para no detectar triple-click
|
||||
state.last_click_time = 0;
|
||||
state.last_click_row = -1;
|
||||
state.last_click_col = -1;
|
||||
} else {
|
||||
// Guardar para próximo click
|
||||
state.last_click_time = current_time;
|
||||
state.last_click_row = row;
|
||||
state.last_click_col = col;
|
||||
}
|
||||
|
||||
return is_double;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Manejo de teclado para edición
|
||||
// =============================================================================
|
||||
|
||||
/// Resultado de procesar teclado en modo edición
|
||||
pub const EditKeyboardResult = struct {
|
||||
/// Se confirmó la edición (Enter)
|
||||
committed: bool = false,
|
||||
/// Se canceló la edición (Escape)
|
||||
cancelled: bool = false,
|
||||
/// Se revirtió al valor original (primer Escape)
|
||||
reverted: bool = false,
|
||||
/// Se debe navegar a siguiente celda (Tab)
|
||||
navigate_next: bool = false,
|
||||
/// Se debe navegar a celda anterior (Shift+Tab)
|
||||
navigate_prev: bool = false,
|
||||
/// El buffer de edición cambió
|
||||
text_changed: bool = false,
|
||||
};
|
||||
|
||||
/// Procesa teclado en modo edición
|
||||
/// Modifica edit_buffer, edit_len, edit_cursor según las teclas
|
||||
pub fn handleEditingKeyboard(
|
||||
ctx: *Context,
|
||||
edit_buffer: []u8,
|
||||
edit_len: *usize,
|
||||
edit_cursor: *usize,
|
||||
escape_count: *u8,
|
||||
original_text: ?[]const u8,
|
||||
) EditKeyboardResult {
|
||||
var result = EditKeyboardResult{};
|
||||
|
||||
// Escape: cancelar o revertir
|
||||
if (ctx.input.keyPressed(.escape)) {
|
||||
escape_count.* += 1;
|
||||
if (escape_count.* >= 2 or original_text == null) {
|
||||
result.cancelled = true;
|
||||
} else {
|
||||
// Revertir al valor original
|
||||
if (original_text) |orig| {
|
||||
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;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Reset escape count en cualquier otra tecla
|
||||
escape_count.* = 0;
|
||||
|
||||
// Enter: confirmar
|
||||
if (ctx.input.keyPressed(.enter)) {
|
||||
result.committed = true;
|
||||
return result;
|
||||
}
|
||||
|
||||
// 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) {
|
||||
edit_buffer[i] = edit_buffer[i - 1];
|
||||
}
|
||||
edit_buffer[edit_cursor.*] = ch;
|
||||
edit_len.* += 1;
|
||||
edit_cursor.* += 1;
|
||||
result.text_changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Utilidades
|
||||
// =============================================================================
|
||||
|
||||
/// Mezcla dos colores con un factor alpha
|
||||
pub fn blendColor(base: Style.Color, overlay: Style.Color, alpha: f32) Style.Color {
|
||||
const inv_alpha = 1.0 - alpha;
|
||||
|
||||
return Style.Color.rgba(
|
||||
@intFromFloat(@as(f32, @floatFromInt(base.r)) * inv_alpha + @as(f32, @floatFromInt(overlay.r)) * alpha),
|
||||
@intFromFloat(@as(f32, @floatFromInt(base.g)) * inv_alpha + @as(f32, @floatFromInt(overlay.g)) * alpha),
|
||||
@intFromFloat(@as(f32, @floatFromInt(base.b)) * inv_alpha + @as(f32, @floatFromInt(overlay.b)) * alpha),
|
||||
base.a,
|
||||
);
|
||||
}
|
||||
|
||||
/// Compara strings case-insensitive para búsqueda incremental
|
||||
pub fn startsWithIgnoreCase(haystack: []const u8, needle: []const u8) bool {
|
||||
if (needle.len > haystack.len) return false;
|
||||
if (needle.len == 0) return true;
|
||||
|
||||
for (needle, 0..) |needle_char, i| {
|
||||
const haystack_char = haystack[i];
|
||||
const needle_lower = if (needle_char >= 'A' and needle_char <= 'Z')
|
||||
needle_char + 32
|
||||
else
|
||||
needle_char;
|
||||
const haystack_lower = if (haystack_char >= 'A' and haystack_char <= 'Z')
|
||||
haystack_char + 32
|
||||
else
|
||||
haystack_char;
|
||||
|
||||
if (needle_lower != haystack_lower) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Tests
|
||||
// =============================================================================
|
||||
|
||||
test "blendColor" {
|
||||
const white = Style.Color.rgb(255, 255, 255);
|
||||
const black = Style.Color.rgb(0, 0, 0);
|
||||
|
||||
const gray = blendColor(white, black, 0.5);
|
||||
try std.testing.expectEqual(@as(u8, 127), gray.r);
|
||||
try std.testing.expectEqual(@as(u8, 127), gray.g);
|
||||
try std.testing.expectEqual(@as(u8, 127), gray.b);
|
||||
}
|
||||
|
||||
test "startsWithIgnoreCase" {
|
||||
try std.testing.expect(startsWithIgnoreCase("Hello World", "Hello"));
|
||||
try std.testing.expect(startsWithIgnoreCase("Hello World", "hello"));
|
||||
try std.testing.expect(startsWithIgnoreCase("hello world", "HELLO"));
|
||||
try std.testing.expect(startsWithIgnoreCase("anything", ""));
|
||||
try std.testing.expect(!startsWithIgnoreCase("Hello", "World"));
|
||||
try std.testing.expect(!startsWithIgnoreCase("Hi", "Hello World"));
|
||||
}
|
||||
|
||||
test "detectDoubleClick" {
|
||||
var state = DoubleClickState{};
|
||||
|
||||
// Primer click
|
||||
const first = detectDoubleClick(&state, 1000, 0, 0);
|
||||
try std.testing.expect(!first);
|
||||
|
||||
// Segundo click rápido en misma celda = doble click
|
||||
const second = detectDoubleClick(&state, 1200, 0, 0);
|
||||
try std.testing.expect(second);
|
||||
|
||||
// Tercer click (estado reseteado)
|
||||
const third = detectDoubleClick(&state, 1400, 0, 0);
|
||||
try std.testing.expect(!third);
|
||||
}
|
||||
422
src/widgets/table_core.zig
Normal file
422
src/widgets/table_core.zig
Normal file
|
|
@ -0,0 +1,422 @@
|
|||
//! Table Core - Funciones compartidas para renderizado de tablas
|
||||
//!
|
||||
//! Este módulo contiene la lógica común de renderizado utilizada por:
|
||||
//! - AdvancedTable (datos en memoria)
|
||||
//! - VirtualAdvancedTable (datos paginados desde DataProvider)
|
||||
//!
|
||||
//! Principio: Una sola implementación de UI, dos estrategias de datos.
|
||||
|
||||
const std = @import("std");
|
||||
const Context = @import("../core/context.zig").Context;
|
||||
const Command = @import("../core/command.zig");
|
||||
const Layout = @import("../core/layout.zig");
|
||||
const Style = @import("../core/style.zig");
|
||||
|
||||
// =============================================================================
|
||||
// Tipos comunes
|
||||
// =============================================================================
|
||||
|
||||
/// Colores para renderizado de tabla
|
||||
pub const TableColors = struct {
|
||||
// Fondos
|
||||
background: Style.Color = Style.Color.rgb(30, 30, 35),
|
||||
row_normal: Style.Color = Style.Color.rgb(35, 35, 40),
|
||||
row_alternate: Style.Color = Style.Color.rgb(40, 40, 45),
|
||||
row_hover: Style.Color = Style.Color.rgb(50, 50, 60),
|
||||
selected_row: Style.Color = Style.Color.rgb(0, 90, 180),
|
||||
selected_row_unfocus: Style.Color = Style.Color.rgb(60, 60, 70),
|
||||
|
||||
// Celda activa
|
||||
selected_cell: Style.Color = Style.Color.rgb(100, 150, 255),
|
||||
selected_cell_unfocus: Style.Color = Style.Color.rgb(80, 80, 90),
|
||||
|
||||
// Edición
|
||||
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),
|
||||
|
||||
// Header
|
||||
header_bg: Style.Color = Style.Color.rgb(45, 45, 50),
|
||||
header_fg: Style.Color = Style.Color.rgb(200, 200, 200),
|
||||
|
||||
// Texto
|
||||
text_normal: Style.Color = Style.Color.rgb(220, 220, 220),
|
||||
text_selected: Style.Color = Style.Color.rgb(255, 255, 255),
|
||||
text_placeholder: Style.Color = Style.Color.rgb(128, 128, 128),
|
||||
|
||||
// Bordes
|
||||
border: Style.Color = Style.Color.rgb(60, 60, 65),
|
||||
focus_ring: Style.Color = Style.Color.rgb(0, 120, 215),
|
||||
};
|
||||
|
||||
/// Información de una celda para renderizado
|
||||
pub const CellRenderInfo = struct {
|
||||
/// Texto a mostrar
|
||||
text: []const u8,
|
||||
/// Posición X de la celda
|
||||
x: i32,
|
||||
/// Ancho de la celda
|
||||
width: u32,
|
||||
/// Es la celda actualmente seleccionada
|
||||
is_selected: bool = false,
|
||||
/// Es editable
|
||||
is_editable: bool = true,
|
||||
/// Alineación del texto (0=left, 1=center, 2=right)
|
||||
text_align: u2 = 0,
|
||||
};
|
||||
|
||||
/// Estado de edición para renderizado
|
||||
pub const EditState = struct {
|
||||
/// Está en modo edición
|
||||
editing: bool = false,
|
||||
/// Fila en edición
|
||||
edit_row: i32 = -1,
|
||||
/// Columna en edición
|
||||
edit_col: i32 = -1,
|
||||
/// Buffer de texto actual
|
||||
edit_text: []const u8 = "",
|
||||
/// Posición del cursor
|
||||
edit_cursor: usize = 0,
|
||||
};
|
||||
|
||||
/// Estado de doble-click
|
||||
pub const DoubleClickState = struct {
|
||||
last_click_time: u64 = 0,
|
||||
last_click_row: i64 = -1,
|
||||
last_click_col: i32 = -1,
|
||||
threshold_ms: u64 = 400,
|
||||
};
|
||||
|
||||
/// Resultado de procesar click en celda
|
||||
pub const CellClickResult = struct {
|
||||
/// Hubo click
|
||||
clicked: bool = false,
|
||||
/// Fue doble-click
|
||||
double_click: bool = false,
|
||||
/// Fila clickeada
|
||||
row: usize = 0,
|
||||
/// Columna clickeada
|
||||
col: usize = 0,
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// Funciones de renderizado
|
||||
// =============================================================================
|
||||
|
||||
/// Dibuja el indicador de celda activa (fondo + borde)
|
||||
/// Llamar ANTES de dibujar el texto de la celda
|
||||
pub fn drawCellActiveIndicator(
|
||||
ctx: *Context,
|
||||
x: i32,
|
||||
y: i32,
|
||||
width: u32,
|
||||
height: u32,
|
||||
row_bg: Style.Color,
|
||||
colors: *const TableColors,
|
||||
has_focus: bool,
|
||||
) void {
|
||||
if (has_focus) {
|
||||
// Con focus: fondo más visible + borde doble
|
||||
const tinted_bg = blendColor(row_bg, colors.selected_cell, 0.35);
|
||||
ctx.pushCommand(Command.rect(x, y, width, height, tinted_bg));
|
||||
ctx.pushCommand(Command.rectOutline(x, y, width, height, colors.selected_cell));
|
||||
ctx.pushCommand(Command.rectOutline(x + 1, y + 1, width -| 2, height -| 2, colors.selected_cell));
|
||||
} else {
|
||||
// Sin focus: indicación más sutil
|
||||
const tinted_bg = blendColor(row_bg, colors.selected_cell_unfocus, 0.15);
|
||||
ctx.pushCommand(Command.rect(x, y, width, height, tinted_bg));
|
||||
ctx.pushCommand(Command.rectOutline(x, y, width, height, colors.border));
|
||||
}
|
||||
}
|
||||
|
||||
/// Dibuja el overlay de edición de celda
|
||||
pub fn drawEditingOverlay(
|
||||
ctx: *Context,
|
||||
x: i32,
|
||||
y: i32,
|
||||
width: u32,
|
||||
height: u32,
|
||||
edit_text: []const u8,
|
||||
cursor_pos: usize,
|
||||
colors: *const TableColors,
|
||||
) void {
|
||||
// Fondo blanco
|
||||
ctx.pushCommand(Command.rect(x, y, width, height, colors.cell_editing_bg));
|
||||
|
||||
// Borde azul
|
||||
ctx.pushCommand(Command.rectOutline(x, y, width, height, colors.cell_editing_border));
|
||||
|
||||
// Texto
|
||||
const text_y = y + @as(i32, @intCast((height -| 16) / 2));
|
||||
const text_to_show = if (edit_text.len > 0) edit_text else "";
|
||||
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
|
||||
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(
|
||||
ctx: *Context,
|
||||
x: i32,
|
||||
y: i32,
|
||||
width: u32,
|
||||
height: u32,
|
||||
text: []const u8,
|
||||
color: Style.Color,
|
||||
text_align: u2,
|
||||
) void {
|
||||
const text_y = y + @as(i32, @intCast((height -| 16) / 2));
|
||||
|
||||
const text_x = switch (text_align) {
|
||||
0 => x + 4, // Left
|
||||
1 => x + @as(i32, @intCast(width / 2)) - @as(i32, @intCast(text.len * 4)), // Center (aprox)
|
||||
2 => x + @as(i32, @intCast(width)) - @as(i32, @intCast(text.len * 8 + 4)), // Right
|
||||
3 => x + 4, // Default left
|
||||
};
|
||||
|
||||
ctx.pushCommand(Command.text(text_x, text_y, text, color));
|
||||
}
|
||||
|
||||
/// Detecta si un click es doble-click
|
||||
pub fn detectDoubleClick(
|
||||
state: *DoubleClickState,
|
||||
current_time: u64,
|
||||
row: i64,
|
||||
col: i32,
|
||||
) bool {
|
||||
const same_cell = state.last_click_row == row and state.last_click_col == col;
|
||||
const time_diff = current_time -| state.last_click_time;
|
||||
const is_double = same_cell and time_diff < state.threshold_ms;
|
||||
|
||||
if (is_double) {
|
||||
// Reset para no detectar triple-click
|
||||
state.last_click_time = 0;
|
||||
state.last_click_row = -1;
|
||||
state.last_click_col = -1;
|
||||
} else {
|
||||
// Guardar para próximo click
|
||||
state.last_click_time = current_time;
|
||||
state.last_click_row = row;
|
||||
state.last_click_col = col;
|
||||
}
|
||||
|
||||
return is_double;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Manejo de teclado para edición
|
||||
// =============================================================================
|
||||
|
||||
/// Resultado de procesar teclado en modo edición
|
||||
pub const EditKeyboardResult = struct {
|
||||
/// Se confirmó la edición (Enter)
|
||||
committed: bool = false,
|
||||
/// Se canceló la edición (Escape)
|
||||
cancelled: bool = false,
|
||||
/// Se revirtió al valor original (primer Escape)
|
||||
reverted: bool = false,
|
||||
/// Se debe navegar a siguiente celda (Tab)
|
||||
navigate_next: bool = false,
|
||||
/// Se debe navegar a celda anterior (Shift+Tab)
|
||||
navigate_prev: bool = false,
|
||||
/// El buffer de edición cambió
|
||||
text_changed: bool = false,
|
||||
};
|
||||
|
||||
/// Procesa teclado en modo edición
|
||||
/// Modifica edit_buffer, edit_len, edit_cursor según las teclas
|
||||
pub fn handleEditingKeyboard(
|
||||
ctx: *Context,
|
||||
edit_buffer: []u8,
|
||||
edit_len: *usize,
|
||||
edit_cursor: *usize,
|
||||
escape_count: *u8,
|
||||
original_text: ?[]const u8,
|
||||
) EditKeyboardResult {
|
||||
var result = EditKeyboardResult{};
|
||||
|
||||
// Escape: cancelar o revertir
|
||||
if (ctx.input.keyPressed(.escape)) {
|
||||
escape_count.* += 1;
|
||||
if (escape_count.* >= 2 or original_text == null) {
|
||||
result.cancelled = true;
|
||||
} else {
|
||||
// Revertir al valor original
|
||||
if (original_text) |orig| {
|
||||
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;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Reset escape count en cualquier otra tecla
|
||||
escape_count.* = 0;
|
||||
|
||||
// Enter: confirmar
|
||||
if (ctx.input.keyPressed(.enter)) {
|
||||
result.committed = true;
|
||||
return result;
|
||||
}
|
||||
|
||||
// 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) {
|
||||
edit_buffer[i] = edit_buffer[i - 1];
|
||||
}
|
||||
edit_buffer[edit_cursor.*] = ch;
|
||||
edit_len.* += 1;
|
||||
edit_cursor.* += 1;
|
||||
result.text_changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Utilidades
|
||||
// =============================================================================
|
||||
|
||||
/// Mezcla dos colores con un factor alpha
|
||||
pub fn blendColor(base: Style.Color, overlay: Style.Color, alpha: f32) Style.Color {
|
||||
const inv_alpha = 1.0 - alpha;
|
||||
|
||||
return Style.Color.rgba(
|
||||
@intFromFloat(@as(f32, @floatFromInt(base.r)) * inv_alpha + @as(f32, @floatFromInt(overlay.r)) * alpha),
|
||||
@intFromFloat(@as(f32, @floatFromInt(base.g)) * inv_alpha + @as(f32, @floatFromInt(overlay.g)) * alpha),
|
||||
@intFromFloat(@as(f32, @floatFromInt(base.b)) * inv_alpha + @as(f32, @floatFromInt(overlay.b)) * alpha),
|
||||
base.a,
|
||||
);
|
||||
}
|
||||
|
||||
/// Compara strings case-insensitive para búsqueda incremental
|
||||
pub fn startsWithIgnoreCase(haystack: []const u8, needle: []const u8) bool {
|
||||
if (needle.len > haystack.len) return false;
|
||||
if (needle.len == 0) return true;
|
||||
|
||||
for (needle, 0..) |needle_char, i| {
|
||||
const haystack_char = haystack[i];
|
||||
const needle_lower = if (needle_char >= 'A' and needle_char <= 'Z')
|
||||
needle_char + 32
|
||||
else
|
||||
needle_char;
|
||||
const haystack_lower = if (haystack_char >= 'A' and haystack_char <= 'Z')
|
||||
haystack_char + 32
|
||||
else
|
||||
haystack_char;
|
||||
|
||||
if (needle_lower != haystack_lower) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Tests
|
||||
// =============================================================================
|
||||
|
||||
test "blendColor" {
|
||||
const white = Style.Color.rgb(255, 255, 255);
|
||||
const black = Style.Color.rgb(0, 0, 0);
|
||||
|
||||
const gray = blendColor(white, black, 0.5);
|
||||
try std.testing.expectEqual(@as(u8, 127), gray.r);
|
||||
try std.testing.expectEqual(@as(u8, 127), gray.g);
|
||||
try std.testing.expectEqual(@as(u8, 127), gray.b);
|
||||
}
|
||||
|
||||
test "startsWithIgnoreCase" {
|
||||
try std.testing.expect(startsWithIgnoreCase("Hello World", "Hello"));
|
||||
try std.testing.expect(startsWithIgnoreCase("Hello World", "hello"));
|
||||
try std.testing.expect(startsWithIgnoreCase("hello world", "HELLO"));
|
||||
try std.testing.expect(startsWithIgnoreCase("anything", ""));
|
||||
try std.testing.expect(!startsWithIgnoreCase("Hello", "World"));
|
||||
try std.testing.expect(!startsWithIgnoreCase("Hi", "Hello World"));
|
||||
}
|
||||
|
||||
test "detectDoubleClick" {
|
||||
var state = DoubleClickState{};
|
||||
|
||||
// Primer click
|
||||
const first = detectDoubleClick(&state, 1000, 0, 0);
|
||||
try std.testing.expect(!first);
|
||||
|
||||
// Segundo click rápido en misma celda = doble click
|
||||
const second = detectDoubleClick(&state, 1200, 0, 0);
|
||||
try std.testing.expect(second);
|
||||
|
||||
// Tercer click (estado reseteado)
|
||||
const third = detectDoubleClick(&state, 1400, 0, 0);
|
||||
try std.testing.expect(!third);
|
||||
}
|
||||
|
|
@ -27,6 +27,22 @@ pub const VirtualAdvancedTableState = struct {
|
|||
/// Cuando el usuario navega con flechas o hace click, se actualiza
|
||||
active_col: usize = 0,
|
||||
|
||||
// =========================================================================
|
||||
// Double-click detection
|
||||
// =========================================================================
|
||||
|
||||
/// Time of last click (ms)
|
||||
last_click_time: u64 = 0,
|
||||
|
||||
/// Row of last click (global index)
|
||||
last_click_row: i64 = -1,
|
||||
|
||||
/// Column of last click
|
||||
last_click_col: i32 = -1,
|
||||
|
||||
/// Double-click threshold in ms
|
||||
double_click_threshold_ms: u64 = 400,
|
||||
|
||||
// =========================================================================
|
||||
// Scroll y ventana
|
||||
// =========================================================================
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ const Layout = @import("../../core/layout.zig");
|
|||
const Style = @import("../../core/style.zig");
|
||||
const Input = @import("../../core/input.zig");
|
||||
const text_input = @import("../text_input.zig");
|
||||
const table_core = @import("../table_core.zig");
|
||||
|
||||
// Re-exports públicos
|
||||
pub const types = @import("types.zig");
|
||||
|
|
@ -756,6 +757,29 @@ fn drawRows(
|
|||
const col_end = x + @as(i32, @intCast(col.width));
|
||||
// Only draw if column is visible
|
||||
if (col_end > content_bounds.x and x < content_bounds.x + @as(i32, @intCast(content_bounds.w))) {
|
||||
// Check if this is the active cell
|
||||
const is_active_cell = is_selected and list_state.active_col == col_idx;
|
||||
|
||||
// Draw active cell indicator BEFORE text
|
||||
if (is_active_cell) {
|
||||
// Convertir colores del config a TableColors para table_core
|
||||
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,
|
||||
.border = colors.border,
|
||||
};
|
||||
table_core.drawCellActiveIndicator(
|
||||
ctx,
|
||||
x,
|
||||
row_y,
|
||||
col.width,
|
||||
row_h,
|
||||
bg_color,
|
||||
&tc_colors,
|
||||
list_state.has_focus,
|
||||
);
|
||||
}
|
||||
|
||||
if (col_idx < row.values.len) {
|
||||
const text_color = if (is_selected and list_state.has_focus)
|
||||
colors.text_selected
|
||||
|
|
@ -1000,8 +1024,6 @@ fn handleMouseClick(
|
|||
list_state: *VirtualAdvancedTableState,
|
||||
result: *VirtualAdvancedTableResult,
|
||||
) void {
|
||||
_ = result;
|
||||
|
||||
const mouse = ctx.input.mousePos();
|
||||
// Content starts after FilterBar + Header
|
||||
const content_y = bounds.y + @as(i32, @intCast(filter_bar_h)) + @as(i32, @intCast(header_h));
|
||||
|
|
@ -1016,22 +1038,54 @@ fn handleMouseClick(
|
|||
const data_idx = window_offset + screen_row;
|
||||
|
||||
if (data_idx < list_state.current_window.len) {
|
||||
list_state.selectById(list_state.current_window[data_idx].id);
|
||||
const global_row = list_state.scroll_offset + screen_row;
|
||||
|
||||
// Detect which column was clicked
|
||||
var clicked_col: usize = 0;
|
||||
const relative_x = mouse.x - bounds.x + list_state.scroll_offset_x;
|
||||
var col_start: i32 = 0;
|
||||
for (config.columns, 0..) |col, col_idx| {
|
||||
const col_end = col_start + @as(i32, @intCast(col.width));
|
||||
if (relative_x >= col_start and relative_x < col_end) {
|
||||
list_state.active_col = col_idx;
|
||||
clicked_col = col_idx;
|
||||
break;
|
||||
}
|
||||
col_start = col_end;
|
||||
}
|
||||
|
||||
// TODO: implement double click detection with timing
|
||||
// For now, double click is not supported
|
||||
// Double-click detection using table_core
|
||||
var dc_state = table_core.DoubleClickState{
|
||||
.last_click_time = list_state.last_click_time,
|
||||
.last_click_row = list_state.last_click_row,
|
||||
.last_click_col = list_state.last_click_col,
|
||||
.threshold_ms = list_state.double_click_threshold_ms,
|
||||
};
|
||||
|
||||
const is_double_click = table_core.detectDoubleClick(
|
||||
&dc_state,
|
||||
ctx.current_time_ms,
|
||||
@intCast(global_row),
|
||||
@intCast(clicked_col),
|
||||
);
|
||||
|
||||
// Update state from detection
|
||||
list_state.last_click_time = dc_state.last_click_time;
|
||||
list_state.last_click_row = dc_state.last_click_row;
|
||||
list_state.last_click_col = dc_state.last_click_col;
|
||||
|
||||
if (is_double_click and !list_state.isEditing()) {
|
||||
// Double-click: start editing
|
||||
const cell = types.CellId{ .row = global_row, .col = clicked_col };
|
||||
// Signal to panel that editing was requested
|
||||
// The panel should provide the current value via callback
|
||||
result.edited_cell = cell;
|
||||
result.double_clicked = true;
|
||||
result.double_click_id = list_state.current_window[data_idx].id;
|
||||
} else {
|
||||
// Single click: select
|
||||
list_state.selectById(list_state.current_window[data_idx].id);
|
||||
list_state.active_col = clicked_col;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,6 +63,9 @@ pub const sheet = @import("sheet.zig");
|
|||
pub const discloser = @import("discloser.zig");
|
||||
pub const selectable = @import("selectable.zig");
|
||||
|
||||
// Core table utilities (shared between AdvancedTable and VirtualAdvancedTable)
|
||||
pub const table_core = @import("table_core.zig");
|
||||
|
||||
// Advanced widgets
|
||||
pub const advanced_table = @import("advanced_table/advanced_table.zig");
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue