//! AdvancedTable Widget - Schema-driven data table //! //! A full-featured table widget with: //! - Schema-driven configuration (TableSchema + ColumnDef) //! - Excel-style cell editing with overlay //! - Auto-CRUD (automatic CREATE/UPDATE/DELETE detection) //! - Keyboard navigation (arrows, Tab, Enter, Escape) //! - Column sorting (click header) //! - Row operations (Ctrl+N/A/B, Ctrl+arrows) //! - Visual state indicators (normal, modified, new, deleted, error) //! - Lookup & auto-fill for related data //! //! This module re-exports types from the advanced_table/ subdirectory. 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 table_core = @import("../table_core/table_core.zig"); // Import submodules const drawing = @import("drawing.zig"); const input = @import("input.zig"); const helpers = @import("helpers.zig"); const sorting = @import("sorting.zig"); // Re-export types pub const types = @import("types.zig"); pub const CellValue = types.CellValue; pub const ColumnType = types.ColumnType; pub const RowState = types.RowState; pub const RowLockState = types.RowLockState; pub const SortDirection = types.SortDirection; pub const CRUDAction = types.CRUDAction; pub const ValidationResult = types.ValidationResult; pub const TableColors = types.TableColors; pub const BasicColors = types.BasicColors; pub const TableConfig = types.TableConfig; pub const Row = types.Row; // Re-export schema pub const schema = @import("schema.zig"); pub const ColumnDef = schema.ColumnDef; pub const ColumnAlign = schema.ColumnAlign; pub const AutoFillMapping = schema.AutoFillMapping; pub const SelectOption = schema.SelectOption; pub const TableSchema = schema.TableSchema; pub const DataStore = schema.DataStore; // Re-export state pub const state = @import("state.zig"); pub const AdvancedTableState = state.AdvancedTableState; pub const AdvancedTableResult = state.AdvancedTableResult; // Re-export datasource pub const datasource = @import("datasource.zig"); pub const MemoryDataSource = datasource.MemoryDataSource; // Re-export table_core types pub const NavigateDirection = table_core.NavigateDirection; // Re-export helpers for external use pub const blendColor = helpers.blendColor; pub const parseValue = helpers.parseValue; pub const startsWithIgnoreCase = sorting.startsWithIgnoreCase; // ============================================================================= // Public API // ============================================================================= /// Draw an AdvancedTable with default layout pub fn advancedTable( ctx: *Context, table_state: *AdvancedTableState, table_schema: *const TableSchema, ) AdvancedTableResult { return advancedTableEx(ctx, table_state, table_schema, null); } /// Draw an AdvancedTable with custom colors pub fn advancedTableEx( ctx: *Context, table_state: *AdvancedTableState, table_schema: *const TableSchema, colors: ?*const TableColors, ) AdvancedTableResult { const bounds = ctx.layout.nextRect(); return advancedTableRect(ctx, bounds, table_state, table_schema, colors); } /// Draw an AdvancedTable in a specific rectangle pub fn advancedTableRect( ctx: *Context, bounds: Layout.Rect, table_state: *AdvancedTableState, table_schema: *const TableSchema, custom_colors: ?*const TableColors, ) AdvancedTableResult { var result = AdvancedTableResult{}; if (bounds.isEmpty() or table_schema.columns.len == 0) return result; // Get colors const default_colors = TableColors{}; const colors = custom_colors orelse table_schema.colors orelse &default_colors; const config = table_schema.config; // Ensure valid selection if table has data (like Table widget does) if (table_state.getRowCount() > 0 and table_schema.columns.len > 0) { if (table_state.selected_row < 0) table_state.selected_row = 0; if (table_state.selected_col < 0) table_state.selected_col = 0; } // Generate unique ID for focus system const widget_id: u64 = @intFromPtr(table_state); // Register as focusable ctx.registerFocusable(widget_id); // Check mouse interaction const mouse = ctx.input.mousePos(); const hovered = bounds.contains(mouse.x, mouse.y); const clicked = hovered and ctx.input.mousePressed(.left); if (clicked) { ctx.requestFocus(widget_id); result.clicked = true; } // Check if we have focus const has_focus = ctx.hasFocus(widget_id); table_state.nav.has_focus = has_focus; // Calculate dimensions const state_col_w: u32 = if (config.show_row_state_indicators) config.state_indicator_width else 0; const header_h: u32 = if (config.show_headers) config.header_height else 0; const content_h = bounds.h -| header_h; const visible_rows: usize = @intCast(content_h / config.row_height); // Begin clipping ctx.pushCommand(Command.clip(bounds.x, bounds.y, bounds.w, bounds.h)); // Draw header if (config.show_headers) { drawing.drawHeader(ctx, bounds, table_state, table_schema, state_col_w, colors, &result); } // Calculate visible row range const first_visible = table_state.nav.scroll_row; const last_visible = @min(first_visible + visible_rows, table_state.getRowCount()); // 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); // Construir ColumnRenderDefs para la función unificada var col_defs: [64]table_core.ColumnRenderDef = undefined; var col_count: usize = 0; for (table_schema.columns) |col| { if (col_count >= 64) break; col_defs[col_count] = .{ .width = col.width, .visible = col.visible, .text_align = 0, // Por ahora left-align }; col_count += 1; } // Crear MemoryDataSource y dibujar filas con función unificada var memory_ds = MemoryDataSource.init(table_state, table_schema.columns); const data_src = memory_ds.toDataSource(); // 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 ctx.pushCommand(Command.rect( bounds.x, bounds.y + @as(i32, @intCast(header_h)), bounds.w, content_h, colors.row_normal, )); // Construir RowRenderColors manualmente (los dos TableColors son tipos diferentes) const render_colors = table_core.RowRenderColors{ .row_normal = colors.row_normal, .row_alternate = colors.row_alternate, .selected_row = colors.selected_row, .selected_row_unfocus = colors.selected_row_unfocus, .selected_cell = colors.selected_cell, .selected_cell_unfocus = Style.Color.rgb(80, 80, 90), // Default similar a table_core .text_normal = colors.text_normal, .text_selected = colors.text_selected, .border = colors.border, .state_modified = colors.state_modified, .state_new = colors.state_new, .state_deleted = colors.state_deleted, .state_error = colors.state_error, }; var cell_buffer: [256]u8 = undefined; _ = table_core.drawRowsWithDataSource(ctx, data_src, .{ .bounds_x = bounds.x, .bounds_y = bounds.y + @as(i32, @intCast(header_h)), .bounds_w = bounds.w, .row_height = config.row_height, .first_row = first_visible, .last_row = last_visible, .has_focus = has_focus, .selected_row = table_state.selected_row, .active_col = @intCast(@max(0, table_state.selected_col)), .colors = render_colors, .columns = col_defs[0..col_count], .state_indicator_width = state_col_w, .apply_state_colors = true, .draw_row_borders = true, .alternating_rows = config.alternating_rows, .edit_buffer = &table_state.row_edit_buffer, }, &cell_buffer); // End clipping ctx.pushCommand(Command.clipEnd()); // Draw focus ring (outside clip) if (has_focus) { if (Style.isFancy()) { ctx.pushCommand(Command.focusRing(bounds.x, bounds.y, bounds.w, bounds.h, 4)); } else { ctx.pushCommand(Command.rectOutline( bounds.x - 1, bounds.y - 1, bounds.w + 2, bounds.h + 2, colors.focus_ring, )); } } // Draw scrollbar if needed if (table_state.getRowCount() > visible_rows) { drawing.drawScrollbar(ctx, bounds, table_state, visible_rows, config, colors); } // Handle keyboard if (has_focus) { if (table_state.isEditing()) { // Handle editing keyboard input.handleEditingKeyboard(ctx, table_state, table_schema, &result); // Draw editing overlay drawing.drawEditingOverlay(ctx, bounds, table_state, table_schema, header_h, state_col_w, colors); } else if (config.keyboard_nav) { // Handle navigation keyboard input.handleKeyboard(ctx, table_state, table_schema, visible_rows, &result); } } // Ensure selection is visible helpers.ensureSelectionVisible(table_state, visible_rows); // Auto-CRUD detection (when row changes) if (config.auto_crud_enabled and result.selection_changed and table_state.rowChanged()) { result.crud_action = helpers.detectCRUDAction(table_state, table_schema); // Capture snapshot of new row if (table_state.selected_row >= 0) { table_state.captureSnapshot(@intCast(table_state.selected_row)) catch {}; } } // Phase 8: Invoke callbacks helpers.invokeCallbacks(ctx, table_state, table_schema, &result); return result; } // ============================================================================= // Tests // ============================================================================= test "AdvancedTable basic rendering" { var ctx = try Context.init(std.testing.allocator, 800, 600); defer ctx.deinit(); var table_state = AdvancedTableState.init(std.testing.allocator); defer table_state.deinit(); const columns = [_]ColumnDef{ .{ .name = "id", .title = "ID", .width = 50, .editable = false }, .{ .name = "name", .title = "Name", .width = 150 }, .{ .name = "value", .title = "Value", .width = 100 }, }; const table_schema = TableSchema{ .table_name = "test", .columns = &columns, }; ctx.beginFrame(); ctx.layout.row_height = 200; _ = advancedTable(&ctx, &table_state, &table_schema); // Should generate commands try std.testing.expect(ctx.commands.items.len > 0); ctx.endFrame(); } test "blendColor" { const white = Style.Color.rgb(255, 255, 255); const black = Style.Color.rgb(0, 0, 0); const gray = blendColor(white, black, 0.5); try std.testing.expectEqual(@as(u8, 127), gray.r); try std.testing.expectEqual(@as(u8, 127), gray.g); try std.testing.expectEqual(@as(u8, 127), gray.b); } test "AdvancedTableResult lookup_success field" { var result = AdvancedTableResult{}; // Default is null (no lookup attempted) try std.testing.expect(result.lookup_success == null); // Can be set to true (lookup found) result.lookup_success = true; try std.testing.expect(result.lookup_success.? == true); // Can be set to false (lookup not found) result.lookup_success = false; try std.testing.expect(result.lookup_success.? == false); } test "AdvancedTableState callback fields" { var table_state = AdvancedTableState.init(std.testing.allocator); defer table_state.deinit(); // Initial state try std.testing.expectEqual(@as(u64, 0), table_state.last_callback_time_ms); try std.testing.expectEqual(@as(i32, -1), table_state.last_notified_row); // Can be updated table_state.last_callback_time_ms = 1000; table_state.last_notified_row = 5; try std.testing.expectEqual(@as(u64, 1000), table_state.last_callback_time_ms); try std.testing.expectEqual(@as(i32, 5), table_state.last_notified_row); } test "ColumnDef hasLookup" { // Column without lookup const col_no_lookup = ColumnDef{ .name = "test", .title = "Test", }; try std.testing.expect(!col_no_lookup.hasLookup()); // Column with lookup enabled but no config const col_partial = ColumnDef{ .name = "test", .title = "Test", .enable_lookup = true, }; try std.testing.expect(!col_partial.hasLookup()); // Missing table/key // Column with full lookup config const col_full = ColumnDef{ .name = "test", .title = "Test", .enable_lookup = true, .lookup_table = "other_table", .lookup_key_column = "id", }; try std.testing.expect(col_full.hasLookup()); } test "startsWithIgnoreCase" { // Basic match try std.testing.expect(startsWithIgnoreCase("Hello World", "Hello")); try std.testing.expect(startsWithIgnoreCase("Hello World", "hello")); try std.testing.expect(startsWithIgnoreCase("hello world", "HELLO")); // Empty needle matches everything try std.testing.expect(startsWithIgnoreCase("anything", "")); // Non-match try std.testing.expect(!startsWithIgnoreCase("Hello", "World")); try std.testing.expect(!startsWithIgnoreCase("Hi", "Hello")); // Needle longer than haystack try std.testing.expect(!startsWithIgnoreCase("Hi", "Hello World")); }