pixels_graphics_lib/ui/
menu_bar.rs

1use crate::ui::menu_item_view::*;
2use crate::ui::prelude::*;
3use std::hash::Hash;
4
5#[derive(Debug)]
6pub struct MenuBar<Key: Hash + Copy + PartialEq + Eq + Debug> {
7    bounds: Rect,
8    full_bounds: Rect,
9    style: MenuBarStyle,
10    items: Vec<MenuItemView<Key>>,
11    state: ViewState,
12    fill_width: bool,
13    screen_size: (usize, usize),
14}
15
16#[derive(Debug)]
17pub struct MenuBarItem<Key: Hash + Copy + PartialEq + Eq + Debug> {
18    pub(crate) id: Key,
19    pub(crate) name: String,
20    pub(crate) children: Option<Vec<MenuBarItem<Key>>>,
21    pub(crate) checkable: Option<bool>,
22}
23
24impl<Key: Hash + Copy + PartialEq + Eq + Debug> MenuBarItem<Key> {
25    /// Menu item that can be clicked
26    pub fn new_button(id: Key, name: &str) -> Self {
27        Self {
28            id,
29            name: name.to_string(),
30            children: None,
31            checkable: None,
32        }
33    }
34
35    /// Menu item that can be toggled on or off
36    /// use [MenuBar::is_checked] to check state
37    pub fn new_checkable(id: Key, name: &str, default: bool) -> Self {
38        Self {
39            id,
40            name: name.to_string(),
41            checkable: Some(default),
42            children: None,
43        }
44    }
45
46    pub fn new_options(id: Key, name: &str, children: &[(Key, &str)], default: usize) -> Self {
47        let default = if default >= children.len() {
48            0
49        } else {
50            default
51        };
52        Self {
53            id,
54            name: name.to_string(),
55            children: Some(
56                children
57                    .iter()
58                    .enumerate()
59                    .map(|(i, p)| MenuBarItem::new_checkable(p.0, p.1, i == default))
60                    .collect(),
61            ),
62            checkable: None,
63        }
64    }
65
66    /// Menu item with children that all act like buttons
67    pub fn new_menu(id: Key, name: &str, children: &[(Key, &str)]) -> Self {
68        Self {
69            id,
70            name: name.to_string(),
71            children: Some(
72                children
73                    .iter()
74                    .map(|p| MenuBarItem::new_button(p.0, p.1))
75                    .collect(),
76            ),
77            checkable: None,
78        }
79    }
80
81    pub fn new(id: Key, name: &str, children: Vec<MenuBarItem<Key>>) -> Self {
82        Self {
83            id,
84            name: name.to_string(),
85            children: Some(children),
86            checkable: None,
87        }
88    }
89}
90
91impl<Key: Hash + Copy + PartialEq + Eq + Debug> MenuBar<Key> {
92    pub fn new(
93        style: &MenuBarStyle,
94        pos: Coord,
95        screen_size: (usize, usize),
96        fill_width: bool,
97        items: &[MenuBarItem<Key>],
98    ) -> MenuBar<Key> {
99        let views: Vec<MenuItemView<Key>> = items
100            .iter()
101            .map(|item| MenuItemView::new(item, true))
102            .collect();
103        let bounds = Rect::new(pos, (0, 0));
104        let mut menu_bar = MenuBar {
105            full_bounds: bounds.clone(),
106            bounds,
107            style: style.clone(),
108            items: views,
109            state: ViewState::Normal,
110            fill_width,
111            screen_size,
112        };
113        menu_bar.layout();
114        menu_bar
115    }
116
117    fn layout(&mut self) {
118        self.bounds = layout_titles(
119            self.bounds.top_left(),
120            &self.style,
121            self.screen_size,
122            self.fill_width,
123            &mut self.items,
124        );
125
126        self.calc_bounds();
127    }
128}
129
130impl<Key: Hash + Copy + PartialEq + Eq + Debug> MenuBar<Key> {
131    pub fn full_bounds(&self) -> &Rect {
132        &self.full_bounds
133    }
134
135    pub fn on_mouse_move(&mut self, xy: Coord) {
136        if self.full_bounds.contains(xy) {
137            self.items.iter_mut().for_each(|c| (*c).on_mouse_move(xy));
138        }
139        self.calc_bounds();
140    }
141
142    /// Returns the path when clicked, if it has no children and is state is normal
143    ///
144    /// Also collapses the menu when clicking on a clickable item or outside the menu
145    ///
146    /// # Returns
147    /// * `None` - outside menu
148    /// * one element - on menu bar title item
149    /// * two or more elements - on dropdown item
150    pub fn on_mouse_click(&mut self, down_at: Coord, up_at: Coord) -> Option<Key> {
151        if !self.full_bounds.contains(up_at) {
152            self.collapse();
153            return None;
154        }
155        let path = on_click_path(&self.items, down_at, up_at);
156        self.collapse();
157        path
158    }
159
160    pub fn is_expanded(&self) -> bool {
161        self.items.iter().any(|v| v.focused)
162    }
163
164    /// Get the text label for a menu item, returns None for invalid IDs
165    pub fn label_for(&self, id: Key) -> Option<&str> {
166        self.get_view(id).map(|v| v.name.as_str())
167    }
168
169    /// Set state for a menu item, does nothing for invalid IDs
170    /// If a parent is disabled then it's children won't be rendered or clickable but
171    /// they have their own state
172    pub fn set_state(&mut self, id: Key, new_state: ViewState) {
173        if let Some(v) = self.get_view_mut(id) {
174            v.state = new_state;
175            if new_state == ViewState::Disabled {
176                v.focused = false;
177            }
178        }
179        self.calc_bounds();
180    }
181
182    /// Get state for a menu item, returns None for invalid IDs
183    /// If a parent is disabled then it's children won't be rendered or clickable but
184    /// they have their own state
185    pub fn get_state(&self, id: Key) -> Option<ViewState> {
186        self.get_view(id).map(|v| v.state)
187    }
188
189    /// Returns None if ID is invalid or view isn't checkable
190    pub fn is_checked(&self, id: Key) -> Option<bool> {
191        self.get_view(id).and_then(|v| {
192            if let ItemContent::Checkable(checked) = v.content {
193                Some(checked)
194            } else {
195                None
196            }
197        })
198    }
199
200    /// Set checked for a menu item, does nothing for invalid paths
201    /// If changing an options group then only `value == true` works
202    pub fn set_checked(&mut self, id: Key, value: bool) {
203        if let Some(view) = self.get_view_mut(id) {
204            if let ItemContent::Checkable(checked) = &mut view.content {
205                *checked = value;
206            }
207        }
208    }
209
210    /// Unchecks all checkable direct children
211    pub fn uncheck_all_children(&mut self, id: Key) {
212        if let Some(view) = self.get_view_mut(id) {
213            if let ItemContent::Parent(children, _, _) = &mut view.content {
214                for child in children {
215                    if matches!(child.content, ItemContent::Checkable(_)) {
216                        child.content = ItemContent::Checkable(false);
217                    }
218                }
219            }
220        }
221    }
222
223    fn get_view(&self, id: Key) -> Option<&MenuItemView<Key>> {
224        Self::get_view_from_list(&self.items, id)
225    }
226
227    fn get_view_from_list(list: &[MenuItemView<Key>], id: Key) -> Option<&MenuItemView<Key>> {
228        for item in list {
229            if item.id == id {
230                return Some(item);
231            } else if let ItemContent::Parent(children, _, _) = &item.content {
232                let result = Self::get_view_from_list(children, id);
233                if result.is_some() {
234                    return result;
235                }
236            }
237        }
238        None
239    }
240
241    fn get_view_mut(&mut self, id: Key) -> Option<&mut MenuItemView<Key>> {
242        Self::get_view_mut_from_list(&mut self.items, id)
243    }
244
245    fn get_view_mut_from_list(
246        list: &mut [MenuItemView<Key>],
247        id: Key,
248    ) -> Option<&mut MenuItemView<Key>> {
249        for item in list {
250            if item.id == id {
251                return Some(item);
252            } else if let ItemContent::Parent(children, _, _) = &mut item.content {
253                let result = Self::get_view_mut_from_list(children, id);
254                if result.is_some() {
255                    return result;
256                }
257            }
258        }
259        None
260    }
261
262    fn calc_bounds(&mut self) {
263        self.full_bounds = self.bounds.clone();
264        for children in &self.items {
265            if let Some(extra) = focused_bounds(children) {
266                self.full_bounds = union(&self.full_bounds, &extra);
267            }
268        }
269    }
270
271    pub fn collapse(&mut self) {
272        collapse_menu(&mut self.items);
273    }
274}
275
276impl<Key: Hash + Copy + PartialEq + Eq + Debug> PixelView for MenuBar<Key> {
277    fn set_position(&mut self, top_left: Coord) {
278        self.bounds = self.bounds.move_to(top_left);
279        self.layout();
280    }
281
282    /// Doesn't include any drop down menus
283    /// See `full_bounds` for current total size
284    fn bounds(&self) -> &Rect {
285        &self.bounds
286    }
287
288    fn render(&self, graphics: &mut Graphics, mouse: &MouseData) {
289        let hovering = self.bounds.contains(mouse.xy);
290        let (error, disabled) = self.state.get_err_dis();
291        if let Some(bg) = self.style.background.get(hovering, error, disabled) {
292            graphics.draw_rect(self.bounds.clone(), fill(bg));
293        }
294        draw_titles(graphics, mouse.xy, &self.style, &self.items);
295    }
296
297    fn update(&mut self, _: &Timing) {}
298
299    fn set_state(&mut self, new_state: ViewState) {
300        self.state = new_state;
301    }
302
303    fn get_state(&self) -> ViewState {
304        self.state
305    }
306}
307
308impl<Key: Hash + Copy + PartialEq + Eq + Debug> LayoutView for MenuBar<Key> {
309    fn set_bounds(&mut self, bounds: Rect) {
310        self.bounds = bounds;
311        self.layout();
312    }
313}