1use super::Widget;
12use alloc::{string::String, vec::Vec};
13use core::marker::PhantomData;
14use embedded_graphics::{
15 pixelcolor::PixelColor, prelude::*, primitives::Rectangle, text::Alignment,
16};
17use zest_core::{Constraints, Length, RenderError, Renderer, TouchPhase, UiAction, WidgetId};
18use zest_theme::Theme;
19
20const ROW_H: u32 = 30;
22const PAD_X: i32 = 10;
24
25struct Entry<M> {
27 label: String,
28 message: M,
29}
30
31pub struct Menu<C: PixelColor, M: Clone> {
33 rect: Rectangle,
34 entries: Vec<Entry<M>>,
35 id: Option<WidgetId>,
36 selected: Option<usize>,
37 focused: Option<usize>,
38 pressed: Option<usize>,
39 row_h: u32,
40 width: Length,
41 height: Length,
42 _color: PhantomData<C>,
43}
44
45impl<C: PixelColor, M: Clone> Menu<C, M> {
46 pub fn new() -> Self {
48 Self {
49 rect: Rectangle::zero(),
50 entries: Vec::new(),
51 id: None,
52 selected: None,
53 focused: None,
54 pressed: None,
55 row_h: ROW_H,
56 width: Length::Fill,
57 height: Length::Shrink,
58 _color: PhantomData,
59 }
60 }
61
62 #[must_use]
64 pub fn entry(mut self, label: impl Into<String>, message: M) -> Self {
65 self.entries.push(Entry {
66 label: label.into(),
67 message,
68 });
69 self
70 }
71
72 #[must_use]
74 pub fn selected(mut self, index: usize) -> Self {
75 self.selected = Some(index);
76 self
77 }
78
79 #[must_use]
81 pub fn id(mut self, id: WidgetId) -> Self {
82 self.id = Some(id);
83 self
84 }
85
86 #[must_use]
88 pub fn row_height(mut self, h: u32) -> Self {
89 self.row_h = h.max(1);
90 self
91 }
92
93 #[must_use]
95 pub fn width(mut self, w: impl Into<Length>) -> Self {
96 self.width = w.into();
97 self
98 }
99
100 #[must_use]
102 pub fn height(mut self, h: impl Into<Length>) -> Self {
103 self.height = h.into();
104 self
105 }
106
107 fn row_rect(&self, i: usize) -> Rectangle {
108 Rectangle::new(
109 self.rect.top_left + Point::new(0, (i as u32 * self.row_h) as i32),
110 Size::new(self.rect.size.width, self.row_h),
111 )
112 }
113
114 fn row_at(&self, p: Point) -> Option<usize> {
115 let left = self.rect.top_left.x;
116 if p.x < left || p.x >= left + self.rect.size.width as i32 {
117 return None;
118 }
119 let dy = p.y - self.rect.top_left.y;
120 if dy < 0 {
121 return None;
122 }
123 let idx = (dy as u32 / self.row_h) as usize;
124 (idx < self.entries.len()).then_some(idx)
125 }
126
127 fn row_id(&self, index: usize) -> Option<WidgetId> {
128 self.id
129 .map(|id| WidgetId::new(id.raw().wrapping_add(index as u64 + 1)))
130 }
131}
132
133impl<C: PixelColor, M: Clone> Default for Menu<C, M> {
134 fn default() -> Self {
135 Self::new()
136 }
137}
138
139impl<C: PixelColor, M: Clone> Widget<C, M> for Menu<C, M> {
140 fn measure(&mut self, constraints: Constraints) -> Size {
141 let intrinsic_h = self.row_h * self.entries.len() as u32;
142 let w = self
143 .width
144 .resolve(constraints.max.width, constraints.max.width);
145 let h = self.height.resolve(intrinsic_h, constraints.max.height);
146 constraints.clamp(Size::new(w, h))
147 }
148
149 fn preferred_size(&self) -> (Length, Length) {
150 (self.width, self.height)
151 }
152
153 fn arrange(&mut self, rect: Rectangle) {
154 self.rect = rect;
155 }
156
157 fn rect(&self) -> Rectangle {
158 self.rect
159 }
160
161 fn handle_touch(&mut self, point: Point, phase: TouchPhase) -> Option<M> {
162 match phase {
163 TouchPhase::Down => {
164 self.pressed = self.row_at(point);
165 None
166 }
167 TouchPhase::Moved => {
168 if self.row_at(point) != self.pressed {
169 self.pressed = None;
170 }
171 None
172 }
173 TouchPhase::Up => {
174 let now = self.row_at(point);
175 let pressed = self.pressed.take();
176 if let (Some(i), Some(p)) = (now, pressed) {
177 if i == p {
178 return Some(self.entries[i].message.clone());
179 }
180 }
181 None
182 }
183 }
184 }
185
186 fn mark_pressed(&mut self, point: Point) {
187 if self.pressed.is_none() {
189 self.pressed = self.row_at(point);
190 }
191 }
192
193 fn collect_focusable(&self, out: &mut Vec<WidgetId>) {
194 for index in 0..self.entries.len() {
195 if let Some(id) = self.row_id(index) {
196 out.push(id);
197 }
198 }
199 }
200
201 fn sync_focus(&mut self, focused: Option<WidgetId>) {
202 self.focused = focused.and_then(|target| {
203 (0..self.entries.len()).find(|index| self.row_id(*index) == Some(target))
204 });
205 }
206
207 fn route_action(&mut self, target: WidgetId, action: UiAction) -> Option<M> {
208 let index = (0..self.entries.len()).find(|index| self.row_id(*index) == Some(target))?;
209 match action {
210 UiAction::Activate => Some(self.entries[index].message.clone()),
211 _ => None,
212 }
213 }
214
215 fn focus_rect(&self, target: WidgetId) -> Option<Rectangle> {
216 let index =
217 (0..self.entries.len()).find(|candidate| self.row_id(*candidate) == Some(target))?;
218 Some(self.row_rect(index))
219 }
220
221 fn focus_at(&self, point: Point) -> Option<WidgetId> {
222 self.row_at(point).and_then(|index| self.row_id(index))
223 }
224
225 fn draw<'t>(
226 &self,
227 renderer: &mut dyn Renderer<C>,
228 theme: &Theme<'t, C>,
229 ) -> Result<(), RenderError> {
230 let font = theme.default_font();
231 let glyph_h = font.character_size.height as i32;
232 for (i, e) in self.entries.iter().enumerate() {
233 let r = self.row_rect(i);
234 let (bg, fg, border) = if Some(i) == self.selected {
235 (theme.accent.base, theme.accent.on_base, theme.button.border)
236 } else if Some(i) == self.focused {
237 (theme.button.base, theme.button.on_base, theme.accent.base)
238 } else if Some(i) == self.pressed {
239 (
240 theme.button.pressed,
241 theme.button.on_base,
242 theme.button.border,
243 )
244 } else {
245 (theme.button.base, theme.button.on_base, theme.button.border)
246 };
247 renderer.fill_rect(r, bg)?;
248 renderer.stroke_rect(r, border)?;
249 renderer.draw_text(
250 &e.label,
251 Point::new(
252 r.top_left.x + PAD_X,
253 r.top_left.y + self.row_h as i32 / 2 + glyph_h / 3,
254 ),
255 font,
256 fg,
257 Alignment::Left,
258 )?;
259 }
260 Ok(())
261 }
262}