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 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 txt.append(Line(" (!)"));
76 }
77
78 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 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 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 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 #[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 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}