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>
295 lines
8.3 KiB
Zig
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);
|
|
}
|