widgetry/widgets/
menu.rs

1use geom::Pt2D;
2
3use crate::{
4    Choice, EventCtx, GfxCtx, Key, Line, Outcome, ScreenDims, ScreenPt, ScreenRectangle, Style,
5    Text, Widget, WidgetImpl, WidgetOutput,
6};
7
8pub struct Menu<T> {
9    choices: Vec<Choice<T>>,
10    current_idx: usize,
11
12    pub(crate) top_left: ScreenPt,
13    dims: ScreenDims,
14}
15
16impl<T: 'static> Menu<T> {
17    pub fn widget(ctx: &EventCtx, choices: Vec<Choice<T>>) -> Widget {
18        Widget::new(Box::new(Self::new(ctx, choices)))
19    }
20
21    pub fn new(ctx: &EventCtx, choices: Vec<Choice<T>>) -> Self {
22        let mut m = Menu {
23            choices,
24            current_idx: 0,
25
26            top_left: ScreenPt::new(0.0, 0.0),
27            dims: ScreenDims::new(0.0, 0.0),
28        };
29        m.dims = m.calculate_txt(ctx.style()).dims(&ctx.prerender.assets);
30        m
31    }
32
33    pub fn take_current_choice(&mut self) -> T {
34        // TODO Make sure it's marked invalid, like button
35        self.choices.remove(self.current_idx).data
36    }
37
38    pub fn set_current(&mut self, idx: usize) {
39        self.current_idx = idx;
40    }
41
42    fn calculate_txt(&self, style: &Style) -> Text {
43        let mut txt = Text::new();
44
45        for (idx, choice) in self.choices.iter().enumerate() {
46            let is_hovered = idx == self.current_idx;
47            let mut text_color = if is_hovered {
48                choice.fg.unwrap_or(style.btn_solid.fg)
49            } else {
50                choice.fg.unwrap_or(style.text_primary_color)
51            };
52
53            if choice.active {
54                if let Some(ref key) = choice.hotkey {
55                    txt.add_appended(vec![
56                        Line(key.describe()).fg(style.text_hotkey_color),
57                        Line(format!(" - {}", choice.label)).fg(text_color),
58                    ]);
59                } else {
60                    txt.add_line(Line(&choice.label).fg(text_color))
61                }
62            } else {
63                text_color = text_color.alpha(0.8);
64                if let Some(ref key) = choice.hotkey {
65                    txt.add_line(
66                        Line(format!("{} - {}", key.describe(), choice.label)).fg(text_color),
67                    );
68                } else {
69                    txt.add_line(Line(&choice.label).fg(text_color));
70                }
71            }
72
73            if choice.tooltip.is_some() {
74                // TODO Ideally unicode info symbol, but the fonts don't seem to have it
75                txt.append(Line(" (!)"));
76            }
77
78            // TODO BG color should be on the TextSpan, so this isn't so terrible?
79            if is_hovered {
80                txt.highlight_last_line(style.btn_solid.bg);
81            }
82        }
83        txt
84    }
85}
86
87impl<T: 'static> WidgetImpl for Menu<T> {
88    fn get_dims(&self) -> ScreenDims {
89        self.dims
90    }
91
92    fn set_pos(&mut self, top_left: ScreenPt) {
93        self.top_left = top_left;
94    }
95
96    fn event(&mut self, ctx: &mut EventCtx, output: &mut WidgetOutput) {
97        if self.choices.is_empty() {
98            return;
99        }
100
101        // Handle the mouse
102        if ctx.redo_mouseover() {
103            if let Some(cursor) = ctx.canvas.get_cursor_in_screen_space() {
104                let mut top_left = self.top_left;
105                for idx in 0..self.choices.len() {
106                    let rect = ScreenRectangle {
107                        x1: top_left.x,
108                        y1: top_left.y,
109                        x2: top_left.x + self.dims.width,
110                        y2: top_left.y + ctx.default_line_height(),
111                    };
112                    if rect.contains(cursor) && self.choices[idx].active {
113                        self.current_idx = idx;
114                        break;
115                    }
116                    top_left.y += ctx.default_line_height();
117                }
118            }
119        }
120        {
121            let choice = &self.choices[self.current_idx];
122            if ctx.normal_left_click() {
123                // Did we actually click the entry?
124                let mut top_left = self.top_left;
125                top_left.y += ctx.default_line_height() * (self.current_idx as f64);
126                let rect = ScreenRectangle {
127                    x1: top_left.x,
128                    y1: top_left.y,
129                    x2: top_left.x + self.dims.width,
130                    y2: top_left.y + ctx.default_line_height(),
131                };
132                if let Some(pt) = ctx.canvas.get_cursor_in_screen_space() {
133                    if rect.contains(pt) && choice.active {
134                        output.outcome = Outcome::Clicked(choice.label.clone());
135                        return;
136                    }
137                }
138                ctx.input.unconsume_event();
139            }
140        }
141
142        // Handle hotkeys
143        for (idx, choice) in self.choices.iter().enumerate() {
144            if !choice.active {
145                continue;
146            }
147            if ctx.input.pressed(choice.hotkey.clone()) {
148                self.current_idx = idx;
149                output.outcome = Outcome::Clicked(choice.label.clone());
150                return;
151            }
152        }
153
154        // Handle nav keys
155        #[allow(clippy::collapsible_if)]
156        if ctx.input.pressed(Key::Enter) || ctx.input.pressed(Key::Space) {
157            let choice = &self.choices[self.current_idx];
158            if choice.active {
159                output.outcome = Outcome::Clicked(choice.label.clone());
160            }
161        } else if ctx.input.pressed(Key::UpArrow) {
162            if self.current_idx > 0 {
163                self.current_idx -= 1;
164            }
165        } else if ctx.input.pressed(Key::DownArrow) {
166            if self.current_idx < self.choices.len() - 1 {
167                self.current_idx += 1;
168            }
169        }
170    }
171
172    fn draw(&self, g: &mut GfxCtx) {
173        if self.choices.is_empty() {
174            return;
175        }
176
177        let draw = g.upload(self.calculate_txt(g.style()).render(g));
178        g.fork(
179            Pt2D::new(0.0, 0.0),
180            self.top_left,
181            1.0,
182            Some(crate::drawing::MENU_Z),
183        );
184        g.redraw(&draw);
185        g.unfork();
186
187        if let Some(ref info) = self.choices[self.current_idx].tooltip {
188            // Hold on, are we actually hovering on that entry right now?
189            let mut top_left = self.top_left;
190            top_left.y += g.default_line_height() * (self.current_idx as f64);
191            let rect = ScreenRectangle {
192                x1: top_left.x,
193                y1: top_left.y,
194                x2: top_left.x + self.dims.width,
195                y2: top_left.y + g.default_line_height(),
196            };
197            if let Some(pt) = g.canvas.get_cursor_in_screen_space() {
198                if rect.contains(pt) {
199                    g.draw_mouse_tooltip(
200                        Text::from(info)
201                            .inner_wrap_to_pixels(0.3 * g.canvas.window_width, &g.prerender.assets),
202                    );
203                }
204            }
205        }
206    }
207}