refactor(tables): FASE 4.5 - AdvancedTable usa drawRowsWithDataSource

- Añadir RowState enum a table_core.zig
- Añadir getRowState a TableDataSource.VTable (opcional)
- Añadir colores de estado a RowRenderColors
- Añadir draw_row_borders a DrawRowsConfig
- Añadir getRowState a MemoryDataSource
- Nueva función handleRowClicks() separando input de rendering
- AdvancedTable usa drawRowsWithDataSource (sin bucle for propio)
- Eliminar drawRow() y drawStateIndicator() locales (~160 líneas)

Objetivo cumplido: un solo bloque de código para renderizar filas
This commit is contained in:
reugenio 2025-12-27 18:52:21 +01:00
parent b8199aec38
commit 08ffcdbac5
3 changed files with 272 additions and 179 deletions

View file

@ -139,21 +139,62 @@ pub fn advancedTableRect(
const first_visible = table_state.scroll_row;
const last_visible = @min(first_visible + visible_rows, table_state.getRowCount());
// 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));
// Manejar clicks en filas (separado del renderizado)
handleRowClicks(ctx, bounds, table_state, table_schema, header_h, state_col_w, first_visible, last_visible, &result);
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);
// 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;
}
// 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());
@ -259,6 +300,101 @@ 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
// =============================================================================
@ -354,167 +490,6 @@ 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.isEditing()) {
// 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,

View file

@ -95,6 +95,20 @@ pub const MemoryDataSource = struct {
// 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
// =========================================================================

View file

@ -94,6 +94,16 @@ 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)
// =============================================================================
@ -380,6 +390,36 @@ 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)
// =============================================================================
@ -387,7 +427,7 @@ pub fn drawCellText(
/// Definición de columna para renderizado unificado
pub const ColumnRenderDef = struct {
/// Ancho de la columna en pixels
width: u16,
width: u32,
/// Alineación: 0=left, 1=center, 2=right
text_align: u2 = 0,
/// Columna visible
@ -396,6 +436,7 @@ pub const ColumnRenderDef = struct {
/// 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,
@ -406,6 +447,12 @@ pub const RowRenderColors = struct {
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 .{
@ -448,6 +495,12 @@ pub const DrawRowsConfig = struct {
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.
@ -474,15 +527,32 @@ pub fn drawRowsWithDataSource(
const is_selected = config.selected_row >= 0 and
@as(usize, @intCast(config.selected_row)) == row_idx;
// Determinar color de fondo de fila
// 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;
const row_bg: Style.Color = if (is_selected)
if (config.has_focus) config.colors.selected_row else config.colors.selected_row_unfocus
else if (is_alternate)
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,
@ -492,8 +562,16 @@ pub fn drawRowsWithDataSource(
row_bg,
));
// Dibujar celdas
// 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;
@ -551,6 +629,17 @@ pub fn drawRowsWithDataSource(
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;
}
@ -1159,6 +1248,10 @@ pub const TableDataSource = struct {
/// 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,
};
@ -1197,6 +1290,14 @@ pub const TableDataSource = struct {
}
}
/// 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;
@ -1216,6 +1317,9 @@ pub fn makeTableDataSource(comptime T: type, impl: *T) TableDataSource {
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);
}