- Crear src/utils/time.zig para centralizar helpers de tiempo (timestamp, milliTimestamp, nanoTimestamp).
- Reemplazar std.io.fixedBufferStream por std.Io.Writer.fixed.
- Migrar de std.fs.cwd() a std.Io.Dir.cwd() y actualizar firmas de lectura/escritura.
- Reemplazar std.Thread.sleep por std.posix.system.nanosleep.
- Corregir capturas descartadas en switches (text_input => {}).
- Actualizar tests y ejemplos para compatibilidad con std.Io y nuevos helpers.
- Actualizar build.zig.zon a 0.16.0.
Co-Authored-By: Gemini <noreply@google.com>
349 lines
13 KiB
Zig
349 lines
13 KiB
Zig
//! Table Demo - Advanced widgets showcase
|
|
//!
|
|
//! Demonstrates:
|
|
//! - Table widget with editing and navigation
|
|
//! - Split panels (horizontal/vertical)
|
|
//! - Panel containers with titles
|
|
//! - Focus management between widgets
|
|
//!
|
|
//! Run with: zig build table-demo
|
|
|
|
const std = @import("std");
|
|
const zcatgui = @import("zcatgui");
|
|
|
|
const Context = zcatgui.Context;
|
|
const Color = zcatgui.Color;
|
|
const Layout = zcatgui.Layout;
|
|
const Command = zcatgui.Command;
|
|
const Framebuffer = zcatgui.render.Framebuffer;
|
|
const SoftwareRenderer = zcatgui.render.SoftwareRenderer;
|
|
const Sdl2Backend = zcatgui.backend.Sdl2Backend;
|
|
|
|
const widgets = zcatgui.widgets;
|
|
const Table = widgets.Table;
|
|
const Split = widgets.Split;
|
|
const Panel = widgets.Panel;
|
|
|
|
const print = std.debug.print;
|
|
|
|
// Sample data for the table
|
|
const ProductData = struct {
|
|
code: []const u8,
|
|
name: []const u8,
|
|
price: []const u8,
|
|
stock: []const u8,
|
|
status: []const u8,
|
|
};
|
|
|
|
const sample_products = [_]ProductData{
|
|
.{ .code = "PRD001", .name = "Widget A", .price = "29.99", .stock = "150", .status = "Active" },
|
|
.{ .code = "PRD002", .name = "Widget B", .price = "49.99", .stock = "75", .status = "Active" },
|
|
.{ .code = "PRD003", .name = "Gadget X", .price = "99.99", .stock = "30", .status = "Low Stock" },
|
|
.{ .code = "PRD004", .name = "Gadget Y", .price = "149.99", .stock = "0", .status = "Out of Stock" },
|
|
.{ .code = "PRD005", .name = "Component Z", .price = "19.99", .stock = "500", .status = "Active" },
|
|
.{ .code = "PRD006", .name = "Assembly Kit", .price = "199.99", .stock = "25", .status = "Active" },
|
|
.{ .code = "PRD007", .name = "Spare Part A", .price = "9.99", .stock = "1000", .status = "Active" },
|
|
.{ .code = "PRD008", .name = "Spare Part B", .price = "14.99", .stock = "800", .status = "Active" },
|
|
.{ .code = "PRD009", .name = "Premium Set", .price = "299.99", .stock = "10", .status = "Limited" },
|
|
.{ .code = "PRD010", .name = "Basic Set", .price = "79.99", .stock = "200", .status = "Active" },
|
|
.{ .code = "PRD011", .name = "Deluxe Pack", .price = "399.99", .stock = "5", .status = "Limited" },
|
|
.{ .code = "PRD012", .name = "Starter Kit", .price = "59.99", .stock = "300", .status = "Active" },
|
|
};
|
|
|
|
// Column definitions
|
|
const columns = [_]widgets.Column{
|
|
.{ .name = "Code", .width = 80, .column_type = .text },
|
|
.{ .name = "Name", .width = 150, .column_type = .text },
|
|
.{ .name = "Price", .width = 80, .column_type = .number },
|
|
.{ .name = "Stock", .width = 60, .column_type = .number },
|
|
.{ .name = "Status", .width = 100, .column_type = .text },
|
|
};
|
|
|
|
// Cell data provider function
|
|
fn getCellData(row: usize, col: usize) []const u8 {
|
|
if (row >= sample_products.len) return "";
|
|
const product = sample_products[row];
|
|
return switch (col) {
|
|
0 => product.code,
|
|
1 => product.name,
|
|
2 => product.price,
|
|
3 => product.stock,
|
|
4 => product.status,
|
|
else => "",
|
|
};
|
|
}
|
|
|
|
pub fn main() !void {
|
|
print("=== zcatgui Table Demo ===\n\n", .{});
|
|
print("This demo showcases advanced widgets:\n", .{});
|
|
print("- Table with keyboard navigation and editing\n", .{});
|
|
print("- Split panels (drag divider to resize)\n", .{});
|
|
print("- Panel containers with title bars\n\n", .{});
|
|
|
|
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
|
defer _ = gpa.deinit();
|
|
const allocator = gpa.allocator();
|
|
|
|
// Initialize backend
|
|
var backend = try Sdl2Backend.init("zcatgui - Table Demo", 1024, 768);
|
|
defer backend.deinit();
|
|
|
|
// Create framebuffer
|
|
var fb = try Framebuffer.init(allocator, 1024, 768);
|
|
defer fb.deinit();
|
|
|
|
// Create renderer
|
|
var renderer = SoftwareRenderer.init(&fb);
|
|
|
|
// Create context
|
|
var ctx = try Context.init(allocator, 1024, 768);
|
|
defer ctx.deinit();
|
|
|
|
// Widget state
|
|
var hsplit_state = widgets.SplitState{ .offset = 0.7 };
|
|
var vsplit_state = widgets.SplitState{ .offset = 0.6 };
|
|
|
|
var main_panel_state = widgets.PanelState{ .focused = true };
|
|
var info_panel_state = widgets.PanelState{};
|
|
var log_panel_state = widgets.PanelState{};
|
|
|
|
var table_state = widgets.TableState{};
|
|
table_state.row_count = sample_products.len;
|
|
|
|
// Mark some rows as modified for demo
|
|
table_state.markModified(2);
|
|
table_state.markNew(sample_products.len - 1);
|
|
|
|
var running = true;
|
|
var frame: u32 = 0;
|
|
|
|
print("Starting event loop...\n", .{});
|
|
print("Controls:\n", .{});
|
|
print(" Arrow keys: Navigate table\n", .{});
|
|
print(" Enter/F2: Edit cell\n", .{});
|
|
print(" Escape: Cancel edit / Exit\n", .{});
|
|
print(" Tab: Move between panels\n", .{});
|
|
print(" Mouse: Click to select, drag dividers\n\n", .{});
|
|
|
|
while (running) {
|
|
// Poll events
|
|
while (backend.pollEvent()) |event| {
|
|
switch (event) {
|
|
.quit => running = false,
|
|
.key => |key| {
|
|
// Pass key event to context for widget keyboard handling
|
|
ctx.input.handleKeyEvent(key);
|
|
|
|
// Handle escape at app level (quit if not editing)
|
|
if (key.key == .escape and key.pressed and !table_state.editing) {
|
|
running = false;
|
|
}
|
|
},
|
|
.mouse => |m| {
|
|
ctx.input.setMousePos(m.x, m.y);
|
|
if (m.button) |btn| {
|
|
ctx.input.setMouseButton(btn, m.pressed);
|
|
}
|
|
},
|
|
.resize => |size| {
|
|
try fb.resize(size.width, size.height);
|
|
ctx.resize(size.width, size.height);
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
|
|
ctx.beginFrame();
|
|
|
|
// Clear
|
|
renderer.clear(Color.background);
|
|
|
|
// Main horizontal split: left (table) | right (info)
|
|
ctx.layout.row_height = @as(u32, @intCast(fb.height));
|
|
const hsplit_result = Split.hsplitEx(&ctx, &hsplit_state, .{
|
|
.divider_size = 6,
|
|
});
|
|
|
|
// Left side: Main panel with table
|
|
{
|
|
const panel_result = Panel.panelRect(
|
|
&ctx,
|
|
hsplit_result.first,
|
|
&main_panel_state,
|
|
.{ .title = "Products", .collapsible = false },
|
|
.{},
|
|
);
|
|
|
|
if (!panel_result.content.isEmpty()) {
|
|
Panel.beginPanel(&ctx, "products_panel", panel_result.content);
|
|
|
|
// Set up layout for table
|
|
ctx.layout.area = panel_result.content;
|
|
ctx.layout.cursor_x = panel_result.content.x;
|
|
ctx.layout.cursor_y = panel_result.content.y;
|
|
ctx.layout.row_height = panel_result.content.h;
|
|
|
|
const table_result = Table.tableEx(
|
|
&ctx,
|
|
&table_state,
|
|
&columns,
|
|
getCellData,
|
|
null, // no edit callback
|
|
.{ .row_height = 24 },
|
|
.{},
|
|
);
|
|
|
|
if (table_result.selection_changed) {
|
|
if (table_state.selectedCell()) |cell| {
|
|
print("Selected: row={}, col={}\n", .{ cell.row, cell.col });
|
|
}
|
|
}
|
|
|
|
if (table_result.cell_edited) {
|
|
if (table_state.selectedCell()) |cell| {
|
|
print("Edited row {} col {}: \"{s}\"\n", .{
|
|
cell.row,
|
|
cell.col,
|
|
table_state.getEditText(),
|
|
});
|
|
table_state.markModified(cell.row);
|
|
}
|
|
}
|
|
|
|
Panel.endPanel(&ctx);
|
|
}
|
|
}
|
|
|
|
// Right side: vertical split for info and log
|
|
{
|
|
const vsplit_result = Split.splitRect(
|
|
&ctx,
|
|
hsplit_result.second,
|
|
&vsplit_state,
|
|
.vertical,
|
|
.{ .divider_size = 6 },
|
|
);
|
|
|
|
// Top right: Info panel
|
|
{
|
|
const info_result = Panel.panelRect(
|
|
&ctx,
|
|
vsplit_result.first,
|
|
&info_panel_state,
|
|
.{ .title = "Details", .collapsible = true },
|
|
.{},
|
|
);
|
|
|
|
if (!info_result.content.isEmpty()) {
|
|
Panel.beginPanel(&ctx, "info_panel", info_result.content);
|
|
|
|
// Show selected product info
|
|
ctx.layout.area = info_result.content;
|
|
ctx.layout.cursor_x = info_result.content.x;
|
|
ctx.layout.cursor_y = info_result.content.y;
|
|
ctx.layout.row_height = 20;
|
|
|
|
if (table_state.selectedCell()) |cell| {
|
|
if (cell.row < sample_products.len) {
|
|
const product = sample_products[cell.row];
|
|
|
|
zcatgui.labelColored(&ctx, "Selected Product:", Color.primary);
|
|
ctx.layout.row_height = 16;
|
|
|
|
var buf: [64]u8 = undefined;
|
|
const code_text = std.fmt.bufPrint(&buf, "Code: {s}", .{product.code}) catch "Error";
|
|
zcatgui.label(&ctx, code_text);
|
|
|
|
const name_text = std.fmt.bufPrint(&buf, "Name: {s}", .{product.name}) catch "Error";
|
|
zcatgui.label(&ctx, name_text);
|
|
|
|
const price_text = std.fmt.bufPrint(&buf, "Price: ${s}", .{product.price}) catch "Error";
|
|
zcatgui.label(&ctx, price_text);
|
|
|
|
const stock_text = std.fmt.bufPrint(&buf, "Stock: {s} units", .{product.stock}) catch "Error";
|
|
zcatgui.label(&ctx, stock_text);
|
|
|
|
const status_text = std.fmt.bufPrint(&buf, "Status: {s}", .{product.status}) catch "Error";
|
|
zcatgui.label(&ctx, status_text);
|
|
}
|
|
} else {
|
|
zcatgui.labelColored(&ctx, "No product selected", Color.secondary);
|
|
}
|
|
|
|
Panel.endPanel(&ctx);
|
|
}
|
|
}
|
|
|
|
// Bottom right: Log panel
|
|
{
|
|
const log_result = Panel.panelRect(
|
|
&ctx,
|
|
vsplit_result.second,
|
|
&log_panel_state,
|
|
.{ .title = "Activity Log", .collapsible = true },
|
|
.{},
|
|
);
|
|
|
|
if (!log_result.content.isEmpty()) {
|
|
Panel.beginPanel(&ctx, "log_panel", log_result.content);
|
|
|
|
ctx.layout.area = log_result.content;
|
|
ctx.layout.cursor_x = log_result.content.x;
|
|
ctx.layout.cursor_y = log_result.content.y;
|
|
ctx.layout.row_height = 14;
|
|
|
|
// Show some log entries
|
|
zcatgui.labelColored(&ctx, "Recent Activity:", Color.secondary);
|
|
|
|
var frame_buf: [64]u8 = undefined;
|
|
const frame_text = std.fmt.bufPrint(&frame_buf, "Frame: {}", .{frame}) catch "Error";
|
|
zcatgui.label(&ctx, frame_text);
|
|
|
|
const commands_text = std.fmt.bufPrint(&frame_buf, "Commands: {}", .{ctx.commands.items.len}) catch "Error";
|
|
zcatgui.label(&ctx, commands_text);
|
|
|
|
const dirty_count = blk: {
|
|
var count: usize = 0;
|
|
for (0..sample_products.len) |i| {
|
|
if (table_state.row_states[i] != .clean) count += 1;
|
|
}
|
|
break :blk count;
|
|
};
|
|
const dirty_text = std.fmt.bufPrint(&frame_buf, "Modified rows: {}", .{dirty_count}) catch "Error";
|
|
zcatgui.label(&ctx, dirty_text);
|
|
|
|
if (table_state.editing) {
|
|
zcatgui.labelColored(&ctx, "Currently editing...", Color.warning);
|
|
}
|
|
|
|
Panel.endPanel(&ctx);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Execute all draw commands
|
|
for (ctx.commands.items) |cmd| {
|
|
renderer.execute(cmd);
|
|
}
|
|
|
|
ctx.endFrame();
|
|
|
|
// Present
|
|
backend.present(&fb);
|
|
|
|
frame += 1;
|
|
|
|
// Cap at ~60 FPS
|
|
const ts = std.posix.timespec{ .sec = 0, .nsec = 16 * std.time.ns_per_ms };
|
|
_ = std.posix.system.nanosleep(&ts, null);
|
|
}
|
|
|
|
print("\n=== Demo Complete ===\n", .{});
|
|
print("Final state:\n", .{});
|
|
if (table_state.selectedCell()) |cell| {
|
|
print(" Selected: row={}, col={}\n", .{ cell.row, cell.col });
|
|
} else {
|
|
print(" No selection\n", .{});
|
|
}
|
|
print(" Frames rendered: {}\n", .{frame});
|
|
}
|