Skip to content
46 changes: 45 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ A delightful TUI framework for Zig, inspired by [Bubble Tea](https://github.com/

- **Elm Architecture** - Model-Update-View pattern for predictable state management
- **Rich Styling** - Comprehensive styling system with colors, borders, padding, margin backgrounds, per-side border colors, tab width control, style ranges, full style inheritance, text transforms, whitespace formatting controls, and unset methods
- **16 Pre-built Components** - TextInput (with autocomplete/word movement), TextArea, List (fuzzy filtering), Table (interactive with row selection), Viewport, Progress (color gradients), Spinner, Tree, StyledList, Sparkline, Notification/Toast, Confirm dialog, Help, Paginator, Timer, FilePicker
- **18 Pre-built Components** - TextInput (with autocomplete/word movement), TextArea, List (fuzzy filtering), Table (interactive with row selection), Viewport, Progress (color gradients), Spinner, Tree, StyledList, Sparkline, Notification/Toast, Confirm dialog, Modal/Popup, Tooltip, Help, Paginator, Timer, FilePicker
- **Focus Management** - `FocusGroup` with Tab/Shift+Tab cycling, comptime focusable protocol, `FocusStyle` for visual focus ring indicators
- **Keybinding Management** - Structured `KeyBinding`/`KeyMap` with matching, display formatting, and Help component integration
- **Color System** - ANSI 16, 256, and TrueColor with adaptive colors, color profile detection, and dark background detection
Expand Down Expand Up @@ -419,6 +419,50 @@ if (confirm.result()) |yes| {
}
```

### Modal

Dialog overlay with buttons, backdrop, and focus support:

```zig
var modal = zz.Modal.info("Notice", "Operation completed successfully.");
modal.show();

// In update:
modal.handleKey(key_event);
if (modal.getResult()) |res| {
switch (res) {
.button_pressed => |idx| { /* button at idx was pressed */ },
.dismissed => { /* user pressed Escape */ },
}
}

// In view:
if (modal.isVisible()) {
return modal.viewWithBackdrop(allocator, ctx.width, ctx.height);
}
```

Presets: `Modal.info()`, `Modal.confirm()`, `Modal.warning()`, `Modal.err()`, or `Modal.init()` for full custom.

### Tooltip

Contextual hint positioned near a target element with cell-based overlay compositing:

```zig
var tip = zz.Tooltip.init("Save the current document");
tip.target_x = 10;
tip.target_y = 5;
tip.placement = .bottom; // .top, .bottom, .left, .right
tip.show();

// In view — overlays onto existing content:
if (tip.isVisible()) {
return tip.overlay(allocator, base_view, ctx.width, ctx.height);
}
```

Presets: `Tooltip.init(text)`, `Tooltip.titled(title, text)`, `Tooltip.help(text)`, `Tooltip.shortcut(label, key)`. Supports `border_bg`, `arrow_bg`, `content_bg`, and `inherit_bg` for full background control.

### More Components

- **Help** - Display key bindings with responsive truncation
Expand Down
4 changes: 4 additions & 0 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ pub fn build(b: *std.Build) void {
"dashboard",
"showcase",
"focus_form",
"modal",
"tooltip",
};

for (examples) |example_name| {
Expand Down Expand Up @@ -58,6 +60,8 @@ pub fn build(b: *std.Build) void {
"tests/unicode_tests.zig",
"tests/program_tests.zig",
"tests/focus_tests.zig",
"tests/modal_tests.zig",
"tests/tooltip_tests.zig",
};

const test_step = b.step("test", "Run unit tests");
Expand Down
153 changes: 153 additions & 0 deletions examples/modal.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
//! ZigZag Modal Example
//! Demonstrates the Modal component with different dialog types.
//!
//! Keys:
//! 1 — Show info modal
//! 2 — Show confirm modal
//! 3 — Show warning modal
//! 4 — Show error modal
//! 5 — Show custom modal
//! q — Quit

const std = @import("std");
const zz = @import("zigzag");

const Model = struct {
modal: zz.Modal,
last_result: []const u8,
status: []const u8,

pub const Msg = union(enum) {
key: zz.KeyEvent,
};

pub fn init(self: *Model, _: *zz.Context) zz.Cmd(Msg) {
self.modal = zz.Modal.init();
self.last_result = "None";
self.status = "Press 1-5 to open a modal";
return .none;
}

pub fn update(self: *Model, m: Msg, ctx: *zz.Context) zz.Cmd(Msg) {
switch (m) {
.key => |k| {
// If modal is visible, let it handle keys
if (self.modal.isVisible()) {
self.modal.handleKey(k);

// Check for result
if (self.modal.getResult()) |res| {
self.last_result = switch (res) {
.button_pressed => |idx| std.fmt.allocPrint(
ctx.persistent_allocator,
"Button {d} pressed",
.{idx},
) catch "Button pressed",
.dismissed => "Dismissed (Escape)",
};
self.status = "Press 1-5 to open another modal";
}
return .none;
}

switch (k.key) {
.char => |c| switch (c) {
'q' => return .quit,
'1' => {
self.modal = zz.Modal.info("Information", "This is an informational message.\nEverything is working correctly.");
self.modal.backdrop = .{};
self.modal.show();
self.status = "Info modal open";
},
'2' => {
self.modal = zz.Modal.confirm("Confirm Action", "Are you sure you want to proceed?\nThis action cannot be undone.");
self.modal.backdrop = .{};
self.modal.show();
self.status = "Confirm modal open";
},
'3' => {
self.modal = zz.Modal.warning("Warning", "Low disk space remaining.\nConsider freeing up some space.");
self.modal.backdrop = .{};
self.modal.show();
self.status = "Warning modal open";
},
'4' => {
self.modal = zz.Modal.err("Error", "Failed to save file.\nPermission denied.");
self.modal.backdrop = .{};
self.modal.show();
self.status = "Error modal open";
},
'5' => {
self.modal = zz.Modal.init();
self.modal.title = "Custom Dialog";
self.modal.body = "This is a fully customized modal.\nWith multiple lines of content.\nAnd custom buttons below.";
self.modal.footer = "Use Tab/arrows to navigate, Enter to select";
self.modal.width = .{ .fixed = 50 };
self.modal.border_chars = zz.Border.double;
self.modal.border_fg = zz.Color.magenta();
self.modal.title_style = blk: {
var s = zz.Style{};
s = s.bold(true).fg(zz.Color.magenta()).inline_style(true);
break :blk s;
};
self.modal.content_bg = zz.Color.gray(2);
self.modal.backdrop = .{};
self.modal.addButton("Save", .{ .char = 's' });
self.modal.addButton("Discard", .{ .char = 'd' });
self.modal.addButton("Cancel", null);
self.modal.show();
self.status = "Custom modal open";
},
else => {},
},
.escape => return .quit,
else => {},
}
},
}
return .none;
}

pub fn view(self: *const Model, ctx: *const zz.Context) []const u8 {
const alloc = ctx.allocator;

// If modal is visible, render it with backdrop
if (self.modal.isVisible()) {
return self.modal.viewWithBackdrop(alloc, ctx.width, ctx.height) catch "Error";
}

// Main view
var title_s = zz.Style{};
title_s = title_s.bold(true).fg(zz.Color.hex("#FF6B6B")).inline_style(true);

var hint_s = zz.Style{};
hint_s = hint_s.fg(zz.Color.gray(14)).inline_style(true);

var result_s = zz.Style{};
result_s = result_s.fg(zz.Color.cyan()).inline_style(true);

var status_s = zz.Style{};
status_s = status_s.fg(zz.Color.gray(12)).inline_style(true);

const title = title_s.render(alloc, "Modal Component Demo") catch "Modal Component Demo";
const hint = hint_s.render(alloc, "1: Info 2: Confirm 3: Warning 4: Error 5: Custom q: Quit") catch "";
const result_label = result_s.render(alloc, std.fmt.allocPrint(alloc, "Last result: {s}", .{self.last_result}) catch "") catch "";
const status = status_s.render(alloc, self.status) catch "";

const content = std.fmt.allocPrint(alloc, "{s}\n\n{s}\n\n{s}\n{s}", .{
title, hint, result_label, status,
}) catch "Error";

return zz.place.place(alloc, ctx.width, ctx.height, .center, .middle, content) catch content;
}
};

pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();

var prog = try zz.Program(Model).init(gpa.allocator());
defer prog.deinit();

try prog.run();
}
Loading