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}