Skip to main content

minifb_ui/ui/
contextmenu.rs

1pub struct ContextMenu {
2    items: Vec<String>,
3    pub open: bool,
4    menu_x: usize,
5    menu_y: usize,
6
7    pub item_height: usize,
8    pub width: usize,
9    pub padding: usize,
10
11    pub bg_color: crate::color::Color,
12    pub hover_color: crate::color::Color,
13    pub text_color: crate::color::Color,
14    pub border_color: crate::color::Color,
15    pub border_size: usize,
16    pub radius: usize,
17
18    pub font: Option<crate::ttf::Font>,
19    pub font_size: f32,
20
21    clicked_item: Option<usize>,
22    lmb_was_down: bool,
23}
24
25impl Default for ContextMenu {
26    fn default() -> Self {
27        Self {
28            items: Vec::new(),
29            open: false,
30            menu_x: 0,
31            menu_y: 0,
32            item_height: 28,
33            width: 160,
34            padding: 4,
35            bg_color: crate::color::Color::new(25, 25, 35),
36            hover_color: crate::color::Color::rgba(108, 92, 231, 40),
37            text_color: crate::color::Color::new(220, 220, 230),
38            border_color: crate::color::Color::new(70, 70, 90),
39            border_size: 1,
40            radius: 6,
41            font: None,
42            font_size: 13.0,
43            clicked_item: None,
44            lmb_was_down: false,
45        }
46    }
47}
48
49impl ContextMenu {
50    pub fn items(mut self, items: &[&str]) -> Self {
51        self.items = items.iter().map(|s| s.to_string()).collect();
52        self
53    }
54
55    pub fn width(mut self, w: usize) -> Self {
56        self.width = w;
57        self
58    }
59
60    pub fn item_height(mut self, h: usize) -> Self {
61        self.item_height = h;
62        self
63    }
64
65    pub fn font(mut self, font: crate::ttf::Font, size: f32) -> Self {
66        self.font = Some(font);
67        self.font_size = size;
68        self
69    }
70
71    pub fn bg_color(mut self, color: crate::color::Color) -> Self {
72        self.bg_color = color;
73        self
74    }
75
76    pub fn hover_color(mut self, color: crate::color::Color) -> Self {
77        self.hover_color = color;
78        self
79    }
80
81    pub fn text_color(mut self, color: crate::color::Color) -> Self {
82        self.text_color = color;
83        self
84    }
85
86    pub fn border_color(mut self, color: crate::color::Color) -> Self {
87        self.border_color = color;
88        self
89    }
90
91    pub fn border(mut self, size: usize) -> Self {
92        self.border_size = size;
93        self
94    }
95
96    pub fn radius(mut self, radius: usize) -> Self {
97        self.radius = radius;
98        self
99    }
100
101    /// Opens the context menu at the given position
102    pub fn open(&mut self, x: usize, y: usize) {
103        self.menu_x = x;
104        self.menu_y = y;
105        self.open = true;
106        self.clicked_item = None;
107    }
108
109    /// Closes the context menu
110    pub fn close(&mut self) {
111        self.open = false;
112        self.clicked_item = None;
113    }
114
115    pub fn is_open(&self) -> bool {
116        self.open
117    }
118
119    /// Returns the index of the item that was clicked this frame, if any
120    pub fn clicked_item(&self) -> Option<usize> {
121        self.clicked_item
122    }
123
124    pub fn draw(&mut self, window: &mut crate::window::Window) {
125        self.clicked_item = None;
126
127        if !self.open {
128            self.lmb_was_down = window.get_mouse_state().lmb_clicked;
129            return;
130        }
131
132        let font = match &self.font {
133            Some(f) => f.clone(),
134            None => return,
135        };
136
137        let mouse = window.get_mouse_state();
138        let mx = mouse.pos_x;
139        let my = mouse.pos_y;
140        let lmb = mouse.lmb_clicked;
141        let lmb_just = lmb && !self.lmb_was_down;
142
143        let total_h = self.padding * 2 + self.items.len() * self.item_height;
144
145        // Background
146        window.draw_rect_f(
147            self.menu_x, self.menu_y, self.width, total_h,
148            self.radius, &self.bg_color, 0,
149        );
150        for i in 0..self.border_size {
151            window.draw_rect(
152                self.menu_x + i, self.menu_y + i,
153                self.width - i * 2, total_h - i * 2,
154                self.radius.saturating_sub(i), &self.border_color,
155            );
156        }
157
158        // Items
159        for (i, item) in self.items.iter().enumerate() {
160            let iy = self.menu_y + self.padding + i * self.item_height;
161            let hovered = mx >= self.menu_x as f32
162                && mx < (self.menu_x + self.width) as f32
163                && my >= iy as f32
164                && my < (iy + self.item_height) as f32;
165
166            if hovered {
167                window.draw_rect_f(
168                    self.menu_x + self.border_size, iy,
169                    self.width - self.border_size * 2, self.item_height,
170                    0, &self.hover_color, 0,
171                );
172
173                if lmb_just {
174                    self.clicked_item = Some(i);
175                    self.open = false;
176                }
177            }
178
179            let text = crate::ui::text::Text::new(item, font.clone());
180            let ty = iy as f32 + (self.item_height as f32 - self.font_size) / 2.0;
181            window.draw_text(self.menu_x + 12, ty as usize, &text, self.font_size, &self.text_color);
182        }
183
184        // Click outside closes
185        if lmb_just {
186            let in_menu = mx >= self.menu_x as f32
187                && mx < (self.menu_x + self.width) as f32
188                && my >= self.menu_y as f32
189                && my < (self.menu_y + total_h) as f32;
190            if !in_menu {
191                self.open = false;
192            }
193        }
194
195        self.lmb_was_down = lmb;
196    }
197}