Compare commits
No commits in common. "08ffcdbac514df206b70c88ce674a193ee9935dd" and "d16019d54ffd6f9232a6d775d3a0b92945e45061" have entirely different histories.
08ffcdbac5
...
d16019d54f
9 changed files with 818 additions and 1229 deletions
|
|
@ -47,10 +47,6 @@ pub const state = @import("state.zig");
|
|||
pub const AdvancedTableState = state.AdvancedTableState;
|
||||
pub const AdvancedTableResult = state.AdvancedTableResult;
|
||||
|
||||
// Re-export datasource
|
||||
pub const datasource = @import("datasource.zig");
|
||||
pub const MemoryDataSource = datasource.MemoryDataSource;
|
||||
|
||||
// Re-export table_core types
|
||||
pub const NavigateDirection = table_core.NavigateDirection;
|
||||
|
||||
|
|
@ -139,62 +135,21 @@ pub fn advancedTableRect(
|
|||
const first_visible = table_state.scroll_row;
|
||||
const last_visible = @min(first_visible + visible_rows, table_state.getRowCount());
|
||||
|
||||
// Manejar clicks en filas (separado del renderizado)
|
||||
handleRowClicks(ctx, bounds, table_state, table_schema, header_h, state_col_w, first_visible, last_visible, &result);
|
||||
// Draw visible rows
|
||||
for (first_visible..last_visible) |row_idx| {
|
||||
const row_y = bounds.y + @as(i32, @intCast(header_h)) +
|
||||
@as(i32, @intCast((row_idx - first_visible) * config.row_height));
|
||||
|
||||
// Construir ColumnRenderDefs para la función unificada
|
||||
var col_defs: [64]table_core.ColumnRenderDef = undefined;
|
||||
var col_count: usize = 0;
|
||||
for (table_schema.columns) |col| {
|
||||
if (col_count >= 64) break;
|
||||
col_defs[col_count] = .{
|
||||
.width = col.width,
|
||||
.visible = col.visible,
|
||||
.text_align = 0, // Por ahora left-align
|
||||
};
|
||||
col_count += 1;
|
||||
const row_bounds = Layout.Rect.init(
|
||||
bounds.x,
|
||||
row_y,
|
||||
bounds.w,
|
||||
config.row_height,
|
||||
);
|
||||
|
||||
drawRow(ctx, row_bounds, table_state, table_schema, row_idx, state_col_w, colors, has_focus, &result);
|
||||
}
|
||||
|
||||
// Crear MemoryDataSource y dibujar filas con función unificada
|
||||
var memory_ds = MemoryDataSource.init(table_state, table_schema.columns);
|
||||
const data_src = memory_ds.toDataSource();
|
||||
|
||||
// Construir RowRenderColors manualmente (los dos TableColors son tipos diferentes)
|
||||
const render_colors = table_core.RowRenderColors{
|
||||
.row_normal = colors.row_normal,
|
||||
.row_alternate = colors.row_alternate,
|
||||
.selected_row = colors.selected_row,
|
||||
.selected_row_unfocus = colors.selected_row_unfocus,
|
||||
.selected_cell = colors.selected_cell,
|
||||
.selected_cell_unfocus = Style.Color.rgb(80, 80, 90), // Default similar a table_core
|
||||
.text_normal = colors.text_normal,
|
||||
.text_selected = colors.text_selected,
|
||||
.border = colors.border,
|
||||
.state_modified = colors.state_modified,
|
||||
.state_new = colors.state_new,
|
||||
.state_deleted = colors.state_deleted,
|
||||
.state_error = colors.state_error,
|
||||
};
|
||||
|
||||
var cell_buffer: [256]u8 = undefined;
|
||||
_ = table_core.drawRowsWithDataSource(ctx, data_src, .{
|
||||
.bounds_x = bounds.x,
|
||||
.bounds_y = bounds.y + @as(i32, @intCast(header_h)),
|
||||
.bounds_w = bounds.w,
|
||||
.row_height = config.row_height,
|
||||
.first_row = first_visible,
|
||||
.last_row = last_visible,
|
||||
.has_focus = has_focus,
|
||||
.selected_row = table_state.selected_row,
|
||||
.active_col = @intCast(@max(0, table_state.selected_col)),
|
||||
.colors = render_colors,
|
||||
.columns = col_defs[0..col_count],
|
||||
.state_indicator_width = state_col_w,
|
||||
.apply_state_colors = true,
|
||||
.draw_row_borders = true,
|
||||
.alternating_rows = config.alternating_rows,
|
||||
}, &cell_buffer);
|
||||
|
||||
// End clipping
|
||||
ctx.pushCommand(Command.clipEnd());
|
||||
|
||||
|
|
@ -220,7 +175,7 @@ pub fn advancedTableRect(
|
|||
|
||||
// Handle keyboard
|
||||
if (has_focus) {
|
||||
if (table_state.isEditing()) {
|
||||
if (table_state.editing) {
|
||||
// Handle editing keyboard
|
||||
handleEditingKeyboard(ctx, table_state, table_schema, &result);
|
||||
|
||||
|
|
@ -300,101 +255,6 @@ fn invokeCallbacks(
|
|||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Row Click Handling (separado del rendering)
|
||||
// =============================================================================
|
||||
|
||||
/// Maneja clicks en las filas de la tabla (single-click y double-click)
|
||||
/// Retorna si hubo algún cambio de selección o edición iniciada
|
||||
fn handleRowClicks(
|
||||
ctx: *Context,
|
||||
bounds: Layout.Rect,
|
||||
table_state: *AdvancedTableState,
|
||||
table_schema: *const TableSchema,
|
||||
header_h: u32,
|
||||
state_col_w: u32,
|
||||
first_visible: usize,
|
||||
last_visible: usize,
|
||||
result: *AdvancedTableResult,
|
||||
) void {
|
||||
const config = table_schema.config;
|
||||
const mouse = ctx.input.mousePos();
|
||||
|
||||
// Solo procesar si hubo click
|
||||
if (!ctx.input.mousePressed(.left)) return;
|
||||
|
||||
// Verificar si el click está en el área de filas
|
||||
const rows_area_y = bounds.y + @as(i32, @intCast(header_h));
|
||||
if (mouse.y < rows_area_y) return;
|
||||
if (mouse.x < bounds.x or mouse.x >= bounds.x + @as(i32, @intCast(bounds.w))) return;
|
||||
|
||||
// Calcular fila clickeada
|
||||
const relative_y = mouse.y - rows_area_y;
|
||||
if (relative_y < 0) return;
|
||||
const row_offset: usize = @intCast(@divFloor(relative_y, @as(i32, @intCast(config.row_height))));
|
||||
const row_idx = first_visible + row_offset;
|
||||
|
||||
if (row_idx >= last_visible or row_idx >= table_state.getRowCount()) return;
|
||||
|
||||
// Calcular columna clickeada
|
||||
var col_x = bounds.x + @as(i32, @intCast(state_col_w));
|
||||
var clicked_col: ?usize = null;
|
||||
|
||||
for (table_schema.columns, 0..) |col, col_idx| {
|
||||
if (!col.visible) continue;
|
||||
|
||||
const col_end = col_x + @as(i32, @intCast(col.width));
|
||||
if (mouse.x >= col_x and mouse.x < col_end) {
|
||||
clicked_col = col_idx;
|
||||
break;
|
||||
}
|
||||
col_x = col_end;
|
||||
}
|
||||
|
||||
if (clicked_col == null) return;
|
||||
|
||||
const col_idx = clicked_col.?;
|
||||
const is_selected_cell = table_state.selected_row == @as(i32, @intCast(row_idx)) and
|
||||
table_state.selected_col == @as(i32, @intCast(col_idx));
|
||||
|
||||
// Detectar doble-click
|
||||
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_idx < table_schema.columns.len and
|
||||
table_schema.columns[col_idx].editable and !table_state.isEditing())
|
||||
{
|
||||
// Double-click: iniciar edición
|
||||
if (table_state.getRow(row_idx)) |row| {
|
||||
const value = row.get(table_schema.columns[col_idx].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: seleccionar celda
|
||||
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;
|
||||
}
|
||||
// Actualizar tracking para posible doble-click
|
||||
table_state.last_click_time = current_time;
|
||||
table_state.last_click_row = @intCast(row_idx);
|
||||
table_state.last_click_col = @intCast(col_idx);
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Internal Rendering
|
||||
// =============================================================================
|
||||
|
|
@ -490,6 +350,167 @@ fn drawHeader(
|
|||
}
|
||||
}
|
||||
|
||||
fn drawRow(
|
||||
ctx: *Context,
|
||||
bounds: Layout.Rect,
|
||||
table_state: *AdvancedTableState,
|
||||
table_schema: *const TableSchema,
|
||||
row_idx: usize,
|
||||
state_col_w: u32,
|
||||
colors: *const TableColors,
|
||||
has_focus: bool,
|
||||
result: *AdvancedTableResult,
|
||||
) void {
|
||||
const config = table_schema.config;
|
||||
const is_selected_row = table_state.selected_row == @as(i32, @intCast(row_idx));
|
||||
const row_state = table_state.getRowState(row_idx);
|
||||
|
||||
// Determine row background color
|
||||
var row_bg = if (config.alternating_rows and row_idx % 2 == 1)
|
||||
colors.row_alternate
|
||||
else
|
||||
colors.row_normal;
|
||||
|
||||
// Apply state color overlay
|
||||
row_bg = switch (row_state) {
|
||||
.modified => blendColor(row_bg, colors.state_modified, 0.2),
|
||||
.new => blendColor(row_bg, colors.state_new, 0.2),
|
||||
.deleted => blendColor(row_bg, colors.state_deleted, 0.3),
|
||||
.@"error" => blendColor(row_bg, colors.state_error, 0.3),
|
||||
.normal => row_bg,
|
||||
};
|
||||
|
||||
// Selection overlay - SOLO la fila seleccionada cambia de color
|
||||
// El color depende de si la tabla tiene focus
|
||||
if (is_selected_row) {
|
||||
row_bg = if (has_focus) colors.selected_row else colors.selected_row_unfocus;
|
||||
}
|
||||
// Las filas NO seleccionadas mantienen row_bg (row_normal o row_alternate)
|
||||
|
||||
// Draw row background
|
||||
ctx.pushCommand(Command.rect(bounds.x, bounds.y, bounds.w, config.row_height, row_bg));
|
||||
|
||||
var col_x = bounds.x;
|
||||
const mouse = ctx.input.mousePos();
|
||||
|
||||
// State indicator column
|
||||
if (state_col_w > 0) {
|
||||
drawStateIndicator(ctx, col_x, bounds.y, state_col_w, config.row_height, row_state, colors);
|
||||
col_x += @as(i32, @intCast(state_col_w));
|
||||
}
|
||||
|
||||
// Data cells
|
||||
for (table_schema.columns, 0..) |col, col_idx| {
|
||||
if (!col.visible) continue;
|
||||
|
||||
const cell_rect = Layout.Rect.init(col_x, bounds.y, col.width, config.row_height);
|
||||
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 - 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
|
||||
if (table_state.getRow(row_idx)) |row| {
|
||||
const value = row.get(col.name);
|
||||
var format_buf: [128]u8 = undefined;
|
||||
const formatted = value.format(&format_buf);
|
||||
|
||||
// Copy text to frame arena to ensure it persists until rendering
|
||||
// (format_buf is stack-allocated and goes out of scope)
|
||||
const text = ctx.frameAllocator().dupe(u8, formatted) catch formatted;
|
||||
|
||||
// Draw cell text
|
||||
const text_y = bounds.y + @as(i32, @intCast((config.row_height - 8) / 2));
|
||||
const text_color = if (is_selected_cell) colors.text_selected else colors.text_normal;
|
||||
ctx.pushCommand(Command.text(col_x + 4, text_y, text, text_color));
|
||||
}
|
||||
|
||||
// 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));
|
||||
}
|
||||
|
||||
// Bottom border
|
||||
ctx.pushCommand(Command.rect(
|
||||
bounds.x,
|
||||
bounds.y + @as(i32, @intCast(config.row_height)) - 1,
|
||||
bounds.w,
|
||||
1,
|
||||
colors.border,
|
||||
));
|
||||
}
|
||||
|
||||
fn drawStateIndicator(
|
||||
ctx: *Context,
|
||||
x: i32,
|
||||
y: i32,
|
||||
w: u32,
|
||||
h: u32,
|
||||
row_state: RowState,
|
||||
colors: *const TableColors,
|
||||
) void {
|
||||
const indicator_size: u32 = 8;
|
||||
const indicator_x = x + @as(i32, @intCast((w - indicator_size) / 2));
|
||||
const indicator_y = y + @as(i32, @intCast((h - indicator_size) / 2));
|
||||
|
||||
const color = switch (row_state) {
|
||||
.modified => colors.indicator_modified,
|
||||
.new => colors.indicator_new,
|
||||
.deleted => colors.indicator_deleted,
|
||||
.@"error" => colors.state_error,
|
||||
.normal => return, // No indicator
|
||||
};
|
||||
|
||||
// Draw circle indicator
|
||||
ctx.pushCommand(Command.rect(indicator_x, indicator_y, indicator_size, indicator_size, color));
|
||||
}
|
||||
|
||||
fn drawScrollbar(
|
||||
ctx: *Context,
|
||||
bounds: Layout.Rect,
|
||||
|
|
@ -573,7 +594,7 @@ fn drawEditingOverlay(
|
|||
ctx.pushCommand(Command.text(col_x + 4, text_y, edit_text, colors.text_selected));
|
||||
|
||||
// Draw cursor
|
||||
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.edit_cursor * 8));
|
||||
ctx.pushCommand(Command.rect(cursor_x, text_y, 1, 8, colors.text_selected));
|
||||
}
|
||||
|
||||
|
|
@ -912,10 +933,10 @@ fn handleEditingKeyboard(
|
|||
// Usar table_core para procesamiento de teclado (DRY)
|
||||
const kb_result = table_core.handleEditingKeyboard(
|
||||
ctx,
|
||||
&table_state.cell_edit.edit_buffer,
|
||||
&table_state.cell_edit.edit_len,
|
||||
&table_state.cell_edit.edit_cursor,
|
||||
&table_state.cell_edit.escape_count,
|
||||
&table_state.edit_buffer,
|
||||
&table_state.edit_len,
|
||||
&table_state.edit_cursor,
|
||||
&table_state.escape_count,
|
||||
original_text,
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,140 +0,0 @@
|
|||
//! MemoryDataSource - Adaptador de TableDataSource para datos en memoria
|
||||
//!
|
||||
//! Wrappea AdvancedTableState + columnas para implementar la interfaz
|
||||
//! TableDataSource de table_core. Permite que AdvancedTable use el mismo
|
||||
//! patrón de renderizado que VirtualAdvancedTable.
|
||||
|
||||
const std = @import("std");
|
||||
const table_core = @import("../table_core.zig");
|
||||
const state_mod = @import("state.zig");
|
||||
const schema_mod = @import("schema.zig");
|
||||
const types = @import("types.zig");
|
||||
|
||||
const AdvancedTableState = state_mod.AdvancedTableState;
|
||||
const ColumnDef = schema_mod.ColumnDef;
|
||||
const CellValue = types.CellValue;
|
||||
const Row = types.Row;
|
||||
const TableDataSource = table_core.TableDataSource;
|
||||
|
||||
// =============================================================================
|
||||
// MemoryDataSource
|
||||
// =============================================================================
|
||||
|
||||
/// Adaptador que implementa TableDataSource para datos en memoria (ArrayList)
|
||||
/// Usa el patrón getCellValueInto para escritura segura en buffer proporcionado
|
||||
pub const MemoryDataSource = struct {
|
||||
/// Referencia al estado de la tabla
|
||||
state: *AdvancedTableState,
|
||||
|
||||
/// Definiciones de columnas (para mapear índice -> nombre)
|
||||
columns: []const ColumnDef,
|
||||
|
||||
/// Buffer interno para formateo (evita allocaciones)
|
||||
format_buffer: [256]u8 = undefined,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
/// Crea un nuevo MemoryDataSource
|
||||
pub fn init(state: *AdvancedTableState, columns: []const ColumnDef) Self {
|
||||
return .{
|
||||
.state = state,
|
||||
.columns = columns,
|
||||
};
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Implementación de TableDataSource
|
||||
// =========================================================================
|
||||
|
||||
/// Retorna el número total de filas
|
||||
pub fn getRowCount(self: *Self) usize {
|
||||
return self.state.getRowCount();
|
||||
}
|
||||
|
||||
/// Escribe el valor de una celda en el buffer proporcionado
|
||||
/// Retorna slice del buffer con el contenido
|
||||
pub fn getCellValueInto(self: *Self, row: usize, col: usize, buf: []u8) []const u8 {
|
||||
// Validar índices
|
||||
if (col >= self.columns.len) return "";
|
||||
|
||||
// Obtener fila
|
||||
const row_data = self.state.getRowConst(row) orelse return "";
|
||||
|
||||
// Obtener nombre de columna
|
||||
const col_name = self.columns[col].name;
|
||||
|
||||
// Obtener valor de celda
|
||||
const cell_value = row_data.get(col_name);
|
||||
|
||||
// Formatear en el buffer proporcionado
|
||||
return cell_value.format(buf);
|
||||
}
|
||||
|
||||
/// Retorna el ID único de una fila
|
||||
/// Para datos en memoria, usamos el índice como ID
|
||||
/// TODO: Si Row tuviera campo 'id', lo usaríamos
|
||||
pub fn getRowId(self: *Self, row: usize) i64 {
|
||||
_ = self;
|
||||
// Por ahora usamos el índice como ID
|
||||
// En el futuro, Row podría tener un campo 'id' explícito
|
||||
return @intCast(row);
|
||||
}
|
||||
|
||||
/// Verifica si una celda es editable
|
||||
pub fn isCellEditable(self: *Self, row: usize, col: usize) bool {
|
||||
_ = row;
|
||||
|
||||
// Verificar configuración de columna
|
||||
if (col >= self.columns.len) return false;
|
||||
return self.columns[col].editable;
|
||||
}
|
||||
|
||||
/// Invalida cache (no aplica para datos en memoria)
|
||||
pub fn invalidate(self: *Self) void {
|
||||
_ = self;
|
||||
// No hay cache que invalidar en datos en memoria
|
||||
}
|
||||
|
||||
/// Retorna el estado de una fila (normal, modified, new, deleted, error)
|
||||
pub fn getRowState(self: *Self, row: usize) table_core.RowState {
|
||||
// Delegar a AdvancedTableState que mantiene el tracking de estados
|
||||
const local_state = self.state.getRowState(row);
|
||||
// Convertir de types.RowState a table_core.RowState
|
||||
return switch (local_state) {
|
||||
.normal => .normal,
|
||||
.modified => .modified,
|
||||
.new => .new,
|
||||
.deleted => .deleted,
|
||||
.@"error" => .@"error",
|
||||
};
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Conversión a TableDataSource
|
||||
// =========================================================================
|
||||
|
||||
/// Crea TableDataSource vtable para este adaptador
|
||||
pub fn toDataSource(self: *Self) TableDataSource {
|
||||
return table_core.makeTableDataSource(Self, self);
|
||||
}
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// Tests
|
||||
// =============================================================================
|
||||
|
||||
test "MemoryDataSource basic" {
|
||||
// Test básico de que compila y los tipos son correctos
|
||||
const columns = [_]ColumnDef{
|
||||
.{ .name = "id", .title = "ID", .width = 50 },
|
||||
.{ .name = "name", .title = "Name", .width = 200 },
|
||||
};
|
||||
|
||||
var state = AdvancedTableState.init(std.testing.allocator);
|
||||
defer state.deinit();
|
||||
|
||||
var ds = MemoryDataSource.init(&state, &columns);
|
||||
const tds = ds.toDataSource();
|
||||
|
||||
try std.testing.expectEqual(@as(usize, 0), tds.getRowCount());
|
||||
}
|
||||
|
|
@ -90,16 +90,27 @@ pub const AdvancedTableState = struct {
|
|||
last_validation_message_len: usize = 0,
|
||||
|
||||
// =========================================================================
|
||||
// Editing (usa CellEditState de table_core para composición)
|
||||
// Editing
|
||||
// =========================================================================
|
||||
|
||||
/// Estado de edición embebido (Fase 2 refactor)
|
||||
cell_edit: table_core.CellEditState = .{},
|
||||
/// Is currently editing a cell
|
||||
editing: bool = false,
|
||||
|
||||
/// Edit buffer for current edit
|
||||
edit_buffer: [MAX_EDIT_BUFFER]u8 = undefined,
|
||||
|
||||
/// Length of text in edit buffer
|
||||
edit_len: usize = 0,
|
||||
|
||||
/// Cursor position in edit buffer
|
||||
edit_cursor: usize = 0,
|
||||
|
||||
/// Original value before editing (for revert on Escape)
|
||||
/// NOTA: Mantenemos esto porque CellValue es más rico que buffer crudo
|
||||
original_value: ?CellValue = null,
|
||||
|
||||
/// Escape count (1 = revert, 2 = cancel)
|
||||
escape_count: u8 = 0,
|
||||
|
||||
// =========================================================================
|
||||
// Double-click detection
|
||||
// =========================================================================
|
||||
|
|
@ -292,7 +303,9 @@ pub const AdvancedTableState = struct {
|
|||
self.prev_selected_col = -1;
|
||||
|
||||
// Clear editing
|
||||
self.cell_edit.stopEditing();
|
||||
self.editing = false;
|
||||
self.edit_len = 0;
|
||||
self.escape_count = 0;
|
||||
|
||||
// Clear sorting
|
||||
self.sort_column = -1;
|
||||
|
|
@ -676,81 +689,76 @@ pub const AdvancedTableState = struct {
|
|||
}
|
||||
|
||||
// =========================================================================
|
||||
// Editing (delega a cell_edit embebido)
|
||||
// Editing
|
||||
// =========================================================================
|
||||
|
||||
/// Start editing current cell
|
||||
/// Usa la celda seleccionada (selected_row, selected_col)
|
||||
pub fn startEditing(self: *AdvancedTableState, initial_value: []const u8) void {
|
||||
const row: usize = if (self.selected_row >= 0) @intCast(self.selected_row) else 0;
|
||||
const col: usize = if (self.selected_col >= 0) @intCast(self.selected_col) else 0;
|
||||
self.cell_edit.startEditing(row, col, initial_value, null);
|
||||
self.editing = true;
|
||||
self.escape_count = 0;
|
||||
|
||||
// Copy initial value to edit buffer
|
||||
const len = @min(initial_value.len, MAX_EDIT_BUFFER);
|
||||
@memcpy(self.edit_buffer[0..len], initial_value[0..len]);
|
||||
self.edit_len = len;
|
||||
self.edit_cursor = len;
|
||||
}
|
||||
|
||||
/// Stop editing
|
||||
pub fn stopEditing(self: *AdvancedTableState) void {
|
||||
self.cell_edit.stopEditing();
|
||||
self.editing = false;
|
||||
self.edit_len = 0;
|
||||
self.edit_cursor = 0;
|
||||
self.escape_count = 0;
|
||||
self.original_value = null;
|
||||
}
|
||||
|
||||
/// Get current edit text
|
||||
pub fn getEditText(self: *const AdvancedTableState) []const u8 {
|
||||
return self.cell_edit.getEditText();
|
||||
}
|
||||
|
||||
/// Check if currently editing
|
||||
pub fn isEditing(self: *const AdvancedTableState) bool {
|
||||
return self.cell_edit.editing;
|
||||
return self.edit_buffer[0..self.edit_len];
|
||||
}
|
||||
|
||||
/// Insert text at cursor position
|
||||
pub fn insertText(self: *AdvancedTableState, text: []const u8) void {
|
||||
for (text) |c| {
|
||||
if (self.cell_edit.edit_len < table_core.MAX_EDIT_BUFFER_SIZE) {
|
||||
if (self.edit_len < MAX_EDIT_BUFFER) {
|
||||
// Shift text after cursor
|
||||
var i = self.cell_edit.edit_len;
|
||||
while (i > self.cell_edit.edit_cursor) : (i -= 1) {
|
||||
self.cell_edit.edit_buffer[i] = self.cell_edit.edit_buffer[i - 1];
|
||||
var i = self.edit_len;
|
||||
while (i > self.edit_cursor) : (i -= 1) {
|
||||
self.edit_buffer[i] = self.edit_buffer[i - 1];
|
||||
}
|
||||
self.cell_edit.edit_buffer[self.cell_edit.edit_cursor] = c;
|
||||
self.cell_edit.edit_len += 1;
|
||||
self.cell_edit.edit_cursor += 1;
|
||||
self.edit_buffer[self.edit_cursor] = c;
|
||||
self.edit_len += 1;
|
||||
self.edit_cursor += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete character before cursor (backspace)
|
||||
pub fn deleteBackward(self: *AdvancedTableState) void {
|
||||
if (self.cell_edit.edit_cursor > 0) {
|
||||
var i = self.cell_edit.edit_cursor - 1;
|
||||
while (i < self.cell_edit.edit_len - 1) : (i += 1) {
|
||||
self.cell_edit.edit_buffer[i] = self.cell_edit.edit_buffer[i + 1];
|
||||
if (self.edit_cursor > 0) {
|
||||
// Shift text after cursor
|
||||
var i = self.edit_cursor - 1;
|
||||
while (i < self.edit_len - 1) : (i += 1) {
|
||||
self.edit_buffer[i] = self.edit_buffer[i + 1];
|
||||
}
|
||||
self.cell_edit.edit_len -= 1;
|
||||
self.cell_edit.edit_cursor -= 1;
|
||||
self.edit_len -= 1;
|
||||
self.edit_cursor -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete character at cursor (delete)
|
||||
pub fn deleteForward(self: *AdvancedTableState) void {
|
||||
if (self.cell_edit.edit_cursor < self.cell_edit.edit_len) {
|
||||
var i = self.cell_edit.edit_cursor;
|
||||
while (i < self.cell_edit.edit_len - 1) : (i += 1) {
|
||||
self.cell_edit.edit_buffer[i] = self.cell_edit.edit_buffer[i + 1];
|
||||
if (self.edit_cursor < self.edit_len) {
|
||||
// Shift text after cursor
|
||||
var i = self.edit_cursor;
|
||||
while (i < self.edit_len - 1) : (i += 1) {
|
||||
self.edit_buffer[i] = self.edit_buffer[i + 1];
|
||||
}
|
||||
self.cell_edit.edit_len -= 1;
|
||||
self.edit_len -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle Escape key (revert or cancel)
|
||||
pub fn handleEditEscape(self: *AdvancedTableState) table_core.CellEditState.EscapeAction {
|
||||
const action = self.cell_edit.handleEscape();
|
||||
if (action == .cancelled) {
|
||||
self.original_value = null;
|
||||
}
|
||||
return action;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Snapshots (for Auto-CRUD)
|
||||
// =========================================================================
|
||||
|
|
@ -1029,10 +1037,10 @@ test "AdvancedTableState editing" {
|
|||
var state = AdvancedTableState.init(std.testing.allocator);
|
||||
defer state.deinit();
|
||||
|
||||
try std.testing.expect(!state.isEditing());
|
||||
try std.testing.expect(!state.editing);
|
||||
|
||||
state.startEditing("Hello");
|
||||
try std.testing.expect(state.isEditing());
|
||||
try std.testing.expect(state.editing);
|
||||
try std.testing.expectEqualStrings("Hello", state.getEditText());
|
||||
|
||||
state.insertText(" World");
|
||||
|
|
@ -1042,7 +1050,7 @@ test "AdvancedTableState editing" {
|
|||
try std.testing.expectEqualStrings("Hello Worl", state.getEditText());
|
||||
|
||||
state.stopEditing();
|
||||
try std.testing.expect(!state.isEditing());
|
||||
try std.testing.expect(!state.editing);
|
||||
}
|
||||
|
||||
test "AdvancedTableState 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);
|
||||
}
|
||||
|
|
@ -5,20 +5,6 @@
|
|||
//! - VirtualAdvancedTable (datos paginados desde DataProvider)
|
||||
//!
|
||||
//! Principio: Una sola implementación de UI, dos estrategias de datos.
|
||||
//!
|
||||
//! ## Protocolo de Propiedad de Memoria
|
||||
//!
|
||||
//! 1. **Strings de celda:** El DataSource retorna punteros a memoria estable.
|
||||
//! El widget NO libera estos strings. Son válidos hasta el próximo fetch.
|
||||
//!
|
||||
//! 2. **Buffers de edición:** El widget mantiene edit_buffer[256] propio.
|
||||
//! Los cambios se copian al DataSource solo en commit.
|
||||
//!
|
||||
//! 3. **Rendering:** Todos los strings pasados a ctx.pushCommand() deben ser
|
||||
//! estables durante todo el frame. Usar buffers persistentes, NO stack.
|
||||
//!
|
||||
//! 4. **getValueInto pattern:** Cuando se necesita formatear valores,
|
||||
//! el caller provee el buffer destino para evitar memory ownership issues.
|
||||
|
||||
const std = @import("std");
|
||||
const Context = @import("../core/context.zig").Context;
|
||||
|
|
@ -79,8 +65,7 @@ pub const CellRenderInfo = struct {
|
|||
text_align: u2 = 0,
|
||||
};
|
||||
|
||||
/// Estado de edición para renderizado (info para draw)
|
||||
/// NOTA: Para estado embebible en widgets, usar CellEditState
|
||||
/// Estado de edición para renderizado
|
||||
pub const EditState = struct {
|
||||
/// Está en modo edición
|
||||
editing: bool = false,
|
||||
|
|
@ -94,201 +79,6 @@ pub const EditState = struct {
|
|||
edit_cursor: usize = 0,
|
||||
};
|
||||
|
||||
/// Estado de una fila (para indicadores visuales)
|
||||
/// Compatible con advanced_table.types.RowState
|
||||
pub const RowState = enum {
|
||||
normal, // Sin cambios
|
||||
modified, // Editada, pendiente de guardar
|
||||
new, // Fila nueva, no existe en BD
|
||||
deleted, // Marcada para eliminar
|
||||
@"error", // Error de validación
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// Estados embebibles (para composición en AdvancedTableState/VirtualAdvancedTableState)
|
||||
// =============================================================================
|
||||
|
||||
/// Tamaño máximo del buffer de edición
|
||||
pub const MAX_EDIT_BUFFER_SIZE: usize = 256;
|
||||
|
||||
/// Estado completo de edición de celda
|
||||
/// Diseñado para ser embebido en AdvancedTableState y VirtualAdvancedTableState
|
||||
pub const CellEditState = struct {
|
||||
/// Está en modo edición
|
||||
editing: bool = false,
|
||||
|
||||
/// Celda en edición (fila, columna)
|
||||
edit_row: usize = 0,
|
||||
edit_col: usize = 0,
|
||||
|
||||
/// Buffer de texto actual
|
||||
edit_buffer: [MAX_EDIT_BUFFER_SIZE]u8 = undefined,
|
||||
edit_len: usize = 0,
|
||||
|
||||
/// Posición del cursor
|
||||
edit_cursor: usize = 0,
|
||||
|
||||
/// Valor original (para revertir con Escape)
|
||||
original_buffer: [MAX_EDIT_BUFFER_SIZE]u8 = undefined,
|
||||
original_len: usize = 0,
|
||||
|
||||
/// Contador de Escapes (1=revertir, 2=cancelar)
|
||||
escape_count: u8 = 0,
|
||||
|
||||
/// Flag: el valor cambió respecto al original
|
||||
value_changed: bool = false,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
/// Inicia edición de una celda
|
||||
pub fn startEditing(self: *Self, row: usize, col: usize, current_value: []const u8, initial_char: ?u8) void {
|
||||
self.editing = true;
|
||||
self.edit_row = row;
|
||||
self.edit_col = col;
|
||||
self.escape_count = 0;
|
||||
self.value_changed = false;
|
||||
|
||||
// Guardar valor original
|
||||
const orig_len = @min(current_value.len, MAX_EDIT_BUFFER_SIZE);
|
||||
@memcpy(self.original_buffer[0..orig_len], current_value[0..orig_len]);
|
||||
self.original_len = orig_len;
|
||||
|
||||
// Inicializar buffer de edición
|
||||
if (initial_char) |c| {
|
||||
// Tecla alfanumérica: empezar con ese caracter
|
||||
self.edit_buffer[0] = c;
|
||||
self.edit_len = 1;
|
||||
self.edit_cursor = 1;
|
||||
} else {
|
||||
// F2/Space/DoubleClick: mostrar valor actual
|
||||
@memcpy(self.edit_buffer[0..orig_len], current_value[0..orig_len]);
|
||||
self.edit_len = orig_len;
|
||||
self.edit_cursor = orig_len;
|
||||
}
|
||||
}
|
||||
|
||||
/// Obtiene el texto actual del editor
|
||||
pub fn getEditText(self: *const Self) []const u8 {
|
||||
return self.edit_buffer[0..self.edit_len];
|
||||
}
|
||||
|
||||
/// Obtiene el valor original
|
||||
pub fn getOriginalValue(self: *const Self) []const u8 {
|
||||
return self.original_buffer[0..self.original_len];
|
||||
}
|
||||
|
||||
/// Verifica si el valor cambió
|
||||
pub fn hasChanged(self: *const Self) bool {
|
||||
const current = self.getEditText();
|
||||
const original = self.getOriginalValue();
|
||||
return !std.mem.eql(u8, current, original);
|
||||
}
|
||||
|
||||
/// 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;
|
||||
}
|
||||
|
||||
/// Finaliza edición
|
||||
pub fn stopEditing(self: *Self) void {
|
||||
self.editing = false;
|
||||
self.edit_len = 0;
|
||||
self.edit_cursor = 0;
|
||||
self.escape_count = 0;
|
||||
}
|
||||
|
||||
/// Resultado de handleEscape
|
||||
pub const EscapeAction = enum { reverted, cancelled, none };
|
||||
|
||||
/// Maneja Escape (retorna acción a tomar)
|
||||
pub fn handleEscape(self: *Self) EscapeAction {
|
||||
if (!self.editing) return .none;
|
||||
|
||||
self.escape_count += 1;
|
||||
if (self.escape_count == 1) {
|
||||
self.revertToOriginal();
|
||||
return .reverted;
|
||||
} else {
|
||||
self.stopEditing();
|
||||
return .cancelled;
|
||||
}
|
||||
}
|
||||
|
||||
/// Convierte a EditState para funciones de renderizado
|
||||
pub fn toEditState(self: *const Self) EditState {
|
||||
return .{
|
||||
.editing = self.editing,
|
||||
.edit_row = @intCast(self.edit_row),
|
||||
.edit_col = @intCast(self.edit_col),
|
||||
.edit_text = self.getEditText(),
|
||||
.edit_cursor = self.edit_cursor,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/// Estado de navegación compartido
|
||||
/// Diseñado para ser embebido en AdvancedTableState y VirtualAdvancedTableState
|
||||
pub const NavigationState = struct {
|
||||
/// Columna activa (para Tab navigation)
|
||||
active_col: usize = 0,
|
||||
|
||||
/// Scroll vertical (en filas)
|
||||
scroll_row: usize = 0,
|
||||
|
||||
/// Scroll horizontal (en pixels)
|
||||
scroll_x: i32 = 0,
|
||||
|
||||
/// El widget tiene focus
|
||||
has_focus: bool = false,
|
||||
|
||||
/// Double-click state
|
||||
double_click: DoubleClickState = .{},
|
||||
|
||||
const Self = @This();
|
||||
|
||||
/// Navega a siguiente celda (Tab)
|
||||
/// Retorna nueva posición y si navegó o salió del widget
|
||||
pub fn tabToNextCell(self: *Self, current_row: usize, num_cols: usize, num_rows: usize, wrap: bool) struct { row: usize, col: usize, result: TabNavigateResult } {
|
||||
const pos = calculateNextCell(current_row, self.active_col, num_cols, num_rows, wrap);
|
||||
if (pos.result == .navigated) {
|
||||
self.active_col = pos.col;
|
||||
}
|
||||
return .{ .row = pos.row, .col = pos.col, .result = pos.result };
|
||||
}
|
||||
|
||||
/// Navega a celda anterior (Shift+Tab)
|
||||
pub fn tabToPrevCell(self: *Self, current_row: usize, num_cols: usize, num_rows: usize, wrap: bool) struct { row: usize, col: usize, result: TabNavigateResult } {
|
||||
const pos = calculatePrevCell(current_row, self.active_col, num_cols, num_rows, wrap);
|
||||
if (pos.result == .navigated) {
|
||||
self.active_col = pos.col;
|
||||
}
|
||||
return .{ .row = pos.row, .col = pos.col, .result = pos.result };
|
||||
}
|
||||
|
||||
/// Mueve a columna anterior
|
||||
pub fn moveToPrevCol(self: *Self) void {
|
||||
if (self.active_col > 0) self.active_col -= 1;
|
||||
}
|
||||
|
||||
/// Mueve a columna siguiente
|
||||
pub fn moveToNextCol(self: *Self, max_cols: usize) void {
|
||||
if (self.active_col + 1 < max_cols) self.active_col += 1;
|
||||
}
|
||||
|
||||
/// Va a primera columna
|
||||
pub fn goToFirstCol(self: *Self) void {
|
||||
self.active_col = 0;
|
||||
}
|
||||
|
||||
/// Va a última columna
|
||||
pub fn goToLastCol(self: *Self, max_cols: usize) void {
|
||||
if (max_cols > 0) self.active_col = max_cols - 1;
|
||||
}
|
||||
};
|
||||
|
||||
/// Estado de doble-click
|
||||
pub const DoubleClickState = struct {
|
||||
last_click_time: u64 = 0,
|
||||
|
|
@ -390,263 +180,6 @@ pub fn drawCellText(
|
|||
ctx.pushCommand(Command.text(text_x, text_y, text, color));
|
||||
}
|
||||
|
||||
/// Dibuja el indicador de estado de fila (círculo/cuadrado pequeño)
|
||||
/// Llamado desde drawRowsWithDataSource cuando state_indicator_width > 0
|
||||
pub fn drawStateIndicator(
|
||||
ctx: *Context,
|
||||
x: i32,
|
||||
y: i32,
|
||||
w: u32,
|
||||
h: u32,
|
||||
row_state: RowState,
|
||||
colors: *const RowRenderColors,
|
||||
) void {
|
||||
// No dibujar nada para estado normal
|
||||
if (row_state == .normal) return;
|
||||
|
||||
const indicator_size: u32 = 8;
|
||||
const indicator_x = x + @as(i32, @intCast((w -| indicator_size) / 2));
|
||||
const indicator_y = y + @as(i32, @intCast((h -| indicator_size) / 2));
|
||||
|
||||
const color = switch (row_state) {
|
||||
.modified => colors.state_modified,
|
||||
.new => colors.state_new,
|
||||
.deleted => colors.state_deleted,
|
||||
.@"error" => colors.state_error,
|
||||
.normal => unreachable, // Ya verificado arriba
|
||||
};
|
||||
|
||||
// Dibujar cuadrado indicador
|
||||
ctx.pushCommand(Command.rect(indicator_x, indicator_y, indicator_size, indicator_size, color));
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Renderizado unificado de filas (FASE 4)
|
||||
// =============================================================================
|
||||
|
||||
/// Definición de columna para renderizado unificado
|
||||
pub const ColumnRenderDef = struct {
|
||||
/// Ancho de la columna en pixels
|
||||
width: u32,
|
||||
/// Alineación: 0=left, 1=center, 2=right
|
||||
text_align: u2 = 0,
|
||||
/// Columna visible
|
||||
visible: bool = true,
|
||||
};
|
||||
|
||||
/// Colores para renderizado unificado de filas
|
||||
pub const RowRenderColors = struct {
|
||||
// Colores base de fila
|
||||
row_normal: Style.Color,
|
||||
row_alternate: Style.Color,
|
||||
selected_row: Style.Color,
|
||||
selected_row_unfocus: Style.Color,
|
||||
selected_cell: Style.Color,
|
||||
selected_cell_unfocus: Style.Color,
|
||||
text_normal: Style.Color,
|
||||
text_selected: Style.Color,
|
||||
border: Style.Color,
|
||||
|
||||
// Colores de estado (para blending)
|
||||
state_modified: Style.Color = Style.Color.rgb(255, 200, 100), // Naranja
|
||||
state_new: Style.Color = Style.Color.rgb(100, 200, 100), // Verde
|
||||
state_deleted: Style.Color = Style.Color.rgb(255, 100, 100), // Rojo
|
||||
state_error: Style.Color = Style.Color.rgb(255, 50, 50), // Rojo intenso
|
||||
|
||||
/// Crea RowRenderColors desde TableColors
|
||||
pub fn fromTableColors(tc: *const TableColors) RowRenderColors {
|
||||
return .{
|
||||
.row_normal = tc.row_normal,
|
||||
.row_alternate = tc.row_alternate,
|
||||
.selected_row = tc.selected_row,
|
||||
.selected_row_unfocus = tc.selected_row_unfocus,
|
||||
.selected_cell = tc.selected_cell,
|
||||
.selected_cell_unfocus = tc.selected_cell_unfocus,
|
||||
.text_normal = tc.text_normal,
|
||||
.text_selected = tc.text_selected,
|
||||
.border = tc.border,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/// Configuración para drawRowsWithDataSource
|
||||
pub const DrawRowsConfig = struct {
|
||||
/// Bounds del área de contenido
|
||||
bounds_x: i32,
|
||||
bounds_y: i32,
|
||||
bounds_w: u32,
|
||||
/// Altura de cada fila
|
||||
row_height: u32,
|
||||
/// Primera fila a dibujar (índice global)
|
||||
first_row: usize,
|
||||
/// Última fila a dibujar (exclusivo)
|
||||
last_row: usize,
|
||||
/// Offset horizontal de scroll
|
||||
scroll_x: i32 = 0,
|
||||
/// Usar colores alternados
|
||||
alternating_rows: bool = true,
|
||||
/// Widget tiene focus
|
||||
has_focus: bool = false,
|
||||
/// Fila seleccionada (-1 = ninguna)
|
||||
selected_row: i32 = -1,
|
||||
/// Columna activa
|
||||
active_col: usize = 0,
|
||||
/// Colores
|
||||
colors: RowRenderColors,
|
||||
/// Columnas
|
||||
columns: []const ColumnRenderDef,
|
||||
/// Ancho de columna de indicadores de estado (0 = deshabilitada)
|
||||
state_indicator_width: u32 = 0,
|
||||
/// Aplicar blending de color según estado de fila
|
||||
apply_state_colors: bool = false,
|
||||
/// Dibujar borde inferior en cada fila
|
||||
draw_row_borders: bool = false,
|
||||
};
|
||||
|
||||
/// Dibuja las filas de una tabla usando TableDataSource.
|
||||
/// Esta es la función unificada que usan tanto AdvancedTable como VirtualAdvancedTable.
|
||||
///
|
||||
/// Parámetros:
|
||||
/// - ctx: Contexto de renderizado
|
||||
/// - datasource: Fuente de datos (MemoryDataSource o PagedDataSource)
|
||||
/// - config: Configuración del renderizado
|
||||
/// - cell_buffer: Buffer para formatear valores de celda (debe persistir durante el frame)
|
||||
///
|
||||
/// Retorna el número de filas dibujadas.
|
||||
pub fn drawRowsWithDataSource(
|
||||
ctx: *Context,
|
||||
datasource: TableDataSource,
|
||||
config: DrawRowsConfig,
|
||||
cell_buffer: []u8,
|
||||
) usize {
|
||||
var rows_drawn: usize = 0;
|
||||
var row_y = config.bounds_y;
|
||||
|
||||
var row_idx = config.first_row;
|
||||
while (row_idx < config.last_row) : (row_idx += 1) {
|
||||
const is_selected = config.selected_row >= 0 and
|
||||
@as(usize, @intCast(config.selected_row)) == row_idx;
|
||||
|
||||
// Obtener estado de la fila
|
||||
const row_state = datasource.getRowState(row_idx);
|
||||
|
||||
// Determinar color de fondo base
|
||||
const is_alternate = config.alternating_rows and row_idx % 2 == 1;
|
||||
var row_bg: Style.Color = if (is_alternate)
|
||||
config.colors.row_alternate
|
||||
else
|
||||
config.colors.row_normal;
|
||||
|
||||
// Aplicar blending de color según estado (si está habilitado)
|
||||
if (config.apply_state_colors) {
|
||||
row_bg = switch (row_state) {
|
||||
.modified => blendColor(row_bg, config.colors.state_modified, 0.2),
|
||||
.new => blendColor(row_bg, config.colors.state_new, 0.2),
|
||||
.deleted => blendColor(row_bg, config.colors.state_deleted, 0.3),
|
||||
.@"error" => blendColor(row_bg, config.colors.state_error, 0.3),
|
||||
.normal => row_bg,
|
||||
};
|
||||
}
|
||||
|
||||
// Aplicar selección (override del estado)
|
||||
if (is_selected) {
|
||||
row_bg = if (config.has_focus) config.colors.selected_row else config.colors.selected_row_unfocus;
|
||||
}
|
||||
|
||||
// Dibujar fondo de fila
|
||||
ctx.pushCommand(Command.rect(
|
||||
config.bounds_x,
|
||||
row_y,
|
||||
config.bounds_w,
|
||||
config.row_height,
|
||||
row_bg,
|
||||
));
|
||||
|
||||
// Posición X inicial (después de state indicator si existe)
|
||||
var col_x = config.bounds_x - config.scroll_x;
|
||||
|
||||
// Dibujar columna de indicador de estado (si está habilitada)
|
||||
if (config.state_indicator_width > 0) {
|
||||
drawStateIndicator(ctx, config.bounds_x, row_y, config.state_indicator_width, config.row_height, row_state, &config.colors);
|
||||
col_x += @as(i32, @intCast(config.state_indicator_width));
|
||||
}
|
||||
|
||||
// Dibujar celdas de datos
|
||||
for (config.columns, 0..) |col, col_idx| {
|
||||
if (!col.visible) continue;
|
||||
|
||||
const col_end = col_x + @as(i32, @intCast(col.width));
|
||||
|
||||
// Solo dibujar si la columna es visible en pantalla
|
||||
if (col_end > config.bounds_x and
|
||||
col_x < config.bounds_x + @as(i32, @intCast(config.bounds_w)))
|
||||
{
|
||||
const is_active_cell = is_selected and config.active_col == col_idx;
|
||||
|
||||
// Indicador de celda activa
|
||||
if (is_active_cell) {
|
||||
drawCellActiveIndicator(
|
||||
ctx,
|
||||
col_x,
|
||||
row_y,
|
||||
col.width,
|
||||
config.row_height,
|
||||
row_bg,
|
||||
&TableColors{
|
||||
.selected_cell = config.colors.selected_cell,
|
||||
.selected_cell_unfocus = config.colors.selected_cell_unfocus,
|
||||
.border = config.colors.border,
|
||||
},
|
||||
config.has_focus,
|
||||
);
|
||||
}
|
||||
|
||||
// Obtener texto de la celda
|
||||
const cell_text = datasource.getCellValueInto(row_idx, col_idx, cell_buffer);
|
||||
|
||||
// Copiar a frame allocator para persistencia durante render
|
||||
const text_to_draw = ctx.frameAllocator().dupe(u8, cell_text) catch cell_text;
|
||||
|
||||
// Color de texto
|
||||
const text_color = if (is_selected and config.has_focus)
|
||||
config.colors.text_selected
|
||||
else
|
||||
config.colors.text_normal;
|
||||
|
||||
// Dibujar texto
|
||||
drawCellText(
|
||||
ctx,
|
||||
col_x,
|
||||
row_y,
|
||||
col.width,
|
||||
config.row_height,
|
||||
text_to_draw,
|
||||
text_color,
|
||||
col.text_align,
|
||||
);
|
||||
}
|
||||
|
||||
col_x = col_end;
|
||||
}
|
||||
|
||||
// Dibujar borde inferior de fila (si está habilitado)
|
||||
if (config.draw_row_borders) {
|
||||
ctx.pushCommand(Command.rect(
|
||||
config.bounds_x,
|
||||
row_y + @as(i32, @intCast(config.row_height)) - 1,
|
||||
config.bounds_w,
|
||||
1,
|
||||
config.colors.border,
|
||||
));
|
||||
}
|
||||
|
||||
row_y += @as(i32, @intCast(config.row_height));
|
||||
rows_drawn += 1;
|
||||
}
|
||||
|
||||
return rows_drawn;
|
||||
}
|
||||
|
||||
/// Detecta si un click es doble-click
|
||||
pub fn detectDoubleClick(
|
||||
state: *DoubleClickState,
|
||||
|
|
@ -1205,133 +738,6 @@ pub fn toggleSort(
|
|||
};
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// TableDataSource Interface (FASE 3)
|
||||
// =============================================================================
|
||||
//
|
||||
// ## Interfaz TableDataSource
|
||||
//
|
||||
// Abstrae el origen de datos para tablas, permitiendo que el mismo widget
|
||||
// renderice datos desde memoria (AdvancedTable) o desde BD paginada (VirtualAdvancedTable).
|
||||
//
|
||||
// ### Protocolo de Memoria
|
||||
//
|
||||
// `getCellValueInto` escribe directamente en el buffer proporcionado por el widget.
|
||||
// Esto elimina problemas de ownership: el widget controla la vida del buffer.
|
||||
//
|
||||
// ### Ejemplo de uso:
|
||||
// ```zig
|
||||
// var buf: [256]u8 = undefined;
|
||||
// const value = data_source.getCellValueInto(row, col, &buf);
|
||||
// // value es un slice de buf, válido mientras buf exista
|
||||
// ```
|
||||
|
||||
/// Interfaz genérica para proveer datos a tablas
|
||||
/// Usa vtable pattern para polimorfismo en runtime
|
||||
pub const TableDataSource = struct {
|
||||
ptr: *anyopaque,
|
||||
vtable: *const VTable,
|
||||
|
||||
pub const VTable = struct {
|
||||
/// Retorna el número total de filas en el datasource
|
||||
getRowCount: *const fn (ptr: *anyopaque) usize,
|
||||
|
||||
/// Escribe el valor de una celda en el buffer proporcionado
|
||||
/// Retorna el slice del buffer con el contenido escrito
|
||||
/// Si la celda no existe o está vacía, retorna ""
|
||||
getCellValueInto: *const fn (ptr: *anyopaque, row: usize, col: usize, buf: []u8) []const u8,
|
||||
|
||||
/// Retorna el ID único de una fila (para selección persistente)
|
||||
/// NEW_ROW_ID (-1) indica fila nueva no guardada
|
||||
getRowId: *const fn (ptr: *anyopaque, row: usize) i64,
|
||||
|
||||
/// Verifica si una celda es editable (opcional, default true)
|
||||
isCellEditable: ?*const fn (ptr: *anyopaque, row: usize, col: usize) bool = null,
|
||||
|
||||
/// Retorna el estado de una fila (opcional, default .normal)
|
||||
/// Usado para colores de estado (modified, new, deleted, error)
|
||||
getRowState: ?*const fn (ptr: *anyopaque, row: usize) RowState = null,
|
||||
|
||||
/// Invalida cache interno (para refresh)
|
||||
invalidate: ?*const fn (ptr: *anyopaque) void = null,
|
||||
};
|
||||
|
||||
// =========================================================================
|
||||
// Métodos de conveniencia
|
||||
// =========================================================================
|
||||
|
||||
/// Obtiene el número de filas
|
||||
pub fn getRowCount(self: TableDataSource) usize {
|
||||
return self.vtable.getRowCount(self.ptr);
|
||||
}
|
||||
|
||||
/// Escribe valor de celda en buffer
|
||||
pub fn getCellValueInto(self: TableDataSource, row: usize, col: usize, buf: []u8) []const u8 {
|
||||
return self.vtable.getCellValueInto(self.ptr, row, col, buf);
|
||||
}
|
||||
|
||||
/// Obtiene ID de fila
|
||||
pub fn getRowId(self: TableDataSource, row: usize) i64 {
|
||||
return self.vtable.getRowId(self.ptr, row);
|
||||
}
|
||||
|
||||
/// Verifica si celda es editable
|
||||
pub fn isCellEditable(self: TableDataSource, row: usize, col: usize) bool {
|
||||
if (self.vtable.isCellEditable) |func| {
|
||||
return func(self.ptr, row, col);
|
||||
}
|
||||
return true; // Default: todas editables
|
||||
}
|
||||
|
||||
/// Invalida cache
|
||||
pub fn invalidate(self: TableDataSource) void {
|
||||
if (self.vtable.invalidate) |func| {
|
||||
func(self.ptr);
|
||||
}
|
||||
}
|
||||
|
||||
/// Obtiene el estado de una fila
|
||||
pub fn getRowState(self: TableDataSource, row: usize) RowState {
|
||||
if (self.vtable.getRowState) |func| {
|
||||
return func(self.ptr, row);
|
||||
}
|
||||
return .normal; // Default: estado normal
|
||||
}
|
||||
|
||||
/// Verifica si la fila es la ghost row (nueva)
|
||||
pub fn isGhostRow(self: TableDataSource, row: usize) bool {
|
||||
return self.getRowId(row) == NEW_ROW_ID;
|
||||
}
|
||||
};
|
||||
|
||||
/// Helper para crear TableDataSource desde un tipo concreto
|
||||
/// El tipo T debe tener los métodos: getRowCount, getCellValueInto, getRowId
|
||||
pub fn makeTableDataSource(comptime T: type, impl: *T) TableDataSource {
|
||||
const vtable = comptime blk: {
|
||||
var vt: TableDataSource.VTable = .{
|
||||
.getRowCount = @ptrCast(&T.getRowCount),
|
||||
.getCellValueInto = @ptrCast(&T.getCellValueInto),
|
||||
.getRowId = @ptrCast(&T.getRowId),
|
||||
};
|
||||
// Métodos opcionales
|
||||
if (@hasDecl(T, "isCellEditable")) {
|
||||
vt.isCellEditable = @ptrCast(&T.isCellEditable);
|
||||
}
|
||||
if (@hasDecl(T, "getRowState")) {
|
||||
vt.getRowState = @ptrCast(&T.getRowState);
|
||||
}
|
||||
if (@hasDecl(T, "invalidate")) {
|
||||
vt.invalidate = @ptrCast(&T.invalidate);
|
||||
}
|
||||
break :blk vt;
|
||||
};
|
||||
|
||||
return .{
|
||||
.ptr = impl,
|
||||
.vtable = &vtable,
|
||||
};
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Tests
|
||||
// =============================================================================
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ pub fn drawCellEditor(
|
|||
));
|
||||
|
||||
// Cursor: posición calculada con measureTextToCursor (TTF-aware)
|
||||
const cursor_offset = ctx.measureTextToCursor(text, state.cell_edit.edit_cursor);
|
||||
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
|
||||
|
|
@ -109,10 +109,10 @@ pub fn drawCellEditor(
|
|||
const original_text = state.getOriginalValue();
|
||||
const kb_result = table_core.handleEditingKeyboard(
|
||||
ctx,
|
||||
&state.cell_edit.edit_buffer,
|
||||
&state.cell_edit.edit_len,
|
||||
&state.cell_edit.edit_cursor,
|
||||
&state.cell_edit.escape_count,
|
||||
&state.edit_buffer,
|
||||
&state.edit_buffer_len,
|
||||
&state.edit_cursor,
|
||||
&state.escape_count,
|
||||
if (original_text.len > 0) original_text else null,
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,184 +0,0 @@
|
|||
//! PagedDataSource - Adaptador de TableDataSource para datos paginados
|
||||
//!
|
||||
//! Wrappea VirtualAdvancedTableState + DataProvider para implementar la interfaz
|
||||
//! TableDataSource de table_core. Permite unificar el pattern de renderizado
|
||||
//! entre AdvancedTable (memoria) y VirtualAdvancedTable (paginado).
|
||||
|
||||
const std = @import("std");
|
||||
const table_core = @import("../table_core.zig");
|
||||
const state_mod = @import("state.zig");
|
||||
const types = @import("types.zig");
|
||||
const data_provider_mod = @import("data_provider.zig");
|
||||
|
||||
const VirtualAdvancedTableState = state_mod.VirtualAdvancedTableState;
|
||||
const ColumnDef = types.ColumnDef;
|
||||
const RowData = types.RowData;
|
||||
const DataProvider = data_provider_mod.DataProvider;
|
||||
const TableDataSource = table_core.TableDataSource;
|
||||
|
||||
// =============================================================================
|
||||
// PagedDataSource
|
||||
// =============================================================================
|
||||
|
||||
/// Adaptador que implementa TableDataSource para datos paginados (ventana virtual).
|
||||
/// Lee valores desde current_window en el state, que es propiedad del DataProvider.
|
||||
pub const PagedDataSource = struct {
|
||||
/// Referencia al estado de la tabla (contiene current_window)
|
||||
state: *VirtualAdvancedTableState,
|
||||
|
||||
/// DataProvider subyacente (para getRowId cuando fuera de ventana)
|
||||
provider: ?DataProvider,
|
||||
|
||||
/// Definiciones de columnas (para validar índices)
|
||||
columns: []const ColumnDef,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
/// Crea un nuevo PagedDataSource
|
||||
pub fn init(
|
||||
state: *VirtualAdvancedTableState,
|
||||
columns: []const ColumnDef,
|
||||
provider: ?DataProvider,
|
||||
) Self {
|
||||
return .{
|
||||
.state = state,
|
||||
.columns = columns,
|
||||
.provider = provider,
|
||||
};
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Implementación de TableDataSource
|
||||
// =========================================================================
|
||||
|
||||
/// Retorna el número total de filas (filtered count)
|
||||
pub fn getRowCount(self: *Self) usize {
|
||||
// Usar conteo filtrado si está disponible
|
||||
const count_info = self.state.getDisplayCount();
|
||||
return count_info.value;
|
||||
}
|
||||
|
||||
/// Escribe el valor de una celda en el buffer proporcionado.
|
||||
/// El row es índice global, se convierte a índice de ventana.
|
||||
/// Retorna slice del buffer con el contenido.
|
||||
pub fn getCellValueInto(self: *Self, row: usize, col: usize, buf: []u8) []const u8 {
|
||||
// Validar columna
|
||||
if (col >= self.columns.len) return "";
|
||||
|
||||
// Convertir índice global a índice de ventana
|
||||
const window_idx = self.state.globalToWindowIndex(row) orelse {
|
||||
// Fila fuera de ventana actual - retornar vacío
|
||||
// (el caller debería asegurarse de pedir solo filas visibles)
|
||||
return "";
|
||||
};
|
||||
|
||||
// Obtener fila de la ventana
|
||||
if (window_idx >= self.state.current_window.len) return "";
|
||||
const row_data = self.state.current_window[window_idx];
|
||||
|
||||
// Obtener valor de la columna
|
||||
if (col >= row_data.values.len) return "";
|
||||
const value = row_data.values[col];
|
||||
|
||||
// Copiar al buffer proporcionado
|
||||
const copy_len = @min(value.len, buf.len);
|
||||
@memcpy(buf[0..copy_len], value[0..copy_len]);
|
||||
return buf[0..copy_len];
|
||||
}
|
||||
|
||||
/// Retorna el ID único de una fila.
|
||||
/// Si está en ventana, usa window data. Si no, consulta al provider.
|
||||
pub fn getRowId(self: *Self, row: usize) i64 {
|
||||
// Intentar obtener de la ventana
|
||||
if (self.state.globalToWindowIndex(row)) |window_idx| {
|
||||
if (window_idx < self.state.current_window.len) {
|
||||
return self.state.current_window[window_idx].id;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback al provider si está disponible
|
||||
if (self.provider) |provider| {
|
||||
return provider.getRowId(row) orelse -1;
|
||||
}
|
||||
|
||||
// Si no hay provider, usar índice como fallback
|
||||
return @intCast(row);
|
||||
}
|
||||
|
||||
/// Verifica si una celda es editable.
|
||||
/// TODO: Podría usar metadata de columnas cuando esté disponible.
|
||||
pub fn isCellEditable(self: *Self, row: usize, col: usize) bool {
|
||||
_ = row;
|
||||
// Por ahora, todas las columnas son no editables por defecto
|
||||
// La edición se maneja a nivel de VirtualAdvancedTable
|
||||
if (col >= self.columns.len) return false;
|
||||
return false; // VirtualAdvancedTable maneja edición de forma específica
|
||||
}
|
||||
|
||||
/// Invalida cache.
|
||||
/// Delega al state para forzar refetch.
|
||||
pub fn invalidate(self: *Self) void {
|
||||
self.state.invalidateWindow();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Conversión a TableDataSource
|
||||
// =========================================================================
|
||||
|
||||
/// Crea TableDataSource vtable para este adaptador
|
||||
pub fn toDataSource(self: *Self) TableDataSource {
|
||||
return table_core.makeTableDataSource(Self, self);
|
||||
}
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// Tests
|
||||
// =============================================================================
|
||||
|
||||
test "PagedDataSource basic" {
|
||||
// Test básico de que compila y los tipos son correctos
|
||||
const columns = [_]ColumnDef{
|
||||
.{ .name = "id", .title = "ID", .width = 50 },
|
||||
.{ .name = "name", .title = "Name", .width = 200 },
|
||||
};
|
||||
|
||||
var state = VirtualAdvancedTableState{};
|
||||
var ds = PagedDataSource.init(&state, &columns, null);
|
||||
const tds = ds.toDataSource();
|
||||
|
||||
// Sin datos, row count es 0
|
||||
try std.testing.expectEqual(@as(usize, 0), tds.getRowCount());
|
||||
}
|
||||
|
||||
test "PagedDataSource getCellValueInto" {
|
||||
const columns = [_]ColumnDef{
|
||||
.{ .name = "id", .title = "ID", .width = 50 },
|
||||
.{ .name = "name", .title = "Name", .width = 200 },
|
||||
};
|
||||
|
||||
// Crear datos de prueba
|
||||
const values1 = [_][]const u8{ "1", "Alice" };
|
||||
const values2 = [_][]const u8{ "2", "Bob" };
|
||||
const rows = [_]RowData{
|
||||
.{ .id = 100, .values = &values1 },
|
||||
.{ .id = 101, .values = &values2 },
|
||||
};
|
||||
|
||||
var state = VirtualAdvancedTableState{};
|
||||
state.current_window = &rows;
|
||||
state.window_start = 10; // Ventana empieza en índice global 10
|
||||
state.filtered_count = .{ .value = 100, .state = .ready };
|
||||
|
||||
var ds = PagedDataSource.init(&state, &columns, null);
|
||||
|
||||
// Buffer para valores
|
||||
var buf: [256]u8 = undefined;
|
||||
|
||||
// Fila en ventana (global 10 = window 0)
|
||||
const val1 = ds.getCellValueInto(10, 1, &buf);
|
||||
try std.testing.expectEqualStrings("Alice", val1);
|
||||
|
||||
// Fila fuera de ventana
|
||||
const val2 = ds.getCellValueInto(0, 0, &buf);
|
||||
try std.testing.expectEqualStrings("", val2);
|
||||
}
|
||||
|
|
@ -147,11 +147,18 @@ pub const VirtualAdvancedTableState = struct {
|
|||
footer_display_len: usize = 0,
|
||||
|
||||
// =========================================================================
|
||||
// Estado de edición CRUD Excel-style (usa CellEditState de table_core)
|
||||
// Estado de edición CRUD Excel-style
|
||||
// =========================================================================
|
||||
|
||||
/// Estado de edición embebido (Fase 2 refactor)
|
||||
cell_edit: table_core.CellEditState = .{},
|
||||
/// Celda actualmente en edición (null = no editando)
|
||||
editing_cell: ?CellId = null,
|
||||
|
||||
/// Valor original de la celda (para Escape revertir)
|
||||
original_value: [256]u8 = undefined,
|
||||
original_value_len: usize = 0,
|
||||
|
||||
/// Contador de Escapes (1 = revertir celda, 2 = descartar fila)
|
||||
escape_count: u8 = 0,
|
||||
|
||||
/// Fila actual tiene cambios sin guardar en BD
|
||||
row_dirty: bool = false,
|
||||
|
|
@ -159,6 +166,11 @@ pub const VirtualAdvancedTableState = struct {
|
|||
/// Última fila editada (para detectar cambio de fila)
|
||||
last_edited_row: ?usize = null,
|
||||
|
||||
/// Buffer de edición (texto actual en el editor)
|
||||
edit_buffer: [256]u8 = undefined,
|
||||
edit_buffer_len: usize = 0,
|
||||
edit_cursor: usize = 0,
|
||||
|
||||
/// Tiempo de última edición (para parpadeo cursor)
|
||||
last_edit_time_ms: u64 = 0,
|
||||
|
||||
|
|
@ -567,76 +579,96 @@ pub const VirtualAdvancedTableState = struct {
|
|||
}
|
||||
|
||||
// =========================================================================
|
||||
// Métodos de edición CRUD Excel-style (delega a cell_edit embebido)
|
||||
// Métodos de edición CRUD Excel-style
|
||||
// =========================================================================
|
||||
|
||||
/// Verifica si hay una celda en edición
|
||||
pub fn isEditing(self: *const Self) bool {
|
||||
return self.cell_edit.editing;
|
||||
}
|
||||
|
||||
/// Obtiene la celda actualmente en edición
|
||||
pub fn getEditingCell(self: *const Self) ?CellId {
|
||||
if (!self.cell_edit.editing) return null;
|
||||
return .{ .row = self.cell_edit.edit_row, .col = self.cell_edit.edit_col };
|
||||
return self.editing_cell != null;
|
||||
}
|
||||
|
||||
/// 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, current_time_ms: u64) void {
|
||||
self.cell_edit.startEditing(cell.row, cell.col, current_value, initial_char);
|
||||
// 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]);
|
||||
self.original_value_len = len;
|
||||
|
||||
// Inicializar buffer de edición
|
||||
if (initial_char) |c| {
|
||||
// Tecla alfanumérica: empezar con ese caracter
|
||||
self.edit_buffer[0] = c;
|
||||
self.edit_buffer_len = 1;
|
||||
self.edit_cursor = 1;
|
||||
} else {
|
||||
// Doble-click/Space: mostrar valor actual
|
||||
@memcpy(self.edit_buffer[0..len], current_value[0..len]);
|
||||
self.edit_buffer_len = len;
|
||||
self.edit_cursor = len;
|
||||
}
|
||||
|
||||
self.editing_cell = cell;
|
||||
self.escape_count = 0;
|
||||
self.last_edit_time_ms = current_time_ms;
|
||||
self.cell_value_changed = false;
|
||||
}
|
||||
|
||||
/// Obtiene el texto actual del editor
|
||||
pub fn getEditText(self: *const Self) []const u8 {
|
||||
return self.cell_edit.getEditText();
|
||||
return self.edit_buffer[0..self.edit_buffer_len];
|
||||
}
|
||||
|
||||
/// Establece el texto del editor
|
||||
pub fn setEditText(self: *Self, text: []const u8) void {
|
||||
const len = @min(text.len, table_core.MAX_EDIT_BUFFER_SIZE);
|
||||
@memcpy(self.cell_edit.edit_buffer[0..len], text[0..len]);
|
||||
self.cell_edit.edit_len = len;
|
||||
self.cell_edit.edit_cursor = len;
|
||||
const len = @min(text.len, self.edit_buffer.len);
|
||||
@memcpy(self.edit_buffer[0..len], text[0..len]);
|
||||
self.edit_buffer_len = len;
|
||||
self.edit_cursor = len;
|
||||
}
|
||||
|
||||
/// Obtiene el valor original (antes de editar)
|
||||
pub fn getOriginalValue(self: *const Self) []const u8 {
|
||||
return self.cell_edit.getOriginalValue();
|
||||
return self.original_value[0..self.original_value_len];
|
||||
}
|
||||
|
||||
/// Verifica si el valor ha cambiado respecto al original
|
||||
pub fn hasValueChanged(self: *const Self) bool {
|
||||
return self.cell_edit.hasChanged();
|
||||
const current = self.getEditText();
|
||||
const original = self.getOriginalValue();
|
||||
return !std.mem.eql(u8, current, original);
|
||||
}
|
||||
|
||||
/// Finaliza edición guardando cambios (retorna true si hubo cambios)
|
||||
pub fn commitEdit(self: *Self) bool {
|
||||
if (!self.cell_edit.editing) return false;
|
||||
if (self.editing_cell == null) return false;
|
||||
|
||||
const changed = self.cell_edit.hasChanged();
|
||||
const changed = self.hasValueChanged();
|
||||
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.cell_edit.edit_row;
|
||||
self.last_edited_row = self.editing_cell.?.row;
|
||||
}
|
||||
|
||||
self.cell_edit.stopEditing();
|
||||
self.editing_cell = null;
|
||||
self.escape_count = 0;
|
||||
return changed;
|
||||
}
|
||||
|
||||
/// Finaliza edición descartando cambios
|
||||
pub fn cancelEdit(self: *Self) void {
|
||||
self.cell_edit.stopEditing();
|
||||
self.editing_cell = null;
|
||||
self.escape_count = 0;
|
||||
self.cell_value_changed = false;
|
||||
}
|
||||
|
||||
/// Revierte el texto de la celda al valor original (Escape 1)
|
||||
pub fn revertCellText(self: *Self) void {
|
||||
self.cell_edit.revertToOriginal();
|
||||
const original = self.getOriginalValue();
|
||||
@memcpy(self.edit_buffer[0..original.len], original);
|
||||
self.edit_buffer_len = original.len;
|
||||
self.edit_cursor = original.len;
|
||||
}
|
||||
|
||||
/// Maneja la tecla Escape (retorna acción a tomar)
|
||||
|
|
@ -650,15 +682,20 @@ pub const VirtualAdvancedTableState = struct {
|
|||
};
|
||||
|
||||
pub fn handleEscape(self: *Self) EscapeAction {
|
||||
const action = self.cell_edit.handleEscape();
|
||||
return switch (action) {
|
||||
.reverted => .reverted,
|
||||
.cancelled => blk: {
|
||||
if (self.editing_cell == null) return .none;
|
||||
|
||||
self.escape_count += 1;
|
||||
|
||||
if (self.escape_count == 1) {
|
||||
// Escape 1: Revertir texto a valor original
|
||||
self.revertCellText();
|
||||
return .reverted;
|
||||
} else {
|
||||
// Escape 2+: Descartar cambios de fila
|
||||
self.cancelEdit();
|
||||
self.row_dirty = false;
|
||||
break :blk .discard_row;
|
||||
},
|
||||
.none => .none,
|
||||
};
|
||||
return .discard_row;
|
||||
}
|
||||
}
|
||||
|
||||
/// Verifica si cambió de fila (para auto-save)
|
||||
|
|
@ -676,9 +713,12 @@ pub const VirtualAdvancedTableState = struct {
|
|||
|
||||
/// Resetea el estado de edición completamente
|
||||
pub fn resetEditState(self: *Self) void {
|
||||
self.cell_edit.stopEditing();
|
||||
self.editing_cell = null;
|
||||
self.escape_count = 0;
|
||||
self.row_dirty = false;
|
||||
self.last_edited_row = null;
|
||||
self.edit_buffer_len = 0;
|
||||
self.edit_cursor = 0;
|
||||
self.cell_value_changed = false;
|
||||
self.row_edit_buffer.clear();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ pub const types = @import("types.zig");
|
|||
pub const data_provider = @import("data_provider.zig");
|
||||
pub const state_mod = @import("state.zig");
|
||||
pub const cell_editor = @import("cell_editor.zig");
|
||||
pub const paged_datasource = @import("paged_datasource.zig");
|
||||
|
||||
// Tipos principales
|
||||
pub const RowData = types.RowData;
|
||||
|
|
@ -42,7 +41,6 @@ pub const CellId = types.CellId;
|
|||
pub const CellGeometry = types.CellGeometry;
|
||||
|
||||
pub const DataProvider = data_provider.DataProvider;
|
||||
pub const PagedDataSource = paged_datasource.PagedDataSource;
|
||||
pub const CellEditorColors = cell_editor.CellEditorColors;
|
||||
pub const CellEditorResult = cell_editor.CellEditorResult;
|
||||
pub const NavigateDirection = cell_editor.NavigateDirection;
|
||||
|
|
@ -281,7 +279,7 @@ pub fn virtualAdvancedTableRect(
|
|||
|
||||
// Draw CellEditor overlay if editing
|
||||
if (list_state.isEditing()) {
|
||||
const editing = list_state.getEditingCell().?;
|
||||
const editing = list_state.editing_cell.?;
|
||||
|
||||
// Calculate cell geometry for the editing cell
|
||||
if (list_state.getCellGeometry(
|
||||
|
|
@ -308,7 +306,7 @@ pub fn virtualAdvancedTableRect(
|
|||
|
||||
// Handle editor results
|
||||
if (editor_result.committed) {
|
||||
const edited_cell = list_state.getEditingCell().?;
|
||||
const edited_cell = list_state.editing_cell.?;
|
||||
const new_value = list_state.getEditText();
|
||||
|
||||
// Añadir cambio al buffer de fila (NO commit inmediato)
|
||||
|
|
@ -782,71 +780,89 @@ fn drawRows(
|
|||
) void {
|
||||
_ = result;
|
||||
|
||||
// Crear PagedDataSource para acceso unificado a datos
|
||||
var datasource = paged_datasource.PagedDataSource.init(list_state, config.columns, null);
|
||||
const table_ds = datasource.toDataSource();
|
||||
const row_h = config.row_height;
|
||||
|
||||
// Convertir selected_id a selected_row (índice global)
|
||||
const selected_row: i32 = if (list_state.findSelectedInWindow()) |window_idx|
|
||||
@intCast(list_state.windowToGlobalIndex(window_idx))
|
||||
// Calculate offset within the window buffer
|
||||
// scroll_offset es la posición global, window_start es donde empieza el buffer
|
||||
const window_offset = list_state.scroll_offset -| list_state.window_start;
|
||||
|
||||
// Draw each visible row
|
||||
var row_idx: usize = 0;
|
||||
while (row_idx < visible_rows) : (row_idx += 1) {
|
||||
const data_idx = window_offset + row_idx;
|
||||
if (data_idx >= list_state.current_window.len) break;
|
||||
|
||||
const row_y = content_bounds.y + @as(i32, @intCast(row_idx * row_h));
|
||||
const global_idx = list_state.scroll_offset + row_idx; // Índice global real
|
||||
const row = list_state.current_window[data_idx];
|
||||
|
||||
// Determine row background
|
||||
const is_selected = list_state.selected_id != null and row.id == list_state.selected_id.?;
|
||||
const is_alternate = global_idx % 2 == 1;
|
||||
|
||||
const bg_color: Style.Color = if (is_selected)
|
||||
if (list_state.has_focus) colors.row_selected else colors.row_selected_unfocus
|
||||
else if (is_alternate)
|
||||
colors.row_alternate
|
||||
else
|
||||
-1;
|
||||
colors.row_normal;
|
||||
|
||||
// Calcular rango de filas a dibujar
|
||||
const first_row = list_state.scroll_offset;
|
||||
const last_row = @min(
|
||||
list_state.scroll_offset + visible_rows,
|
||||
list_state.window_start + list_state.current_window.len,
|
||||
);
|
||||
// Row background
|
||||
ctx.pushCommand(Command.rect(
|
||||
content_bounds.x,
|
||||
row_y,
|
||||
content_bounds.w,
|
||||
row_h,
|
||||
bg_color,
|
||||
));
|
||||
|
||||
// Convertir columnas a ColumnRenderDef
|
||||
var render_cols: [32]table_core.ColumnRenderDef = undefined;
|
||||
const num_cols = @min(config.columns.len, 32);
|
||||
for (config.columns[0..num_cols], 0..) |col, i| {
|
||||
render_cols[i] = .{
|
||||
.width = col.width,
|
||||
.text_align = 0, // TODO: mapear col.alignment si existe
|
||||
.visible = true,
|
||||
};
|
||||
}
|
||||
// Draw cells (with horizontal scroll offset)
|
||||
var x: i32 = content_bounds.x - scroll_offset_x;
|
||||
for (config.columns, 0..) |col, col_idx| {
|
||||
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;
|
||||
|
||||
// Convertir colores
|
||||
const render_colors = table_core.RowRenderColors{
|
||||
.row_normal = colors.row_normal,
|
||||
.row_alternate = colors.row_alternate,
|
||||
.selected_row = colors.row_selected,
|
||||
.selected_row_unfocus = colors.row_selected_unfocus,
|
||||
.selected_cell = Style.Color.rgb(100, 200, 255), // Cyan brillante
|
||||
// Draw active cell indicator BEFORE text
|
||||
if (is_active_cell) {
|
||||
// Usar colores contrastantes para el indicador
|
||||
const tc_colors = table_core.TableColors{
|
||||
// Blanco/cyan brillante para máximo contraste
|
||||
.selected_cell = Style.Color.rgb(100, 200, 255),
|
||||
.selected_cell_unfocus = Style.Color.rgb(150, 150, 160),
|
||||
.text_normal = colors.text,
|
||||
.text_selected = colors.text_selected,
|
||||
.border = colors.border,
|
||||
};
|
||||
|
||||
// Buffer para valores de celda
|
||||
var cell_buffer: [256]u8 = undefined;
|
||||
|
||||
// Llamar a la función unificada
|
||||
_ = table_core.drawRowsWithDataSource(
|
||||
table_core.drawCellActiveIndicator(
|
||||
ctx,
|
||||
table_ds,
|
||||
.{
|
||||
.bounds_x = content_bounds.x,
|
||||
.bounds_y = content_bounds.y,
|
||||
.bounds_w = content_bounds.w,
|
||||
.row_height = config.row_height,
|
||||
.first_row = first_row,
|
||||
.last_row = last_row,
|
||||
.scroll_x = scroll_offset_x,
|
||||
.alternating_rows = true,
|
||||
.has_focus = list_state.has_focus,
|
||||
.selected_row = selected_row,
|
||||
.active_col = list_state.active_col,
|
||||
.colors = render_colors,
|
||||
.columns = render_cols[0..num_cols],
|
||||
},
|
||||
&cell_buffer,
|
||||
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
|
||||
else
|
||||
colors.text;
|
||||
|
||||
ctx.pushCommand(Command.text(
|
||||
x + 4,
|
||||
row_y + 3, // Centrado vertical mejorado
|
||||
row.values[col_idx],
|
||||
text_color,
|
||||
));
|
||||
}
|
||||
}
|
||||
x = col_end;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
|
|
|
|||
Loading…
Reference in a new issue