1use ratatui::layout::Rect;
2use ratatui::style::{Color, Modifier, Style};
3use ratatui::widgets::{Block, BorderType, Borders, Paragraph};
4use ratatui::Frame;
5
6fn display_width(s: &str) -> usize {
9 s.chars()
10 .map(|c| {
11 let code = c as u32;
12 if (0xF000..=0xF8FF).contains(&code) {
13 2
15 } else {
16 unicode_width::UnicodeWidthChar::width(c).unwrap_or(1)
18 }
19 })
20 .sum()
21}
22
23#[derive(Debug, Clone)]
25pub struct MenuItem {
26 pub name: String,
28 pub icon: Option<String>,
30 pub value: usize,
32 pub selected: bool,
34 pub hovered: bool,
36 pub area: Option<Rect>,
38}
39
40impl MenuItem {
41 pub fn new(name: impl Into<String>, value: usize) -> Self {
43 Self {
44 name: name.into(),
45 icon: None,
46 value,
47 selected: false,
48 hovered: false,
49 area: None,
50 }
51 }
52
53 pub fn with_icon(name: impl Into<String>, icon: impl Into<String>, value: usize) -> Self {
55 Self {
56 name: name.into(),
57 icon: Some(icon.into()),
58 value,
59 selected: false,
60 hovered: false,
61 area: None,
62 }
63 }
64
65 pub fn display_label(&self) -> String {
67 if let Some(ref icon) = self.icon {
68 format!("{} {}", icon, self.name)
69 } else {
70 self.name.clone()
71 }
72 }
73}
74
75#[derive(Debug, Clone)]
77pub struct MenuBar {
78 pub items: Vec<MenuItem>,
79 pub area: Option<Rect>,
80
81 pub normal_style: Style,
83 pub selected_style: Style,
84 pub hover_style: Style,
85 pub selected_hover_style: Style,
86}
87
88impl MenuBar {
89 pub fn new(items: Vec<MenuItem>) -> Self {
91 Self {
92 items,
93 area: None,
94 normal_style: Style::default().fg(Color::White),
95 selected_style: Style::default()
96 .fg(Color::Cyan)
97 .add_modifier(Modifier::BOLD),
98 hover_style: Style::default().fg(Color::Cyan),
99 selected_hover_style: Style::default()
100 .fg(Color::Cyan)
101 .add_modifier(Modifier::BOLD),
102 }
103 }
104
105 pub fn with_selected(mut self, index: usize) -> Self {
107 if index < self.items.len() {
108 self.items[index].selected = true;
109 }
110 self
111 }
112
113 pub fn normal_style(mut self, style: Style) -> Self {
115 self.normal_style = style;
116 self
117 }
118
119 pub fn selected_style(mut self, style: Style) -> Self {
121 self.selected_style = style;
122 self
123 }
124
125 pub fn hover_style(mut self, style: Style) -> Self {
127 self.hover_style = style;
128 self
129 }
130
131 pub fn selected_hover_style(mut self, style: Style) -> Self {
133 self.selected_hover_style = style;
134 self
135 }
136
137 pub fn update_hover(&mut self, column: u16, row: u16) {
139 for item in &mut self.items {
140 item.hovered = if let Some(area) = item.area {
141 column >= area.x
142 && column < area.x + area.width
143 && row >= area.y
144 && row < area.y + area.height
145 } else {
146 false
147 };
148 }
149 }
150
151 pub fn handle_click(&mut self, column: u16, row: u16) -> Option<usize> {
153 let clicked_index = self.items.iter().enumerate().find_map(|(i, item)| {
155 if let Some(area) = item.area {
156 if column >= area.x
157 && column < area.x + area.width
158 && row >= area.y
159 && row < area.y + area.height
160 {
161 return Some(i);
162 }
163 }
164 None
165 });
166
167 if let Some(clicked) = clicked_index {
169 for (i, item) in self.items.iter_mut().enumerate() {
171 item.selected = i == clicked;
172 }
173 }
174
175 clicked_index
176 }
177
178 pub fn selected(&self) -> Option<usize> {
180 self.items.iter().position(|item| item.selected)
181 }
182
183 pub fn render(&mut self, frame: &mut Frame, area: Rect) {
185 self.render_with_offset(frame, area, 0);
186 }
187
188 pub fn render_with_offset(&mut self, frame: &mut Frame, area: Rect, left_offset: u16) {
190 if self.items.is_empty() {
191 return;
192 }
193
194 let total_label_width: usize = self
197 .items
198 .iter()
199 .map(|item| display_width(&item.display_label()))
200 .sum();
201 let separators = (self.items.len() - 1) * 3; let needed_width = (total_label_width + separators + 4) as u16; let available_width = area.width.saturating_sub(left_offset);
206
207 let button_group_area = Rect {
209 x: area.x + left_offset,
210 y: area.y,
211 width: needed_width.min(available_width),
212 height: area.height,
213 };
214
215 self.area = Some(button_group_area);
216
217 let block = Block::default()
219 .borders(Borders::ALL)
220 .border_type(BorderType::Rounded);
221
222 let inner_area = block.inner(button_group_area);
223 frame.render_widget(block, button_group_area);
224
225 let mut x_offset = inner_area.x + 1;
227 let button_count = self.items.len();
228
229 for (i, item) in self.items.iter_mut().enumerate() {
230 let label = item.display_label();
232 let item_width = display_width(&label) as u16;
233
234 let available_width = (inner_area.x + inner_area.width).saturating_sub(x_offset);
236 if available_width == 0 {
237 break; }
239
240 let actual_item_width = item_width.min(available_width);
242
243 let item_area = Rect {
244 x: x_offset,
245 y: inner_area.y,
246 width: actual_item_width,
247 height: inner_area.height,
248 };
249
250 item.area = Some(item_area);
251
252 let style = match (item.selected, item.hovered) {
254 (true, true) => self.selected_hover_style,
255 (true, false) => self.selected_style,
256 (false, true) => self.hover_style,
257 (false, false) => self.normal_style,
258 };
259
260 let display_label = if actual_item_width < item_width {
263 label
265 .chars()
266 .take(actual_item_width as usize)
267 .collect::<String>()
268 } else {
269 label
270 };
271 let paragraph = Paragraph::new(display_label).style(style);
272 frame.render_widget(paragraph, item_area);
273
274 x_offset += actual_item_width;
275
276 if i < button_count - 1 && x_offset + 3 <= inner_area.x + inner_area.width {
279 let separator_area = Rect {
280 x: x_offset,
281 y: inner_area.y,
282 width: 3, height: inner_area.height,
284 };
285 let separator = Paragraph::new(" │ ");
286 frame.render_widget(separator, separator_area);
287 x_offset += 3;
288 }
289 }
290 }
291
292 pub fn render_centered(&mut self, frame: &mut Frame, area: Rect) {
294 use ratatui::layout::{Constraint, Direction, Layout};
295
296 let total_chars: usize = self
298 .items
299 .iter()
300 .map(|item| display_width(&item.display_label()) + 4)
301 .sum(); let needed_width = total_chars as u16;
303
304 let chunks = Layout::default()
306 .direction(Direction::Horizontal)
307 .constraints([
308 Constraint::Length((area.width.saturating_sub(needed_width)) / 2),
309 Constraint::Length(needed_width.min(area.width)),
310 Constraint::Min(0),
311 ])
312 .split(area);
313
314 self.render(frame, chunks[1]);
315 }
316}
317
318impl Default for MenuBar {
319 fn default() -> Self {
320 Self::new(vec![MenuItem::new("Menu Item", 0)])
321 }
322}