cadmus_core/view/
menu.rs

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
36// TOP MENU       C
37//    ───         B
38//  ↓  A       ↑  A
39//     B         ───
40//     C     BOTTOM MENU
41
42impl 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}