zcatgui/src/widgets/advanced_table/schema.zig
reugenio 83049a99be feat: AdvancedTable widget - Fases 1-6 IMPLEMENTADO (pendiente aprobacion)
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>
2025-12-17 11:25:48 +01:00

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);
}