embedded_menu/
builder.rs

1use crate::{
2    collection::{MenuItemCollection, MenuItems},
3    interaction::{InputAdapterSource, InputState},
4    items::{menu_item::SelectValue, MenuItem, MenuListItem},
5    selection_indicator::{style::IndicatorStyle, SelectionIndicatorController},
6    theme::Theme,
7    Menu, MenuState, MenuStyle, NoItems,
8};
9use core::marker::PhantomData;
10use embedded_layout::{
11    layout::linear::LinearLayout,
12    object_chain::ChainElement,
13    prelude::*,
14    view_group::{EmptyViewGroup, ViewGroup},
15};
16
17pub struct MenuBuilder<T, IT, LL, R, P, S, C>
18where
19    T: AsRef<str>,
20    IT: InputAdapterSource<R>,
21    S: IndicatorStyle,
22    P: SelectionIndicatorController,
23    C: Theme,
24{
25    title: T,
26    items: LL,
27    style: MenuStyle<S, IT, P, R, C>,
28}
29
30impl<T, R, S, IT, P, C> MenuBuilder<T, IT, NoItems, R, P, S, C>
31where
32    T: AsRef<str>,
33    S: IndicatorStyle,
34    IT: InputAdapterSource<R>,
35    P: SelectionIndicatorController,
36    C: Theme,
37{
38    /// Creates a new menu builder with the given title and style.
39    pub const fn new(title: T, style: MenuStyle<S, IT, P, R, C>) -> Self {
40        Self {
41            title,
42            items: NoItems,
43            style,
44        }
45    }
46}
47
48impl<T, IT, R, P, S, C> MenuBuilder<T, IT, NoItems, R, P, S, C>
49where
50    T: AsRef<str>,
51    IT: InputAdapterSource<R>,
52    P: SelectionIndicatorController,
53    S: IndicatorStyle,
54    C: Theme,
55{
56    /// Append a non-selectable menu item to the menu with the given title.
57    pub fn add_section_title<T2: AsRef<str>>(
58        self,
59        title: T2,
60    ) -> MenuBuilder<T, IT, Chain<MenuItem<T2, R, (), false>>, R, P, S, C> {
61        self.add_menu_item(
62            MenuItem::new(title, ())
63                .with_value_converter(|_| unreachable!())
64                .selectable::<false>(),
65        )
66    }
67
68    /// Append an interactable menu item to the menu.
69    ///
70    /// The menu item will initially have the given value. Upon interaction, the value will be
71    /// updated according to the value type's `next` method. The menu will use the callback
72    /// passed in the third argument to convert the value to the global return type.
73    pub fn add_item<T2: AsRef<str>, V: SelectValue>(
74        self,
75        title: T2,
76        value: V,
77        converter: fn(V) -> R,
78    ) -> MenuBuilder<T, IT, Chain<MenuItem<T2, R, V, true>>, R, P, S, C> {
79        self.add_menu_item(MenuItem::new(title, value).with_value_converter(converter))
80    }
81
82    /// Append an arbitrary [`MenuListItem`] implementation to the menu.
83    pub fn add_menu_item<I: MenuListItem<R>>(
84        self,
85        mut item: I,
86    ) -> MenuBuilder<T, IT, Chain<I>, R, P, S, C> {
87        item.set_style(&self.style.text_style());
88
89        MenuBuilder {
90            title: self.title,
91            items: Chain::new(item),
92            style: self.style,
93        }
94    }
95
96    /// Append multiple arbitrary [`MenuListItem`] implementations to the menu.
97    pub fn add_menu_items<I, IC>(
98        self,
99        mut items: IC,
100    ) -> MenuBuilder<T, IT, Chain<MenuItems<IC, I, R>>, R, P, S, C>
101    where
102        I: MenuListItem<R>,
103        IC: AsRef<[I]> + AsMut<[I]>,
104    {
105        items
106            .as_mut()
107            .iter_mut()
108            .for_each(|i| i.set_style(&self.style.text_style()));
109
110        MenuBuilder {
111            title: self.title,
112            items: Chain::new(MenuItems::new(items)),
113            style: self.style,
114        }
115    }
116}
117
118impl<T, IT, CE, R, P, S, C> MenuBuilder<T, IT, CE, R, P, S, C>
119where
120    T: AsRef<str>,
121    IT: InputAdapterSource<R>,
122    CE: MenuItemCollection<R> + ChainElement,
123    P: SelectionIndicatorController,
124    S: IndicatorStyle,
125    C: Theme,
126{
127    /// Append a non-selectable menu item to the menu with the given title.
128    pub fn add_section_title<T2: AsRef<str>>(
129        self,
130        title: T2,
131    ) -> MenuBuilder<T, IT, Link<MenuItem<T2, R, (), false>, CE>, R, P, S, C> {
132        self.add_menu_item(
133            MenuItem::new(title, ())
134                .with_value_converter(|_| unreachable!())
135                .selectable::<false>(),
136        )
137    }
138
139    /// Append an interactable menu item to the menu.
140    ///
141    /// The menu item will initially have the given value. Upon interaction, the value will be
142    /// updated according to the value type's `next` method. The menu will use the callback
143    /// passed in the third argument to convert the value to the global return type.
144    pub fn add_item<T2: AsRef<str>, V: SelectValue>(
145        self,
146        title: T2,
147        value: V,
148        converter: fn(V) -> R,
149    ) -> MenuBuilder<T, IT, Link<MenuItem<T2, R, V, true>, CE>, R, P, S, C> {
150        self.add_menu_item(MenuItem::new(title, value).with_value_converter(converter))
151    }
152
153    /// Append an arbitrary [`MenuListItem`] implementation to the menu.
154    pub fn add_menu_item<I: MenuListItem<R>>(
155        self,
156        mut item: I,
157    ) -> MenuBuilder<T, IT, Link<I, CE>, R, P, S, C> {
158        item.set_style(&self.style.text_style());
159
160        MenuBuilder {
161            title: self.title,
162            items: Link {
163                parent: self.items,
164                object: item,
165            },
166            style: self.style,
167        }
168    }
169
170    /// Append multiple arbitrary [`MenuListItem`] implementations to the menu.
171    pub fn add_menu_items<I, IC>(
172        self,
173        mut items: IC,
174    ) -> MenuBuilder<T, IT, Link<MenuItems<IC, I, R>, CE>, R, P, S, C>
175    where
176        I: MenuListItem<R>,
177        IC: AsRef<[I]> + AsMut<[I]>,
178    {
179        items
180            .as_mut()
181            .iter_mut()
182            .for_each(|i| i.set_style(&self.style.text_style()));
183
184        MenuBuilder {
185            title: self.title,
186            items: Link {
187                parent: self.items,
188                object: MenuItems::new(items),
189            },
190            style: self.style,
191        }
192    }
193}
194
195impl<T, IT, VG, R, P, S, C> MenuBuilder<T, IT, VG, R, P, S, C>
196where
197    T: AsRef<str>,
198    IT: InputAdapterSource<R>,
199    VG: ViewGroup + MenuItemCollection<R>,
200    P: SelectionIndicatorController,
201    S: IndicatorStyle,
202    C: Theme,
203{
204    /// Builds the menu and initializes it to a default state.
205    pub fn build(self) -> Menu<T, IT, VG, R, P, S, C> {
206        self.build_with_state(MenuState {
207            selected: 0,
208            list_offset: 0,
209            interaction_state: Default::default(),
210            indicator_state: Default::default(),
211            last_input_state: InputState::Idle,
212        })
213    }
214
215    /// Builds the menu, assigning to it the given state.
216    pub fn build_with_state(
217        mut self,
218        mut state: MenuState<IT::InputAdapter, P, S>,
219    ) -> Menu<T, IT, VG, R, P, S, C> {
220        // We have less menu items than before. Avoid crashing.
221        let max_idx = self.items.count().saturating_sub(1);
222
223        LinearLayout::vertical(EmptyViewGroup).arrange_view_group(&mut self.items);
224
225        state.set_selected_item(state.selected, &self.items, &self.style);
226        if max_idx < state.selected {
227            self.style
228                .indicator
229                .jump_to_target(&mut state.indicator_state);
230        }
231
232        Menu {
233            state,
234            _return_type: PhantomData,
235            title: self.title,
236            items: self.items,
237            style: self.style,
238        }
239    }
240}