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>
299 lines
9 KiB
Zig
299 lines
9 KiB
Zig
//! Panel system demo for zcatui.
|
|
//!
|
|
//! Demonstrates:
|
|
//! - Panel with borders and focus
|
|
//! - TabbedPanel
|
|
//! - DockingPanel (dockable/floating)
|
|
//! - PanelManager
|
|
//!
|
|
//! Run with: zig build panel-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 Block = zcatui.widgets.Block;
|
|
const Borders = zcatui.widgets.Borders;
|
|
const Paragraph = zcatui.widgets.Paragraph;
|
|
const Panel = zcatui.widgets.Panel;
|
|
const TabbedPanel = zcatui.widgets.TabbedPanel;
|
|
const DockingPanel = zcatui.widgets.DockingPanel;
|
|
const DockPosition = zcatui.widgets.DockPosition;
|
|
const PanelManager = zcatui.widgets.PanelManager;
|
|
const StatusBar = zcatui.widgets.StatusBar;
|
|
|
|
const AppState = struct {
|
|
running: bool = true,
|
|
manager: PanelManager,
|
|
tabbed: TabbedPanel,
|
|
show_floating: bool = false,
|
|
floating_x: u16 = 20,
|
|
floating_y: u16 = 5,
|
|
|
|
fn init(allocator: std.mem.Allocator) !AppState {
|
|
var manager = PanelManager.init(allocator);
|
|
|
|
// Add sidebar (left docked)
|
|
_ = try manager.add(
|
|
DockingPanel.init("Sidebar")
|
|
.setPosition(.left)
|
|
.setDockSize(20),
|
|
);
|
|
|
|
// Add bottom panel
|
|
_ = try manager.add(
|
|
DockingPanel.init("Console")
|
|
.setPosition(.bottom)
|
|
.setDockSize(25),
|
|
);
|
|
|
|
// Focus first panel
|
|
manager.focus(0);
|
|
|
|
return .{
|
|
.manager = manager,
|
|
.tabbed = TabbedPanel.init(&.{ "Overview", "Details", "Settings" }),
|
|
};
|
|
}
|
|
|
|
fn deinit(self: *AppState) void {
|
|
self.manager.deinit();
|
|
}
|
|
};
|
|
|
|
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 = try AppState.init(allocator);
|
|
defer state.deinit();
|
|
|
|
while (state.running) {
|
|
try term.drawWithContext(&state, render);
|
|
|
|
if (try term.pollEvent(100)) |event| {
|
|
handleEvent(&state, event);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn handleEvent(state: *AppState, event: Event) void {
|
|
switch (event) {
|
|
.key => |key| {
|
|
switch (key.code) {
|
|
.esc => state.running = false,
|
|
.tab => state.manager.focusNext(),
|
|
.char => |c| {
|
|
switch (c) {
|
|
'q', 'Q' => state.running = false,
|
|
'f', 'F' => state.show_floating = !state.show_floating,
|
|
'1' => state.tabbed.select(0),
|
|
'2' => state.tabbed.select(1),
|
|
'3' => state.tabbed.select(2),
|
|
'h', 'H' => {
|
|
if (state.manager.get(0)) |p| {
|
|
p.visible = !p.visible;
|
|
}
|
|
},
|
|
'j', 'J' => {
|
|
if (state.manager.get(1)) |p| {
|
|
p.visible = !p.visible;
|
|
}
|
|
},
|
|
else => {},
|
|
}
|
|
},
|
|
.left => {
|
|
if (state.show_floating) {
|
|
state.floating_x -|= 1;
|
|
} else {
|
|
state.tabbed.selectPrev();
|
|
}
|
|
},
|
|
.right => {
|
|
if (state.show_floating) {
|
|
state.floating_x += 1;
|
|
} else {
|
|
state.tabbed.selectNext();
|
|
}
|
|
},
|
|
.up => {
|
|
if (state.show_floating) {
|
|
state.floating_y -|= 1;
|
|
}
|
|
},
|
|
.down => {
|
|
if (state.show_floating) {
|
|
state.floating_y += 1;
|
|
}
|
|
},
|
|
else => {},
|
|
}
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
|
|
fn render(state: *AppState, area: Rect, buf: *Buffer) void {
|
|
// Layout: main area and status bar
|
|
if (area.height < 3) return;
|
|
|
|
const content_area = Rect.init(area.x, area.y, area.width, area.height - 1);
|
|
const status_area = Rect.init(area.x, area.y + area.height - 1, area.width, 1);
|
|
|
|
// Render docked panels (modifies remaining area)
|
|
state.manager.render(content_area, buf);
|
|
|
|
// Calculate center area (after docked panels)
|
|
var center_area = content_area;
|
|
|
|
// Adjust for sidebar
|
|
if (state.manager.get(0)) |sidebar| {
|
|
if (sidebar.visible) {
|
|
const sidebar_width = content_area.width * sidebar.dock_size / 100;
|
|
center_area.x += sidebar_width;
|
|
center_area.width -|= sidebar_width;
|
|
|
|
// Render sidebar content
|
|
const sb_area = Rect.init(content_area.x, content_area.y, sidebar_width, content_area.height);
|
|
renderSidebar(sb_area, buf);
|
|
}
|
|
}
|
|
|
|
// Adjust for bottom panel
|
|
if (state.manager.get(1)) |bottom| {
|
|
if (bottom.visible) {
|
|
const bottom_height = content_area.height * bottom.dock_size / 100;
|
|
center_area.height -|= bottom_height;
|
|
|
|
// Render console content
|
|
const console_area = Rect.init(
|
|
center_area.x,
|
|
center_area.y + center_area.height,
|
|
center_area.width,
|
|
bottom_height,
|
|
);
|
|
renderConsole(console_area, buf);
|
|
}
|
|
}
|
|
|
|
// Render tabbed panel in center
|
|
state.tabbed.render(center_area, buf);
|
|
|
|
// Render tab content
|
|
const tab_content = Rect.init(
|
|
center_area.x + 1,
|
|
center_area.y + 2,
|
|
center_area.width -| 2,
|
|
center_area.height -| 4,
|
|
);
|
|
renderTabContent(state.tabbed.selected, tab_content, buf);
|
|
|
|
// Floating panel
|
|
if (state.show_floating) {
|
|
renderFloatingPanel(state, area, buf);
|
|
}
|
|
|
|
// Status bar
|
|
const status = StatusBar.init()
|
|
.setLeft("Tab: focus | H: sidebar | J: console | F: floating | 1-3: tabs")
|
|
.setRight("Esc: exit");
|
|
status.render(status_area, buf);
|
|
}
|
|
|
|
fn renderSidebar(area: Rect, buf: *Buffer) void {
|
|
const items = [_][]const u8{
|
|
" Files",
|
|
" Search",
|
|
" Git",
|
|
" Debug",
|
|
" Extensions",
|
|
};
|
|
|
|
var y = area.y + 2;
|
|
for (items) |item| {
|
|
if (y < area.y + area.height - 1) {
|
|
_ = buf.setString(area.x + 1, y, item, Style.default);
|
|
y += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
fn renderConsole(area: Rect, buf: *Buffer) void {
|
|
const lines = [_][]const u8{
|
|
"> Build started...",
|
|
"> Compiling src/main.zig",
|
|
"> Linking...",
|
|
"> Build completed successfully!",
|
|
};
|
|
|
|
var y = area.y + 1;
|
|
for (lines) |line| {
|
|
if (y < area.y + area.height - 1) {
|
|
_ = buf.setString(area.x + 1, y, line, Style.default.fg(Color.green));
|
|
y += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
fn renderTabContent(tab: usize, area: Rect, buf: *Buffer) void {
|
|
const content = switch (tab) {
|
|
0 => "Welcome to the Panel Demo!\n\nThis demonstrates the LEGO panel system.",
|
|
1 => "Details tab content.\n\nPanels can be:\n- Docked (left/right/top/bottom)\n- Floating\n- Tabbed",
|
|
2 => "Settings tab content.\n\nConfigure your preferences here.",
|
|
else => "",
|
|
};
|
|
|
|
var y = area.y;
|
|
var iter = std.mem.splitScalar(u8, content, '\n');
|
|
while (iter.next()) |line| {
|
|
if (y < area.y + area.height) {
|
|
_ = buf.setString(area.x, y, line, Style.default);
|
|
y += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
fn renderFloatingPanel(state: *AppState, bounds: Rect, buf: *Buffer) void {
|
|
const width: u16 = 30;
|
|
const height: u16 = 10;
|
|
const x = @min(state.floating_x, bounds.width -| width);
|
|
const y = @min(state.floating_y, bounds.height -| height);
|
|
|
|
const float_area = Rect.init(x, y, width, height);
|
|
|
|
// Clear background
|
|
var cy = float_area.y;
|
|
while (cy < float_area.y + float_area.height) : (cy += 1) {
|
|
var cx = float_area.x;
|
|
while (cx < float_area.x + float_area.width) : (cx += 1) {
|
|
if (buf.getCell(cx, cy)) |cell| {
|
|
cell.setChar(' ');
|
|
cell.setStyle(Style.default.bg(Color.indexed(236)));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Border
|
|
const block = Block.init()
|
|
.title(" Floating Panel ")
|
|
.setBorders(Borders.all)
|
|
.style(Style.default.fg(Color.yellow).bg(Color.indexed(236)));
|
|
block.render(float_area, buf);
|
|
|
|
// Content
|
|
const inner = block.inner(float_area);
|
|
_ = buf.setString(inner.x, inner.y, "This panel floats!", Style.default.bg(Color.indexed(236)));
|
|
_ = buf.setString(inner.x, inner.y + 2, "Arrow keys to move", Style.default.fg(Color.indexed(8)).bg(Color.indexed(236)));
|
|
_ = buf.setString(inner.x, inner.y + 3, "F to toggle", Style.default.fg(Color.indexed(8)).bg(Color.indexed(236)));
|
|
}
|