refactor(virtual_advanced_table): Extraer drawing.zig e input.zig
Modularización del archivo principal (1367 LOC → 468 LOC): - drawing.zig (495 LOC): FilterBar, Header, Rows, Footer, Scrollbars - input.zig (232 LOC): Keyboard, MouseClick, helpers Archivos sin cambios: state.zig (847 LOC - candidato futuro)
This commit is contained in:
parent
fa5854fa21
commit
b9f412b64f
4 changed files with 869 additions and 956 deletions
85
docs/PLAN_REFACTOR_VIRTUAL_ADVANCED_TABLE.md
Normal file
85
docs/PLAN_REFACTOR_VIRTUAL_ADVANCED_TABLE.md
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
# PLAN: Modularización de virtual_advanced_table.zig
|
||||||
|
|
||||||
|
**Fecha:** 2025-12-29
|
||||||
|
**Estado:** En progreso
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Objetivo
|
||||||
|
|
||||||
|
Dividir `virtual_advanced_table.zig` (1367 LOC) en módulos más pequeños.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Estado Actual
|
||||||
|
|
||||||
|
```
|
||||||
|
virtual_advanced_table/
|
||||||
|
├── cell_editor.zig # 179 LOC ✓
|
||||||
|
├── data_provider.zig # 296 LOC ✓
|
||||||
|
├── paged_datasource.zig # 188 LOC ✓
|
||||||
|
├── state.zig # 847 LOC (revisar después)
|
||||||
|
├── types.zig # 242 LOC ✓
|
||||||
|
└── virtual_advanced_table.zig # 1367 LOC ⚠️ DIVIDIR
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Estructura Final
|
||||||
|
|
||||||
|
```
|
||||||
|
virtual_advanced_table/
|
||||||
|
├── cell_editor.zig # 179 LOC (sin cambios)
|
||||||
|
├── data_provider.zig # 296 LOC (sin cambios)
|
||||||
|
├── paged_datasource.zig # 188 LOC (sin cambios)
|
||||||
|
├── state.zig # 847 LOC (sin cambios por ahora)
|
||||||
|
├── types.zig # 242 LOC (sin cambios)
|
||||||
|
├── drawing.zig # ~400 LOC (NUEVO)
|
||||||
|
├── input.zig # ~200 LOC (NUEVO)
|
||||||
|
└── virtual_advanced_table.zig # ~500 LOC (reducido)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Mapeo de Funciones
|
||||||
|
|
||||||
|
### → drawing.zig (~400 LOC)
|
||||||
|
| Función | Líneas orig | Descripción |
|
||||||
|
|---------|-------------|-------------|
|
||||||
|
| drawFilterBar | 604-830 | Barra de filtro superior |
|
||||||
|
| drawHeaderAt | 836-911 | Cabecera de tabla |
|
||||||
|
| drawRows | 917-1008 | Filas de datos |
|
||||||
|
| drawFooter | 1014-1056 | Pie de tabla |
|
||||||
|
| drawScrollbar | 1062-1087 | Scrollbar vertical |
|
||||||
|
| drawScrollbarH | 1093-1120 | Scrollbar horizontal |
|
||||||
|
|
||||||
|
### → input.zig (~200 LOC)
|
||||||
|
| Función | Líneas orig | Descripción |
|
||||||
|
|---------|-------------|-------------|
|
||||||
|
| handleKeyboard | 1169-1273 | Manejo de teclado |
|
||||||
|
| handleMouseClick | 1279-1345 | Manejo de clicks |
|
||||||
|
| ensureColumnVisible | 1127-1160 | Scroll a columna |
|
||||||
|
| needsRefetch | 577-598 | Detectar necesidad de refetch |
|
||||||
|
|
||||||
|
### → virtual_advanced_table.zig (reducido)
|
||||||
|
- VirtualAdvancedTableResult struct
|
||||||
|
- virtualAdvancedTable() (API pública)
|
||||||
|
- virtualAdvancedTableRect() (orquestación)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fases
|
||||||
|
|
||||||
|
- [ ] FASE 1: Extraer drawing.zig
|
||||||
|
- [ ] FASE 2: Extraer input.zig
|
||||||
|
- [ ] FASE 3: Actualizar imports en virtual_advanced_table.zig
|
||||||
|
- [ ] FASE 4: Verificar compilación
|
||||||
|
- [ ] FASE 5: Commit y push
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Historial
|
||||||
|
|
||||||
|
| Fecha | Hora | Acción | Estado |
|
||||||
|
|-------|------|--------|--------|
|
||||||
|
| 2025-12-29 | ~02:30 | Plan creado | En progreso |
|
||||||
495
src/widgets/virtual_advanced_table/drawing.zig
Normal file
495
src/widgets/virtual_advanced_table/drawing.zig
Normal file
|
|
@ -0,0 +1,495 @@
|
||||||
|
//! VirtualAdvancedTable - Funciones de Dibujo
|
||||||
|
//!
|
||||||
|
//! Funciones de renderizado extraídas del archivo principal para mejorar
|
||||||
|
//! modularidad y reducir el tamaño del archivo principal.
|
||||||
|
|
||||||
|
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");
|
||||||
|
const table_core = @import("../table_core/table_core.zig");
|
||||||
|
|
||||||
|
const types = @import("types.zig");
|
||||||
|
const state_mod = @import("state.zig");
|
||||||
|
const paged_datasource = @import("paged_datasource.zig");
|
||||||
|
|
||||||
|
pub const VirtualAdvancedTableState = state_mod.VirtualAdvancedTableState;
|
||||||
|
pub const VirtualAdvancedTableConfig = types.VirtualAdvancedTableConfig;
|
||||||
|
pub const FilterBarConfig = types.FilterBarConfig;
|
||||||
|
pub const VirtualAdvancedTableResult = @import("virtual_advanced_table.zig").VirtualAdvancedTableResult;
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Draw: FilterBar
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
pub fn drawFilterBar(
|
||||||
|
ctx: *Context,
|
||||||
|
bounds: Layout.Rect,
|
||||||
|
config: FilterBarConfig,
|
||||||
|
colors: *const VirtualAdvancedTableConfig.Colors,
|
||||||
|
list_state: *VirtualAdvancedTableState,
|
||||||
|
result: *VirtualAdvancedTableResult,
|
||||||
|
) void {
|
||||||
|
const padding: i32 = 6;
|
||||||
|
const chip_h: u32 = 22;
|
||||||
|
const chip_padding: i32 = 10;
|
||||||
|
const chip_spacing: i32 = 6;
|
||||||
|
const chip_radius: u8 = 11;
|
||||||
|
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 = list_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)) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 = &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 = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Draw: Header
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
pub fn drawHeaderAt(
|
||||||
|
ctx: *Context,
|
||||||
|
bounds: Layout.Rect,
|
||||||
|
header_y: i32,
|
||||||
|
config: VirtualAdvancedTableConfig,
|
||||||
|
colors: *const VirtualAdvancedTableConfig.Colors,
|
||||||
|
list_state: *VirtualAdvancedTableState,
|
||||||
|
result: *VirtualAdvancedTableResult,
|
||||||
|
scroll_offset_x: i32,
|
||||||
|
) void {
|
||||||
|
const header_h = config.row_height;
|
||||||
|
|
||||||
|
ctx.pushCommand(Command.rect(
|
||||||
|
bounds.x,
|
||||||
|
header_y,
|
||||||
|
bounds.w,
|
||||||
|
header_h,
|
||||||
|
colors.header_background,
|
||||||
|
));
|
||||||
|
|
||||||
|
var x: i32 = bounds.x - scroll_offset_x;
|
||||||
|
for (config.columns) |col| {
|
||||||
|
const col_end = x + @as(i32, @intCast(col.width));
|
||||||
|
if (col_end > bounds.x and x < bounds.x + @as(i32, @intCast(bounds.w))) {
|
||||||
|
ctx.pushCommand(Command.text(
|
||||||
|
x + 4,
|
||||||
|
header_y + 3,
|
||||||
|
col.title,
|
||||||
|
colors.text,
|
||||||
|
));
|
||||||
|
|
||||||
|
if (list_state.sort_column) |sort_col| {
|
||||||
|
if (std.mem.eql(u8, sort_col, col.name)) {
|
||||||
|
const indicator = list_state.sort_direction.symbol();
|
||||||
|
ctx.pushCommand(Command.text(
|
||||||
|
x + @as(i32, @intCast(col.width)) - 20,
|
||||||
|
header_y + 3,
|
||||||
|
indicator,
|
||||||
|
colors.text,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (col.sortable) {
|
||||||
|
const header_bounds = Layout.Rect.init(x, header_y, col.width, header_h);
|
||||||
|
const mouse = ctx.input.mousePos();
|
||||||
|
if (header_bounds.contains(mouse.x, mouse.y) and ctx.input.mousePressed(.left)) {
|
||||||
|
list_state.toggleSort(col.name);
|
||||||
|
result.sort_requested = true;
|
||||||
|
result.sort_column = col.name;
|
||||||
|
result.sort_direction = list_state.sort_direction;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.pushCommand(Command.rect(col_end - 1, header_y, 1, header_h, colors.border));
|
||||||
|
}
|
||||||
|
|
||||||
|
x = col_end;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.pushCommand(Command.rect(
|
||||||
|
bounds.x,
|
||||||
|
header_y + @as(i32, @intCast(header_h)) - 1,
|
||||||
|
bounds.w,
|
||||||
|
1,
|
||||||
|
colors.border,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Draw: Rows
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
pub fn drawRows(
|
||||||
|
ctx: *Context,
|
||||||
|
content_bounds: Layout.Rect,
|
||||||
|
config: VirtualAdvancedTableConfig,
|
||||||
|
colors: *const VirtualAdvancedTableConfig.Colors,
|
||||||
|
list_state: *VirtualAdvancedTableState,
|
||||||
|
visible_rows: usize,
|
||||||
|
result: *VirtualAdvancedTableResult,
|
||||||
|
scroll_offset_x: i32,
|
||||||
|
) void {
|
||||||
|
_ = result;
|
||||||
|
|
||||||
|
const pds_ptr = ctx.frameAllocator().create(paged_datasource.PagedDataSource) catch {
|
||||||
|
std.debug.print("[VT-ERROR] No se pudo crear PagedDataSource en frame arena\n", .{});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
pds_ptr.* = paged_datasource.PagedDataSource.init(list_state, config.columns, null);
|
||||||
|
const table_ds = pds_ptr.toDataSource();
|
||||||
|
|
||||||
|
const selected_row: i32 = if (list_state.findSelectedInWindow()) |window_idx|
|
||||||
|
@intCast(list_state.windowToGlobalIndex(window_idx))
|
||||||
|
else
|
||||||
|
-1;
|
||||||
|
|
||||||
|
const first_row = list_state.nav.scroll_row;
|
||||||
|
const window_rows = list_state.current_window.len;
|
||||||
|
const last_row = @min(
|
||||||
|
list_state.nav.scroll_row + visible_rows,
|
||||||
|
list_state.window_start + window_rows,
|
||||||
|
);
|
||||||
|
|
||||||
|
var render_cols: [32]table_core.ColumnRenderDef = undefined;
|
||||||
|
const num_cols = @min(config.columns.len, 32);
|
||||||
|
for (config.columns[0..num_cols], 0..) |col, i| {
|
||||||
|
render_cols[i] = .{
|
||||||
|
.width = col.width,
|
||||||
|
.text_align = 0,
|
||||||
|
.visible = true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const render_colors = table_core.RowRenderColors{
|
||||||
|
.row_normal = colors.row_normal,
|
||||||
|
.row_alternate = colors.row_alternate,
|
||||||
|
.selected_row = colors.row_selected,
|
||||||
|
.selected_row_unfocus = colors.row_selected_unfocus,
|
||||||
|
.selected_cell = Style.Color.rgb(100, 200, 255),
|
||||||
|
.selected_cell_unfocus = Style.Color.rgb(150, 150, 160),
|
||||||
|
.text_normal = colors.text,
|
||||||
|
.text_selected = colors.text_selected,
|
||||||
|
.border = colors.border,
|
||||||
|
};
|
||||||
|
|
||||||
|
var cell_buffer: [256]u8 = undefined;
|
||||||
|
|
||||||
|
const dirty_id: ?i64 = if (list_state.row_edit_buffer.has_changes)
|
||||||
|
list_state.row_edit_buffer.row_id
|
||||||
|
else
|
||||||
|
null;
|
||||||
|
|
||||||
|
_ = table_core.drawRowsWithDataSource(
|
||||||
|
ctx,
|
||||||
|
table_ds,
|
||||||
|
.{
|
||||||
|
.bounds_x = content_bounds.x,
|
||||||
|
.bounds_y = content_bounds.y,
|
||||||
|
.bounds_w = content_bounds.w,
|
||||||
|
.row_height = config.row_height,
|
||||||
|
.first_row = first_row,
|
||||||
|
.last_row = last_row,
|
||||||
|
.scroll_x = scroll_offset_x,
|
||||||
|
.alternating_rows = true,
|
||||||
|
.has_focus = list_state.has_focus,
|
||||||
|
.selected_row = selected_row,
|
||||||
|
.active_col = list_state.nav.active_col,
|
||||||
|
.colors = render_colors,
|
||||||
|
.columns = render_cols[0..num_cols],
|
||||||
|
.dirty_row_id = dirty_id,
|
||||||
|
.edit_buffer = &list_state.row_edit_buffer,
|
||||||
|
},
|
||||||
|
&cell_buffer,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Draw: Footer
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
pub fn drawFooter(
|
||||||
|
ctx: *Context,
|
||||||
|
bounds: Layout.Rect,
|
||||||
|
colors: *const VirtualAdvancedTableConfig.Colors,
|
||||||
|
list_state: *VirtualAdvancedTableState,
|
||||||
|
) void {
|
||||||
|
ctx.pushCommand(Command.rect(
|
||||||
|
bounds.x,
|
||||||
|
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|
|
||||||
|
std.fmt.bufPrint(&pos_buf, "{d}", .{list_state.windowToGlobalIndex(idx) + 1}) catch "?"
|
||||||
|
else
|
||||||
|
"?"
|
||||||
|
else
|
||||||
|
"-";
|
||||||
|
|
||||||
|
const display_str = std.fmt.bufPrint(&list_state.footer_display_buf, "{s} de {s}", .{ pos_str, count_str }) catch "...";
|
||||||
|
list_state.footer_display_len = display_str.len;
|
||||||
|
|
||||||
|
ctx.pushCommand(Command.text(
|
||||||
|
bounds.x + 4,
|
||||||
|
bounds.y + 2,
|
||||||
|
list_state.footer_display_buf[0..list_state.footer_display_len],
|
||||||
|
colors.text,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Draw: Scrollbar Vertical
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
pub fn drawScrollbar(
|
||||||
|
ctx: *Context,
|
||||||
|
bounds: Layout.Rect,
|
||||||
|
header_h: u32,
|
||||||
|
footer_h: u32,
|
||||||
|
list_state: *VirtualAdvancedTableState,
|
||||||
|
visible_rows: usize,
|
||||||
|
total_rows: usize,
|
||||||
|
colors: *const VirtualAdvancedTableConfig.Colors,
|
||||||
|
) void {
|
||||||
|
const scrollbar_w: u32 = 12;
|
||||||
|
const content_h = bounds.h -| header_h -| footer_h;
|
||||||
|
|
||||||
|
table_core.drawVerticalScrollbar(ctx, .{
|
||||||
|
.track_x = bounds.x + @as(i32, @intCast(bounds.w - scrollbar_w)),
|
||||||
|
.track_y = bounds.y + @as(i32, @intCast(header_h)),
|
||||||
|
.width = scrollbar_w,
|
||||||
|
.height = content_h,
|
||||||
|
.visible_count = visible_rows,
|
||||||
|
.total_count = total_rows,
|
||||||
|
.scroll_pos = list_state.nav.scroll_row,
|
||||||
|
.track_color = colors.row_alternate,
|
||||||
|
.thumb_color = colors.border,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Draw: Scrollbar Horizontal
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
pub fn drawScrollbarH(
|
||||||
|
ctx: *Context,
|
||||||
|
bounds: Layout.Rect,
|
||||||
|
footer_h: u32,
|
||||||
|
scrollbar_h: u32,
|
||||||
|
scroll_offset_x: i32,
|
||||||
|
max_scroll_x: i32,
|
||||||
|
available_width: u32,
|
||||||
|
colors: *const VirtualAdvancedTableConfig.Colors,
|
||||||
|
) void {
|
||||||
|
const scrollbar_v_w: u32 = 12;
|
||||||
|
const track_w = bounds.w -| scrollbar_v_w;
|
||||||
|
const total_width = available_width + @as(u32, @intCast(@max(0, max_scroll_x)));
|
||||||
|
|
||||||
|
table_core.drawHorizontalScrollbar(ctx, .{
|
||||||
|
.track_x = bounds.x,
|
||||||
|
.track_y = bounds.y + @as(i32, @intCast(bounds.h - footer_h - scrollbar_h)),
|
||||||
|
.width = track_w,
|
||||||
|
.height = scrollbar_h,
|
||||||
|
.visible_width = available_width,
|
||||||
|
.total_width = total_width,
|
||||||
|
.scroll_x = scroll_offset_x,
|
||||||
|
.max_scroll_x = max_scroll_x,
|
||||||
|
.track_color = colors.row_alternate,
|
||||||
|
.thumb_color = colors.border,
|
||||||
|
});
|
||||||
|
}
|
||||||
232
src/widgets/virtual_advanced_table/input.zig
Normal file
232
src/widgets/virtual_advanced_table/input.zig
Normal file
|
|
@ -0,0 +1,232 @@
|
||||||
|
//! VirtualAdvancedTable - Funciones de Input
|
||||||
|
//!
|
||||||
|
//! Manejo de teclado y mouse extraído del archivo principal.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const Context = @import("../../core/context.zig").Context;
|
||||||
|
const Layout = @import("../../core/layout.zig");
|
||||||
|
const table_core = @import("../table_core/table_core.zig");
|
||||||
|
|
||||||
|
const types = @import("types.zig");
|
||||||
|
const state_mod = @import("state.zig");
|
||||||
|
const data_provider = @import("data_provider.zig");
|
||||||
|
|
||||||
|
pub const VirtualAdvancedTableState = state_mod.VirtualAdvancedTableState;
|
||||||
|
pub const VirtualAdvancedTableConfig = types.VirtualAdvancedTableConfig;
|
||||||
|
pub const DataProvider = data_provider.DataProvider;
|
||||||
|
pub const VirtualAdvancedTableResult = @import("virtual_advanced_table.zig").VirtualAdvancedTableResult;
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Helper: Check if refetch needed
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
pub fn needsRefetch(list_state: *VirtualAdvancedTableState, visible_rows: usize, buffer_size: usize) bool {
|
||||||
|
// Manual invalidation
|
||||||
|
if (list_state.needs_window_refresh) {
|
||||||
|
list_state.needs_window_refresh = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// First load
|
||||||
|
if (list_state.current_window.len == 0) return true;
|
||||||
|
|
||||||
|
// Check if scroll is outside current window
|
||||||
|
const scroll = list_state.nav.scroll_row;
|
||||||
|
const window_end = list_state.window_start + list_state.current_window.len;
|
||||||
|
|
||||||
|
// Refetch if scroll is near edges of window
|
||||||
|
const margin = visible_rows;
|
||||||
|
if (scroll < list_state.window_start + margin and list_state.window_start > 0) return true;
|
||||||
|
if (scroll + visible_rows + margin > window_end) return true;
|
||||||
|
|
||||||
|
_ = buffer_size;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Auto-scroll horizontal helper
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
pub fn ensureColumnVisible(
|
||||||
|
list_state: *VirtualAdvancedTableState,
|
||||||
|
columns: []const types.ColumnDef,
|
||||||
|
visible_width: u32,
|
||||||
|
max_scroll_x: i32,
|
||||||
|
) void {
|
||||||
|
const active_col = list_state.nav.active_col;
|
||||||
|
if (active_col >= columns.len) return;
|
||||||
|
|
||||||
|
// Calcular posición X de la columna activa
|
||||||
|
var col_start: i32 = 0;
|
||||||
|
for (columns, 0..) |col, i| {
|
||||||
|
if (i == active_col) break;
|
||||||
|
col_start += @as(i32, @intCast(col.width));
|
||||||
|
}
|
||||||
|
const col_end = col_start + @as(i32, @intCast(columns[active_col].width));
|
||||||
|
|
||||||
|
// Posición visible
|
||||||
|
const visible_start = list_state.nav.scroll_x;
|
||||||
|
const visible_end = visible_start + @as(i32, @intCast(visible_width));
|
||||||
|
|
||||||
|
// Ajustar scroll
|
||||||
|
if (col_start < visible_start) {
|
||||||
|
list_state.nav.scroll_x = col_start;
|
||||||
|
} else if (col_end > visible_end) {
|
||||||
|
list_state.nav.scroll_x = col_end - @as(i32, @intCast(visible_width));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clamp
|
||||||
|
if (list_state.nav.scroll_x < 0) list_state.nav.scroll_x = 0;
|
||||||
|
if (list_state.nav.scroll_x > max_scroll_x) list_state.nav.scroll_x = max_scroll_x;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Handle: Keyboard (Brain-in-Core pattern)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
pub fn handleKeyboard(
|
||||||
|
ctx: *Context,
|
||||||
|
list_state: *VirtualAdvancedTableState,
|
||||||
|
provider: DataProvider,
|
||||||
|
visible_rows: usize,
|
||||||
|
total_rows: usize,
|
||||||
|
max_scroll_x: i32,
|
||||||
|
columns: []const types.ColumnDef,
|
||||||
|
visible_width: u32,
|
||||||
|
result: *VirtualAdvancedTableResult,
|
||||||
|
) void {
|
||||||
|
_ = provider;
|
||||||
|
|
||||||
|
const h_scroll_step: i32 = 40;
|
||||||
|
const num_columns = columns.len;
|
||||||
|
|
||||||
|
// Delegar al Core
|
||||||
|
const events = table_core.processTableEvents(ctx, list_state.isEditing());
|
||||||
|
|
||||||
|
if (!events.handled) return;
|
||||||
|
|
||||||
|
const prev_col = list_state.nav.active_col;
|
||||||
|
|
||||||
|
// Aplicar navegación
|
||||||
|
if (events.move_up) list_state.moveUp();
|
||||||
|
if (events.move_down) list_state.moveDown(visible_rows);
|
||||||
|
if (events.move_left) list_state.moveToPrevCol();
|
||||||
|
if (events.move_right) list_state.moveToNextCol(num_columns);
|
||||||
|
if (events.page_up) list_state.pageUp(visible_rows);
|
||||||
|
if (events.page_down) list_state.pageDown(visible_rows, total_rows);
|
||||||
|
if (events.go_to_first_col) list_state.goToFirstCol();
|
||||||
|
if (events.go_to_last_col) list_state.goToLastCol(num_columns);
|
||||||
|
if (events.go_to_first_row) list_state.goToStart();
|
||||||
|
if (events.go_to_last_row) list_state.goToEnd(visible_rows, total_rows);
|
||||||
|
if (events.scroll_left) list_state.scrollLeft(h_scroll_step);
|
||||||
|
if (events.scroll_right) list_state.scrollRight(h_scroll_step, max_scroll_x);
|
||||||
|
|
||||||
|
// Auto-scroll horizontal
|
||||||
|
if (list_state.nav.active_col != prev_col) {
|
||||||
|
ensureColumnVisible(list_state, columns, visible_width, max_scroll_x);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ctrl+N
|
||||||
|
if (events.insert_row) {
|
||||||
|
result.insert_row_requested = true;
|
||||||
|
list_state.enterInsertionMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ctrl+Delete/B
|
||||||
|
if (events.delete_row) {
|
||||||
|
result.delete_row_requested = true;
|
||||||
|
if (list_state.selected_id) |id| {
|
||||||
|
result.deleted_row_id = id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ordenación
|
||||||
|
if (events.sort_by_column) |col| {
|
||||||
|
if (col < num_columns) {
|
||||||
|
result.sort_requested = true;
|
||||||
|
result.sort_column_index = col;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Edición
|
||||||
|
if (events.start_editing) {
|
||||||
|
if (list_state.getActiveCell()) |cell| {
|
||||||
|
if (events.initial_char) |ch| {
|
||||||
|
list_state.startEditing(cell, "", ch, ctx.current_time_ms);
|
||||||
|
} else {
|
||||||
|
result.cell_committed = false;
|
||||||
|
result.edited_cell = cell;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tab sin edición
|
||||||
|
if (events.tab_out and !list_state.isEditing()) {
|
||||||
|
result.tab_out = true;
|
||||||
|
result.tab_shift = events.tab_shift;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Handle: Mouse Click
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
pub fn handleMouseClick(
|
||||||
|
ctx: *Context,
|
||||||
|
bounds: Layout.Rect,
|
||||||
|
filter_bar_h: u32,
|
||||||
|
header_h: u32,
|
||||||
|
config: VirtualAdvancedTableConfig,
|
||||||
|
list_state: *VirtualAdvancedTableState,
|
||||||
|
result: *VirtualAdvancedTableResult,
|
||||||
|
) void {
|
||||||
|
const mouse = ctx.input.mousePos();
|
||||||
|
const content_y = bounds.y + @as(i32, @intCast(filter_bar_h)) + @as(i32, @intCast(header_h));
|
||||||
|
|
||||||
|
if (mouse.y >= content_y) {
|
||||||
|
const relative_y = mouse.y - content_y;
|
||||||
|
const screen_row = @as(usize, @intCast(relative_y)) / config.row_height;
|
||||||
|
|
||||||
|
const window_offset = list_state.nav.scroll_row -| list_state.window_start;
|
||||||
|
const data_idx = window_offset + screen_row;
|
||||||
|
|
||||||
|
if (data_idx < list_state.current_window.len) {
|
||||||
|
const global_row = list_state.nav.scroll_row + screen_row;
|
||||||
|
|
||||||
|
// Detect column
|
||||||
|
var clicked_col: usize = 0;
|
||||||
|
const relative_x = mouse.x - bounds.x + list_state.nav.scroll_x;
|
||||||
|
var col_start: i32 = 0;
|
||||||
|
for (config.columns, 0..) |col, col_idx| {
|
||||||
|
const col_end = col_start + @as(i32, @intCast(col.width));
|
||||||
|
if (relative_x >= col_start and relative_x < col_end) {
|
||||||
|
clicked_col = col_idx;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
col_start = col_end;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Double-click detection
|
||||||
|
var dc_state = list_state.nav.double_click;
|
||||||
|
|
||||||
|
const is_double_click = table_core.detectDoubleClick(
|
||||||
|
&dc_state,
|
||||||
|
ctx.current_time_ms,
|
||||||
|
@intCast(global_row),
|
||||||
|
@intCast(clicked_col),
|
||||||
|
);
|
||||||
|
|
||||||
|
list_state.nav.double_click = dc_state;
|
||||||
|
|
||||||
|
if (is_double_click and !list_state.isEditing()) {
|
||||||
|
const cell = types.CellId{ .row = global_row, .col = clicked_col };
|
||||||
|
result.edited_cell = cell;
|
||||||
|
result.double_clicked = true;
|
||||||
|
result.double_click_id = list_state.current_window[data_idx].id;
|
||||||
|
} else {
|
||||||
|
list_state.selectById(list_state.current_window[data_idx].id);
|
||||||
|
list_state.nav.active_col = clicked_col;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue