Skip to main content

20_menu_bar/
20_menu_bar.rs

1//! Example 20: Menu Bar with Keyboard Navigation
2//!
3//! Demonstrates the menu bar with full keyboard support:
4//! - Tab to focus the menu bar
5//! - Enter/Space to open a menu
6//! - Up/Down arrows to navigate items within a menu
7//! - Left/Right arrows to switch between menus
8//! - Enter to execute the selected item
9//! - Escape to close the menu
10//!
11//! Run with: `cargo run -p telex-tui --example 20_menu_bar`
12
13use crossterm::event::KeyCode;
14use telex::prelude::*;
15use telex::Color;
16
17telex::require_api!(0, 2);
18
19fn main() {
20    telex::run_with_theme(App, telex::theme::Theme::nord()).unwrap();
21}
22
23struct App;
24
25impl Component for App {
26    fn render(&self, cx: Scope) -> View {
27        let show_help = state!(cx, || false);
28
29        // F1 toggles help
30        cx.use_command(
31            KeyBinding::key(KeyCode::F(1)),
32            with!(show_help => move || show_help.update(|v| *v = !*v)),
33        );
34
35        // Menu state
36        let active_menu = state!(cx, || Option::<usize>::None);
37        let highlighted_menu = state!(cx, || 0usize);
38        let selected_item = state!(cx, || 0usize);
39
40        // App state
41        let message = state!(cx, || "Use Tab to focus menu bar, then Enter to open".to_string());
42        let counter = state!(cx, || 0i32);
43
44        // Command handler - executes menu commands
45        let handle_command = with!(message, counter, active_menu, selected_item => move |cmd_id: &'static str| {
46            let msg = match cmd_id {
47                "file.new" => "Created new file".to_string(),
48                "file.open" => "Opening file...".to_string(),
49                "file.save" => "File saved".to_string(),
50                "file.quit" => "Use Ctrl+Q to quit".to_string(),
51                "edit.undo" => "Undone".to_string(),
52                "edit.redo" => "Redone".to_string(),
53                "edit.cut" => "Cut to clipboard".to_string(),
54                "edit.copy" => "Copied to clipboard".to_string(),
55                "edit.paste" => "Pasted from clipboard".to_string(),
56                "counter.increment" => {
57                    counter.update(|n| *n += 1);
58                    format!("Counter: {}", counter.get())
59                }
60                "counter.decrement" => {
61                    counter.update(|n| *n -= 1);
62                    format!("Counter: {}", counter.get())
63                }
64                "counter.reset" => {
65                    counter.set(0);
66                    "Counter reset".to_string()
67                }
68                _ => format!("Unknown: {}", cmd_id),
69            };
70            message.set(msg);
71            // Close menu after executing command
72            active_menu.set(None);
73            selected_item.set(0);
74        });
75
76        // Menu change handler - opens/closes menus
77        let on_menu_change = with!(active_menu, highlighted_menu, selected_item => move |idx: usize| {
78            if active_menu.get() == Some(idx) {
79                // Clicking same menu toggles it closed
80                active_menu.set(None);
81            } else {
82                active_menu.set(Some(idx));
83                highlighted_menu.set(idx); // Keep highlight in sync
84                selected_item.set(0);
85            }
86        });
87
88        // Highlight change handler - arrow key navigation when no menu is open
89        let on_highlight_change = with!(highlighted_menu => move |idx: usize| {
90            highlighted_menu.set(idx);
91        });
92
93        // Item change handler - navigates within menu
94        let on_item_change = with!(selected_item => move |idx: usize| {
95            selected_item.set(idx);
96        });
97
98        // Build menus
99        let file_menu = Menu::new("File")
100            .command_with_shortcut("file.new", "New", "Ctrl+N")
101            .command_with_shortcut("file.open", "Open", "Ctrl+O")
102            .command_with_shortcut("file.save", "Save", "Ctrl+S")
103            .separator()
104            .command_with_shortcut("file.quit", "Quit", "Ctrl+Q");
105
106        let edit_menu = Menu::new("Edit")
107            .command_with_shortcut("edit.undo", "Undo", "Ctrl+Z")
108            .command_with_shortcut("edit.redo", "Redo", "Ctrl+Y")
109            .separator()
110            .command_with_shortcut("edit.cut", "Cut", "Ctrl+X")
111            .command_with_shortcut("edit.copy", "Copy", "Ctrl+C")
112            .command_with_shortcut("edit.paste", "Paste", "Ctrl+V");
113
114        let counter_menu = Menu::new("Counter")
115            .command("counter.increment", "Increment")
116            .command("counter.decrement", "Decrement")
117            .separator()
118            .command("counter.reset", "Reset to Zero");
119
120        View::vstack()
121            .child(
122                View::menu_bar()
123                    .menu(file_menu)
124                    .menu(edit_menu)
125                    .menu(counter_menu)
126                    .active_menu(active_menu.get())
127                    .highlighted_menu(highlighted_menu.get())
128                    .selected_item(selected_item.get())
129                    .on_select(handle_command)
130                    .on_menu_change(on_menu_change)
131                    .on_highlight_change(on_highlight_change)
132                    .on_item_change(on_item_change)
133                    .build(),
134            )
135            .child(
136                View::boxed()
137                    .flex(1)
138                    .border(true)
139                    .padding(2)
140                    .child(
141                        View::vstack()
142                            .spacing(1)
143                            .child(View::styled_text("Menu Bar Demo").bold().build())
144                            .child(
145                                View::styled_text(format!("Counter: {}", counter.get()))
146                                    .color(Color::Cyan)
147                                    .bold()
148                                    .build(),
149                            )
150                            .child(
151                                View::styled_text(format!("Status: {}", message.get()))
152                                    .dim()
153                                    .build(),
154                            )
155                            .child(View::spacer())
156                            .child(View::styled_text("Keyboard Navigation:").bold().build())
157                            .child(View::text("  Tab         Focus menu bar"))
158                            .child(View::text("  Enter       Open menu / Execute item"))
159                            .child(View::text("  Up/Down     Navigate menu items"))
160                            .child(View::text("  Left/Right  Switch between menus"))
161                            .child(View::text("  Escape      Close menu"))
162                            .child(View::text("  Ctrl+Q      Quit"))
163                            .child(View::text("  F1          Help"))
164                            .build(),
165                    )
166                    .build(),
167            )
168            .child(
169                View::modal()
170                    .visible(show_help.get())
171                    .title("Example 20: Menu Bar")
172                    .on_dismiss(with!(show_help => move || show_help.set(false)))
173                    .child(
174                        View::vstack()
175                            .child(View::styled_text("What you're seeing").bold().build())
176                            .child(View::text("• Dropdown menu bar with keyboard nav"))
177                            .child(View::text("• Menu items with shortcuts"))
178                            .child(View::text("• Separators in menus"))
179                            .child(View::gap(1))
180                            .child(View::styled_text("Key concepts").bold().build())
181                            .child(View::text("• View::menu_bar() creates menu system"))
182                            .child(View::text("• Menu::new().command() adds items"))
183                            .child(View::text("• .command_with_shortcut() shows key hints"))
184                            .child(View::text("• on_select receives command ID"))
185                            .child(View::gap(1))
186                            .child(View::styled_text("Try this").bold().build())
187                            .child(View::text("• Tab to menu, Enter to open"))
188                            .child(View::text("• Arrow keys navigate menus"))
189                            .child(View::text("• Try the Counter menu"))
190                            .child(View::gap(1))
191                            .child(View::styled_text("Next up").bold().build())
192                            .child(View::text("→ 21_toasts: toast notifications"))
193                            .child(View::gap(1))
194                            .child(View::styled_text("Press Escape to close").dim().build())
195                            .build(),
196                    )
197                    .build(),
198            )
199            .build()
200    }
201}