refactor(tables): FASE 4 - Add unified drawRowsWithDataSource

Add unified row rendering function to table_core.zig that uses
TableDataSource interface for data access.

Changes:
- Add ColumnRenderDef, RowRenderColors, DrawRowsConfig types
- Add drawRowsWithDataSource() unified rendering function
- Update VirtualAdvancedTable.drawRows to use unified function
  with PagedDataSource

Note: AdvancedTable not yet integrated due to additional complexity
(state indicators, click handling interleaved with drawing).
See PLAN_REFACTOR_TABLES_CONTINUIDAD.md for details.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
R.Eugenio 2025-12-27 18:02:45 +01:00
parent cf2f91f8bc
commit b8199aec38
2 changed files with 239 additions and 79 deletions

View file

@ -380,6 +380,184 @@ pub fn drawCellText(
ctx.pushCommand(Command.text(text_x, text_y, text, color)); ctx.pushCommand(Command.text(text_x, text_y, text, 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: u16,
/// 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 {
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,
/// 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,
};
/// 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;
// Determinar color de fondo de fila
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)
config.colors.row_alternate
else
config.colors.row_normal;
// Dibujar fondo de fila
ctx.pushCommand(Command.rect(
config.bounds_x,
row_y,
config.bounds_w,
config.row_height,
row_bg,
));
// Dibujar celdas
var col_x = config.bounds_x - config.scroll_x;
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;
}
row_y += @as(i32, @intCast(config.row_height));
rows_drawn += 1;
}
return rows_drawn;
}
/// Detecta si un click es doble-click /// Detecta si un click es doble-click
pub fn detectDoubleClick( pub fn detectDoubleClick(
state: *DoubleClickState, state: *DoubleClickState,

View file

@ -782,89 +782,71 @@ fn drawRows(
) void { ) void {
_ = result; _ = result;
const row_h = config.row_height; // Crear PagedDataSource para acceso unificado a datos
var datasource = paged_datasource.PagedDataSource.init(list_state, config.columns, null);
const table_ds = datasource.toDataSource();
// Calculate offset within the window buffer // Convertir selected_id a selected_row (índice global)
// scroll_offset es la posición global, window_start es donde empieza el buffer const selected_row: i32 = if (list_state.findSelectedInWindow()) |window_idx|
const window_offset = list_state.scroll_offset -| list_state.window_start; @intCast(list_state.windowToGlobalIndex(window_idx))
// 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 else
colors.row_normal; -1;
// Row background // Calcular rango de filas a dibujar
ctx.pushCommand(Command.rect( const first_row = list_state.scroll_offset;
content_bounds.x, const last_row = @min(
row_y, list_state.scroll_offset + visible_rows,
content_bounds.w, list_state.window_start + list_state.current_window.len,
row_h, );
bg_color,
));
// Draw cells (with horizontal scroll offset) // Convertir columnas a ColumnRenderDef
var x: i32 = content_bounds.x - scroll_offset_x; var render_cols: [32]table_core.ColumnRenderDef = undefined;
for (config.columns, 0..) |col, col_idx| { const num_cols = @min(config.columns.len, 32);
const col_end = x + @as(i32, @intCast(col.width)); for (config.columns[0..num_cols], 0..) |col, i| {
// Only draw if column is visible render_cols[i] = .{
if (col_end > content_bounds.x and x < content_bounds.x + @as(i32, @intCast(content_bounds.w))) { .width = col.width,
// Check if this is the active cell .text_align = 0, // TODO: mapear col.alignment si existe
const is_active_cell = is_selected and list_state.active_col == col_idx; .visible = true,
};
}
// Draw active cell indicator BEFORE text // Convertir colores
if (is_active_cell) { const render_colors = table_core.RowRenderColors{
// Usar colores contrastantes para el indicador .row_normal = colors.row_normal,
const tc_colors = table_core.TableColors{ .row_alternate = colors.row_alternate,
// Blanco/cyan brillante para máximo contraste .selected_row = colors.row_selected,
.selected_cell = Style.Color.rgb(100, 200, 255), .selected_row_unfocus = colors.row_selected_unfocus,
.selected_cell = Style.Color.rgb(100, 200, 255), // Cyan brillante
.selected_cell_unfocus = Style.Color.rgb(150, 150, 160), .selected_cell_unfocus = Style.Color.rgb(150, 150, 160),
.text_normal = colors.text,
.text_selected = colors.text_selected,
.border = colors.border, .border = colors.border,
}; };
table_core.drawCellActiveIndicator(
// Buffer para valores de celda
var cell_buffer: [256]u8 = undefined;
// Llamar a la función unificada
_ = table_core.drawRowsWithDataSource(
ctx, ctx,
x, table_ds,
row_y, .{
col.width, .bounds_x = content_bounds.x,
row_h, .bounds_y = content_bounds.y,
bg_color, .bounds_w = content_bounds.w,
&tc_colors, .row_height = config.row_height,
list_state.has_focus, .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,
); );
}
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;
}
}
} }
// ============================================================================= // =============================================================================