Skip to main content

tray_controls/
lib.rs

1use std::collections::{HashMap, HashSet};
2use std::hash::Hash;
3use std::ops::Not;
4use std::rc::Rc;
5
6use getset::Getters;
7use tray_icon::menu::{CheckMenuItem, MenuId, MenuItemKind};
8
9#[derive(Clone, Getters)]
10#[getset(get = "pub")]
11pub struct MenuItemMeta<G> {
12    kind: MenuItemKind,
13    group: Option<G>,
14}
15
16impl<G> PartialEq for MenuItemMeta<G>
17where
18    G: PartialEq,
19{
20    fn eq(&self, other: &Self) -> bool {
21        self.group == other.group && self.kind.id() == other.kind.id()
22    }
23}
24
25#[derive(Clone, Getters)]
26#[getset(get = "pub")]
27struct RadioGroup {
28    members: HashSet<Rc<MenuId>>,
29    default: Option<MenuId>,
30}
31
32#[derive(Clone)]
33pub struct MenuRegistry<G>
34where
35    G: Clone + Copy + Eq + Hash + PartialEq + std::fmt::Debug,
36{
37    items: HashMap<Rc<MenuId>, MenuItemMeta<G>>,
38    radio_groups: HashMap<G, RadioGroup>,
39    checkbox_groups: HashMap<G, HashSet<Rc<MenuId>>>,
40}
41
42impl<G> Default for MenuRegistry<G>
43where
44    G: Clone + Copy + Eq + Hash + PartialEq + std::fmt::Debug,
45{
46    fn default() -> Self {
47        Self::new()
48    }
49}
50
51impl<G> MenuRegistry<G>
52where
53    G: Clone + Copy + Eq + Hash + PartialEq + std::fmt::Debug,
54{
55    pub fn new() -> Self {
56        Self {
57            items: HashMap::new(),
58            radio_groups: HashMap::new(),
59            checkbox_groups: HashMap::new(),
60        }
61    }
62
63    pub fn register_normal(&mut self, kind: MenuItemKind) {
64        let id = Rc::new(kind.id().clone());
65        self.items.insert(id, MenuItemMeta { kind, group: None });
66    }
67
68    pub fn register_checkbox(&mut self, kind: MenuItemKind, group: G) -> bool {
69        if kind.as_check_menuitem().is_none() {
70            return false;
71        }
72
73        let id = Rc::new(kind.id().clone());
74
75        self.items.insert(
76            id.clone(),
77            MenuItemMeta {
78                kind,
79                group: Some(group),
80            },
81        );
82
83        self.checkbox_groups.entry(group).or_default().insert(id);
84
85        true
86    }
87
88    pub fn register_radio(
89        &mut self,
90        kind: MenuItemKind,
91        group: G,
92        default: Option<MenuId>,
93    ) -> bool {
94        if kind.as_check_menuitem().is_none() {
95            return false;
96        }
97
98        let id = Rc::new(kind.id().clone());
99
100        self.items.insert(
101            id.clone(),
102            MenuItemMeta {
103                kind,
104                group: Some(group),
105            },
106        );
107
108        self.radio_groups
109            .entry(group)
110            .or_insert_with(|| RadioGroup {
111                members: HashSet::new(),
112                default,
113            })
114            .members
115            .insert(id);
116
117        true
118    }
119
120    pub fn deregister_normal(&mut self, id: &MenuId) -> bool {
121        self.items.remove(id).is_some()
122    }
123
124    pub fn deregister_checkbox(&mut self, id: &MenuId, group: G) -> bool {
125        self.items.remove(id);
126
127        self.checkbox_groups
128            .get_mut(&group)
129            .map(|checkbox_group| checkbox_group.remove(id))
130            .unwrap_or_default()
131    }
132
133    pub fn deregister_radio(&mut self, id: &MenuId, group: G) -> bool {
134        self.items.remove(id);
135
136        self.radio_groups
137            .get_mut(&group)
138            .map(|radio_group| radio_group.members.remove(id))
139            .unwrap_or_default()
140    }
141
142    pub fn handle_event(&mut self, id: &MenuId) -> Result<&MenuItemMeta<G>, String> {
143        let menu_item_meta = self
144            .items
145            .get(id)
146            .ok_or_else(|| format!("The menu not found: {id:?}"))?;
147
148        let menu_group = menu_item_meta.group();
149
150        let menu_kind = &menu_item_meta.kind();
151
152        // Clicked menu is not in any group, return directly
153        let Some(menu_group) = menu_group else {
154            return Ok(menu_item_meta);
155        };
156
157        // Clicked menu is not in any [Radio] group, return directly
158        let Some(radio_group) = self.radio_groups.get(menu_group) else {
159            if self.checkbox_groups.contains_key(menu_group)
160                && menu_kind.as_check_menuitem().is_some().not()
161            {
162                return Err(format!(
163                    "Menu({id:?}) is not a [CheckMenuItem] on the checkbox group({menu_group:?})"
164                ));
165            }
166
167            return Ok(menu_item_meta);
168        };
169
170        // <---Handle [Radio] menu--->
171        let radio_menus_id = radio_group.members();
172
173        let clickd_radio_menu_is_checked = menu_kind
174            .as_check_menuitem()
175            .ok_or_else(|| {
176                format!("Menu({id:?}) is not a [CheckMenuItem] on the radio group({menu_group:?})")
177            })?
178            .is_checked();
179
180        // Clicked menu is selected, deselect other raodio menus
181        if clickd_radio_menu_is_checked {
182            radio_menus_id
183                .iter()
184                .filter(|menu_id| menu_id.as_ref().ne(&id))
185                .filter_map(|id| self.items.get(id))
186                .filter_map(|menu_meta| menu_meta.kind().as_check_menuitem())
187                .for_each(|check_menu| check_menu.set_checked(false));
188
189            Ok(menu_item_meta)
190        // Clicked menu is not selected, check if there is a default menu in the group
191        } else {
192            let Some(default_menu_id) = radio_group.default().as_ref() else {
193                // No default menu, return and uncheck all menus
194                self.get_radio_menu_from_group(menu_group)
195                    .ok_or_else(|| format!("Failed to get radio menus from {menu_group:?}"))?
196                    .iter()
197                    .for_each(|check_menu| check_menu.set_checked(false));
198
199                return Ok(menu_item_meta);
200            };
201
202            let default_menu_meta = self
203                .items
204                .get(default_menu_id)
205                .ok_or_else(|| format!("Default menu({default_menu_id:?}) meta not found"))?;
206
207            let default_menu_item = default_menu_meta.kind().as_check_menuitem()
208                .ok_or_else(|| format!("Default Menu({default_menu_id:?}) is not a [CheckMenuItem] on the radio group({menu_group:?})"))?;
209
210            // Uncheck all other menus except the default menu
211            default_menu_item.set_checked(true);
212            radio_menus_id
213                .iter()
214                .filter(|menu_id| menu_id.as_ref().ne(&default_menu_id))
215                .filter_map(|id| self.items.get(id))
216                .filter_map(|menu_meta| menu_meta.kind().as_check_menuitem())
217                .for_each(|check_menu| check_menu.set_checked(false));
218
219            Ok(default_menu_meta)
220        }
221    }
222
223    pub fn get_menu_meta_from_id(&self, id: &MenuId) -> Option<&MenuItemMeta<G>> {
224        self.items.get(id)
225    }
226
227    pub fn get_menu_kind_from_id(&self, id: &MenuId) -> Option<&MenuItemKind> {
228        self.items.get(id).map(|meta| &meta.kind)
229    }
230
231    pub fn get_menu_group_from_id(&self, id: &MenuId) -> Option<G> {
232        self.items.get(id).and_then(|meta| meta.group)
233    }
234
235    pub fn get_checkbox_id_from_group(&self, group: G) -> Option<&HashSet<Rc<MenuId>>> {
236        self.checkbox_groups.get(&group)
237    }
238
239    pub fn get_checkbox_menu_from_group(&self, group: G) -> Option<Vec<&CheckMenuItem>> {
240        self.get_checkbox_id_from_group(group).map(|ids| {
241            ids.iter()
242                .filter_map(|id| self.items.get(id))
243                .filter_map(|meta| meta.kind.as_check_menuitem())
244                .collect::<Vec<_>>()
245        })
246    }
247
248    pub fn get_radio_id_from_group(&self, group: &G) -> Option<&HashSet<Rc<MenuId>>> {
249        self.radio_groups.get(group).map(|r| r.members())
250    }
251
252    pub fn get_radio_menu_from_group(&self, group: &G) -> Option<Vec<&CheckMenuItem>> {
253        self.get_radio_id_from_group(group).map(|ids| {
254            ids.iter()
255                .filter_map(|id| self.items.get(id))
256                .filter_map(|meta| meta.kind.as_check_menuitem())
257                .collect::<Vec<_>>()
258        })
259    }
260}