//! Menu and Popup demo for zcatui. //! //! Demonstrates: //! - MenuBar with dropdown menus //! - Menu navigation (arrow keys, Enter) //! - Modal dialogs (confirm, alert) //! - Popup overlays //! //! Run with: zig build menu-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 Paragraph = zcatui.widgets.Paragraph; const Menu = zcatui.widgets.Menu; const MenuItem = zcatui.widgets.MenuItem; const MenuBar = zcatui.widgets.MenuBar; const MenuBarItem = zcatui.widgets.MenuBarItem; const Modal = zcatui.widgets.Modal; const Popup = zcatui.widgets.Popup; const confirmDialog = zcatui.widgets.confirmDialog; const alertDialog = zcatui.widgets.alertDialog; const AppMode = enum { normal, menu_open, modal_open, popup_open, }; const AppState = struct { running: bool = true, mode: AppMode = .normal, // Menu bar menu_bar: MenuBar, // Dropdown menus file_menu: Menu, edit_menu: Menu, view_menu: Menu, help_menu: Menu, // Modal dialog modal: Modal, // Status message status: []const u8 = "Press Alt+F to open File menu, F1 for help", fn init() AppState { // File menu const file_menu = Menu.init().setItems(&.{ MenuItem.action("New", 'n').setShortcut("Ctrl+", 'N'), MenuItem.action("Open...", 'o').setShortcut("Ctrl+", 'O'), MenuItem.action("Save", 's').setShortcut("Ctrl+", 'S'), MenuItem.action("Save As...", null), MenuItem.separator(), MenuItem.action("Exit", 'q').setShortcut("Ctrl+", 'Q'), }); // Edit menu const edit_menu = Menu.init().setItems(&.{ MenuItem.action("Undo", 'u').setShortcut("Ctrl+", 'Z'), MenuItem.action("Redo", 'r').setShortcut("Ctrl+", 'Y'), MenuItem.separator(), MenuItem.action("Cut", 'x').setShortcut("Ctrl+", 'X'), MenuItem.action("Copy", 'c').setShortcut("Ctrl+", 'C'), MenuItem.action("Paste", 'v').setShortcut("Ctrl+", 'V'), MenuItem.separator(), MenuItem.action("Select All", 'a').setShortcut("Ctrl+", 'A'), }); // View menu const view_menu = Menu.init().setItems(&.{ MenuItem.toggle("Show Toolbar", true), MenuItem.toggle("Show Statusbar", true), MenuItem.separator(), MenuItem.toggle("Word Wrap", false), MenuItem.toggle("Line Numbers", true), MenuItem.separator(), MenuItem.action("Zoom In", '+').setShortcut("Ctrl+", '+'), MenuItem.action("Zoom Out", '-').setShortcut("Ctrl+", '-'), }); // Help menu const help_menu = Menu.init().setItems(&.{ MenuItem.action("Documentation", null).setShortcut("", 'F').setShortcut("", '1'), MenuItem.action("Keyboard Shortcuts", null), MenuItem.separator(), MenuItem.action("About", null), }); // Menu bar const menu_bar = MenuBar.init().setItems(&.{ MenuBarItem.init("File", file_menu), MenuBarItem.init("Edit", edit_menu), MenuBarItem.init("View", view_menu), MenuBarItem.init("Help", help_menu), }); return .{ .menu_bar = menu_bar, .file_menu = file_menu, .edit_menu = edit_menu, .view_menu = view_menu, .help_menu = help_menu, .modal = Modal.init(), }; } fn getCurrentMenu(self: *AppState) *Menu { return switch (self.menu_bar.selected) { 0 => &self.file_menu, 1 => &self.edit_menu, 2 => &self.view_menu, 3 => &self.help_menu, else => &self.file_menu, }; } fn showAlert(self: *AppState, title: []const u8, message: []const u8) void { self.modal = alertDialog(title, &.{message}); self.mode = .modal_open; } fn showConfirm(self: *AppState, title: []const u8, message: []const u8) void { self.modal = confirmDialog(title, &.{message}); self.mode = .modal_open; } }; 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) { 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 (state.mode) { .normal => handleNormalMode(state, key.code), .menu_open => handleMenuMode(state, key.code), .modal_open => handleModalMode(state, key.code), .popup_open => handlePopupMode(state, key.code), } }, else => {}, } } fn handleNormalMode(state: *AppState, code: KeyCode) void { switch (code) { .esc => state.running = false, .char => |c| { if (c == '?') { state.showAlert("Help", "This is the menu demo. Use letter keys to open menus."); return; } switch (c) { 'q', 'Q' => state.running = false, 'f', 'F' => { // Alt+F to open File menu state.menu_bar.selected = 0; state.menu_bar.openSelected(); state.mode = .menu_open; state.status = "File menu open - use arrows to navigate"; }, 'e', 'E' => { state.menu_bar.selected = 1; state.menu_bar.openSelected(); state.mode = .menu_open; state.status = "Edit menu open"; }, 'v', 'V' => { state.menu_bar.selected = 2; state.menu_bar.openSelected(); state.mode = .menu_open; state.status = "View menu open"; }, 'h', 'H' => { state.menu_bar.selected = 3; state.menu_bar.openSelected(); state.mode = .menu_open; state.status = "Help menu open"; }, 'm', 'M' => { state.showConfirm("Confirm", "Do you want to continue?"); }, 'p', 'P' => { state.mode = .popup_open; state.status = "Popup open - press Esc to close"; }, else => {}, } }, else => {}, } } fn handleMenuMode(state: *AppState, code: KeyCode) void { switch (code) { .esc => { state.menu_bar.closeMenus(); state.mode = .normal; state.status = "Menu closed"; }, .left => { state.menu_bar.selectPrev(); }, .right => { state.menu_bar.selectNext(); }, .up => { var menu = state.getCurrentMenu(); menu.selectPrev(); }, .down => { var menu = state.getCurrentMenu(); menu.selectNext(); }, .enter => { const menu = state.getCurrentMenu(); if (menu.getSelectedItem()) |item| { state.menu_bar.closeMenus(); state.mode = .normal; state.status = item.label; // Handle special items if (std.mem.eql(u8, item.label, "Exit")) { state.showConfirm("Exit", "Are you sure you want to exit?"); } else if (std.mem.eql(u8, item.label, "About")) { state.showAlert("About", "zcatui Menu Demo v1.0"); } } }, else => {}, } } fn handleModalMode(state: *AppState, code: KeyCode) void { switch (code) { .esc => { state.mode = .normal; state.status = "Modal cancelled"; }, .left, .tab => { state.modal.focusPrev(); }, .right => { state.modal.focusNext(); }, .enter => { const btn_idx = state.modal.getFocusedButton(); state.mode = .normal; if (state.modal.buttons.len > 0) { const label = state.modal.buttons[btn_idx].label; if (std.mem.eql(u8, label, "OK")) { if (std.mem.eql(u8, state.modal.title, "Exit")) { state.running = false; } else { state.status = "OK pressed"; } } else if (std.mem.eql(u8, label, "Cancel")) { state.status = "Cancelled"; } } }, else => {}, } } fn handlePopupMode(state: *AppState, code: KeyCode) void { switch (code) { .esc, .enter => { state.mode = .normal; state.status = "Popup closed"; }, else => {}, } } fn render(state: *AppState, area: Rect, buf: *Buffer) void { // Main layout const chunks = Layout.vertical(&.{ Constraint.length(1), // Menu bar Constraint.min(0), // Content Constraint.length(1), // Status bar }).split(area); // Render menu bar state.menu_bar.render(chunks.get(0), buf); // Render main content renderContent(state, chunks.get(1), buf); // Render status bar renderStatusBar(state, chunks.get(2), buf); // Render dropdown if menu is open if (state.mode == .menu_open) { if (state.menu_bar.open_menu) |menu_idx| { const dropdown_area = state.menu_bar.getDropdownArea(chunks.get(0), menu_idx); const menu = state.getCurrentMenu(); menu.render(dropdown_area, buf); } } // Render modal if open if (state.mode == .modal_open) { state.modal.render(area, buf); } // Render popup if open if (state.mode == .popup_open) { renderPopup(area, buf); } } fn renderContent(state: *AppState, area: Rect, buf: *Buffer) void { _ = state; const block = Block.init() .title(" Menu Demo ") .setBorders(Borders.all) .style(Style.default.fg(Color.cyan)); block.render(area, buf); const inner = block.inner(area); var y = inner.top(); const lines = [_][]const u8{ "", " Keyboard shortcuts:", "", " f - Open File menu", " e - Open Edit menu", " v - Open View menu", " h - Open Help menu", "", " m - Show modal dialog", " p - Show popup", "", " Arrow keys - Navigate menus", " Enter - Select menu item", " Esc - Close menu/dialog", "", " q - Quit", }; for (lines) |line| { if (y < inner.bottom()) { _ = buf.setString(inner.left(), y, line, Style.default); y += 1; } } } fn renderStatusBar(state: *AppState, area: Rect, buf: *Buffer) void { // Fill background var x = area.left(); while (x < area.right()) : (x += 1) { if (buf.getCell(x, area.top())) |cell| { cell.setChar(' '); cell.setStyle(Style.default.bg(Color.blue).fg(Color.white)); } } // Status text _ = buf.setString(area.left() + 1, area.top(), state.status, Style.default.bg(Color.blue).fg(Color.white)); } fn renderPopup(area: Rect, buf: *Buffer) void { const popup = Popup.init() .setSize(40, 8) .setBlock( Block.init() .title(" Information ") .setBorders(Borders.all) .style(Style.default.fg(Color.yellow)), ) .center(); popup.render(area, buf); // Get content area const popup_area = popup.getPopupArea(area); const block = Block.init().setBorders(Borders.all); const inner = block.inner(popup_area); // Render content _ = buf.setString(inner.left() + 1, inner.top() + 1, "This is a simple popup!", Style.default); _ = buf.setString(inner.left() + 1, inner.top() + 3, "Press Enter or Esc to close.", Style.default.dim()); }