zcatui/examples/panel_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

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