cycle_menu/
lib.rs

1#![cfg_attr(not(feature = "std"), no_std)]
2
3#[cfg(test)]
4mod tests;
5
6use core::cell::RefCell;
7
8#[cfg(not(feature = "std"))]
9mod types {
10    pub type ActionFunc<'a> = &'a (dyn Fn() + 'a + Send);
11    pub type ItemList<'a> = &'a [super::Item<'a>];
12    pub type DispFunc<'a> = &'a (dyn Fn(Name) + 'a + Send);
13    pub type Name = &'static str;
14}
15#[cfg(feature = "std")]
16mod types {
17    pub type ActionFunc<'a> = Box<dyn Fn() + 'a + Send>;
18    pub type ItemList<'a> = Vec<super::Item<'a>>;
19    pub type DispFunc<'a> = Box<dyn Fn(&str) + 'a + Send>;
20    pub type Name = String;
21}
22
23pub struct Action<'a> {
24    name: types::Name,
25    f: types::ActionFunc<'a>,
26    #[cfg(feature = "std")]
27    show_active: Option<Box<dyn Fn() -> bool + 'a + Send>>,
28}
29pub struct SubMenu<'a> {
30    name: types::Name,
31    position: RefCell<u8>,
32    items: types::ItemList<'a>,
33    #[cfg(feature = "std")]
34    show_active: Option<Box<dyn Fn() -> bool + 'a + Send>>,
35}
36
37impl SubMenu<'_> {
38    fn get_text(&self) -> types::Name {
39        self.get_item().get_name()
40    }
41
42    fn get_item(&self) -> &Item {
43        if *self.position.borrow() >= self.items.len() as u8 {
44            &Item::Back
45        } else {
46            &self.items[*self.position.borrow() as usize]
47        }
48    }
49
50    fn go_next(&self, no_back: bool) {
51        let next_pos = if no_back {
52            (*self.position.borrow() + 1) % self.items.len() as u8
53        } else {
54            (*self.position.borrow() + 1) % (self.items.len() as u8 + 1)
55        };
56        *self.position.borrow_mut() = next_pos;
57    }
58}
59
60pub enum Item<'a> {
61    Action(Action<'a>),
62    SubMenu(SubMenu<'a>),
63    Back,
64}
65
66impl<'a> Item<'a> {
67    #[cfg(not(feature = "std"))]
68    pub fn new_action(name: types::Name, f: types::ActionFunc<'a>) -> Self {
69        Self::Action(Action { name, f })
70    }
71    #[cfg(feature = "std")]
72    pub fn new_action(name: impl Into<types::Name>, f: impl Fn() + 'a + Send) -> Self {
73        Self::Action(Action {
74            name: name.into(),
75            f: Box::new(f),
76            show_active: None,
77        })
78    }
79    pub fn new_submenu(name: impl Into<types::Name>, items: types::ItemList<'a>) -> Self {
80        Self::SubMenu(SubMenu {
81            position: RefCell::new(0),
82            name: name.into(),
83            #[cfg(feature = "std")]
84            show_active: None,
85            items,
86        })
87    }
88    #[cfg(feature = "std")]
89    pub fn show_active(mut self, check_active: impl Fn() -> bool + 'a + Send) -> Self {
90        match &mut self {
91            Item::Action(action) => action.show_active = Some(Box::new(check_active)),
92            Item::SubMenu(sub) => sub.show_active = Some(Box::new(check_active)),
93            Item::Back => {}
94        }
95        self
96    }
97    #[cfg(feature = "std")]
98    fn get_name(&self) -> types::Name {
99        match self {
100            Item::Action(action) => get_text(
101                &action.name,
102                action.show_active.as_ref().is_some_and(|f| f()),
103            ),
104            Item::SubMenu(sub) => {
105                get_text(&sub.name, sub.show_active.as_ref().is_some_and(|f| f()))
106            }
107            Item::Back => "Back".into(),
108        }
109    }
110    #[cfg(not(feature = "std"))]
111    fn get_name(&self) -> types::Name {
112        match self {
113            Item::Action(action) => action.name,
114            Item::SubMenu(sub) => sub.name,
115            Item::Back => "Back".into(),
116        }
117    }
118}
119
120#[cfg(feature = "std")]
121fn get_text(name: &str, active: bool) -> String {
122    if active {
123        format!("* {name} *")
124    } else {
125        name.into()
126    }
127}
128
129pub struct Menu<'a> {
130    root: SubMenu<'a>,
131    depth: u8,
132    disp: types::DispFunc<'a>,
133}
134
135impl<'a> Menu<'a> {
136    #[cfg(not(feature = "std"))]
137    pub fn new(items: types::ItemList<'a>, disp: types::DispFunc<'a>) -> Self {
138        Self::inner_new(items, disp)
139    }
140    #[cfg(feature = "std")]
141    pub fn new(items: types::ItemList<'a>, disp: impl Fn(&str) + 'a + Send) -> Self {
142        Self::inner_new(items, Box::new(disp))
143    }
144
145    fn inner_new(items: types::ItemList<'a>, disp: types::DispFunc<'a>) -> Self {
146        let menu = Self {
147            disp,
148            depth: 0,
149            root: SubMenu {
150                name: "root".into(),
151                position: RefCell::new(0),
152                #[cfg(feature = "std")]
153                show_active: None,
154                items,
155            },
156        };
157        menu.display();
158        menu
159    }
160
161    /// go forward in the menu
162    pub fn skip(&mut self) {
163        let skip_back = self.depth == 0;
164        self.get_submenu().go_next(skip_back);
165        self.display();
166    }
167
168    /// accept current selection
169    pub fn ok(&mut self) {
170        let menu = self.get_submenu();
171        let item = menu.get_item();
172        match item {
173            Item::Action(action) => (action.f)(),
174            Item::SubMenu(_) => self.depth += 1,
175            Item::Back => {
176                *menu.position.borrow_mut() = 0;
177                self.depth -= 1;
178            }
179        }
180
181        self.display();
182    }
183
184    fn display(&self) {
185        let text = self.get_submenu().get_text();
186        (self.disp)(&text);
187    }
188
189    /// get currently active submenu
190    fn get_submenu(&self) -> &SubMenu {
191        let mut menu = &self.root;
192        for _ in 0..self.depth {
193            if let Item::SubMenu(sub) = &menu.items[*menu.position.borrow() as usize] {
194                menu = sub;
195            } else {
196                panic!("attemped to select sub_menu on wrong item");
197            }
198        }
199        menu
200    }
201}