cadmus_core/view/
menu_entry.rs

1use super::icon::ICONS_PIXMAPS;
2use super::{Bus, EntryKind, Event, Hub, Id, RenderData, RenderQueue, View, ID_FEEDER};
3use crate::color::{TEXT_INVERTED_HARD, TEXT_NORMAL};
4use crate::context::Context;
5use crate::device::CURRENT_DEVICE;
6use crate::font::{font_from_style, Fonts, NORMAL_STYLE, SPECIAL_STYLE};
7use crate::framebuffer::{Framebuffer, UpdateMode};
8use crate::geom::{CornerSpec, Rectangle};
9use crate::gesture::GestureEvent;
10use crate::input::{DeviceEvent, FingerStatus};
11use std::mem;
12
13pub struct MenuEntry {
14    id: Id,
15    rect: Rectangle,
16    children: Vec<Box<dyn View>>,
17    kind: EntryKind,
18    corner_spec: Option<CornerSpec>,
19    anchor: Rectangle,
20    active: bool,
21    disabled: bool,
22}
23
24impl MenuEntry {
25    pub fn new(
26        rect: Rectangle,
27        kind: EntryKind,
28        anchor: Rectangle,
29        corner_spec: Option<CornerSpec>,
30    ) -> MenuEntry {
31        MenuEntry {
32            id: ID_FEEDER.next(),
33            rect,
34            children: Vec::new(),
35            kind,
36            corner_spec,
37            anchor,
38            active: false,
39            disabled: false,
40        }
41    }
42
43    pub fn update(&mut self, value: bool, rq: &mut RenderQueue) {
44        if let Some(v) = self.kind.get() {
45            if v != value {
46                self.kind.set(value);
47                rq.add(RenderData::new(self.id, self.rect, UpdateMode::Gui));
48            }
49        }
50    }
51
52    pub fn set_disabled(&mut self, value: bool, rq: &mut RenderQueue) {
53        if self.disabled == value {
54            return;
55        }
56        self.disabled = value;
57        rq.add(RenderData::new(self.id, self.rect, UpdateMode::Gui));
58    }
59}
60
61impl View for MenuEntry {
62    #[cfg_attr(feature = "otel", tracing::instrument(skip(self, _hub, bus, rq, _context), fields(event = ?evt), ret(level=tracing::Level::TRACE)))]
63    fn handle_event(
64        &mut self,
65        evt: &Event,
66        _hub: &Hub,
67        bus: &mut Bus,
68        rq: &mut RenderQueue,
69        _context: &mut Context,
70    ) -> bool {
71        match *evt {
72            Event::Device(DeviceEvent::Finger {
73                status, position, ..
74            }) => match status {
75                FingerStatus::Down if self.rect.includes(position) => {
76                    self.active = true;
77                    rq.add(RenderData::new(self.id, self.rect, UpdateMode::Fast));
78                    true
79                }
80                FingerStatus::Up if self.active => {
81                    self.active = false;
82                    rq.add(RenderData::new(self.id, self.rect, UpdateMode::Gui));
83                    true
84                }
85                _ => false,
86            },
87            Event::Gesture(GestureEvent::Tap(center))
88            | Event::Gesture(GestureEvent::HoldFingerShort(center, ..))
89                if self.rect.includes(center) && !self.disabled =>
90            {
91                match self.kind {
92                    EntryKind::CheckBox(_, _, ref mut value) => {
93                        *value = !*value;
94                        rq.add(RenderData::new(self.id, self.rect, UpdateMode::Gui));
95                    }
96                    EntryKind::RadioButton(_, _, ref mut value) if !*value => {
97                        *value = true;
98                        rq.add(RenderData::new(self.id, self.rect, UpdateMode::Gui));
99                    }
100                    _ => (),
101                };
102                match self.kind {
103                    EntryKind::Command(_, ref id)
104                    | EntryKind::CheckBox(_, ref id, _)
105                    | EntryKind::RadioButton(_, ref id, _) => {
106                        bus.push_back(Event::Select(id.clone()));
107                        if let Event::Gesture(GestureEvent::Tap { .. }) = *evt {
108                            bus.push_back(Event::Validate);
109                        }
110                    }
111                    EntryKind::SubMenu(_, ref entries) | EntryKind::More(ref entries) => {
112                        bus.push_back(Event::SubMenu(self.anchor, entries.clone()));
113                    }
114                    EntryKind::Message(..) => {
115                        bus.push_back(Event::Validate);
116                    }
117                    _ => (),
118                };
119                true
120            }
121            Event::PropagateSelect(ref other_id) => match self.kind {
122                EntryKind::RadioButton(_, ref id, ref mut value) if *value => {
123                    if mem::discriminant(id) == mem::discriminant(other_id) && id != other_id {
124                        *value = false;
125                        rq.add(RenderData::new(self.id, self.rect, UpdateMode::Gui));
126                        true
127                    } else {
128                        false
129                    }
130                }
131                _ => false,
132            },
133            _ => false,
134        }
135    }
136
137    #[cfg_attr(feature = "otel", tracing::instrument(skip(self, fb, fonts, _rect), fields(rect = ?_rect)))]
138    fn render(&self, fb: &mut dyn Framebuffer, _rect: Rectangle, fonts: &mut Fonts) {
139        let dpi = CURRENT_DEVICE.dpi;
140        let style = if matches!(self.kind, EntryKind::More(..)) {
141            SPECIAL_STYLE
142        } else {
143            NORMAL_STYLE
144        };
145        let font = font_from_style(fonts, &style, dpi);
146        let x_height = font.x_heights.0 as i32;
147        let padding = 4 * font.em() as i32;
148
149        let scheme = if self.active {
150            TEXT_INVERTED_HARD
151        } else {
152            TEXT_NORMAL
153        };
154        let foreground = if self.disabled { scheme[2] } else { scheme[1] };
155
156        if let Some(ref cs) = self.corner_spec {
157            fb.draw_rounded_rectangle(&self.rect, cs, scheme[0]);
158        } else {
159            fb.draw_rectangle(&self.rect, scheme[0]);
160        }
161
162        let max_width = self.rect.width() as i32 - padding;
163        let plan = font.plan(self.kind.text(), Some(max_width), None);
164        let dy = (self.rect.height() as i32 - x_height) / 2;
165        let pt = pt!(self.rect.min.x + padding / 2, self.rect.max.y - dy);
166
167        font.render(fb, foreground, &plan, pt);
168
169        let (icon_name, x_offset) = match self.kind {
170            EntryKind::CheckBox(_, _, value) if value => ("check_mark", 0),
171            EntryKind::RadioButton(_, _, value) if value => ("bullet", 0),
172            EntryKind::Message(_, Some(ref name)) => (name.as_str(), 0),
173            EntryKind::SubMenu(..) | EntryKind::More(..) => {
174                ("angle-right-small", self.rect.width() as i32 - padding / 2)
175            }
176            _ => ("", 0),
177        };
178
179        if let Some(pixmap) = ICONS_PIXMAPS.get(icon_name) {
180            let dx = x_offset + (padding / 2 - pixmap.width as i32) / 2;
181            let dy = (self.rect.height() as i32 - pixmap.height as i32) / 2;
182            let pt = self.rect.min + pt!(dx, dy);
183
184            fb.draw_blended_pixmap(pixmap, pt, foreground);
185        }
186    }
187
188    fn rect(&self) -> &Rectangle {
189        &self.rect
190    }
191
192    fn rect_mut(&mut self) -> &mut Rectangle {
193        &mut self.rect
194    }
195
196    fn children(&self) -> &Vec<Box<dyn View>> {
197        &self.children
198    }
199
200    fn children_mut(&mut self) -> &mut Vec<Box<dyn View>> {
201        &mut self.children
202    }
203
204    fn id(&self) -> Id {
205        self.id
206    }
207}