1use super::common::locate_by_id;
2use super::filler::Filler;
3use super::menu_entry::MenuEntry;
4use super::{Bus, Event, Hub, RenderData, RenderQueue, View};
5use super::{EntryKind, Id, ViewId, CLOSE_IGNITION_DELAY, ID_FEEDER};
6use super::{BORDER_RADIUS_MEDIUM, SMALL_BAR_HEIGHT, THICKNESS_LARGE, THICKNESS_MEDIUM};
7use crate::color::{BLACK, SEPARATOR_NORMAL, SEPARATOR_STRONG, WHITE};
8use crate::context::Context;
9use crate::device::CURRENT_DEVICE;
10use crate::font::{font_from_style, Fonts, NORMAL_STYLE};
11use crate::framebuffer::{Framebuffer, UpdateMode};
12use crate::geom::{big_half, small_half, BorderSpec, CornerSpec, Point, Rectangle};
13use crate::gesture::GestureEvent;
14use crate::unit::scale_by_dpi;
15use std::thread;
16
17pub struct Menu {
18 id: Id,
19 rect: Rectangle,
20 children: Vec<Box<dyn View>>,
21 view_id: ViewId,
22 kind: MenuKind,
23 center: Point,
24 root: bool,
25 sub_id: u8,
26 dir: i32,
27}
28
29#[derive(Debug, Copy, Clone, Eq, PartialEq)]
30pub enum MenuKind {
31 DropDown,
32 SubMenu,
33 Contextual,
34}
35
36impl Menu {
43 pub fn new(
44 target: Rectangle,
45 view_id: ViewId,
46 kind: MenuKind,
47 mut entries: Vec<EntryKind>,
48 context: &mut Context,
49 ) -> Menu {
50 let id = ID_FEEDER.next();
51 let mut children = Vec::new();
52 let dpi = CURRENT_DEVICE.dpi;
53 let (width, height) = context.display.dims;
54 let small_height = scale_by_dpi(SMALL_BAR_HEIGHT, dpi) as i32;
55
56 let thickness = scale_by_dpi(THICKNESS_MEDIUM, dpi) as i32;
57 let border_thickness = scale_by_dpi(THICKNESS_LARGE, dpi) as i32;
58 let border_radius = scale_by_dpi(BORDER_RADIUS_MEDIUM - THICKNESS_LARGE, dpi) as i32;
59
60 let sep_color = if context.fb.monochrome() {
61 SEPARATOR_STRONG
62 } else {
63 SEPARATOR_NORMAL
64 };
65 let font = font_from_style(&mut context.fonts, &NORMAL_STYLE, dpi);
66 let entry_height = font.x_heights.0 as i32 * 5;
67 let padding = 4 * font.em() as i32;
68
69 let north_space = target.min.y;
70 let south_space = height as i32 - target.max.y;
71 let center = target.center();
72
73 let (dir, y_start): (i32, i32) = if kind == MenuKind::SubMenu {
74 if north_space < south_space {
75 (1, target.min.y - border_thickness)
76 } else {
77 (-1, target.max.y + border_thickness)
78 }
79 } else {
80 if north_space < south_space {
81 (1, target.max.y)
82 } else {
83 (-1, target.min.y)
84 }
85 };
86
87 let top_min = small_height + big_half(thickness);
88 let bottom_max = height as i32 - small_height - small_half(thickness);
89
90 let usable_space = if dir.is_positive() {
91 bottom_max - y_start
92 } else {
93 y_start - top_min
94 };
95
96 let border_space = if kind == MenuKind::DropDown {
97 border_thickness
98 } else {
99 2 * border_thickness
100 };
101
102 let max_entries = ((usable_space - border_space) / entry_height) as usize;
103 let total_entries = entries.iter().filter(|e| !e.is_separator()).count();
104
105 if total_entries > max_entries {
106 let mut kind_counts = [0, 0];
107 for e in &entries {
108 kind_counts[e.is_separator() as usize] += 1;
109 if kind_counts[0] >= max_entries {
110 break;
111 }
112 }
113 let index = kind_counts[0] + kind_counts[1] - 1;
114 let more = entries.drain(index..).collect::<Vec<EntryKind>>();
115 entries.push(EntryKind::More(more));
116 }
117
118 let mut y_pos = y_start + dir * (border_space - border_thickness);
119
120 let max_width = 2 * width as i32 / 3;
121 let free_width = padding
122 + 2 * border_thickness
123 + entries
124 .iter()
125 .map(|e| font.plan(e.text(), None, None).width)
126 .max()
127 .unwrap();
128
129 let entry_width = free_width.min(max_width);
130
131 let (mut x_min, mut x_max) = if kind == MenuKind::SubMenu {
132 let west_space = target.min.x;
133 let east_space = width as i32 - target.max.x;
134 if west_space > east_space {
135 (target.min.x - entry_width, target.min.x)
136 } else {
137 (target.max.x, target.max.x + entry_width)
138 }
139 } else {
140 (
141 center.x - small_half(entry_width),
142 center.x + big_half(entry_width),
143 )
144 };
145
146 if x_min < 0 {
147 x_max -= x_min;
148 x_min = 0;
149 }
150
151 if x_max > width as i32 {
152 x_min += width as i32 - x_max;
153 x_max = width as i32;
154 }
155
156 let entries_count = entries.len();
157
158 for i in 0..entries_count {
159 if entries[i].is_separator() {
160 let rect = rect![
161 x_min + border_thickness,
162 y_pos - small_half(thickness),
163 x_max - border_thickness,
164 y_pos + big_half(thickness)
165 ];
166 let separator = Filler::new(rect, sep_color);
167 children.push(Box::new(separator) as Box<dyn View>);
168 } else {
169 let (y_min, y_max) = if dir.is_positive() {
170 (y_pos, y_pos + entry_height)
171 } else {
172 (y_pos - entry_height, y_pos)
173 };
174
175 let mut rect = rect![
176 x_min + border_thickness,
177 y_min,
178 x_max - border_thickness,
179 y_max
180 ];
181
182 let anchor = rect;
183
184 if i > 0 && entries[i - 1].is_separator() {
185 if dir.is_positive() {
186 rect.min.y += big_half(thickness);
187 } else {
188 rect.max.y -= small_half(thickness);
189 }
190 }
191
192 if i < entries_count - 1 && entries[i + 1].is_separator() {
193 if dir.is_positive() {
194 rect.max.y -= small_half(thickness);
195 } else {
196 rect.min.y += big_half(thickness);
197 }
198 }
199
200 let corner_spec = if kind != MenuKind::DropDown && entries_count == 1 {
201 Some(CornerSpec::Uniform(border_radius))
202 } else if i == entries_count - 1 {
203 if dir.is_positive() {
204 Some(CornerSpec::South(border_radius))
205 } else {
206 Some(CornerSpec::North(border_radius))
207 }
208 } else if kind != MenuKind::DropDown && i == 0 {
209 if dir.is_positive() {
210 Some(CornerSpec::North(border_radius))
211 } else {
212 Some(CornerSpec::South(border_radius))
213 }
214 } else {
215 None
216 };
217
218 let menu_entry = MenuEntry::new(rect, entries[i].clone(), anchor, corner_spec);
219
220 children.push(Box::new(menu_entry) as Box<dyn View>);
221
222 y_pos += dir * entry_height;
223 }
224 }
225
226 let triangle_space = if kind == MenuKind::Contextual {
227 font.x_heights.1 as i32
228 } else {
229 0
230 };
231
232 let total_entries = entries.iter().filter(|e| !e.is_separator()).count();
233 let menu_height = total_entries as i32 * entry_height + border_space;
234
235 let (y_min, y_max) = if dir.is_positive() {
236 (y_start - triangle_space, y_start + menu_height)
237 } else {
238 (y_start - menu_height, y_start + triangle_space)
239 };
240
241 let rect = rect![x_min, y_min, x_max, y_max];
242
243 Menu {
244 id,
245 rect,
246 children,
247 view_id,
248 kind,
249 center,
250 root: true,
251 sub_id: 0,
252 dir,
253 }
254 }
255
256 pub fn root(mut self, root: bool) -> Menu {
257 self.root = root;
258 self
259 }
260}
261
262impl View for Menu {
263 #[cfg_attr(feature = "otel", tracing::instrument(skip(self, hub, bus, rq, context), fields(event = ?evt), ret(level=tracing::Level::TRACE)))]
264 fn handle_event(
265 &mut self,
266 evt: &Event,
267 hub: &Hub,
268 bus: &mut Bus,
269 rq: &mut RenderQueue,
270 context: &mut Context,
271 ) -> bool {
272 match *evt {
273 Event::Select(ref entry_id) if self.root => {
274 self.handle_event(
275 &Event::PropagateSelect(entry_id.clone()),
276 hub,
277 bus,
278 rq,
279 context,
280 );
281 false
282 }
283 Event::PropagateSelect(..) => {
284 for c in &mut self.children {
285 if c.handle_event(evt, hub, bus, rq, context) {
286 break;
287 }
288 }
289 true
290 }
291 Event::Validate if self.root => {
292 let hub2 = hub.clone();
293 let view_id = self.view_id;
294 thread::spawn(move || {
295 thread::sleep(CLOSE_IGNITION_DELAY);
296 hub2.send(Event::Close(view_id)).ok();
297 });
298 true
299 }
300 Event::Gesture(GestureEvent::Tap(center)) if !self.rect.includes(center) => {
301 if self.root {
302 bus.push_back(Event::Close(self.view_id));
303 } else {
304 bus.push_back(Event::CloseSub(self.view_id));
305 }
306 self.root
307 }
308 Event::Gesture(GestureEvent::HoldFingerShort(center, ..))
309 if !self.rect.includes(center) =>
310 {
311 self.root
312 }
313 Event::SubMenu(rect, ref entries) => {
314 let menu = Menu::new(
315 rect,
316 ViewId::SubMenu(self.sub_id),
317 MenuKind::SubMenu,
318 entries.clone(),
319 context,
320 )
321 .root(false);
322 rq.add(RenderData::new(menu.id(), *menu.rect(), UpdateMode::Gui));
323 self.children.push(Box::new(menu) as Box<dyn View>);
324 self.sub_id = self.sub_id.wrapping_add(1);
325 true
326 }
327 Event::CloseSub(id) => {
328 if let Some(index) = locate_by_id(self, id) {
329 rq.add(RenderData::expose(
330 *self.children[index].rect(),
331 UpdateMode::Gui,
332 ));
333 self.children.remove(index);
334 }
335 true
336 }
337 Event::Gesture(..) => true,
338 _ => false,
339 }
340 }
341
342 #[cfg_attr(feature = "otel", tracing::instrument(skip(self, fb, fonts, _rect), fields(rect = ?_rect)))]
343 fn render(&self, fb: &mut dyn Framebuffer, _rect: Rectangle, fonts: &mut Fonts) {
344 let dpi = CURRENT_DEVICE.dpi;
345 let border_radius = scale_by_dpi(BORDER_RADIUS_MEDIUM, dpi) as i32;
346 let border_thickness = scale_by_dpi(THICKNESS_LARGE, dpi) as u16;
347
348 let corners = if self.kind == MenuKind::DropDown {
349 if self.dir.is_positive() {
350 CornerSpec::South(border_radius)
351 } else {
352 CornerSpec::North(border_radius)
353 }
354 } else {
355 CornerSpec::Uniform(border_radius)
356 };
357
358 if self.kind == MenuKind::Contextual {
359 let font = font_from_style(fonts, &NORMAL_STYLE, dpi);
360 let triangle_space = font.x_heights.1 as i32;
361 let mut rect = self.rect;
362
363 if self.dir.is_positive() {
364 rect.min.y += triangle_space
365 } else {
366 rect.max.y -= triangle_space
367 }
368
369 fb.draw_rounded_rectangle_with_border(
370 &rect,
371 &corners,
372 &BorderSpec {
373 thickness: border_thickness,
374 color: BLACK,
375 },
376 &WHITE,
377 );
378
379 let y_b = if self.dir.is_positive() {
380 self.rect.min.y
381 } else {
382 self.rect.max.y - 1
383 };
384
385 let side = triangle_space + border_thickness as i32;
386 let x_b = self
387 .center
388 .x
389 .max(rect.min.x + 2 * side)
390 .min(rect.max.x - 2 * side);
391
392 let mut b = pt!(x_b, y_b);
393 let mut a = b + pt!(-side, self.dir * side);
394 let mut c = a + pt!(2 * side, 0);
395
396 fb.draw_triangle(&[a, b, c], BLACK);
397 let drift = (border_thickness as f32 * ::std::f32::consts::SQRT_2) as i32;
398
399 b += pt!(0, self.dir * drift);
400 a += pt!(drift, 0);
401 c -= pt!(drift, 0);
402
403 fb.draw_triangle(&[a, b, c], WHITE);
404 } else {
405 fb.draw_rounded_rectangle_with_border(
406 &self.rect,
407 &corners,
408 &BorderSpec {
409 thickness: border_thickness,
410 color: BLACK,
411 },
412 &WHITE,
413 );
414 }
415 }
416
417 fn is_background(&self) -> bool {
418 true
419 }
420
421 fn rect(&self) -> &Rectangle {
422 &self.rect
423 }
424
425 fn rect_mut(&mut self) -> &mut Rectangle {
426 &mut self.rect
427 }
428
429 fn children(&self) -> &Vec<Box<dyn View>> {
430 &self.children
431 }
432
433 fn children_mut(&mut self) -> &mut Vec<Box<dyn View>> {
434 &mut self.children
435 }
436
437 fn id(&self) -> Id {
438 self.id
439 }
440
441 fn view_id(&self) -> Option<ViewId> {
442 Some(self.view_id)
443 }
444}