New modules (13): - src/resize.zig: SIGWINCH terminal resize detection - src/drag.zig: Mouse drag state and Splitter panels - src/diagnostic.zig: Elm-style error messages with code snippets - src/debug.zig: Debug overlay (FPS, timing, widget count) - src/profile.zig: Performance profiling with scoped timers - src/sixel.zig: Sixel graphics encoding for terminal images - src/async_loop.zig: epoll-based async event loop with timers - src/compose.zig: Widget composition utilities - src/shortcuts.zig: Keyboard shortcut registry - src/widgets/logo.zig: ASCII art logo widget Enhanced modules: - src/layout.zig: Added Constraint.ratio(num, denom) - src/terminal.zig: Integrated resize handling - src/root.zig: Re-exports all new modules New examples (9): - resize_demo, splitter_demo, dirtree_demo - help_demo, markdown_demo, progress_demo - spinner_demo, syntax_demo, viewport_demo Package manager: - build.zig.zon: Zig package manager support Stats: 60+ source files, 186+ tests, 20 executables 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
200 lines
7.5 KiB
Zig
200 lines
7.5 KiB
Zig
//! Splitter Demo - Resizable panels with mouse drag
|
|
//!
|
|
//! Demonstrates mouse drag to resize panels.
|
|
//! Run with: zig build splitter-demo
|
|
|
|
const std = @import("std");
|
|
const zcatui = @import("zcatui");
|
|
|
|
const Terminal = zcatui.Terminal;
|
|
const Rect = zcatui.Rect;
|
|
const Buffer = zcatui.Buffer;
|
|
const Style = zcatui.Style;
|
|
const Color = zcatui.Color;
|
|
const Block = zcatui.widgets.Block;
|
|
const Borders = zcatui.widgets.Borders;
|
|
const DragState = zcatui.DragState;
|
|
const DragType = zcatui.DragType;
|
|
const Splitter = zcatui.Splitter;
|
|
|
|
/// State for the demo
|
|
const State = struct {
|
|
// Horizontal splitter (splits left/right)
|
|
h_splitter: Splitter = Splitter.horizontal(30).setMinSizes(10, 20),
|
|
// Vertical splitter for right side (splits top/bottom)
|
|
v_splitter: Splitter = Splitter.vertical(50).setMinSizes(5, 5),
|
|
// Drag state
|
|
drag_state: DragState = .{},
|
|
// Which splitter is being dragged
|
|
active_splitter: ActiveSplitter = .none,
|
|
// Current area for hit testing
|
|
current_area: Rect = Rect.init(0, 0, 80, 24),
|
|
running: bool = true,
|
|
};
|
|
|
|
const ActiveSplitter = enum { none, horizontal, vertical };
|
|
|
|
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();
|
|
|
|
// Enable mouse capture for drag support
|
|
try term.enableMouseCapture();
|
|
term.enableAutoResize();
|
|
|
|
var state = State{};
|
|
|
|
while (state.running) {
|
|
try term.drawWithContext(&state, render);
|
|
|
|
if (try term.pollEvent(50)) |event| {
|
|
switch (event) {
|
|
.key => |key| {
|
|
switch (key.code) {
|
|
.char => |c| {
|
|
if (c == 'q') state.running = false;
|
|
// Keyboard shortcuts to adjust splitters
|
|
if (c == 'h' or c == 'H') {
|
|
const delta: i32 = if (c == 'H') -5 else 5;
|
|
state.h_splitter.adjustPosition(state.current_area, delta);
|
|
}
|
|
if (c == 'v' or c == 'V') {
|
|
const parts = state.h_splitter.split(state.current_area);
|
|
const delta: i32 = if (c == 'V') -5 else 5;
|
|
state.v_splitter.adjustPosition(parts.second, delta);
|
|
}
|
|
},
|
|
.esc => state.running = false,
|
|
else => {},
|
|
}
|
|
},
|
|
.mouse => |mouse| {
|
|
handleMouse(&state, mouse);
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn handleMouse(state: *State, mouse: zcatui.event.MouseEvent) void {
|
|
switch (mouse.kind) {
|
|
.down => {
|
|
if (mouse.button == .left) {
|
|
// Check if on horizontal splitter
|
|
if (state.h_splitter.isOnHandle(state.current_area, mouse.column, mouse.row)) {
|
|
state.drag_state.start(.horizontal_resize, mouse.column, mouse.row);
|
|
state.active_splitter = .horizontal;
|
|
} else {
|
|
// Check if on vertical splitter (in second panel)
|
|
const parts = state.h_splitter.split(state.current_area);
|
|
if (state.v_splitter.isOnHandle(parts.second, mouse.column, mouse.row)) {
|
|
state.drag_state.start(.vertical_resize, mouse.column, mouse.row);
|
|
state.active_splitter = .vertical;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
.drag => {
|
|
if (state.drag_state.isDragging()) {
|
|
const old_x = state.drag_state.current_x;
|
|
const old_y = state.drag_state.current_y;
|
|
state.drag_state.update(mouse.column, mouse.row);
|
|
|
|
// Apply the delta to the active splitter
|
|
switch (state.active_splitter) {
|
|
.horizontal => {
|
|
const delta = @as(i32, mouse.column) - @as(i32, old_x);
|
|
if (delta != 0) {
|
|
state.h_splitter.adjustPosition(state.current_area, delta);
|
|
}
|
|
},
|
|
.vertical => {
|
|
const parts = state.h_splitter.split(state.current_area);
|
|
const delta = @as(i32, mouse.row) - @as(i32, old_y);
|
|
if (delta != 0) {
|
|
state.v_splitter.adjustPosition(parts.second, delta);
|
|
}
|
|
},
|
|
.none => {},
|
|
}
|
|
}
|
|
},
|
|
.up => {
|
|
state.drag_state.end();
|
|
state.active_splitter = .none;
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
|
|
fn render(state: *State, area: Rect, buf: *Buffer) void {
|
|
// Store area for mouse hit testing
|
|
state.current_area = area;
|
|
|
|
// Get the split areas
|
|
const h_parts = state.h_splitter.split(area);
|
|
const v_parts = state.v_splitter.split(h_parts.second);
|
|
|
|
// Draw left panel
|
|
drawPanel(buf, h_parts.first, " Left Panel ", Color.blue, "This is the left panel.\n\nDrag the vertical bar to resize.\n\nOr press h/H to adjust.");
|
|
|
|
// Draw top-right panel
|
|
drawPanel(buf, v_parts.first, " Top Right ", Color.green, "This is the top-right panel.\n\nDrag the horizontal bar to resize.\n\nOr press v/V to adjust.");
|
|
|
|
// Draw bottom-right panel
|
|
drawPanel(buf, v_parts.second, " Bottom Right ", Color.yellow, "This is the bottom-right panel.\n\nTry dragging both splitters!");
|
|
|
|
// Draw splitter handles
|
|
drawSplitter(buf, h_parts.handle, true, state.active_splitter == .horizontal);
|
|
drawSplitter(buf, v_parts.handle, false, state.active_splitter == .vertical);
|
|
|
|
// Draw status bar
|
|
var status_buf: [128]u8 = undefined;
|
|
const status = std.fmt.bufPrint(&status_buf, "H-split: {d}% | V-split: {d}% | {s}", .{
|
|
state.h_splitter.position,
|
|
state.v_splitter.position,
|
|
if (state.drag_state.isDragging()) "Dragging..." else "Drag splitters or press h/H v/V | q to quit",
|
|
}) catch "...";
|
|
_ = buf.setString(0, area.height -| 1, status, (Style{}).fg(Color.white).bg(Color.rgb(40, 40, 40)));
|
|
}
|
|
|
|
fn drawPanel(buf: *Buffer, area: Rect, title: []const u8, color: Color, content: []const u8) void {
|
|
const block = Block.init()
|
|
.title(title)
|
|
.setBorders(Borders.all)
|
|
.borderStyle((Style{}).fg(color));
|
|
block.render(area, buf);
|
|
|
|
// Draw content
|
|
const inner = Rect.init(area.x + 1, area.y + 1, area.width -| 2, area.height -| 2);
|
|
var y: u16 = 0;
|
|
var lines = std.mem.splitScalar(u8, content, '\n');
|
|
while (lines.next()) |line| {
|
|
if (y >= inner.height) break;
|
|
const max_len = @min(line.len, inner.width);
|
|
_ = buf.setString(inner.x, inner.y + y, line[0..max_len], Style{});
|
|
y += 1;
|
|
}
|
|
}
|
|
|
|
fn drawSplitter(buf: *Buffer, handle: Rect, is_vertical: bool, is_active: bool) void {
|
|
const color = if (is_active) Color.cyan else Color.rgb(100, 100, 100);
|
|
const char: []const u8 = if (is_vertical) "|" else "-";
|
|
|
|
if (is_vertical) {
|
|
var y: u16 = 0;
|
|
while (y < handle.height) : (y += 1) {
|
|
_ = buf.setString(handle.x, handle.y + y, char, (Style{}).fg(color));
|
|
}
|
|
} else {
|
|
var x: u16 = 0;
|
|
while (x < handle.width) : (x += 1) {
|
|
_ = buf.setString(handle.x + x, handle.y, char, (Style{}).fg(color));
|
|
}
|
|
}
|
|
}
|