floem/
menu.rs

1use std::sync::atomic::AtomicU64;
2
3/// An entry in a menu.
4///
5/// An entry is either a [`MenuItem`], a submenu (i.e. [`Menu`]).
6pub enum MenuEntry {
7    Separator,
8    Item(MenuItem),
9    SubMenu(Menu),
10}
11
12pub struct Menu {
13    pub(crate) popup: bool,
14    pub(crate) item: MenuItem,
15    pub(crate) children: Vec<MenuEntry>,
16}
17
18impl From<Menu> for MenuEntry {
19    fn from(m: Menu) -> MenuEntry {
20        MenuEntry::SubMenu(m)
21    }
22}
23
24impl Menu {
25    pub fn new(title: impl Into<String>) -> Self {
26        Self {
27            popup: false,
28            item: MenuItem::new(title),
29            children: Vec::new(),
30        }
31    }
32
33    pub(crate) fn popup(mut self) -> Self {
34        self.popup = true;
35        self
36    }
37
38    /// Append a menu entry to this menu, returning the modified menu.
39    pub fn entry(mut self, entry: impl Into<MenuEntry>) -> Self {
40        self.children.push(entry.into());
41        self
42    }
43
44    /// Append a separator to this menu, returning the modified menu.
45    pub fn separator(self) -> Self {
46        self.entry(MenuEntry::Separator)
47    }
48
49    #[cfg(any(target_os = "windows", target_os = "macos"))]
50    pub(crate) fn platform_menu(&self) -> muda::Menu {
51        let menu = muda::Menu::new();
52        for entry in &self.children {
53            match entry {
54                MenuEntry::Separator => {
55                    let _ = menu.append(&muda::PredefinedMenuItem::separator());
56                }
57                MenuEntry::Item(item) => {
58                    let _ = menu.append(&muda::MenuItem::with_id(
59                        item.id.clone(),
60                        item.title.clone(),
61                        item.enabled,
62                        None,
63                    ));
64                }
65                MenuEntry::SubMenu(floem_menu) => {
66                    let _ = menu.append(&floem_menu.platform_submenu());
67                }
68            }
69        }
70        menu
71    }
72
73    #[cfg(any(target_os = "windows", target_os = "macos"))]
74    pub(crate) fn platform_submenu(&self) -> muda::Submenu {
75        let menu = muda::Submenu::new(self.item.title.clone(), self.item.enabled);
76        for entry in &self.children {
77            match entry {
78                MenuEntry::Separator => {
79                    let _ = menu.append(&muda::PredefinedMenuItem::separator());
80                }
81                MenuEntry::Item(item) => {
82                    let _ = menu.append(&muda::MenuItem::with_id(
83                        item.id.clone(),
84                        item.title.clone(),
85                        item.enabled,
86                        None,
87                    ));
88                }
89                MenuEntry::SubMenu(floem_menu) => {
90                    let _ = menu.append(&floem_menu.platform_submenu());
91                }
92            }
93        }
94        menu
95    }
96}
97
98pub struct MenuItem {
99    pub(crate) id: String,
100    pub(crate) title: String,
101    pub(crate) enabled: bool,
102    pub(crate) action: Option<Box<dyn Fn()>>,
103}
104
105impl From<MenuItem> for MenuEntry {
106    fn from(i: MenuItem) -> MenuEntry {
107        MenuEntry::Item(i)
108    }
109}
110
111impl MenuItem {
112    pub fn new(title: impl Into<String>) -> Self {
113        static COUNTER: AtomicU64 = AtomicU64::new(0);
114        let id = COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
115        Self {
116            id: id.to_string(),
117            title: title.into(),
118            enabled: true,
119            action: None,
120        }
121    }
122
123    pub fn action(mut self, action: impl Fn() + 'static) -> Self {
124        self.action = Some(Box::new(action));
125        self
126    }
127
128    pub fn enabled(mut self, enabled: bool) -> Self {
129        self.enabled = enabled;
130        self
131    }
132}