1#![cfg_attr(not(test), no_std)]
2
3pub mod adapters;
4pub mod builder;
5pub mod collection;
6pub mod interaction;
7pub mod items;
8pub mod selection_indicator;
9pub mod theme;
10
11mod margin;
12
13use crate::{
14 builder::MenuBuilder,
15 collection::MenuItemCollection,
16 interaction::{
17 programmed::Programmed, Action, InputAdapter, InputAdapterSource, InputResult, InputState,
18 Interaction, Navigation,
19 },
20 selection_indicator::{
21 style::{line::Line as LineIndicator, IndicatorStyle},
22 AnimatedPosition, Indicator, SelectionIndicatorController, State as IndicatorState,
23 StaticPosition,
24 },
25 theme::Theme,
26};
27use core::marker::PhantomData;
28use embedded_graphics::{
29 draw_target::DrawTarget,
30 geometry::{AnchorPoint, AnchorX, AnchorY},
31 mono_font::{ascii::FONT_6X10, MonoFont, MonoTextStyle},
32 pixelcolor::BinaryColor,
33 prelude::{Dimensions, DrawTargetExt, Point},
34 primitives::{Line, Primitive, PrimitiveStyle, Rectangle},
35 Drawable,
36};
37use embedded_layout::{layout::linear::LinearLayout, prelude::*, view_group::ViewGroup};
38use embedded_text::{
39 style::{HeightMode, TextBoxStyle},
40 TextBox,
41};
42
43pub use embedded_menu_macros::SelectValue;
44
45#[derive(Copy, Clone, Debug)]
46pub enum DisplayScrollbar {
47 Display,
48 Hide,
49 Auto,
50}
51
52#[derive(Copy, Clone, Debug)]
53pub struct MenuStyle<S, IT, P, R, T> {
54 pub(crate) theme: T,
55 pub(crate) scrollbar: DisplayScrollbar,
56 pub(crate) font: &'static MonoFont<'static>,
57 pub(crate) title_font: &'static MonoFont<'static>,
58 pub(crate) input_adapter: IT,
59 pub(crate) indicator: Indicator<P, S>,
60 _marker: PhantomData<R>,
61}
62
63impl<R> Default for MenuStyle<LineIndicator, Programmed, StaticPosition, R, BinaryColor> {
64 fn default() -> Self {
65 Self::new(BinaryColor::On)
66 }
67}
68
69impl<T, R> MenuStyle<LineIndicator, Programmed, StaticPosition, R, T>
70where
71 T: Theme,
72{
73 pub const fn new(theme: T) -> Self {
74 Self {
75 theme,
76 scrollbar: DisplayScrollbar::Auto,
77 font: &FONT_6X10,
78 title_font: &FONT_6X10,
79 input_adapter: Programmed,
80 indicator: Indicator {
81 style: LineIndicator,
82 controller: StaticPosition,
83 },
84 _marker: PhantomData,
85 }
86 }
87}
88
89impl<S, IT, P, R, T> MenuStyle<S, IT, P, R, T>
90where
91 S: IndicatorStyle,
92 IT: InputAdapterSource<R>,
93 P: SelectionIndicatorController,
94 T: Theme,
95{
96 pub const fn with_font(self, font: &'static MonoFont<'static>) -> Self {
97 Self { font, ..self }
98 }
99
100 pub const fn with_title_font(self, title_font: &'static MonoFont<'static>) -> Self {
101 Self { title_font, ..self }
102 }
103
104 pub const fn with_scrollbar_style(self, scrollbar: DisplayScrollbar) -> Self {
105 Self { scrollbar, ..self }
106 }
107
108 pub const fn with_selection_indicator<S2>(
109 self,
110 indicator_style: S2,
111 ) -> MenuStyle<S2, IT, P, R, T>
112 where
113 S2: IndicatorStyle,
114 {
115 MenuStyle {
116 theme: self.theme,
117 scrollbar: self.scrollbar,
118 font: self.font,
119 title_font: self.title_font,
120 input_adapter: self.input_adapter,
121 indicator: Indicator {
122 style: indicator_style,
123 controller: self.indicator.controller,
124 },
125 _marker: PhantomData,
126 }
127 }
128
129 pub const fn with_input_adapter<IT2>(self, input_adapter: IT2) -> MenuStyle<S, IT2, P, R, T>
130 where
131 IT2: InputAdapterSource<R>,
132 {
133 MenuStyle {
134 theme: self.theme,
135 input_adapter,
136 scrollbar: self.scrollbar,
137 font: self.font,
138 title_font: self.title_font,
139 indicator: self.indicator,
140 _marker: PhantomData,
141 }
142 }
143
144 pub const fn with_animated_selection_indicator(
145 self,
146 frames: i32,
147 ) -> MenuStyle<S, IT, AnimatedPosition, R, T> {
148 MenuStyle {
149 theme: self.theme,
150 input_adapter: self.input_adapter,
151 scrollbar: self.scrollbar,
152 font: self.font,
153 title_font: self.title_font,
154 indicator: Indicator {
155 style: self.indicator.style,
156 controller: AnimatedPosition::new(frames),
157 },
158 _marker: PhantomData,
159 }
160 }
161
162 pub fn text_style(&self) -> MonoTextStyle<'static, BinaryColor> {
163 MonoTextStyle::new(self.font, BinaryColor::On)
164 }
165
166 pub fn title_style(&self) -> MonoTextStyle<'static, T::Color> {
167 MonoTextStyle::new(self.title_font, self.theme.text_color())
168 }
169}
170
171pub struct NoItems;
172
173pub struct MenuState<IT, P, S>
174where
175 IT: InputAdapter,
176 P: SelectionIndicatorController,
177 S: IndicatorStyle,
178{
179 selected: usize,
180 list_offset: i32,
181 interaction_state: IT::State,
182 indicator_state: IndicatorState<P, S>,
183 last_input_state: InputState,
184}
185
186impl<IT, P, S> Default for MenuState<IT, P, S>
187where
188 IT: InputAdapter,
189 P: SelectionIndicatorController,
190 S: IndicatorStyle,
191{
192 fn default() -> Self {
193 Self {
194 selected: 0,
195 list_offset: Default::default(),
196 interaction_state: Default::default(),
197 indicator_state: Default::default(),
198 last_input_state: InputState::Idle,
199 }
200 }
201}
202
203impl<IT, P, S> Clone for MenuState<IT, P, S>
204where
205 IT: InputAdapter,
206 P: SelectionIndicatorController,
207 S: IndicatorStyle,
208{
209 fn clone(&self) -> Self {
210 *self
211 }
212}
213
214impl<IT, P, S> Copy for MenuState<IT, P, S>
215where
216 IT: InputAdapter,
217 P: SelectionIndicatorController,
218 S: IndicatorStyle,
219{
220}
221
222impl<IT, P, S> MenuState<IT, P, S>
223where
224 IT: InputAdapter,
225 P: SelectionIndicatorController,
226 S: IndicatorStyle,
227{
228 pub fn reset_interaction(&mut self) {
229 self.interaction_state = Default::default();
230 }
231
232 fn set_selected_item<ITS, R, T>(
233 &mut self,
234 selected: usize,
235 items: &impl MenuItemCollection<R>,
236 style: &MenuStyle<S, ITS, P, R, T>,
237 ) where
238 ITS: InputAdapterSource<R, InputAdapter = IT>,
239 T: Theme,
240 {
241 let selected =
242 Navigation::JumpTo(selected)
243 .calculate_selection(self.selected, items.count(), |i| items.selectable(i));
244 self.selected = selected;
245
246 let selected_offset = items.bounds_of(selected).top_left.y;
247
248 style
249 .indicator
250 .change_selected_item(selected_offset, &mut self.indicator_state);
251 }
252}
253
254pub struct Menu<T, IT, VG, R, P, S, C>
255where
256 T: AsRef<str>,
257 IT: InputAdapterSource<R>,
258 P: SelectionIndicatorController,
259 S: IndicatorStyle,
260 C: Theme,
261{
262 _return_type: PhantomData<R>,
263 title: T,
264 items: VG,
265 style: MenuStyle<S, IT, P, R, C>,
266 state: MenuState<IT::InputAdapter, P, S>,
267}
268
269impl<T, R, S, C> Menu<T, Programmed, NoItems, R, StaticPosition, S, C>
270where
271 T: AsRef<str>,
272 S: IndicatorStyle,
273 C: Theme,
274{
275 pub fn build(title: T) -> MenuBuilder<T, Programmed, NoItems, R, StaticPosition, S, C>
277 where
278 MenuStyle<S, Programmed, StaticPosition, R, C>: Default,
279 {
280 Self::with_style(title, MenuStyle::default())
281 }
282}
283
284impl<T, IT, R, P, S, C> Menu<T, IT, NoItems, R, P, S, C>
285where
286 T: AsRef<str>,
287 S: IndicatorStyle,
288 IT: InputAdapterSource<R>,
289 P: SelectionIndicatorController,
290 C: Theme,
291{
292 pub fn with_style(
294 title: T,
295 style: MenuStyle<S, IT, P, R, C>,
296 ) -> MenuBuilder<T, IT, NoItems, R, P, S, C> {
297 MenuBuilder::new(title, style)
298 }
299}
300
301impl<T, IT, VG, R, P, S, C> Menu<T, IT, VG, R, P, S, C>
302where
303 T: AsRef<str>,
304 IT: InputAdapterSource<R>,
305 VG: MenuItemCollection<R>,
306 P: SelectionIndicatorController,
307 S: IndicatorStyle,
308 C: Theme,
309{
310 pub fn interact(&mut self, input: <IT::InputAdapter as InputAdapter>::Input) -> Option<R> {
311 let input = self
312 .style
313 .input_adapter
314 .adapter()
315 .handle_input(&mut self.state.interaction_state, input);
316
317 self.state.last_input_state = match input {
318 InputResult::Interaction(_) => InputState::Idle,
319 InputResult::StateUpdate(state) => state,
320 };
321
322 match input {
323 InputResult::Interaction(interaction) => match interaction {
324 Interaction::Navigation(navigation) => {
325 let count = self.items.count();
326 let new_selected =
327 navigation.calculate_selection(self.state.selected, count, |i| {
328 self.items.selectable(i)
329 });
330 if new_selected != self.state.selected {
331 self.state
332 .set_selected_item(new_selected, &self.items, &self.style);
333 }
334 None
335 }
336 Interaction::Action(Action::Select) => {
337 let value = self.items.interact_with(self.state.selected);
338 Some(value)
339 }
340 Interaction::Action(Action::Return(value)) => Some(value),
341 },
342 _ => None,
343 }
344 }
345
346 pub fn state(&self) -> MenuState<IT::InputAdapter, P, S> {
347 self.state
348 }
349}
350
351impl<T, IT, VG, R, P, S, C> Menu<T, IT, VG, R, P, S, C>
352where
353 T: AsRef<str>,
354 R: Copy,
355 IT: InputAdapterSource<R>,
356 VG: MenuItemCollection<R>,
357 C: Theme,
358 P: SelectionIndicatorController,
359 S: IndicatorStyle,
360{
361 pub fn selected_value(&self) -> R {
362 self.items.value_of(self.state.selected)
363 }
364}
365
366impl<T, IT, VG, R, C, P, S> Menu<T, IT, VG, R, P, S, C>
367where
368 T: AsRef<str>,
369 IT: InputAdapterSource<R>,
370 VG: ViewGroup + MenuItemCollection<R>,
371 P: SelectionIndicatorController,
372 S: IndicatorStyle,
373 C: Theme,
374{
375 fn header<'t>(
376 &self,
377 title: &'t str,
378 display_area: Rectangle,
379 ) -> Option<impl View + 't + Drawable<Color = C::Color>>
380 where
381 C: Theme + 't,
382 {
383 if title.is_empty() {
384 return None;
385 }
386
387 let text_style = self.style.title_style();
388 let thin_stroke = PrimitiveStyle::with_stroke(self.style.theme.text_color(), 1);
389 let header = LinearLayout::vertical(
390 Chain::new(TextBox::with_textbox_style(
391 title,
392 display_area,
393 text_style,
394 TextBoxStyle::with_height_mode(HeightMode::FitToText),
395 ))
396 .append(
397 Line::new(
399 display_area.top_left,
400 display_area.anchor_point(AnchorPoint::TopRight),
401 )
402 .into_styled(thin_stroke),
403 ),
404 )
405 .arrange();
406
407 Some(header)
408 }
409
410 fn top_offset(&self) -> i32 {
411 self.style.indicator.offset(&self.state.indicator_state) - self.state.list_offset
412 }
413
414 pub fn update(&mut self, display: &impl Dimensions) {
415 self.style
417 .indicator
418 .update(self.state.last_input_state, &mut self.state.indicator_state);
419
420 let top_distance = self.top_offset();
422
423 let list_offset_change = if top_distance > 0 {
424 let display_area = display.bounding_box();
425 let display_height = display_area.size().height as i32;
426
427 let header_height = if let Some(header) = self.header(self.title.as_ref(), display_area)
428 {
429 header.size().height as i32
430 } else {
431 0
432 };
433
434 let selected_height = MenuItemCollection::bounds_of(&self.items, self.state.selected)
435 .size()
436 .height as i32;
437 let indicator_height = self
438 .style
439 .indicator
440 .item_height(selected_height, &self.state.indicator_state);
441
442 (top_distance + indicator_height + header_height - display_height).max(0)
445 } else {
446 top_distance
448 };
449
450 self.state.list_offset += list_offset_change;
452 }
453}
454
455impl<T, IT, VG, R, C, P, S> Drawable for Menu<T, IT, VG, R, P, S, C>
456where
457 T: AsRef<str>,
458 IT: InputAdapterSource<R>,
459 VG: ViewGroup + MenuItemCollection<R>,
460 P: SelectionIndicatorController,
461 S: IndicatorStyle,
462 C: Theme,
463{
464 type Color = C::Color;
465 type Output = ();
466
467 fn draw<D>(&self, display: &mut D) -> Result<(), D::Error>
468 where
469 D: DrawTarget<Color = C::Color>,
470 {
471 let display_area = display.bounding_box();
472
473 let header = self.header(self.title.as_ref(), display_area);
474 let content_area = if let Some(header) = header {
475 header.draw(display)?;
476 display_area.resized_height(
477 display_area.size().height - header.size().height,
478 AnchorY::Bottom,
479 )
480 } else {
481 display_area
482 };
483
484 let menu_height = content_area.size().height as i32;
485 let list_height = self.items.bounds().size().height as i32;
486
487 let draw_scrollbar = match self.style.scrollbar {
488 DisplayScrollbar::Display => true,
489 DisplayScrollbar::Hide => false,
490 DisplayScrollbar::Auto => list_height > menu_height,
491 };
492
493 let menu_display_area = if draw_scrollbar {
494 let scrollbar_area = content_area.resized_width(2, AnchorX::Right);
495 let thin_stroke = PrimitiveStyle::with_stroke(self.style.theme.text_color(), 1);
496
497 let scale = |value| value * menu_height / list_height;
498
499 let scrollbar_height = scale(menu_height).max(1);
500 let mut scrollbar_display = display.cropped(&scrollbar_area);
501
502 Line::new(Point::new(0, 1), Point::new(0, scrollbar_height))
504 .into_styled(thin_stroke)
505 .translate(Point::new(1, scale(self.state.list_offset)))
506 .draw(&mut scrollbar_display)?;
507
508 content_area.resized_width(
509 content_area.size().width - scrollbar_area.size().width,
510 AnchorX::Left,
511 )
512 } else {
513 content_area
514 };
515
516 let selected_menuitem_height =
517 MenuItemCollection::bounds_of(&self.items, self.state.selected)
518 .size()
519 .height as i32;
520
521 self.style.indicator.draw(
522 selected_menuitem_height,
523 self.top_offset(),
524 self.state.last_input_state,
525 display.cropped(&menu_display_area),
526 &self.items,
527 &self.style,
528 &self.state,
529 )?;
530
531 Ok(())
532 }
533}