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