Skip to main content

rsnaker/graphics/menus/
main_menu.rs

1use crate::graphics::menus::edge_snake::EdgeSnake;
2use crate::graphics::menus::messages::{CONTROLS_TABLE, SNAKE_LOGO};
3use crate::graphics::menus::utils_layout::frame_vertically_centered_rect;
4use clap::ValueEnum;
5use ratatui::style::Stylize;
6use ratatui::style::{Color, Modifier, Style};
7use ratatui::text::{Line, Span, Text};
8use ratatui::widgets::{Block, BorderType, Paragraph};
9use ratatui::{DefaultTerminal, Frame};
10use std::cmp::PartialEq;
11use std::sync::{Mutex, OnceLock};
12use unicode_segmentation::UnicodeSegmentation;
13
14//A bit overkill, but for the example of keeping data state outside of struct,
15// initialized at the first call
16// and keeping track of the edge snake emoji position around call
17static EDGE_SNAKE: OnceLock<Mutex<EdgeSnake>> = OnceLock::new();
18
19/// Print the wanted welcome screen controls
20/// Show Fruit and Speed menus alongside
21/// Sadly `slow_blink` and `fast_blink` are not rendered anymore on modern terminal...
22/// # Panics
23/// Will panic if no suitable terminal for displaying ios provided
24pub fn display_main_menu(
25    terminal: &mut DefaultTerminal,
26    selected_switch_menu_for_display_bracket: &SwitchMenu,
27) {
28    //terminal.clear().expect("Clearing terminal fail ");
29    terminal
30        .draw(|frame| {
31            let area = frame.area();
32            big_snake_menu(frame, selected_switch_menu_for_display_bracket);
33            //set a border all around the terminal
34            frame.render_widget(Block::bordered().border_type(BorderType::Double), area);
35
36            // Render the edge snake emoji
37            // Emojis often take 2 cells, we use width 2 to avoid clipping in some terminals
38            let mut edge_snake = EDGE_SNAKE
39                .get_or_init(|| Mutex::new(EdgeSnake::new()))
40                .lock()
41                .unwrap();
42            edge_snake.update(&area);
43            edge_snake.render(frame, &area);
44        })
45        .expect("Unusable terminal render");
46}
47
48fn big_snake_menu(frame: &mut Frame, to_display_switch: &SwitchMenu) {
49    let mut lines = vec![];
50    // Add logo lines
51    for logo_line in SNAKE_LOGO.lines() {
52        lines.push(Line::from(logo_line));
53    }
54
55    // Add navigation buttons
56    lines.push(Line::from(get_button_span(to_display_switch)));
57
58    // Add the control table
59    for table_line in CONTROLS_TABLE.lines() {
60        lines.push(Line::from(table_line));
61    }
62
63    // Add a greeting message
64    lines.push(Line::from("Have a good 🐍 game ! 🎮".green()));
65    let nb_lines = lines.len();
66
67    frame.render_widget(
68        Paragraph::new(Text::from(lines)).centered(),
69        frame_vertically_centered_rect(frame.area(), nb_lines),
70    );
71}
72/// Represents a button in the menu interface
73pub struct Button {
74    pub name: &'static str,
75    pub selected: bool,
76}
77
78impl Button {
79    /// Creates a new button with the given name and hotkey
80    #[must_use]
81    pub const fn new(name: &'static str) -> Self {
82        Self {
83            name,
84            selected: false,
85        }
86    }
87
88    /// Sets the selected state of the button
89    pub fn selected(&mut self, selected: bool) {
90        self.selected = selected;
91    }
92
93    /// Converts the button to a vector of spans for rendering
94    #[must_use]
95    pub fn to_spans(&self) -> Vec<Span<'static>> {
96        if self.selected {
97            vec![
98                Span::styled(" [ ", Style::default().fg(Color::Red)).add_modifier(Modifier::BOLD),
99                Span::raw(self.name),
100                Span::styled(
101                    " ] ",
102                    Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
103                ),
104            ]
105        } else {
106            let (first, rest) = match self.name.graphemes(true).next() {
107                Some(grapheme) => {
108                    // A grapheme is a &str, so we can get its byte length with .len()
109                    let rest = &self.name[grapheme.len()..];
110                    (grapheme, rest) // Both are &str
111                }
112                None => ("", ""), // Handle empty string
113            };
114            vec![
115                Span::raw("["),
116                // Span::styled can take a &str directly
117                Span::styled(first, Style::default().fg(Color::Yellow)),
118                Span::raw(format!("{rest}]")),
119            ]
120        }
121    }
122}
123
124/// Option that can be selected from the main menu, using a lateral switcher
125#[derive(PartialEq, ValueEnum, Clone)]
126pub enum SwitchMenu {
127    Highs,
128    Fruits,
129    Speed,
130    Run,
131    Parameters,
132    Doc,
133    Main,
134}
135const BUTTONS: [(SwitchMenu, Button); 6] = [
136    (SwitchMenu::Highs, Button::new("Highs💯")),
137    (SwitchMenu::Fruits, Button::new("Fruit")),
138    (SwitchMenu::Speed, Button::new("Speed")),
139    (SwitchMenu::Run, Button::new("Run")),
140    (SwitchMenu::Parameters, Button::new("Edit⚙️")),
141    (SwitchMenu::Doc, Button::new("Docℹ️")),
142];
143/// Returns a vector of spans representing the button navigation menu
144fn get_button_span(selected: &SwitchMenu) -> Vec<Span<'static>> {
145    let mut vec_line_button = vec![Span::raw("↔")];
146    // Add each button to the menu, marking the selected one
147    for (menu, mut button) in BUTTONS {
148        button.selected(selected == &menu);
149        vec_line_button.extend(button.to_spans());
150    }
151    vec_line_button
152}