diff --git a/src/widgets/advanced_table/advanced_table.zig b/src/widgets/advanced_table/advanced_table.zig index 696ae77..7614082 100644 --- a/src/widgets/advanced_table/advanced_table.zig +++ b/src/widgets/advanced_table/advanced_table.zig @@ -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, diff --git a/src/widgets/advanced_table/datasource.zig b/src/widgets/advanced_table/datasource.zig index e3e8753..27556a4 100644 --- a/src/widgets/advanced_table/datasource.zig +++ b/src/widgets/advanced_table/datasource.zig @@ -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 // ========================================================================= diff --git a/src/widgets/table_core.zig b/src/widgets/table_core.zig index e5611bb..d15c000 100644 --- a/src/widgets/table_core.zig +++ b/src/widgets/table_core.zig @@ -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); }