Widget AdvancedTable portado de Go (simifactu-fyne) a Zig. 2,526 LOC en 4 archivos, 370 tests pasan. Archivos: - types.zig (369 LOC): CellValue, ColumnType, RowState, TableColors - schema.zig (373 LOC): ColumnDef, TableSchema, DataStore interface - state.zig (762 LOC): Selection, editing, dirty tracking, snapshots - advanced_table.zig (1,022 LOC): Widget, rendering, keyboard Fases implementadas: 1. Core (types, schema, state) 2. Navigation (arrows, Tab, PgUp/Dn, Home/End, Ctrl+Home/End) 3. Cell Editing (F2/Enter start, Escape cancel, text input) 4. Sorting (header click, visual indicators) 5. Auto-CRUD (CREATE/UPDATE/DELETE detection on row change) 6. Row Operations (Ctrl+N insert, Ctrl+Delete remove) Fases diferidas (7-8): Lookup & Auto-fill, Callbacks avanzados ESTADO: Compilado y tests pasan. NO probado en uso real. REQUIERE: Aprobacion antes de tag de version. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
373 lines
12 KiB
Zig
373 lines
12 KiB
Zig
//! AdvancedTable Schema - Column and table definitions
|
|
//!
|
|
//! Schema-driven configuration for AdvancedTable.
|
|
//! Defines columns, their types, and behaviors.
|
|
|
|
const std = @import("std");
|
|
const types = @import("types.zig");
|
|
|
|
pub const CellValue = types.CellValue;
|
|
pub const ColumnType = types.ColumnType;
|
|
pub const RowLockState = types.RowLockState;
|
|
pub const Row = types.Row;
|
|
pub const ValidationResult = types.ValidationResult;
|
|
pub const TableColors = types.TableColors;
|
|
pub const TableConfig = types.TableConfig;
|
|
pub const ValidatorFn = types.ValidatorFn;
|
|
pub const FormatterFn = types.FormatterFn;
|
|
pub const ParserFn = types.ParserFn;
|
|
pub const GetRowLockStateFn = types.GetRowLockStateFn;
|
|
pub const OnRowSelectedFn = types.OnRowSelectedFn;
|
|
pub const OnCellChangedFn = types.OnCellChangedFn;
|
|
pub const OnActiveRowChangedFn = types.OnActiveRowChangedFn;
|
|
|
|
// =============================================================================
|
|
// Auto-Fill Mapping
|
|
// =============================================================================
|
|
|
|
/// Mapping for auto-fill after lookup
|
|
pub const AutoFillMapping = struct {
|
|
/// Field name in lookup table
|
|
source_field: []const u8,
|
|
/// Column name in this table to fill
|
|
target_column: []const u8,
|
|
};
|
|
|
|
// =============================================================================
|
|
// Column Alignment
|
|
// =============================================================================
|
|
|
|
/// Text alignment for column content
|
|
pub const ColumnAlign = enum {
|
|
left,
|
|
center,
|
|
right,
|
|
};
|
|
|
|
// =============================================================================
|
|
// Column Definition
|
|
// =============================================================================
|
|
|
|
/// Complete column definition
|
|
pub const ColumnDef = struct {
|
|
/// Internal name (key in row data)
|
|
name: []const u8,
|
|
|
|
/// Display title in header
|
|
title: []const u8,
|
|
|
|
/// Width in pixels
|
|
width: u32 = 100,
|
|
|
|
/// Data type
|
|
column_type: ColumnType = .string,
|
|
|
|
/// Is editable
|
|
editable: bool = true,
|
|
|
|
/// Is sortable
|
|
sortable: bool = true,
|
|
|
|
/// Minimum width when resizing
|
|
min_width: u32 = 40,
|
|
|
|
/// Maximum width (0 = unlimited)
|
|
max_width: u32 = 0,
|
|
|
|
/// Text alignment
|
|
text_align: ColumnAlign = .left,
|
|
|
|
/// Is visible (can be hidden via config)
|
|
visible: bool = true,
|
|
|
|
// =========================================================================
|
|
// Lookup Configuration
|
|
// =========================================================================
|
|
|
|
/// Enable database lookup
|
|
enable_lookup: bool = false,
|
|
|
|
/// Table name for lookup
|
|
lookup_table: ?[]const u8 = null,
|
|
|
|
/// Column in lookup table to match against
|
|
lookup_key_column: ?[]const u8 = null,
|
|
|
|
/// Columns to auto-fill after successful lookup
|
|
auto_fill_columns: ?[]const AutoFillMapping = null,
|
|
|
|
// =========================================================================
|
|
// Callbacks (per-column, optional)
|
|
// =========================================================================
|
|
|
|
/// Custom validator for this column
|
|
validator: ?ValidatorFn = null,
|
|
|
|
/// Custom formatter for display
|
|
formatter: ?FormatterFn = null,
|
|
|
|
/// Custom parser for text input
|
|
parser: ?ParserFn = null,
|
|
|
|
// =========================================================================
|
|
// Select options (for ColumnType.select)
|
|
// =========================================================================
|
|
|
|
/// Options for select dropdown
|
|
select_options: ?[]const SelectOption = null,
|
|
|
|
// =========================================================================
|
|
// Helper Methods
|
|
// =========================================================================
|
|
|
|
/// Get effective width (clamped to min/max)
|
|
pub fn getEffectiveWidth(self: *const ColumnDef, requested: u32) u32 {
|
|
var w = requested;
|
|
if (w < self.min_width) w = self.min_width;
|
|
if (self.max_width > 0 and w > self.max_width) w = self.max_width;
|
|
return w;
|
|
}
|
|
|
|
/// Check if this column can be edited
|
|
pub fn canEdit(self: *const ColumnDef) bool {
|
|
return self.editable and self.visible;
|
|
}
|
|
|
|
/// Check if this column has lookup enabled
|
|
pub fn hasLookup(self: *const ColumnDef) bool {
|
|
return self.enable_lookup and
|
|
self.lookup_table != null and
|
|
self.lookup_key_column != null;
|
|
}
|
|
};
|
|
|
|
/// Option for select dropdown
|
|
pub const SelectOption = struct {
|
|
/// Value stored in data
|
|
value: CellValue,
|
|
/// Display label
|
|
label: []const u8,
|
|
};
|
|
|
|
// =============================================================================
|
|
// DataStore Interface
|
|
// =============================================================================
|
|
|
|
/// Interface for data persistence (database, file, API, etc.)
|
|
pub const DataStore = struct {
|
|
ptr: *anyopaque,
|
|
vtable: *const VTable,
|
|
|
|
pub const VTable = struct {
|
|
/// Load all rows from data source
|
|
load: *const fn (ptr: *anyopaque, allocator: std.mem.Allocator) anyerror!std.ArrayList(Row),
|
|
|
|
/// Save row (INSERT or UPDATE depending on ID)
|
|
save: *const fn (ptr: *anyopaque, row: *Row) anyerror!void,
|
|
|
|
/// Delete row
|
|
delete: *const fn (ptr: *anyopaque, row: *Row) anyerror!void,
|
|
|
|
/// Lookup in related table
|
|
lookup: *const fn (ptr: *anyopaque, table: []const u8, key_column: []const u8, key_value: CellValue, allocator: std.mem.Allocator) anyerror!?Row,
|
|
};
|
|
|
|
/// Load all rows
|
|
pub fn load(self: DataStore, allocator: std.mem.Allocator) !std.ArrayList(Row) {
|
|
return self.vtable.load(self.ptr, allocator);
|
|
}
|
|
|
|
/// Save row (INSERT or UPDATE)
|
|
pub fn save(self: DataStore, row: *Row) !void {
|
|
return self.vtable.save(self.ptr, row);
|
|
}
|
|
|
|
/// Delete row
|
|
pub fn delete(self: DataStore, row: *Row) !void {
|
|
return self.vtable.delete(self.ptr, row);
|
|
}
|
|
|
|
/// Lookup in related table
|
|
pub fn lookup(self: DataStore, table: []const u8, key_column: []const u8, key_value: CellValue, allocator: std.mem.Allocator) !?Row {
|
|
return self.vtable.lookup(self.ptr, table, key_column, key_value, allocator);
|
|
}
|
|
};
|
|
|
|
// =============================================================================
|
|
// Table Schema
|
|
// =============================================================================
|
|
|
|
/// Complete schema definition for AdvancedTable
|
|
pub const TableSchema = struct {
|
|
/// Table name (for DataStore operations)
|
|
table_name: []const u8,
|
|
|
|
/// Column definitions
|
|
columns: []const ColumnDef,
|
|
|
|
/// Configuration
|
|
config: TableConfig = .{},
|
|
|
|
/// Colors (optional override)
|
|
colors: ?*const TableColors = null,
|
|
|
|
/// DataStore for persistence (optional)
|
|
data_store: ?DataStore = null,
|
|
|
|
// =========================================================================
|
|
// Global Callbacks
|
|
// =========================================================================
|
|
|
|
/// Called when row is selected
|
|
on_row_selected: ?OnRowSelectedFn = null,
|
|
|
|
/// Called when cell value changes
|
|
on_cell_changed: ?OnCellChangedFn = null,
|
|
|
|
/// Called when active row changes (for loading detail panels)
|
|
on_active_row_changed: ?OnActiveRowChangedFn = null,
|
|
|
|
/// Called to get row lock state
|
|
get_row_lock_state: ?GetRowLockStateFn = null,
|
|
|
|
// =========================================================================
|
|
// Helper Methods
|
|
// =========================================================================
|
|
|
|
/// Get column by name
|
|
pub fn getColumn(self: *const TableSchema, name: []const u8) ?*const ColumnDef {
|
|
for (self.columns) |*col| {
|
|
if (std.mem.eql(u8, col.name, name)) {
|
|
return col;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// Get column index by name
|
|
pub fn getColumnIndex(self: *const TableSchema, name: []const u8) ?usize {
|
|
for (self.columns, 0..) |col, i| {
|
|
if (std.mem.eql(u8, col.name, name)) {
|
|
return i;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// Get visible column count
|
|
pub fn getVisibleColumnCount(self: *const TableSchema) usize {
|
|
var count: usize = 0;
|
|
for (self.columns) |col| {
|
|
if (col.visible) count += 1;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
/// Get total width of all visible columns
|
|
pub fn getTotalWidth(self: *const TableSchema) u32 {
|
|
var total: u32 = 0;
|
|
for (self.columns) |col| {
|
|
if (col.visible) total += col.width;
|
|
}
|
|
return total;
|
|
}
|
|
|
|
/// Find first editable column
|
|
pub fn getFirstEditableColumn(self: *const TableSchema) ?usize {
|
|
for (self.columns, 0..) |col, i| {
|
|
if (col.canEdit()) {
|
|
return i;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// Find next editable column after given index
|
|
pub fn getNextEditableColumn(self: *const TableSchema, after: usize) ?usize {
|
|
var i = after + 1;
|
|
while (i < self.columns.len) : (i += 1) {
|
|
if (self.columns[i].canEdit()) {
|
|
return i;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// Find previous editable column before given index
|
|
pub fn getPrevEditableColumn(self: *const TableSchema, before: usize) ?usize {
|
|
if (before == 0) return null;
|
|
var i = before - 1;
|
|
while (true) {
|
|
if (self.columns[i].canEdit()) {
|
|
return i;
|
|
}
|
|
if (i == 0) break;
|
|
i -= 1;
|
|
}
|
|
return null;
|
|
}
|
|
};
|
|
|
|
// =============================================================================
|
|
// Tests
|
|
// =============================================================================
|
|
|
|
test "ColumnDef getEffectiveWidth" {
|
|
const col = ColumnDef{
|
|
.name = "test",
|
|
.title = "Test",
|
|
.width = 100,
|
|
.min_width = 50,
|
|
.max_width = 200,
|
|
};
|
|
|
|
try std.testing.expectEqual(@as(u32, 100), col.getEffectiveWidth(100));
|
|
try std.testing.expectEqual(@as(u32, 50), col.getEffectiveWidth(30)); // clamped to min
|
|
try std.testing.expectEqual(@as(u32, 200), col.getEffectiveWidth(300)); // clamped to max
|
|
}
|
|
|
|
test "ColumnDef canEdit" {
|
|
const editable = ColumnDef{ .name = "a", .title = "A", .editable = true, .visible = true };
|
|
const not_editable = ColumnDef{ .name = "b", .title = "B", .editable = false, .visible = true };
|
|
const hidden = ColumnDef{ .name = "c", .title = "C", .editable = true, .visible = false };
|
|
|
|
try std.testing.expect(editable.canEdit());
|
|
try std.testing.expect(!not_editable.canEdit());
|
|
try std.testing.expect(!hidden.canEdit());
|
|
}
|
|
|
|
test "TableSchema getColumn" {
|
|
const columns = [_]ColumnDef{
|
|
.{ .name = "id", .title = "ID" },
|
|
.{ .name = "name", .title = "Name" },
|
|
.{ .name = "value", .title = "Value" },
|
|
};
|
|
|
|
const schema = TableSchema{
|
|
.table_name = "test",
|
|
.columns = &columns,
|
|
};
|
|
|
|
const name_col = schema.getColumn("name");
|
|
try std.testing.expect(name_col != null);
|
|
try std.testing.expectEqualStrings("Name", name_col.?.title);
|
|
|
|
const unknown_col = schema.getColumn("unknown");
|
|
try std.testing.expect(unknown_col == null);
|
|
}
|
|
|
|
test "TableSchema getFirstEditableColumn" {
|
|
const columns = [_]ColumnDef{
|
|
.{ .name = "id", .title = "ID", .editable = false },
|
|
.{ .name = "name", .title = "Name", .editable = true },
|
|
.{ .name = "value", .title = "Value", .editable = true },
|
|
};
|
|
|
|
const schema = TableSchema{
|
|
.table_name = "test",
|
|
.columns = &columns,
|
|
};
|
|
|
|
const first = schema.getFirstEditableColumn();
|
|
try std.testing.expectEqual(@as(?usize, 1), first);
|
|
}
|