fll_rs/graphics/
menu.rs

1use crate::graphics::display;
2use crate::graphics::display::{Display, Renderer};
3use crate::input::Input;
4
5/// Simple ui to select an item from a list
6pub struct Menu<'a> {
7    top: usize,
8    selected: usize,
9    rows: usize,
10
11    items: Vec<MenuItem<'a>>,
12}
13
14/// Repersents an item displayed in a `Menu`
15struct MenuItem<'a> {
16    name: String,
17    callback: &'a dyn Fn(),
18}
19
20impl<'a> Menu<'a> {
21    /// Creats a menu that displays `rows` rows at a time
22    pub fn new(rows: usize) -> Self {
23        Self {
24            top: 0,
25            selected: 0,
26            rows,
27
28            items: vec![],
29        }
30    }
31
32    /// Adds a new `MenuItem` to the bottom of the `Menu`
33    pub fn push<F: Fn() + 'a>(&mut self, name: &str, code: &'a F) {
34        self.items.push(MenuItem {
35            name: name.to_owned(),
36            callback: code,
37        });
38    }
39
40    /// Makes sure the cursor is in a valid location
41    fn update_bounds(&mut self) {
42        // Wraps cursor to be a valid entry in the list
43        self.selected %= self.items.len();
44
45        // Updates what the top entry displayed is to follow the cursor
46        let offset = self.selected as isize - self.top as isize;
47        if offset < 0 {
48            self.top = self.selected;
49        } else if offset >= self.rows as isize {
50            self.top = self.selected - self.rows + 1;
51        }
52    }
53}
54
55impl Menu<'_> {
56    /// Renders the `Menu` to the provided `renderer`
57    pub fn render<D: Display>(&self, renderer: &mut Renderer<D>) {
58        let start = self.top;
59        let end = usize::min(start + self.rows, self.items.len());
60
61        let row_height = renderer.height() / self.rows as u32;
62
63        renderer.clear();
64
65        for idx in start..end {
66            let name = &self.items[idx].name;
67
68            if idx == self.selected {
69                renderer.draw_rectangle_solid(
70                    0,
71                    row_height as i32 * (idx as i32 - start as i32),
72                    renderer.width(),
73                    row_height,
74                    display::BLACK,
75                );
76                renderer.draw_text(
77                    name,
78                    10,
79                    row_height as i32 * (idx as i32 - start as i32),
80                    row_height as f32,
81                    display::WHITE,
82                );
83            } else {
84                renderer.draw_text(
85                    name,
86                    10,
87                    row_height as i32 * (idx as i32 - start as i32),
88                    row_height as f32,
89                    display::BLACK,
90                );
91            }
92        }
93
94        renderer.update();
95    }
96
97    /// Handles user input
98    ///
99    /// Return true if the menu should remain open
100    pub fn notify_input(&mut self, input: &Input) -> bool {
101        // Update cursor
102        if input.is_down() {
103            self.selected += 1;
104        }
105        if input.is_up() {
106            self.selected += self.items.len().max(1) - 1;
107        }
108
109        self.update_bounds();
110
111        if input.is_enter() {
112            if let Some(item) = self.items.get_mut(self.selected) {
113                // Enter the selected menu
114                (item.callback)();
115            } else {
116                eprintln!("Menu cursor in invalid position");
117            }
118        }
119
120        // If the user pressed left, we should return to the parent menu
121        !input.is_left()
122    }
123}