zcatui/examples/form_demo.zig
reugenio 79c0bb1a58 feat: Add form widgets, status bar, toast system, and documentation
Form widgets:
- Checkbox and CheckboxGroup for boolean inputs
- RadioGroup for single-selection options
- Select dropdown with keyboard navigation
- Slider and RangeSlider for numeric inputs
- TextArea for multi-line text input

UI utilities:
- StatusBar for bottom-of-screen information
- Toast and ToastManager for notifications

Examples:
- form_demo.zig: Interactive form widgets showcase
- panel_demo.zig: Docking panel system demo

Documentation:
- Complete README.md with Quick Start, widget examples, and API reference

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-08 18:02:06 +01:00

295 lines
8.3 KiB
Zig

//! Form widgets demo for zcatui.
//!
//! Demonstrates:
//! - Checkbox, RadioGroup
//! - Select dropdown
//! - Slider
//! - TextArea
//! - StatusBar with toast notifications
//!
//! Run with: zig build form-demo
const std = @import("std");
const zcatui = @import("zcatui");
const Terminal = zcatui.Terminal;
const Buffer = zcatui.Buffer;
const Rect = zcatui.Rect;
const Style = zcatui.Style;
const Color = zcatui.Color;
const Event = zcatui.Event;
const KeyCode = zcatui.KeyCode;
const Layout = zcatui.Layout;
const Constraint = zcatui.Constraint;
const Block = zcatui.widgets.Block;
const Borders = zcatui.widgets.Borders;
const Checkbox = zcatui.widgets.Checkbox;
const RadioGroup = zcatui.widgets.RadioGroup;
const Select = zcatui.widgets.Select;
const Slider = zcatui.widgets.Slider;
const StatusBar = zcatui.widgets.StatusBar;
const Toast = zcatui.widgets.Toast;
const ToastManager = zcatui.widgets.ToastManager;
const FormField = enum {
checkbox1,
checkbox2,
radio,
select,
slider,
};
const AppState = struct {
running: bool = true,
current_field: FormField = .checkbox1,
// Form values
checkbox1: bool = false,
checkbox2: bool = true,
radio_selected: usize = 0,
select: Select,
slider_value: f64 = 50,
// UI state
toast_manager: ToastManager = ToastManager.init(),
status: []const u8 = "Use Tab to navigate, Space/Enter to interact",
fn init() AppState {
return .{
.select = Select.init(&.{
"Option 1 - Basic",
"Option 2 - Standard",
"Option 3 - Premium",
"Option 4 - Enterprise",
}),
};
}
fn getCurrentFieldName(self: AppState) []const u8 {
return switch (self.current_field) {
.checkbox1 => "Dark Mode",
.checkbox2 => "Notifications",
.radio => "Theme",
.select => "Plan",
.slider => "Volume",
};
}
fn nextField(self: *AppState) void {
self.current_field = switch (self.current_field) {
.checkbox1 => .checkbox2,
.checkbox2 => .radio,
.radio => .select,
.select => .slider,
.slider => .checkbox1,
};
self.status = self.getCurrentFieldName();
}
fn prevField(self: *AppState) void {
self.current_field = switch (self.current_field) {
.checkbox1 => .slider,
.checkbox2 => .checkbox1,
.radio => .checkbox2,
.select => .radio,
.slider => .select,
};
self.status = self.getCurrentFieldName();
}
};
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
var term = try Terminal.init(allocator);
defer term.deinit();
var state = AppState.init();
while (state.running) {
// Update toasts
state.toast_manager.update();
try term.drawWithContext(&state, render);
if (try term.pollEvent(50)) |event| {
handleEvent(&state, event);
}
}
}
fn handleEvent(state: *AppState, event: Event) void {
switch (event) {
.key => |key| {
switch (key.code) {
.esc => state.running = false,
.tab => state.nextField(),
.char => |c| {
switch (c) {
'q', 'Q' => state.running = false,
' ' => handleSpace(state),
else => {},
}
},
.enter => handleEnter(state),
.up => handleUp(state),
.down => handleDown(state),
.left => handleLeft(state),
.right => handleRight(state),
else => {},
}
},
else => {},
}
}
fn handleSpace(state: *AppState) void {
switch (state.current_field) {
.checkbox1 => {
state.checkbox1 = !state.checkbox1;
if (state.checkbox1) {
state.toast_manager.info("Dark mode enabled");
} else {
state.toast_manager.info("Dark mode disabled");
}
},
.checkbox2 => {
state.checkbox2 = !state.checkbox2;
},
.select => {
state.select.toggle();
},
else => {},
}
}
fn handleEnter(state: *AppState) void {
switch (state.current_field) {
.select => {
if (state.select.open) {
state.select.confirm();
state.toast_manager.success("Plan selected!");
} else {
state.select.toggle();
}
},
.radio => {
state.toast_manager.info("Theme applied");
},
else => handleSpace(state),
}
}
fn handleUp(state: *AppState) void {
switch (state.current_field) {
.radio => {
if (state.radio_selected > 0) {
state.radio_selected -= 1;
}
},
.select => state.select.highlightPrev(),
.slider => state.slider_value = @min(state.slider_value + 5, 100),
else => state.prevField(),
}
}
fn handleDown(state: *AppState) void {
switch (state.current_field) {
.radio => {
if (state.radio_selected < 2) {
state.radio_selected += 1;
}
},
.select => state.select.highlightNext(),
.slider => state.slider_value = @max(state.slider_value - 5, 0),
else => state.nextField(),
}
}
fn handleLeft(state: *AppState) void {
if (state.current_field == .slider) {
state.slider_value = @max(state.slider_value - 1, 0);
}
}
fn handleRight(state: *AppState) void {
if (state.current_field == .slider) {
state.slider_value = @min(state.slider_value + 1, 100);
}
}
fn render(state: *AppState, area: Rect, buf: *Buffer) void {
// Main layout
const chunks = Layout.vertical(&.{
Constraint.min(0), // Content
Constraint.length(1), // Status bar
}).split(area);
// Content area
const content = chunks.get(0);
const block = Block.init()
.title(" Form Demo - Tab to navigate, Space/Enter to interact ")
.setBorders(Borders.all)
.style(Style.default.fg(Color.cyan));
block.render(content, buf);
const inner = block.inner(content);
// Form fields layout
const form_chunks = Layout.vertical(&.{
Constraint.length(2), // Checkbox 1
Constraint.length(2), // Checkbox 2
Constraint.length(5), // Radio
Constraint.length(4), // Select
Constraint.length(2), // Slider
}).split(inner);
// Checkbox 1 - Dark Mode
const cb1 = Checkbox.init("Enable Dark Mode")
.setChecked(state.checkbox1)
.setFocused(state.current_field == .checkbox1);
cb1.render(form_chunks.get(0), buf);
// Checkbox 2 - Notifications
const cb2 = Checkbox.init("Enable Notifications")
.setChecked(state.checkbox2)
.setFocused(state.current_field == .checkbox2);
cb2.render(form_chunks.get(1), buf);
// Radio - Theme
_ = buf.setString(form_chunks.get(2).x, form_chunks.get(2).y, "Theme:", Style.default);
var radio = RadioGroup.init(&.{ "Light", "Dark", "System" })
.setFocused(state.current_field == .radio);
radio.selected = state.radio_selected;
radio.focused = state.radio_selected;
radio.render(
Rect.init(form_chunks.get(2).x, form_chunks.get(2).y + 1, form_chunks.get(2).width, 3),
buf,
);
// Select - Plan
_ = buf.setString(form_chunks.get(3).x, form_chunks.get(3).y, "Plan:", Style.default);
state.select.focused = (state.current_field == .select);
state.select.render(
Rect.init(form_chunks.get(3).x, form_chunks.get(3).y + 1, 30, form_chunks.get(3).height - 1),
buf,
);
// Slider - Volume
const slider = Slider.init(0, 100)
.setValue(state.slider_value)
.setLabel("Volume")
.setFocused(state.current_field == .slider);
slider.render(form_chunks.get(4), buf);
// Status bar
const status = StatusBar.init()
.setLeft(state.status)
.setRight("Esc to exit");
status.render(chunks.get(1), buf);
// Toasts
state.toast_manager.render(area, buf);
}