feat(table_core): DRY - FilterBar y Footer compartidos + paridad AdvancedTable
FASES 1-7 completadas: - FilterBar extraído a table_core/filter_bar.zig (DRY) - Footer extraído a table_core/footer.zig (DRY) - AdvancedTable ahora soporta FilterBar + Footer opcional - validateCell añadido a TableDataSource VTable - VirtualAdvancedTable migrado a usar componentes compartidos Paridad UX completa entre AdvancedTable y VirtualAdvancedTable. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
1155e904cb
commit
1f2d4abb0b
10 changed files with 1004 additions and 230 deletions
|
|
@ -59,6 +59,11 @@ pub const MemoryDataSource = datasource.MemoryDataSource;
|
||||||
|
|
||||||
// Re-export table_core types
|
// Re-export table_core types
|
||||||
pub const NavigateDirection = table_core.NavigateDirection;
|
pub const NavigateDirection = table_core.NavigateDirection;
|
||||||
|
// Re-export FilterBar types for consumers
|
||||||
|
pub const FilterBarConfig = schema.FilterBarConfig;
|
||||||
|
pub const FilterChipDef = schema.FilterChipDef;
|
||||||
|
pub const ChipSelectMode = schema.ChipSelectMode;
|
||||||
|
pub const FilterBarState = state.FilterBarState;
|
||||||
|
|
||||||
// Re-export helpers for external use
|
// Re-export helpers for external use
|
||||||
pub const blendColor = helpers.blendColor;
|
pub const blendColor = helpers.blendColor;
|
||||||
|
|
@ -134,24 +139,68 @@ pub fn advancedTableRect(
|
||||||
|
|
||||||
// Calculate dimensions
|
// Calculate dimensions
|
||||||
const state_col_w: u32 = if (config.show_row_state_indicators) config.state_indicator_width else 0;
|
const state_col_w: u32 = if (config.show_row_state_indicators) config.state_indicator_width else 0;
|
||||||
|
const filter_bar_h: u32 = if (table_schema.filter_bar) |fb| fb.height else 0;
|
||||||
const header_h: u32 = if (config.show_headers) config.header_height else 0;
|
const header_h: u32 = if (config.show_headers) config.header_height else 0;
|
||||||
const content_h = bounds.h -| header_h;
|
const footer_h: u32 = if (config.show_footer) config.footer_height else 0;
|
||||||
|
const content_h = bounds.h -| filter_bar_h -| header_h -| footer_h;
|
||||||
const visible_rows: usize = @intCast(content_h / config.row_height);
|
const visible_rows: usize = @intCast(content_h / config.row_height);
|
||||||
|
|
||||||
|
// Adjusted bounds for content area (after filter bar, before footer)
|
||||||
|
const content_bounds = Layout.Rect.init(
|
||||||
|
bounds.x,
|
||||||
|
bounds.y + @as(i32, @intCast(filter_bar_h)),
|
||||||
|
bounds.w,
|
||||||
|
bounds.h -| filter_bar_h -| footer_h,
|
||||||
|
);
|
||||||
|
|
||||||
// Begin clipping
|
// Begin clipping
|
||||||
ctx.pushCommand(Command.clip(bounds.x, bounds.y, bounds.w, bounds.h));
|
ctx.pushCommand(Command.clip(bounds.x, bounds.y, bounds.w, bounds.h));
|
||||||
|
|
||||||
// Draw header
|
// Draw FilterBar (if configured)
|
||||||
if (config.show_headers) {
|
if (table_schema.filter_bar) |fb_config| {
|
||||||
drawing.drawHeader(ctx, bounds, table_state, table_schema, state_col_w, colors, &result);
|
const filter_bounds = Layout.Rect.init(bounds.x, bounds.y, bounds.w, fb_config.height);
|
||||||
|
|
||||||
|
// Convert TableColors to FilterBarColors
|
||||||
|
const fb_colors = table_core.FilterBarColors{
|
||||||
|
.header_background = colors.header_bg,
|
||||||
|
.border = colors.border,
|
||||||
|
.text = colors.text_normal,
|
||||||
|
.row_selected = colors.selected_row,
|
||||||
|
.text_selected = colors.text_selected,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Draw FilterBar using shared implementation
|
||||||
|
var fb_result = table_core.FilterBarResult{};
|
||||||
|
table_core.drawFilterBar(ctx, filter_bounds, fb_config, fb_colors, &table_state.filter_bar_state, &fb_result);
|
||||||
|
|
||||||
|
// Handle FilterBar results
|
||||||
|
if (fb_result.filter_changed) {
|
||||||
|
result.filter_changed = true;
|
||||||
|
result.filter_text = table_state.filter_bar_state.getFilterText();
|
||||||
|
table_state.filter_changed = true;
|
||||||
|
|
||||||
|
// Apply filter
|
||||||
|
table_state.applyFilter(table_schema.columns) catch {};
|
||||||
|
}
|
||||||
|
if (fb_result.chip_changed) {
|
||||||
|
result.chip_changed = true;
|
||||||
|
result.chip_index = fb_result.chip_index;
|
||||||
|
result.chip_active = fb_result.chip_active;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate visible row range
|
// Draw header
|
||||||
|
if (config.show_headers) {
|
||||||
|
drawing.drawHeader(ctx, content_bounds, table_state, table_schema, state_col_w, colors, &result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate visible row range (use filtered count if filter active)
|
||||||
|
const row_count = if (table_state.filter_active) table_state.getVisibleRowCount() else table_state.getRowCount();
|
||||||
const first_visible = table_state.nav.scroll_row;
|
const first_visible = table_state.nav.scroll_row;
|
||||||
const last_visible = @min(first_visible + visible_rows, table_state.getRowCount());
|
const last_visible = @min(first_visible + visible_rows, row_count);
|
||||||
|
|
||||||
// Manejar clicks en filas (separado del renderizado)
|
// Manejar clicks en filas (separado del renderizado)
|
||||||
input.handleRowClicks(ctx, bounds, table_state, table_schema, header_h, state_col_w, first_visible, last_visible, &result);
|
input.handleRowClicks(ctx, content_bounds, table_state, table_schema, header_h, state_col_w, first_visible, last_visible, &result);
|
||||||
|
|
||||||
// Construir ColumnRenderDefs para la función unificada
|
// Construir ColumnRenderDefs para la función unificada
|
||||||
var col_defs: [64]table_core.ColumnRenderDef = undefined;
|
var col_defs: [64]table_core.ColumnRenderDef = undefined;
|
||||||
|
|
@ -173,9 +222,9 @@ pub fn advancedTableRect(
|
||||||
// Z-Design: Pintar fondo del área de contenido ANTES de las filas
|
// Z-Design: Pintar fondo del área de contenido ANTES de las filas
|
||||||
// Esto asegura que tablas vacías o con pocas filas no muestren negro
|
// Esto asegura que tablas vacías o con pocas filas no muestren negro
|
||||||
ctx.pushCommand(Command.rect(
|
ctx.pushCommand(Command.rect(
|
||||||
bounds.x,
|
content_bounds.x,
|
||||||
bounds.y + @as(i32, @intCast(header_h)),
|
content_bounds.y + @as(i32, @intCast(header_h)),
|
||||||
bounds.w,
|
content_bounds.w,
|
||||||
content_h,
|
content_h,
|
||||||
colors.row_normal,
|
colors.row_normal,
|
||||||
));
|
));
|
||||||
|
|
@ -199,9 +248,9 @@ pub fn advancedTableRect(
|
||||||
|
|
||||||
var cell_buffer: [256]u8 = undefined;
|
var cell_buffer: [256]u8 = undefined;
|
||||||
_ = table_core.drawRowsWithDataSource(ctx, data_src, .{
|
_ = table_core.drawRowsWithDataSource(ctx, data_src, .{
|
||||||
.bounds_x = bounds.x,
|
.bounds_x = content_bounds.x,
|
||||||
.bounds_y = bounds.y + @as(i32, @intCast(header_h)),
|
.bounds_y = content_bounds.y + @as(i32, @intCast(header_h)),
|
||||||
.bounds_w = bounds.w,
|
.bounds_w = content_bounds.w,
|
||||||
.row_height = config.row_height,
|
.row_height = config.row_height,
|
||||||
.first_row = first_visible,
|
.first_row = first_visible,
|
||||||
.last_row = last_visible,
|
.last_row = last_visible,
|
||||||
|
|
@ -217,6 +266,49 @@ pub fn advancedTableRect(
|
||||||
.edit_buffer = &table_state.row_edit_buffer,
|
.edit_buffer = &table_state.row_edit_buffer,
|
||||||
}, &cell_buffer);
|
}, &cell_buffer);
|
||||||
|
|
||||||
|
// Draw Footer (if configured)
|
||||||
|
if (config.show_footer) {
|
||||||
|
const footer_y = bounds.y + @as(i32, @intCast(bounds.h - footer_h));
|
||||||
|
const footer_bounds = Layout.Rect.init(bounds.x, footer_y, bounds.w, footer_h);
|
||||||
|
|
||||||
|
// Build position info
|
||||||
|
const current_pos: ?usize = if (table_state.selected_row >= 0)
|
||||||
|
@as(usize, @intCast(table_state.selected_row)) + 1
|
||||||
|
else
|
||||||
|
null;
|
||||||
|
|
||||||
|
const pos_info = table_core.FooterPositionInfo{
|
||||||
|
.current_position = current_pos,
|
||||||
|
.total_count = .{
|
||||||
|
.value = table_state.rows.items.len,
|
||||||
|
.state = .ready,
|
||||||
|
},
|
||||||
|
.filtered_count = if (table_state.filter_active)
|
||||||
|
.{ .value = table_state.filtered_indices.items.len, .state = .ready }
|
||||||
|
else
|
||||||
|
null,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Format and draw footer
|
||||||
|
var footer_state = table_core.FooterState{};
|
||||||
|
const display_text = footer_state.formatDisplay(pos_info);
|
||||||
|
|
||||||
|
// Copy to persistent buffer
|
||||||
|
@memcpy(table_state.footer_display_buf[0..display_text.len], display_text);
|
||||||
|
table_state.footer_display_len = display_text.len;
|
||||||
|
|
||||||
|
table_core.drawFooter(
|
||||||
|
ctx,
|
||||||
|
footer_bounds,
|
||||||
|
table_core.FooterColors{
|
||||||
|
.background = colors.header_bg,
|
||||||
|
.text = colors.text_normal,
|
||||||
|
.border = colors.border,
|
||||||
|
},
|
||||||
|
table_state.footer_display_buf[0..table_state.footer_display_len],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// End clipping
|
// End clipping
|
||||||
ctx.pushCommand(Command.clipEnd());
|
ctx.pushCommand(Command.clipEnd());
|
||||||
|
|
||||||
|
|
@ -236,8 +328,8 @@ pub fn advancedTableRect(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw scrollbar if needed
|
// Draw scrollbar if needed
|
||||||
if (table_state.getRowCount() > visible_rows) {
|
if (row_count > visible_rows) {
|
||||||
drawing.drawScrollbar(ctx, bounds, table_state, visible_rows, config, colors);
|
drawing.drawScrollbar(ctx, content_bounds, table_state, visible_rows, config, colors);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle keyboard
|
// Handle keyboard
|
||||||
|
|
@ -247,7 +339,7 @@ pub fn advancedTableRect(
|
||||||
input.handleEditingKeyboard(ctx, table_state, table_schema, &result);
|
input.handleEditingKeyboard(ctx, table_state, table_schema, &result);
|
||||||
|
|
||||||
// Draw editing overlay
|
// Draw editing overlay
|
||||||
drawing.drawEditingOverlay(ctx, bounds, table_state, table_schema, header_h, state_col_w, colors);
|
drawing.drawEditingOverlay(ctx, content_bounds, table_state, table_schema, header_h, state_col_w, colors);
|
||||||
} else if (config.keyboard_nav) {
|
} else if (config.keyboard_nav) {
|
||||||
// Handle navigation keyboard
|
// Handle navigation keyboard
|
||||||
input.handleKeyboard(ctx, table_state, table_schema, visible_rows, &result);
|
input.handleKeyboard(ctx, table_state, table_schema, visible_rows, &result);
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,25 @@ pub const AdvancedTableResult = struct {
|
||||||
// Focus
|
// Focus
|
||||||
clicked: bool = false,
|
clicked: bool = false,
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// FilterBar (PARIDAD con VirtualAdvancedTable - Enero 2026)
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/// El filtro de texto cambió
|
||||||
|
filter_changed: bool = false,
|
||||||
|
|
||||||
|
/// Texto del filtro (válido si filter_changed)
|
||||||
|
filter_text: ?[]const u8 = null,
|
||||||
|
|
||||||
|
/// Un chip cambió de estado
|
||||||
|
chip_changed: bool = false,
|
||||||
|
|
||||||
|
/// Índice del chip que cambió
|
||||||
|
chip_index: ?u4 = null,
|
||||||
|
|
||||||
|
/// ¿El chip está ahora activo?
|
||||||
|
chip_active: bool = false,
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// Edición CRUD Excel-style (simétrico con VirtualAdvancedTableResult)
|
// Edición CRUD Excel-style (simétrico con VirtualAdvancedTableResult)
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,13 @@
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const types = @import("types.zig");
|
const types = @import("types.zig");
|
||||||
|
const table_core = @import("../table_core/table_core.zig");
|
||||||
|
|
||||||
pub const CellValue = types.CellValue;
|
pub const CellValue = types.CellValue;
|
||||||
|
// Re-export FilterBar types from table_core
|
||||||
|
pub const FilterBarConfig = table_core.FilterBarConfig;
|
||||||
|
pub const FilterChipDef = table_core.FilterChipDef;
|
||||||
|
pub const ChipSelectMode = table_core.ChipSelectMode;
|
||||||
pub const ColumnType = types.ColumnType;
|
pub const ColumnType = types.ColumnType;
|
||||||
pub const RowLockState = types.RowLockState;
|
pub const RowLockState = types.RowLockState;
|
||||||
pub const Row = types.Row;
|
pub const Row = types.Row;
|
||||||
|
|
@ -214,6 +219,9 @@ pub const TableSchema = struct {
|
||||||
/// DataStore for persistence (optional)
|
/// DataStore for persistence (optional)
|
||||||
data_store: ?DataStore = null,
|
data_store: ?DataStore = null,
|
||||||
|
|
||||||
|
/// FilterBar configuration (optional, null = no filter bar)
|
||||||
|
filter_bar: ?FilterBarConfig = null,
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// Global Callbacks
|
// Global Callbacks
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,9 @@ pub const CRUDAction = types.CRUDAction;
|
||||||
pub const Row = types.Row;
|
pub const Row = types.Row;
|
||||||
pub const TableSchema = schema_mod.TableSchema;
|
pub const TableSchema = schema_mod.TableSchema;
|
||||||
pub const MAX_EDIT_BUFFER = types.MAX_EDIT_BUFFER;
|
pub const MAX_EDIT_BUFFER = types.MAX_EDIT_BUFFER;
|
||||||
|
// FilterBar types
|
||||||
|
pub const FilterBarState = table_core.FilterBarState;
|
||||||
|
pub const FilterBarResult = table_core.FilterBarResult;
|
||||||
|
|
||||||
// Re-export AdvancedTableResult desde result.zig
|
// Re-export AdvancedTableResult desde result.zig
|
||||||
pub const AdvancedTableResult = result_mod.AdvancedTableResult;
|
pub const AdvancedTableResult = result_mod.AdvancedTableResult;
|
||||||
|
|
@ -78,6 +81,32 @@ pub const AdvancedTableState = struct {
|
||||||
/// Search timeout in ms (reset after this)
|
/// Search timeout in ms (reset after this)
|
||||||
search_timeout_ms: u64 = 1000,
|
search_timeout_ms: u64 = 1000,
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// FilterBar State (PARIDAD con VirtualAdvancedTable - Enero 2026)
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/// Estado del FilterBar (buffer de texto, cursor, chips activos)
|
||||||
|
filter_bar_state: FilterBarState = .{},
|
||||||
|
|
||||||
|
/// Índices de filas que pasan el filtro (si filter_active)
|
||||||
|
filtered_indices: std.ArrayListUnmanaged(usize) = .{},
|
||||||
|
|
||||||
|
/// ¿Hay un filtro activo?
|
||||||
|
filter_active: bool = false,
|
||||||
|
|
||||||
|
/// ¿El filtro cambió este frame?
|
||||||
|
filter_changed: bool = false,
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Footer State (PARIDAD con VirtualAdvancedTable - Enero 2026)
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/// Buffer de display del footer
|
||||||
|
footer_display_buf: [96]u8 = [_]u8{0} ** 96,
|
||||||
|
|
||||||
|
/// Longitud del texto del footer
|
||||||
|
footer_display_len: usize = 0,
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// Cell Validation (from Table widget)
|
// Cell Validation (from Table widget)
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
@ -271,6 +300,9 @@ pub const AdvancedTableState = struct {
|
||||||
}
|
}
|
||||||
self.original_order.deinit(self.allocator);
|
self.original_order.deinit(self.allocator);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Deinit filtered indices
|
||||||
|
self.filtered_indices.deinit(self.allocator);
|
||||||
}
|
}
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
@ -657,6 +689,84 @@ pub const AdvancedTableState = struct {
|
||||||
self.search_len = 0;
|
self.search_len = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// FilterBar Methods (PARIDAD con VirtualAdvancedTable - Enero 2026)
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/// Obtiene el número de filas visible (filtradas o todas)
|
||||||
|
pub fn getVisibleRowCount(self: *const AdvancedTableState) usize {
|
||||||
|
if (self.filter_active) {
|
||||||
|
return self.filtered_indices.items.len;
|
||||||
|
}
|
||||||
|
return self.rows.items.len;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convierte índice visible a índice real
|
||||||
|
/// Si hay filtro activo: índice en filtered_indices -> índice en rows
|
||||||
|
/// Si no: identidad
|
||||||
|
pub fn visibleToRealIndex(self: *const AdvancedTableState, visible_idx: usize) usize {
|
||||||
|
if (self.filter_active) {
|
||||||
|
if (visible_idx < self.filtered_indices.items.len) {
|
||||||
|
return self.filtered_indices.items[visible_idx];
|
||||||
|
}
|
||||||
|
return visible_idx; // fallback
|
||||||
|
}
|
||||||
|
return visible_idx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convierte índice real a índice visible
|
||||||
|
/// Retorna null si la fila no está visible (filtrada)
|
||||||
|
pub fn realToVisibleIndex(self: *const AdvancedTableState, real_idx: usize) ?usize {
|
||||||
|
if (self.filter_active) {
|
||||||
|
for (self.filtered_indices.items, 0..) |idx, visible| {
|
||||||
|
if (idx == real_idx) return visible;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return real_idx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Aplica el filtro sobre las filas
|
||||||
|
/// search_columns: columnas a buscar (null = todas)
|
||||||
|
pub fn applyFilter(self: *AdvancedTableState, columns: []const schema_mod.ColumnDef) !void {
|
||||||
|
const filter_text = self.filter_bar_state.getFilterText();
|
||||||
|
|
||||||
|
self.filtered_indices.clearRetainingCapacity();
|
||||||
|
|
||||||
|
if (filter_text.len == 0) {
|
||||||
|
// Sin filtro: todas las filas visibles
|
||||||
|
self.filter_active = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.filter_active = true;
|
||||||
|
|
||||||
|
// Filtrar filas que contienen el texto (case-insensitive)
|
||||||
|
var format_buf: [256]u8 = undefined;
|
||||||
|
for (self.rows.items, 0..) |row, i| {
|
||||||
|
for (columns) |col| {
|
||||||
|
const value = row.get(col.name);
|
||||||
|
const text = value.format(&format_buf);
|
||||||
|
if (table_core.startsWithIgnoreCase(text, filter_text)) {
|
||||||
|
try self.filtered_indices.append(self.allocator, i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Limpia el filtro
|
||||||
|
pub fn clearFilter(self: *AdvancedTableState) void {
|
||||||
|
self.filter_bar_state.clearFilterText();
|
||||||
|
self.filter_active = false;
|
||||||
|
self.filtered_indices.clearRetainingCapacity();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Obtiene el texto del filtro actual
|
||||||
|
pub fn getFilterText(self: *const AdvancedTableState) []const u8 {
|
||||||
|
return self.filter_bar_state.getFilterText();
|
||||||
|
}
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// Cell Validation (from Table widget)
|
// Cell Validation (from Table widget)
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
|
||||||
|
|
@ -408,10 +408,12 @@ pub const TableConfig = struct {
|
||||||
row_height: u32 = 24,
|
row_height: u32 = 24,
|
||||||
state_indicator_width: u32 = 24,
|
state_indicator_width: u32 = 24,
|
||||||
min_column_width: u32 = 40,
|
min_column_width: u32 = 40,
|
||||||
|
footer_height: u32 = 20,
|
||||||
|
|
||||||
// Features
|
// Features
|
||||||
show_headers: bool = true,
|
show_headers: bool = true,
|
||||||
show_row_state_indicators: bool = true,
|
show_row_state_indicators: bool = true,
|
||||||
|
show_footer: bool = false, // PARIDAD VirtualAdvancedTable - Enero 2026
|
||||||
alternating_rows: bool = true,
|
alternating_rows: bool = true,
|
||||||
|
|
||||||
// Editing
|
// Editing
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,12 @@ pub const TableDataSource = struct {
|
||||||
|
|
||||||
/// Invalida cache interno (para refresh)
|
/// Invalida cache interno (para refresh)
|
||||||
invalidate: ?*const fn (ptr: *anyopaque) void = null,
|
invalidate: ?*const fn (ptr: *anyopaque) void = null,
|
||||||
|
|
||||||
|
/// Valida el valor de una celda antes de commit (opcional)
|
||||||
|
/// Retorna null si es válido, o un mensaje de error si no lo es.
|
||||||
|
/// El CellEditor debe llamar a esta función antes de guardar.
|
||||||
|
/// PARIDAD VirtualAdvancedTable - Enero 2026
|
||||||
|
validateCell: ?*const fn (ptr: *anyopaque, row: usize, col: usize, value: []const u8) ?[]const u8 = null,
|
||||||
};
|
};
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
@ -97,6 +103,15 @@ pub const TableDataSource = struct {
|
||||||
pub fn isGhostRow(self: TableDataSource, row: usize) bool {
|
pub fn isGhostRow(self: TableDataSource, row: usize) bool {
|
||||||
return self.getRowId(row) == NEW_ROW_ID;
|
return self.getRowId(row) == NEW_ROW_ID;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Valida el valor de una celda
|
||||||
|
/// Retorna null si válido, mensaje de error si no
|
||||||
|
pub fn validateCell(self: TableDataSource, row: usize, col: usize, value: []const u8) ?[]const u8 {
|
||||||
|
if (self.vtable.validateCell) |func| {
|
||||||
|
return func(self.ptr, row, col, value);
|
||||||
|
}
|
||||||
|
return null; // Default: siempre válido
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Helper para crear TableDataSource desde un tipo concreto
|
/// Helper para crear TableDataSource desde un tipo concreto
|
||||||
|
|
@ -118,6 +133,9 @@ pub fn makeTableDataSource(comptime T: type, impl: *T) TableDataSource {
|
||||||
if (@hasDecl(T, "invalidate")) {
|
if (@hasDecl(T, "invalidate")) {
|
||||||
vt.invalidate = @ptrCast(&T.invalidate);
|
vt.invalidate = @ptrCast(&T.invalidate);
|
||||||
}
|
}
|
||||||
|
if (@hasDecl(T, "validateCell")) {
|
||||||
|
vt.validateCell = @ptrCast(&T.validateCell);
|
||||||
|
}
|
||||||
break :blk vt;
|
break :blk vt;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
409
src/widgets/table_core/filter_bar.zig
Normal file
409
src/widgets/table_core/filter_bar.zig
Normal file
|
|
@ -0,0 +1,409 @@
|
||||||
|
//! FilterBar - Barra de filtros compartida para tablas
|
||||||
|
//!
|
||||||
|
//! Componente visual con chips y campo de búsqueda.
|
||||||
|
//! Usado por AdvancedTable y VirtualAdvancedTable.
|
||||||
|
|
||||||
|
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");
|
||||||
|
const text_input = @import("../text_input.zig");
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Types
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/// Definición de un chip/prefiltro
|
||||||
|
pub const FilterChipDef = struct {
|
||||||
|
/// ID único del chip (ej: "todos", "pendientes")
|
||||||
|
id: []const u8,
|
||||||
|
|
||||||
|
/// Texto visible (ej: "[T]odos", "[P]end.")
|
||||||
|
label: []const u8,
|
||||||
|
|
||||||
|
/// Tecla de atajo con Ctrl (ej: 'T' para Ctrl+T), null si no tiene
|
||||||
|
shortcut: ?u8 = null,
|
||||||
|
|
||||||
|
/// Activo por defecto al iniciar
|
||||||
|
is_default: bool = false,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Modo de selección de chips
|
||||||
|
pub const ChipSelectMode = enum {
|
||||||
|
/// Solo uno puede estar activo (radio buttons)
|
||||||
|
single,
|
||||||
|
|
||||||
|
/// Varios pueden estar activos (checkboxes)
|
||||||
|
multi,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Configuración de la barra de filtros
|
||||||
|
pub const FilterBarConfig = struct {
|
||||||
|
/// Mostrar campo de búsqueda
|
||||||
|
show_search: bool = true,
|
||||||
|
|
||||||
|
/// Placeholder del campo búsqueda
|
||||||
|
search_placeholder: []const u8 = "Buscar...",
|
||||||
|
|
||||||
|
/// Debounce en milisegundos (0 = sin debounce)
|
||||||
|
search_debounce_ms: u32 = 300,
|
||||||
|
|
||||||
|
/// Chips/prefiltros a mostrar
|
||||||
|
chips: []const FilterChipDef = &.{},
|
||||||
|
|
||||||
|
/// Modo de selección de chips
|
||||||
|
chip_mode: ChipSelectMode = .single,
|
||||||
|
|
||||||
|
/// Mostrar botón limpiar (✖)
|
||||||
|
show_clear_button: bool = true,
|
||||||
|
|
||||||
|
/// Altura de la barra de filtros
|
||||||
|
height: u16 = 28,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Colores para la barra de filtros
|
||||||
|
pub const FilterBarColors = struct {
|
||||||
|
header_background: Style.Color = Style.Color.rgb(224, 224, 224),
|
||||||
|
border: Style.Color = Style.Color.rgb(204, 204, 204),
|
||||||
|
text: Style.Color = Style.Color.rgb(0, 0, 0),
|
||||||
|
row_selected: Style.Color = Style.Color.rgb(0, 120, 212),
|
||||||
|
text_selected: Style.Color = Style.Color.rgb(255, 255, 255),
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Estado del FilterBar (para almacenar en el state del componente padre)
|
||||||
|
pub const FilterBarState = struct {
|
||||||
|
/// Buffer de texto del filtro
|
||||||
|
filter_buf: [256]u8 = [_]u8{0} ** 256,
|
||||||
|
|
||||||
|
/// Longitud del texto del filtro
|
||||||
|
filter_len: usize = 0,
|
||||||
|
|
||||||
|
/// Cursor del campo de búsqueda
|
||||||
|
search_cursor: usize = 0,
|
||||||
|
|
||||||
|
/// Inicio de selección (null = sin selección)
|
||||||
|
search_selection_start: ?usize = null,
|
||||||
|
|
||||||
|
/// ¿El campo de búsqueda tiene focus?
|
||||||
|
search_has_focus: bool = false,
|
||||||
|
|
||||||
|
/// Chips activos (bitfield, max 16 chips)
|
||||||
|
active_chips: u16 = 0,
|
||||||
|
|
||||||
|
/// Flag para notificar cambios de texto
|
||||||
|
filter_text_changed: bool = false,
|
||||||
|
|
||||||
|
/// Verifica si un chip está activo
|
||||||
|
pub fn isChipActive(self: *const FilterBarState, idx: u4) bool {
|
||||||
|
return (self.active_chips & (@as(u16, 1) << idx)) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Activa/desactiva un chip según el modo
|
||||||
|
pub fn activateChip(self: *FilterBarState, idx: u4, mode: ChipSelectMode) void {
|
||||||
|
const mask = @as(u16, 1) << idx;
|
||||||
|
switch (mode) {
|
||||||
|
.single => {
|
||||||
|
// En modo single, solo uno activo a la vez
|
||||||
|
self.active_chips = mask;
|
||||||
|
},
|
||||||
|
.multi => {
|
||||||
|
// En modo multi, toggle
|
||||||
|
self.active_chips ^= mask;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Limpia el texto del filtro
|
||||||
|
pub fn clearFilterText(self: *FilterBarState) void {
|
||||||
|
self.filter_len = 0;
|
||||||
|
@memset(&self.filter_buf, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Obtiene el texto del filtro actual
|
||||||
|
pub fn getFilterText(self: *const FilterBarState) []const u8 {
|
||||||
|
return self.filter_buf[0..self.filter_len];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Inicializa el estado con chips por defecto
|
||||||
|
pub fn initWithDefaults(self: *FilterBarState, chips: []const FilterChipDef) void {
|
||||||
|
self.active_chips = 0;
|
||||||
|
for (chips, 0..) |chip, idx| {
|
||||||
|
if (chip.is_default and idx < 16) {
|
||||||
|
self.active_chips |= @as(u16, 1) << @intCast(idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Resultado del FilterBar (cambios a reportar al padre)
|
||||||
|
pub const FilterBarResult = struct {
|
||||||
|
/// El texto del filtro cambió
|
||||||
|
filter_changed: bool = false,
|
||||||
|
|
||||||
|
/// Texto actual del filtro (solo válido si filter_changed)
|
||||||
|
filter_text: []const u8 = "",
|
||||||
|
|
||||||
|
/// Un chip cambió de estado
|
||||||
|
chip_changed: bool = false,
|
||||||
|
|
||||||
|
/// Índice del chip que cambió (solo válido si chip_changed)
|
||||||
|
chip_index: u4 = 0,
|
||||||
|
|
||||||
|
/// ¿El chip está ahora activo?
|
||||||
|
chip_active: bool = false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Drawing
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/// Dibuja la barra de filtros
|
||||||
|
pub fn drawFilterBar(
|
||||||
|
ctx: *Context,
|
||||||
|
bounds: Layout.Rect,
|
||||||
|
config: FilterBarConfig,
|
||||||
|
colors: FilterBarColors,
|
||||||
|
state: *FilterBarState,
|
||||||
|
result: *FilterBarResult,
|
||||||
|
) void {
|
||||||
|
const padding: i32 = 6;
|
||||||
|
const chip_h: u32 = 22;
|
||||||
|
const chip_padding: i32 = 10;
|
||||||
|
const chip_spacing: i32 = 6;
|
||||||
|
const chip_radius: u8 = 4; // Z-Design V2: consistente con botones
|
||||||
|
const clear_btn_w: u32 = 22;
|
||||||
|
|
||||||
|
// Background
|
||||||
|
ctx.pushCommand(Command.rect(
|
||||||
|
bounds.x,
|
||||||
|
bounds.y,
|
||||||
|
bounds.w,
|
||||||
|
bounds.h,
|
||||||
|
colors.header_background,
|
||||||
|
));
|
||||||
|
|
||||||
|
// Línea inferior
|
||||||
|
ctx.pushCommand(Command.rect(
|
||||||
|
bounds.x,
|
||||||
|
bounds.y + @as(i32, @intCast(bounds.h)) - 1,
|
||||||
|
bounds.w,
|
||||||
|
1,
|
||||||
|
colors.border,
|
||||||
|
));
|
||||||
|
|
||||||
|
var current_x = bounds.x + padding;
|
||||||
|
const item_y = bounds.y + @divTrunc(@as(i32, @intCast(bounds.h)) - @as(i32, @intCast(chip_h)), 2);
|
||||||
|
const item_h = bounds.h -| @as(u32, @intCast(padding * 2));
|
||||||
|
const mouse = ctx.input.mousePos();
|
||||||
|
|
||||||
|
// Draw Chips
|
||||||
|
if (config.chips.len > 0) {
|
||||||
|
for (config.chips, 0..) |chip, idx| {
|
||||||
|
const chip_idx: u4 = @intCast(idx);
|
||||||
|
const is_active = state.isChipActive(chip_idx);
|
||||||
|
|
||||||
|
const label_len = chip.label.len;
|
||||||
|
const chip_w: u32 = @intCast(label_len * 7 + chip_padding * 2);
|
||||||
|
|
||||||
|
const chip_bounds = Layout.Rect.init(
|
||||||
|
current_x,
|
||||||
|
item_y,
|
||||||
|
chip_w,
|
||||||
|
chip_h,
|
||||||
|
);
|
||||||
|
|
||||||
|
const chip_hovered = chip_bounds.contains(mouse.x, mouse.y);
|
||||||
|
|
||||||
|
const chip_bg = if (is_active)
|
||||||
|
colors.row_selected
|
||||||
|
else if (chip_hovered)
|
||||||
|
Style.Color.rgb(
|
||||||
|
colors.header_background.r -| 15,
|
||||||
|
colors.header_background.g -| 15,
|
||||||
|
colors.header_background.b -| 15,
|
||||||
|
)
|
||||||
|
else
|
||||||
|
colors.header_background;
|
||||||
|
|
||||||
|
const chip_text_color = if (is_active)
|
||||||
|
colors.text_selected
|
||||||
|
else
|
||||||
|
colors.text;
|
||||||
|
|
||||||
|
const chip_border = if (is_active)
|
||||||
|
colors.row_selected
|
||||||
|
else
|
||||||
|
colors.border;
|
||||||
|
|
||||||
|
ctx.pushCommand(Command.roundedRect(
|
||||||
|
chip_bounds.x,
|
||||||
|
chip_bounds.y,
|
||||||
|
chip_bounds.w,
|
||||||
|
chip_bounds.h,
|
||||||
|
chip_bg,
|
||||||
|
chip_radius,
|
||||||
|
));
|
||||||
|
|
||||||
|
if (!is_active) {
|
||||||
|
ctx.pushCommand(Command.roundedRectOutline(
|
||||||
|
chip_bounds.x,
|
||||||
|
chip_bounds.y,
|
||||||
|
chip_bounds.w,
|
||||||
|
chip_bounds.h,
|
||||||
|
chip_border,
|
||||||
|
chip_radius,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.pushCommand(Command.text(
|
||||||
|
chip_bounds.x + chip_padding,
|
||||||
|
chip_bounds.y + 4,
|
||||||
|
chip.label,
|
||||||
|
chip_text_color,
|
||||||
|
));
|
||||||
|
|
||||||
|
if (chip_hovered and ctx.input.mousePressed(.left)) {
|
||||||
|
state.activateChip(chip_idx, config.chip_mode);
|
||||||
|
result.chip_changed = true;
|
||||||
|
result.chip_index = chip_idx;
|
||||||
|
result.chip_active = state.isChipActive(chip_idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
current_x += @as(i32, @intCast(chip_w)) + chip_spacing;
|
||||||
|
}
|
||||||
|
|
||||||
|
current_x += padding;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw Search Input
|
||||||
|
if (config.show_search) {
|
||||||
|
const clear_space: u32 = if (config.show_clear_button) clear_btn_w + @as(u32, @intCast(padding)) else 0;
|
||||||
|
const search_end = bounds.x + @as(i32, @intCast(bounds.w)) - padding - @as(i32, @intCast(clear_space));
|
||||||
|
const search_w: u32 = @intCast(@max(60, search_end - current_x));
|
||||||
|
|
||||||
|
const search_bounds = Layout.Rect.init(
|
||||||
|
current_x,
|
||||||
|
item_y,
|
||||||
|
search_w,
|
||||||
|
item_h,
|
||||||
|
);
|
||||||
|
|
||||||
|
var text_state = text_input.TextInputState{
|
||||||
|
.buffer = &state.filter_buf,
|
||||||
|
.len = state.filter_len,
|
||||||
|
.cursor = state.search_cursor,
|
||||||
|
.selection_start = state.search_selection_start,
|
||||||
|
.focused = state.search_has_focus,
|
||||||
|
};
|
||||||
|
|
||||||
|
const text_result = text_input.textInputRect(ctx, search_bounds, &text_state, .{
|
||||||
|
.placeholder = config.search_placeholder,
|
||||||
|
.padding = 3,
|
||||||
|
});
|
||||||
|
|
||||||
|
state.filter_len = text_state.len;
|
||||||
|
state.search_cursor = text_state.cursor;
|
||||||
|
state.search_selection_start = text_state.selection_start;
|
||||||
|
|
||||||
|
if (text_result.clicked) {
|
||||||
|
state.search_has_focus = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text_result.changed) {
|
||||||
|
state.filter_text_changed = true;
|
||||||
|
result.filter_changed = true;
|
||||||
|
result.filter_text = state.filter_buf[0..state.filter_len];
|
||||||
|
}
|
||||||
|
|
||||||
|
current_x += @as(i32, @intCast(search_w)) + padding;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw Clear Button
|
||||||
|
if (config.show_clear_button and state.filter_len > 0) {
|
||||||
|
const clear_x = bounds.x + @as(i32, @intCast(bounds.w - clear_btn_w)) - padding;
|
||||||
|
const clear_bounds = Layout.Rect.init(
|
||||||
|
clear_x,
|
||||||
|
item_y,
|
||||||
|
clear_btn_w,
|
||||||
|
chip_h,
|
||||||
|
);
|
||||||
|
|
||||||
|
const clear_hovered = clear_bounds.contains(mouse.x, mouse.y);
|
||||||
|
|
||||||
|
const clear_bg = if (clear_hovered)
|
||||||
|
Style.Color.rgb(220, 80, 80)
|
||||||
|
else
|
||||||
|
Style.Color.rgb(180, 60, 60);
|
||||||
|
|
||||||
|
const clear_text = Style.Color.rgb(255, 255, 255);
|
||||||
|
|
||||||
|
ctx.pushCommand(Command.roundedRect(
|
||||||
|
clear_bounds.x,
|
||||||
|
clear_bounds.y,
|
||||||
|
clear_bounds.w,
|
||||||
|
clear_bounds.h,
|
||||||
|
clear_bg,
|
||||||
|
chip_radius,
|
||||||
|
));
|
||||||
|
|
||||||
|
ctx.pushCommand(Command.text(
|
||||||
|
clear_bounds.x + 7,
|
||||||
|
clear_bounds.y + 4,
|
||||||
|
"X",
|
||||||
|
clear_text,
|
||||||
|
));
|
||||||
|
|
||||||
|
if (clear_hovered and ctx.input.mousePressed(.left)) {
|
||||||
|
state.clearFilterText();
|
||||||
|
state.search_cursor = 0;
|
||||||
|
state.search_selection_start = null;
|
||||||
|
result.filter_changed = true;
|
||||||
|
result.filter_text = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Tests
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
test "FilterBarState chip operations" {
|
||||||
|
var state = FilterBarState{};
|
||||||
|
|
||||||
|
// Test single mode
|
||||||
|
state.activateChip(0, .single);
|
||||||
|
try std.testing.expect(state.isChipActive(0));
|
||||||
|
try std.testing.expect(!state.isChipActive(1));
|
||||||
|
|
||||||
|
state.activateChip(1, .single);
|
||||||
|
try std.testing.expect(!state.isChipActive(0));
|
||||||
|
try std.testing.expect(state.isChipActive(1));
|
||||||
|
|
||||||
|
// Test multi mode
|
||||||
|
state.active_chips = 0;
|
||||||
|
state.activateChip(0, .multi);
|
||||||
|
state.activateChip(2, .multi);
|
||||||
|
try std.testing.expect(state.isChipActive(0));
|
||||||
|
try std.testing.expect(!state.isChipActive(1));
|
||||||
|
try std.testing.expect(state.isChipActive(2));
|
||||||
|
|
||||||
|
// Toggle off
|
||||||
|
state.activateChip(0, .multi);
|
||||||
|
try std.testing.expect(!state.isChipActive(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "FilterBarState filter text" {
|
||||||
|
var state = FilterBarState{};
|
||||||
|
|
||||||
|
// Set filter text
|
||||||
|
const text = "test";
|
||||||
|
@memcpy(state.filter_buf[0..text.len], text);
|
||||||
|
state.filter_len = text.len;
|
||||||
|
|
||||||
|
try std.testing.expectEqualStrings("test", state.getFilterText());
|
||||||
|
|
||||||
|
// Clear
|
||||||
|
state.clearFilterText();
|
||||||
|
try std.testing.expectEqualStrings("", state.getFilterText());
|
||||||
|
}
|
||||||
206
src/widgets/table_core/footer.zig
Normal file
206
src/widgets/table_core/footer.zig
Normal file
|
|
@ -0,0 +1,206 @@
|
||||||
|
//! Footer - Pie de tabla compartido
|
||||||
|
//!
|
||||||
|
//! Muestra información de posición y conteo: "X de Y"
|
||||||
|
//! Usado por AdvancedTable y VirtualAdvancedTable.
|
||||||
|
|
||||||
|
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");
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Types
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/// Estado de carga para contadores
|
||||||
|
pub const LoadState = enum {
|
||||||
|
/// No se ha iniciado la carga
|
||||||
|
unknown,
|
||||||
|
|
||||||
|
/// Carga en progreso (mostrar "...")
|
||||||
|
loading,
|
||||||
|
|
||||||
|
/// Carga parcial (mostrar "500+...")
|
||||||
|
partial,
|
||||||
|
|
||||||
|
/// Carga completa (mostrar número final)
|
||||||
|
ready,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Información de conteo con estado
|
||||||
|
pub const CountInfo = struct {
|
||||||
|
/// Valor actual (puede ser parcial)
|
||||||
|
value: usize = 0,
|
||||||
|
|
||||||
|
/// Estado de la carga
|
||||||
|
state: LoadState = .unknown,
|
||||||
|
|
||||||
|
/// Formato para mostrar según estado
|
||||||
|
pub fn format(self: CountInfo, buf: []u8) []const u8 {
|
||||||
|
return switch (self.state) {
|
||||||
|
.unknown, .loading => "...",
|
||||||
|
.partial => std.fmt.bufPrint(buf, "{d}+...", .{self.value}) catch "...",
|
||||||
|
.ready => std.fmt.bufPrint(buf, "{d}", .{self.value}) catch "?",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Configuración del footer
|
||||||
|
pub const FooterConfig = struct {
|
||||||
|
/// Altura del footer
|
||||||
|
height: u16 = 20,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Colores del footer
|
||||||
|
pub const FooterColors = struct {
|
||||||
|
background: Style.Color = Style.Color.rgb(224, 224, 224),
|
||||||
|
text: Style.Color = Style.Color.rgb(0, 0, 0),
|
||||||
|
border: Style.Color = Style.Color.rgb(204, 204, 204),
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Información de posición para el footer
|
||||||
|
pub const FooterPositionInfo = struct {
|
||||||
|
/// Posición actual (1-based, o null si no hay selección)
|
||||||
|
current_position: ?usize = null,
|
||||||
|
|
||||||
|
/// Total de elementos
|
||||||
|
total_count: CountInfo = .{},
|
||||||
|
|
||||||
|
/// Total filtrado (si hay filtro activo)
|
||||||
|
filtered_count: ?CountInfo = null,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Estado del footer (buffer de display)
|
||||||
|
pub const FooterState = struct {
|
||||||
|
display_buf: [96]u8 = [_]u8{0} ** 96,
|
||||||
|
display_len: usize = 0,
|
||||||
|
|
||||||
|
/// Formatea y almacena el texto de display
|
||||||
|
pub fn formatDisplay(self: *FooterState, info: FooterPositionInfo) []const u8 {
|
||||||
|
var pos_buf: [32]u8 = undefined;
|
||||||
|
var count_buf: [64]u8 = undefined;
|
||||||
|
|
||||||
|
const pos_str = if (info.current_position) |pos|
|
||||||
|
std.fmt.bufPrint(&pos_buf, "{d}", .{pos}) catch "?"
|
||||||
|
else
|
||||||
|
"-";
|
||||||
|
|
||||||
|
const count_str = if (info.filtered_count) |filtered| blk: {
|
||||||
|
var total_buf: [32]u8 = undefined;
|
||||||
|
const total_str = info.total_count.format(&total_buf);
|
||||||
|
const filtered_str = filtered.format(&count_buf);
|
||||||
|
break :blk std.fmt.bufPrint(&self.display_buf, "{s} de {s} ({s})", .{ pos_str, filtered_str, total_str }) catch "...";
|
||||||
|
} else blk: {
|
||||||
|
const total_str = info.total_count.format(&count_buf);
|
||||||
|
break :blk std.fmt.bufPrint(&self.display_buf, "{s} de {s}", .{ pos_str, total_str }) catch "...";
|
||||||
|
};
|
||||||
|
|
||||||
|
self.display_len = count_str.len;
|
||||||
|
return count_str;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Drawing
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/// Dibuja el footer con información de posición
|
||||||
|
pub fn drawFooter(
|
||||||
|
ctx: *Context,
|
||||||
|
bounds: Layout.Rect,
|
||||||
|
colors: FooterColors,
|
||||||
|
display_text: []const u8,
|
||||||
|
) void {
|
||||||
|
// Background
|
||||||
|
ctx.pushCommand(Command.rect(
|
||||||
|
bounds.x,
|
||||||
|
bounds.y,
|
||||||
|
bounds.w,
|
||||||
|
bounds.h,
|
||||||
|
colors.background,
|
||||||
|
));
|
||||||
|
|
||||||
|
// Línea superior (separador)
|
||||||
|
ctx.pushCommand(Command.rect(
|
||||||
|
bounds.x,
|
||||||
|
bounds.y,
|
||||||
|
bounds.w,
|
||||||
|
1,
|
||||||
|
colors.border,
|
||||||
|
));
|
||||||
|
|
||||||
|
// Texto
|
||||||
|
ctx.pushCommand(Command.text(
|
||||||
|
bounds.x + 4,
|
||||||
|
bounds.y + 2,
|
||||||
|
display_text,
|
||||||
|
colors.text,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Versión con formateo automático
|
||||||
|
pub fn drawFooterWithInfo(
|
||||||
|
ctx: *Context,
|
||||||
|
bounds: Layout.Rect,
|
||||||
|
colors: FooterColors,
|
||||||
|
info: FooterPositionInfo,
|
||||||
|
state: *FooterState,
|
||||||
|
) void {
|
||||||
|
const display_text = state.formatDisplay(info);
|
||||||
|
drawFooter(ctx, bounds, colors, display_text);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Tests
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
test "CountInfo format" {
|
||||||
|
var buf: [32]u8 = undefined;
|
||||||
|
|
||||||
|
var info = CountInfo{ .state = .loading };
|
||||||
|
try std.testing.expectEqualStrings("...", info.format(&buf));
|
||||||
|
|
||||||
|
info = CountInfo{ .value = 500, .state = .partial };
|
||||||
|
try std.testing.expectEqualStrings("500+...", info.format(&buf));
|
||||||
|
|
||||||
|
info = CountInfo{ .value = 1234, .state = .ready };
|
||||||
|
try std.testing.expectEqualStrings("1234", info.format(&buf));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "FooterState formatDisplay basic" {
|
||||||
|
var state = FooterState{};
|
||||||
|
|
||||||
|
const info = FooterPositionInfo{
|
||||||
|
.current_position = 5,
|
||||||
|
.total_count = .{ .value = 100, .state = .ready },
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = state.formatDisplay(info);
|
||||||
|
try std.testing.expectEqualStrings("5 de 100", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "FooterState formatDisplay with filter" {
|
||||||
|
var state = FooterState{};
|
||||||
|
|
||||||
|
const info = FooterPositionInfo{
|
||||||
|
.current_position = 3,
|
||||||
|
.total_count = .{ .value = 1000, .state = .ready },
|
||||||
|
.filtered_count = .{ .value = 25, .state = .ready },
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = state.formatDisplay(info);
|
||||||
|
try std.testing.expectEqualStrings("3 de 25 (1000)", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "FooterState formatDisplay no selection" {
|
||||||
|
var state = FooterState{};
|
||||||
|
|
||||||
|
const info = FooterPositionInfo{
|
||||||
|
.current_position = null,
|
||||||
|
.total_count = .{ .value = 50, .state = .ready },
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = state.formatDisplay(info);
|
||||||
|
try std.testing.expectEqualStrings("- de 50", result);
|
||||||
|
}
|
||||||
|
|
@ -120,6 +120,27 @@ pub const utils = @import("utils.zig");
|
||||||
pub const blendColor = utils.blendColor;
|
pub const blendColor = utils.blendColor;
|
||||||
pub const startsWithIgnoreCase = utils.startsWithIgnoreCase;
|
pub const startsWithIgnoreCase = utils.startsWithIgnoreCase;
|
||||||
|
|
||||||
|
// FilterBar - Barra de filtros compartida
|
||||||
|
pub const filter_bar = @import("filter_bar.zig");
|
||||||
|
pub const FilterBarConfig = filter_bar.FilterBarConfig;
|
||||||
|
pub const FilterBarColors = filter_bar.FilterBarColors;
|
||||||
|
pub const FilterBarState = filter_bar.FilterBarState;
|
||||||
|
pub const FilterBarResult = filter_bar.FilterBarResult;
|
||||||
|
pub const FilterChipDef = filter_bar.FilterChipDef;
|
||||||
|
pub const ChipSelectMode = filter_bar.ChipSelectMode;
|
||||||
|
pub const drawFilterBar = filter_bar.drawFilterBar;
|
||||||
|
|
||||||
|
// Footer - Pie de tabla compartido
|
||||||
|
pub const footer = @import("footer.zig");
|
||||||
|
pub const FooterConfig = footer.FooterConfig;
|
||||||
|
pub const FooterColors = footer.FooterColors;
|
||||||
|
pub const FooterState = footer.FooterState;
|
||||||
|
pub const FooterPositionInfo = footer.FooterPositionInfo;
|
||||||
|
pub const CountInfo = footer.CountInfo;
|
||||||
|
pub const LoadState = footer.LoadState;
|
||||||
|
pub const drawFooter = footer.drawFooter;
|
||||||
|
pub const drawFooterWithInfo = footer.drawFooterWithInfo;
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Tests (re-export de todos los módulos)
|
// Tests (re-export de todos los módulos)
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
@ -134,4 +155,6 @@ test {
|
||||||
_ = @import("rendering.zig");
|
_ = @import("rendering.zig");
|
||||||
_ = @import("scrollbars.zig");
|
_ = @import("scrollbars.zig");
|
||||||
_ = @import("utils.zig");
|
_ = @import("utils.zig");
|
||||||
|
_ = @import("filter_bar.zig");
|
||||||
|
_ = @import("footer.zig");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,9 @@
|
||||||
//!
|
//!
|
||||||
//! Funciones de renderizado extraídas del archivo principal para mejorar
|
//! Funciones de renderizado extraídas del archivo principal para mejorar
|
||||||
//! modularidad y reducir el tamaño del archivo principal.
|
//! modularidad y reducir el tamaño del archivo principal.
|
||||||
|
//!
|
||||||
|
//! NOTA DRY (2026-01): drawFilterBar y drawFooter delegan a table_core
|
||||||
|
//! para compartir código con AdvancedTable.
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const Context = @import("../../core/context.zig").Context;
|
const Context = @import("../../core/context.zig").Context;
|
||||||
|
|
@ -21,9 +24,37 @@ pub const FilterBarConfig = types.FilterBarConfig;
|
||||||
pub const VirtualAdvancedTableResult = @import("virtual_advanced_table.zig").VirtualAdvancedTableResult;
|
pub const VirtualAdvancedTableResult = @import("virtual_advanced_table.zig").VirtualAdvancedTableResult;
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Draw: FilterBar
|
// Draw: FilterBar (delega a table_core para DRY)
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
|
/// Wrapper que adapta VirtualAdvancedTableState a FilterBarState interface
|
||||||
|
const VirtualFilterBarAdapter = struct {
|
||||||
|
state: *VirtualAdvancedTableState,
|
||||||
|
|
||||||
|
fn toFilterBarState(self: *VirtualFilterBarAdapter) table_core.FilterBarState {
|
||||||
|
var fb_state = table_core.FilterBarState{
|
||||||
|
.filter_len = self.state.filter_len,
|
||||||
|
.search_cursor = self.state.search_cursor,
|
||||||
|
.search_selection_start = self.state.search_selection_start,
|
||||||
|
.search_has_focus = self.state.search_has_focus,
|
||||||
|
.active_chips = self.state.active_chips,
|
||||||
|
.filter_text_changed = self.state.filter_text_changed,
|
||||||
|
};
|
||||||
|
@memcpy(&fb_state.filter_buf, &self.state.filter_buf);
|
||||||
|
return fb_state;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn syncFromFilterBarState(self: *VirtualFilterBarAdapter, fb_state: *const table_core.FilterBarState) void {
|
||||||
|
@memcpy(&self.state.filter_buf, &fb_state.filter_buf);
|
||||||
|
self.state.filter_len = fb_state.filter_len;
|
||||||
|
self.state.search_cursor = fb_state.search_cursor;
|
||||||
|
self.state.search_selection_start = fb_state.search_selection_start;
|
||||||
|
self.state.search_has_focus = fb_state.search_has_focus;
|
||||||
|
self.state.active_chips = fb_state.active_chips;
|
||||||
|
self.state.filter_text_changed = fb_state.filter_text_changed;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
pub fn drawFilterBar(
|
pub fn drawFilterBar(
|
||||||
ctx: *Context,
|
ctx: *Context,
|
||||||
bounds: Layout.Rect,
|
bounds: Layout.Rect,
|
||||||
|
|
@ -32,200 +63,45 @@ pub fn drawFilterBar(
|
||||||
list_state: *VirtualAdvancedTableState,
|
list_state: *VirtualAdvancedTableState,
|
||||||
result: *VirtualAdvancedTableResult,
|
result: *VirtualAdvancedTableResult,
|
||||||
) void {
|
) void {
|
||||||
const padding: i32 = 6;
|
// Convertir tipos de Virtual a tipos genéricos de table_core
|
||||||
const chip_h: u32 = 22;
|
const tc_config = table_core.FilterBarConfig{
|
||||||
const chip_padding: i32 = 10;
|
.show_search = config.show_search,
|
||||||
const chip_spacing: i32 = 6;
|
.search_placeholder = config.search_placeholder,
|
||||||
const chip_radius: u8 = 4; // Z-Design V2: consistente con botones
|
.search_debounce_ms = config.search_debounce_ms,
|
||||||
const clear_btn_w: u32 = 22;
|
.chips = @ptrCast(config.chips), // FilterChipDef es compatible
|
||||||
|
.chip_mode = @enumFromInt(@intFromEnum(config.chip_mode)),
|
||||||
|
.show_clear_button = config.show_clear_button,
|
||||||
|
.height = config.height,
|
||||||
|
};
|
||||||
|
|
||||||
// Background
|
const tc_colors = table_core.FilterBarColors{
|
||||||
ctx.pushCommand(Command.rect(
|
.header_background = colors.header_background,
|
||||||
bounds.x,
|
.border = colors.border,
|
||||||
bounds.y,
|
.text = colors.text,
|
||||||
bounds.w,
|
.row_selected = colors.row_selected,
|
||||||
bounds.h,
|
.text_selected = colors.text_selected,
|
||||||
colors.header_background,
|
};
|
||||||
));
|
|
||||||
|
|
||||||
// Línea inferior
|
// Crear adapter para sincronizar estado
|
||||||
ctx.pushCommand(Command.rect(
|
var adapter = VirtualFilterBarAdapter{ .state = list_state };
|
||||||
bounds.x,
|
var fb_state = adapter.toFilterBarState();
|
||||||
bounds.y + @as(i32, @intCast(bounds.h)) - 1,
|
|
||||||
bounds.w,
|
|
||||||
1,
|
|
||||||
colors.border,
|
|
||||||
));
|
|
||||||
|
|
||||||
var current_x = bounds.x + padding;
|
// Llamar a implementación compartida
|
||||||
const item_y = bounds.y + @divTrunc(@as(i32, @intCast(bounds.h)) - @as(i32, @intCast(chip_h)), 2);
|
var fb_result = table_core.FilterBarResult{};
|
||||||
const item_h = bounds.h -| @as(u32, @intCast(padding * 2));
|
table_core.drawFilterBar(ctx, bounds, tc_config, tc_colors, &fb_state, &fb_result);
|
||||||
const mouse = ctx.input.mousePos();
|
|
||||||
|
|
||||||
// Draw Chips
|
// Sincronizar estado de vuelta
|
||||||
if (config.chips.len > 0) {
|
adapter.syncFromFilterBarState(&fb_state);
|
||||||
for (config.chips, 0..) |chip, idx| {
|
|
||||||
const chip_idx: u4 = @intCast(idx);
|
|
||||||
const is_active = list_state.isChipActive(chip_idx);
|
|
||||||
|
|
||||||
const label_len = chip.label.len;
|
// Propagar resultados
|
||||||
const chip_w: u32 = @intCast(label_len * 7 + chip_padding * 2);
|
if (fb_result.filter_changed) {
|
||||||
|
result.filter_changed = true;
|
||||||
const chip_bounds = Layout.Rect.init(
|
result.filter_text = list_state.filter_buf[0..list_state.filter_len];
|
||||||
current_x,
|
|
||||||
item_y,
|
|
||||||
chip_w,
|
|
||||||
chip_h,
|
|
||||||
);
|
|
||||||
|
|
||||||
const chip_hovered = chip_bounds.contains(mouse.x, mouse.y);
|
|
||||||
|
|
||||||
const chip_bg = if (is_active)
|
|
||||||
colors.row_selected
|
|
||||||
else if (chip_hovered)
|
|
||||||
Style.Color.rgb(
|
|
||||||
colors.header_background.r -| 15,
|
|
||||||
colors.header_background.g -| 15,
|
|
||||||
colors.header_background.b -| 15,
|
|
||||||
)
|
|
||||||
else
|
|
||||||
colors.header_background;
|
|
||||||
|
|
||||||
const chip_text_color = if (is_active)
|
|
||||||
colors.text_selected
|
|
||||||
else
|
|
||||||
colors.text;
|
|
||||||
|
|
||||||
const chip_border = if (is_active)
|
|
||||||
colors.row_selected
|
|
||||||
else
|
|
||||||
colors.border;
|
|
||||||
|
|
||||||
ctx.pushCommand(Command.roundedRect(
|
|
||||||
chip_bounds.x,
|
|
||||||
chip_bounds.y,
|
|
||||||
chip_bounds.w,
|
|
||||||
chip_bounds.h,
|
|
||||||
chip_bg,
|
|
||||||
chip_radius,
|
|
||||||
));
|
|
||||||
|
|
||||||
if (!is_active) {
|
|
||||||
ctx.pushCommand(Command.roundedRectOutline(
|
|
||||||
chip_bounds.x,
|
|
||||||
chip_bounds.y,
|
|
||||||
chip_bounds.w,
|
|
||||||
chip_bounds.h,
|
|
||||||
chip_border,
|
|
||||||
chip_radius,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.pushCommand(Command.text(
|
|
||||||
chip_bounds.x + chip_padding,
|
|
||||||
chip_bounds.y + 4,
|
|
||||||
chip.label,
|
|
||||||
chip_text_color,
|
|
||||||
));
|
|
||||||
|
|
||||||
if (chip_hovered and ctx.input.mousePressed(.left)) {
|
|
||||||
list_state.activateChip(chip_idx, config.chip_mode);
|
|
||||||
result.chip_changed = true;
|
|
||||||
result.chip_index = chip_idx;
|
|
||||||
result.chip_active = list_state.isChipActive(chip_idx);
|
|
||||||
}
|
|
||||||
|
|
||||||
current_x += @as(i32, @intCast(chip_w)) + chip_spacing;
|
|
||||||
}
|
|
||||||
|
|
||||||
current_x += padding;
|
|
||||||
}
|
}
|
||||||
|
if (fb_result.chip_changed) {
|
||||||
// Draw Search Input
|
result.chip_changed = true;
|
||||||
if (config.show_search) {
|
result.chip_index = fb_result.chip_index;
|
||||||
const clear_space: u32 = if (config.show_clear_button) clear_btn_w + @as(u32, @intCast(padding)) else 0;
|
result.chip_active = fb_result.chip_active;
|
||||||
const search_end = bounds.x + @as(i32, @intCast(bounds.w)) - padding - @as(i32, @intCast(clear_space));
|
|
||||||
const search_w: u32 = @intCast(@max(60, search_end - current_x));
|
|
||||||
|
|
||||||
const search_bounds = Layout.Rect.init(
|
|
||||||
current_x,
|
|
||||||
item_y,
|
|
||||||
search_w,
|
|
||||||
item_h,
|
|
||||||
);
|
|
||||||
|
|
||||||
var text_state = text_input.TextInputState{
|
|
||||||
.buffer = &list_state.filter_buf,
|
|
||||||
.len = list_state.filter_len,
|
|
||||||
.cursor = list_state.search_cursor,
|
|
||||||
.selection_start = list_state.search_selection_start,
|
|
||||||
.focused = list_state.search_has_focus,
|
|
||||||
};
|
|
||||||
|
|
||||||
const text_result = text_input.textInputRect(ctx, search_bounds, &text_state, .{
|
|
||||||
.placeholder = config.search_placeholder,
|
|
||||||
.padding = 3,
|
|
||||||
});
|
|
||||||
|
|
||||||
list_state.filter_len = text_state.len;
|
|
||||||
list_state.search_cursor = text_state.cursor;
|
|
||||||
list_state.search_selection_start = text_state.selection_start;
|
|
||||||
|
|
||||||
if (text_result.clicked) {
|
|
||||||
list_state.search_has_focus = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (text_result.changed) {
|
|
||||||
list_state.filter_text_changed = true;
|
|
||||||
result.filter_changed = true;
|
|
||||||
result.filter_text = list_state.filter_buf[0..list_state.filter_len];
|
|
||||||
}
|
|
||||||
|
|
||||||
current_x += @as(i32, @intCast(search_w)) + padding;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw Clear Button
|
|
||||||
if (config.show_clear_button and list_state.filter_len > 0) {
|
|
||||||
const clear_x = bounds.x + @as(i32, @intCast(bounds.w - clear_btn_w)) - padding;
|
|
||||||
const clear_bounds = Layout.Rect.init(
|
|
||||||
clear_x,
|
|
||||||
item_y,
|
|
||||||
clear_btn_w,
|
|
||||||
chip_h,
|
|
||||||
);
|
|
||||||
|
|
||||||
const clear_hovered = clear_bounds.contains(mouse.x, mouse.y);
|
|
||||||
|
|
||||||
const clear_bg = if (clear_hovered)
|
|
||||||
Style.Color.rgb(220, 80, 80)
|
|
||||||
else
|
|
||||||
Style.Color.rgb(180, 60, 60);
|
|
||||||
|
|
||||||
const clear_text = Style.Color.rgb(255, 255, 255);
|
|
||||||
|
|
||||||
ctx.pushCommand(Command.roundedRect(
|
|
||||||
clear_bounds.x,
|
|
||||||
clear_bounds.y,
|
|
||||||
clear_bounds.w,
|
|
||||||
clear_bounds.h,
|
|
||||||
clear_bg,
|
|
||||||
chip_radius,
|
|
||||||
));
|
|
||||||
|
|
||||||
ctx.pushCommand(Command.text(
|
|
||||||
clear_bounds.x + 7,
|
|
||||||
clear_bounds.y + 4,
|
|
||||||
"X",
|
|
||||||
clear_text,
|
|
||||||
));
|
|
||||||
|
|
||||||
if (clear_hovered and ctx.input.mousePressed(.left)) {
|
|
||||||
list_state.clearFilterText();
|
|
||||||
list_state.search_cursor = 0;
|
|
||||||
list_state.search_selection_start = null;
|
|
||||||
result.filter_changed = true;
|
|
||||||
result.filter_text = "";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -391,7 +267,7 @@ pub fn drawRows(
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Draw: Footer
|
// Draw: Footer (delega a table_core para DRY)
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
pub fn drawFooter(
|
pub fn drawFooter(
|
||||||
|
|
@ -400,36 +276,47 @@ pub fn drawFooter(
|
||||||
colors: *const VirtualAdvancedTableConfig.Colors,
|
colors: *const VirtualAdvancedTableConfig.Colors,
|
||||||
list_state: *VirtualAdvancedTableState,
|
list_state: *VirtualAdvancedTableState,
|
||||||
) void {
|
) void {
|
||||||
ctx.pushCommand(Command.rect(
|
// Construir información de posición
|
||||||
bounds.x,
|
const current_pos: ?usize = if (list_state.selected_id != null)
|
||||||
bounds.y,
|
|
||||||
bounds.w,
|
|
||||||
bounds.h,
|
|
||||||
colors.header_background,
|
|
||||||
));
|
|
||||||
|
|
||||||
var count_buf: [64]u8 = undefined;
|
|
||||||
const count_info = list_state.getDisplayCount();
|
|
||||||
const count_str = count_info.format(&count_buf);
|
|
||||||
|
|
||||||
var pos_buf: [32]u8 = undefined;
|
|
||||||
const pos_str = if (list_state.selected_id != null)
|
|
||||||
if (list_state.findSelectedInWindow()) |idx|
|
if (list_state.findSelectedInWindow()) |idx|
|
||||||
std.fmt.bufPrint(&pos_buf, "{d}", .{list_state.windowToGlobalIndex(idx) + 1}) catch "?"
|
list_state.windowToGlobalIndex(idx) + 1
|
||||||
else
|
else
|
||||||
"?"
|
null
|
||||||
else
|
else
|
||||||
"-";
|
null;
|
||||||
|
|
||||||
const display_str = std.fmt.bufPrint(&list_state.footer_display_buf, "{s} de {s}", .{ pos_str, count_str }) catch "...";
|
// Convertir CountInfo de Virtual a table_core
|
||||||
list_state.footer_display_len = display_str.len;
|
const display_count = list_state.getDisplayCount();
|
||||||
|
const tc_total = table_core.CountInfo{
|
||||||
|
.value = display_count.value,
|
||||||
|
.state = @enumFromInt(@intFromEnum(display_count.state)),
|
||||||
|
};
|
||||||
|
|
||||||
ctx.pushCommand(Command.text(
|
const pos_info = table_core.FooterPositionInfo{
|
||||||
bounds.x + 4,
|
.current_position = current_pos,
|
||||||
bounds.y + 2,
|
.total_count = tc_total,
|
||||||
|
.filtered_count = null, // VirtualAdvancedTable ya usa filtered_count internamente
|
||||||
|
};
|
||||||
|
|
||||||
|
// Usar buffer del state para persistencia
|
||||||
|
var footer_state = table_core.FooterState{};
|
||||||
|
const display_text = footer_state.formatDisplay(pos_info);
|
||||||
|
|
||||||
|
// Copiar al buffer persistente del list_state
|
||||||
|
@memcpy(list_state.footer_display_buf[0..display_text.len], display_text);
|
||||||
|
list_state.footer_display_len = display_text.len;
|
||||||
|
|
||||||
|
// Llamar a implementación compartida
|
||||||
|
table_core.drawFooter(
|
||||||
|
ctx,
|
||||||
|
bounds,
|
||||||
|
table_core.FooterColors{
|
||||||
|
.background = colors.header_background,
|
||||||
|
.text = colors.text,
|
||||||
|
.border = colors.border,
|
||||||
|
},
|
||||||
list_state.footer_display_buf[0..list_state.footer_display_len],
|
list_state.footer_display_buf[0..list_state.footer_display_len],
|
||||||
colors.text,
|
);
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue