1use ratatui::{
2 layout::Rect,
3 style::{Color, Style},
4 text::{Line, Span},
5 widgets::{Clear, StatefulWidget, Widget},
6};
7use std::{borrow::Cow, marker::PhantomData};
8
9#[derive(Debug)]
12pub enum MenuEvent<T> {
13 Selected(T),
15}
16
17pub struct MenuState<T> {
19 root_item: MenuItem<T>,
21 events: Vec<MenuEvent<T>>,
23}
24
25impl<T: Clone> MenuState<T> {
26 pub fn new(items: Vec<MenuItem<T>>) -> Self {
41 let mut root_item = MenuItem::group("root", items);
42 root_item.is_highlight = true;
45
46 Self {
47 root_item,
48 events: Default::default(),
49 }
50 }
51
52 pub fn activate(&mut self) {
74 self.root_item.highlight_next();
75 }
76
77 pub fn up(&mut self) {
101 match self.active_depth() {
102 0 | 1 => {
103 }
105 2 => match self
106 .root_item
107 .highlight_child()
108 .and_then(|child| child.highlight_child_index())
109 {
110 Some(0) => {
112 self.pop();
113 }
114 _ => {
115 self.prev();
116 }
117 },
118 _ => {
119 self.prev();
120 }
121 }
122 }
123
124 pub fn down(&mut self) {
147 if self.active_depth() == 1 {
148 self.push();
149 } else {
150 self.next();
151 }
152 }
153
154 pub fn left(&mut self) {
177 if self.active_depth() == 0 {
178 } else if self.active_depth() == 1 {
180 self.prev();
181 } else if self.active_depth() == 2 {
182 self.pop();
183 self.prev();
184 } else {
185 self.pop();
186 }
187 }
188
189 pub fn right(&mut self) {
212 if self.active_depth() == 0 {
213 } else if self.active_depth() == 1 {
215 self.next();
216 } else if self.active_depth() == 2 {
217 if self.push().is_none() {
218 self.pop();
221 self.next();
222 }
223 } else {
224 self.push();
225 }
226 }
227
228 fn prev(&mut self) {
231 if let Some(item) = self.root_item.highlight_last_but_one() {
232 item.highlight_prev();
233 } else {
234 self.root_item.highlight_prev();
235 }
236 }
237
238 fn next(&mut self) {
241 if let Some(item) = self.root_item.highlight_last_but_one() {
242 item.highlight_next();
243 } else {
244 self.root_item.highlight_next();
245 }
246 }
247
248 fn active_depth(&self) -> usize {
252 let mut item = self.root_item.highlight_child();
253 let mut depth = 0;
254 while let Some(inner_item) = item {
255 depth += 1;
256 item = inner_item.highlight_child();
257 }
258 depth
259 }
260
261 fn dropdown_count(&self) -> u16 {
265 let mut node = &self.root_item;
266 let mut count = 0;
267 loop {
268 match node.highlight_child() {
269 None => {
270 return count;
271 }
272 Some(highlight_child) => {
273 if highlight_child.is_group() {
274 count += 1;
276 } else if node.children.iter().any(|c| c.is_group()) {
277 count += 1;
280 }
281
282 node = highlight_child;
283 }
284 }
285 }
286 }
287
288 pub fn select(&mut self) {
291 if let Some(item) = self.root_item.highlight_mut() {
292 if !item.children.is_empty() {
293 self.push();
294 } else if let Some(ref data) = item.data {
295 self.events.push(MenuEvent::Selected(data.clone()));
296 }
297 }
298 }
299
300 pub fn push(&mut self) -> Option<()> {
304 self.root_item.highlight_mut()?.highlight_first_child()
305 }
306
307 pub fn pop(&mut self) {
309 if let Some(item) = self.root_item.highlight_mut() {
310 item.clear_highlight();
311 }
312 }
313
314 pub fn reset(&mut self) {
317 self.root_item
318 .children
319 .iter_mut()
320 .for_each(|c| c.clear_highlight());
321 }
322
323 pub fn drain_events(&mut self) -> impl Iterator<Item = MenuEvent<T>> {
326 std::mem::take(&mut self.events).into_iter()
327 }
328
329 pub fn highlight(&self) -> Option<&MenuItem<T>> {
331 self.root_item.highlight()
332 }
333}
334
335pub struct MenuItem<T> {
338 name: Cow<'static, str>,
339 pub data: Option<T>,
340 children: Vec<MenuItem<T>>,
341 is_highlight: bool,
342}
343
344impl<T> MenuItem<T> {
345 pub fn item(name: impl Into<Cow<'static, str>>, data: T) -> Self {
347 Self {
348 name: name.into(),
349 data: Some(data),
350 is_highlight: false,
351 children: vec![],
352 }
353 }
354
355 pub fn group(name: impl Into<Cow<'static, str>>, children: Vec<Self>) -> Self {
370 Self {
371 name: name.into(),
372 data: None,
373 is_highlight: false,
374 children,
375 }
376 }
377
378 #[cfg(test)]
379 fn with_highlight(mut self, highlight: bool) -> Self {
380 self.is_highlight = highlight;
381 self
382 }
383
384 pub fn is_group(&self) -> bool {
386 !self.children.is_empty()
387 }
388
389 fn name(&self) -> &str {
391 &self.name
392 }
393
394 fn highlight_first_child(&mut self) -> Option<()> {
396 if !self.children.is_empty() {
397 if let Some(it) = self.children.get_mut(0) {
398 it.is_highlight = true;
399 }
400 Some(())
401 } else {
402 None
403 }
404 }
405
406 fn highlight_prev(&mut self) {
408 let Some(current_index) = self.highlight_child_index() else {
410 self.highlight_first_child();
411 return;
412 };
413
414 let index_to_highlight = if current_index > 0 {
415 current_index - 1
416 } else {
417 0
418 };
419
420 self.children[current_index].clear_highlight();
421 self.children[index_to_highlight].is_highlight = true;
422 }
423
424 fn highlight_next(&mut self) {
426 let Some(current_index) = self.highlight_child_index() else {
428 self.highlight_first_child();
429 return;
430 };
431
432 let index_to_highlight = (current_index + 1).min(self.children.len() - 1);
433 self.children[current_index].clear_highlight();
434 self.children[index_to_highlight].is_highlight = true;
435 }
436
437 fn highlight_child_index(&self) -> Option<usize> {
439 for (idx, child) in self.children.iter().enumerate() {
440 if child.is_highlight {
441 return Some(idx);
442 }
443 }
444
445 None
446 }
447
448 fn highlight_child(&self) -> Option<&Self> {
450 self.children.iter().filter(|i| i.is_highlight).nth(0)
451 }
452
453 fn highlight_child_mut(&mut self) -> Option<&mut Self> {
455 self.children.iter_mut().filter(|i| i.is_highlight).nth(0)
456 }
457
458 fn clear_highlight(&mut self) {
460 self.is_highlight = false;
461 for child in self.children.iter_mut() {
462 child.clear_highlight();
463 }
464 }
465
466 pub fn highlight(&self) -> Option<&Self> {
468 if !self.is_highlight {
469 return None;
470 }
471
472 let mut highlight_item = self;
473 while highlight_item.highlight_child().is_some() {
474 highlight_item = highlight_item.highlight_child().unwrap();
475 }
476
477 Some(highlight_item)
478 }
479
480 fn highlight_mut(&mut self) -> Option<&mut Self> {
482 if !self.is_highlight {
483 return None;
484 }
485
486 let mut highlight_item = self;
487 while highlight_item.highlight_child_mut().is_some() {
488 highlight_item = highlight_item.highlight_child_mut().unwrap();
489 }
490
491 Some(highlight_item)
492 }
493
494 fn highlight_last_but_one(&mut self) -> Option<&mut Self> {
496 if !self.is_highlight || self.highlight_child_mut().is_none() {
498 return None;
499 }
500
501 let mut last_but_one = self;
502 while last_but_one
503 .highlight_child_mut()
504 .and_then(|x| x.highlight_child_mut())
505 .is_some()
506 {
507 last_but_one = last_but_one.highlight_child_mut().unwrap();
508 }
509 Some(last_but_one)
510 }
511}
512
513pub struct Menu<T> {
515 default_item_style: Style,
517 highlight_item_style: Style,
519 drop_down_width: u16,
521 drop_down_style: Style,
523 _priv: PhantomData<T>,
524}
525
526impl<T> Menu<T> {
527 pub fn new() -> Self {
528 Self {
529 highlight_item_style: Style::default().fg(Color::White).bg(Color::LightBlue),
530 default_item_style: Style::default().fg(Color::White),
531 drop_down_width: 20,
532 drop_down_style: Style::default().bg(Color::DarkGray),
533 _priv: Default::default(),
534 }
535 }
536
537 pub fn default_style(mut self, style: Style) -> Self {
539 self.default_item_style = style;
540 self
541 }
542
543 pub fn highlight(mut self, style: Style) -> Self {
545 self.highlight_item_style = style;
546 self
547 }
548
549 pub fn dropdown_width(mut self, width: u16) -> Self {
551 self.drop_down_width = width;
552 self
553 }
554
555 pub fn dropdown_style(mut self, style: Style) -> Self {
557 self.drop_down_style = style;
558 self
559 }
560
561 fn render_dropdown(
563 &self,
564 x: u16,
565 y: u16,
566 group: &[MenuItem<T>],
567 buf: &mut ratatui::buffer::Buffer,
568 dropdown_count_to_go: u16, ) {
570 let drop_down_width = self.drop_down_width.min(buf.area.width);
572
573 let b_plus_c = dropdown_count_to_go * drop_down_width;
579 let x_max = buf.area().right().saturating_sub(b_plus_c);
580
581 let x = x.min(x_max);
582
583 let area = Rect::new(x, y, drop_down_width, group.len() as u16);
584
585 let area = area.clamp(*buf.area());
587
588 Clear.render(area, buf);
589
590 buf.set_style(area, self.drop_down_style);
591
592 let mut active_group: Option<_> = None;
593 for (idx, item) in group.iter().enumerate() {
594 let item_y = y + idx as u16;
595 let is_active = item.is_highlight;
596
597 let item_name = item.name();
598
599 let mut item_name = format!("{: <width$}", item_name, width = drop_down_width as usize);
601
602 if !item.children.is_empty() {
603 item_name.pop();
604 item_name.push('>');
605 }
606
607 buf.set_span(
608 x,
609 item_y,
610 &Span::styled(
611 item_name,
612 if is_active {
613 self.highlight_item_style
614 } else {
615 self.default_item_style
616 },
617 ),
618 drop_down_width,
619 );
620
621 if is_active && !item.children.is_empty() {
622 active_group = Some((x + drop_down_width, item_y, item));
623 }
624 }
625
626 if let Some((x, y, item)) = active_group {
628 self.render_dropdown(x, y, &item.children, buf, dropdown_count_to_go - 1);
629 }
630 }
631}
632
633impl<T> Default for Menu<T> {
634 fn default() -> Self {
635 Self::new()
636 }
637}
638
639impl<T: Clone> StatefulWidget for Menu<T> {
640 type State = MenuState<T>;
641
642 fn render(self, area: Rect, buf: &mut ratatui::buffer::Buffer, state: &mut Self::State) {
643 let area = area.clamp(*buf.area());
644
645 let mut spans = vec![];
646 let mut x_pos = area.x;
647 let y_pos = area.y;
648
649 let dropdown_count = state.dropdown_count();
650
651 for (idx, item) in state.root_item.children.iter().enumerate() {
652 let is_highlight = item.is_highlight;
653 let item_style = if is_highlight {
654 self.highlight_item_style
655 } else {
656 self.default_item_style
657 };
658 let has_children = !item.children.is_empty();
659
660 let group_x_pos = x_pos;
661 let span = Span::styled(item.name(), item_style);
662 x_pos += span.width() as u16;
663 spans.push(span);
664
665 if has_children && is_highlight {
666 self.render_dropdown(group_x_pos, y_pos + 1, &item.children, buf, dropdown_count);
667 }
668
669 if idx < state.root_item.children.len() - 1 {
670 let span = Span::raw(" | ");
671 x_pos += span.width() as u16;
672 spans.push(span);
673 }
674 }
675 buf.set_line(area.x, area.y, &Line::from(spans), x_pos);
676 }
677}
678
679#[cfg(test)]
680mod tests {
681 use crate::MenuState;
682
683 type MenuItem = super::MenuItem<i32>;
684
685 #[test]
686 fn test_active_depth() {
687 {
688 let menu_state = MenuState::new(vec![MenuItem::item("item1", 0)]);
689 assert_eq!(menu_state.active_depth(), 0);
690 }
691
692 {
693 let menu_state = MenuState::new(vec![MenuItem::item("item1", 0).with_highlight(true)]);
694 assert_eq!(menu_state.active_depth(), 1);
695 }
696
697 {
698 let menu_state = MenuState::new(vec![MenuItem::group("layer1", vec![])]);
699 assert_eq!(menu_state.active_depth(), 0);
700 }
701
702 {
703 let menu_state =
704 MenuState::new(vec![MenuItem::group("layer1", vec![]).with_highlight(true)]);
705 assert_eq!(menu_state.active_depth(), 1);
706 }
707
708 {
709 let menu_state = MenuState::new(vec![MenuItem::group(
710 "layer_1",
711 vec![MenuItem::item("item_layer_2", 0)],
712 )
713 .with_highlight(true)]);
714 assert_eq!(menu_state.active_depth(), 1);
715 }
716
717 {
718 let menu_state = MenuState::new(vec![MenuItem::group(
719 "layer_1",
720 vec![MenuItem::item("item_layer_2", 0).with_highlight(true)],
721 )
722 .with_highlight(true)]);
723 assert_eq!(menu_state.active_depth(), 2);
724 }
725 }
726
727 #[test]
728 fn test_dropdown_count() {
729 {
730 let menu_state = MenuState::new(vec![MenuItem::item("item1", 0)]);
732 assert_eq!(menu_state.dropdown_count(), 0);
733 }
734
735 {
736 let menu_state = MenuState::new(vec![MenuItem::group(
738 "menu bar",
739 vec![MenuItem::item("item layer 1", 0)],
740 )
741 .with_highlight(true)]);
742 assert_eq!(menu_state.dropdown_count(), 1);
743 }
744
745 {
746 let menu_state = MenuState::new(vec![MenuItem::group(
748 "menu bar 1",
749 vec![
750 MenuItem::group("dropdown 1", vec![MenuItem::item("item layer 2", 0)])
751 .with_highlight(true),
752 MenuItem::item("item layer 1", 0),
753 ],
754 )
755 .with_highlight(true)]);
756 assert_eq!(menu_state.dropdown_count(), 2);
757 }
758
759 {
760 let menu_state = MenuState::new(vec![MenuItem::group(
764 "menu bar 1",
765 vec![
766 MenuItem::group(
767 "dropdown 1",
768 vec![
769 MenuItem::item("item layer 2", 0),
770 MenuItem::group(
771 "group layer 2",
772 vec![MenuItem::item("item layer 3", 0)],
773 ),
774 ],
775 )
776 .with_highlight(true),
777 MenuItem::item("item layer 1", 0),
778 ],
779 )
780 .with_highlight(true)]);
781 assert_eq!(menu_state.dropdown_count(), 2);
782 }
783
784 {
785 let menu_state = MenuState::new(vec![MenuItem::group(
789 "menu bar 1",
790 vec![
791 MenuItem::group(
792 "dropdown 1",
793 vec![
794 MenuItem::item("item layer 2", 0).with_highlight(true),
795 MenuItem::group(
796 "group layer 2",
797 vec![MenuItem::item("item layer 3", 0)],
798 ),
799 ],
800 )
801 .with_highlight(true),
802 MenuItem::item("item layer 1", 0),
803 ],
804 )
805 .with_highlight(true)]);
806 assert_eq!(menu_state.dropdown_count(), 3);
807 }
808 }
809}