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