tray_controls/lib.rs
1use std::collections::HashMap;
2use std::hash::Hash;
3use std::rc::Rc;
4
5use tray_icon::menu::{CheckMenuItem, IconMenuItem, MenuId, MenuItem};
6
7type DefaultMenuId = MenuId;
8
9/// Represents different types of checkable menu items with their associated data
10///
11/// This enum defines three types of checkable menu items:
12///
13/// ## Variants
14///
15/// ### `CheckBox`
16/// - Contains: `Rc<CheckMenuItem>` and group identifier `G`
17/// - Purpose: A standard checkbox that can be checked/unchecked independently
18/// - Grouping: Items with the same `G` value belong to the same logical group
19///
20/// ### `Radio`
21/// - Contains: `Rc<CheckMenuItem>`, default `MenuId`, and group identifier `G`
22/// - Purpose: A radio button where only one item in the same group can be selected
23/// - Default ID: Specifies which menu should be selected when no radio in the group is checked
24/// - Grouping: All radio buttons with the same `G` value form a single selection group
25///
26/// ### `Separate`
27/// - Contains: `Rc<CheckMenuItem>` only
28/// - Purpose: A standalone checkbox with no grouping requirements
29/// - Use case: For independent toggle options that don't belong to any group
30///
31/// ## Type Parameters
32///
33/// - `G`: Group identifier type for organizing related checkable items
34/// - Used by both `CheckBox` and `Radio` variants
35/// - Must implement `Clone` (for storing in the manager)
36/// - Typically use `&'static str` or enum variants for type safety
37///
38/// ## Example
39///
40/// ```
41/// use std::rc::Rc;
42/// use tray_controls::CheckMenuKind;
43/// use tray_icon::menu::{CheckMenuItem, MenuId};
44///
45/// // Create a checkbox belonging to "display_group" group
46/// let checkbox = CheckMenuItem::with_id("show_toolbar", "Show Toolbar", true, false, None);
47/// let check_kind = CheckMenuKind::CheckBox(Rc::new(checkbox), "display_group");
48///
49/// // Create a radio button in "theme_group" group with default selection
50/// let radio = CheckMenuItem::with_id("light_theme", "Light Theme", true, true, None);
51/// let radio_kind = CheckMenuKind::Radio(
52/// Rc::new(radio),
53/// Rc::new(MenuId::new("light_theme")),
54/// "theme_group"
55/// );
56///
57/// // Create a standalone checkbox
58/// let separate = CheckMenuItem::new("Auto-save", true, true, None);
59/// let separate_kind: CheckMenuKind<&str> = CheckMenuKind::Separate(Rc::new(separate));
60/// ```
61#[derive(Clone)]
62pub enum CheckMenuKind<G> {
63 CheckBox(Rc<CheckMenuItem>, G),
64 Radio(
65 Rc<CheckMenuItem>,
66 /* Default Radio Menu ID*/ Rc<DefaultMenuId>,
67 G,
68 ),
69 Separate(Rc<CheckMenuItem>),
70}
71
72#[derive(Clone)]
73pub enum MenuControl<G> {
74 MenuItem(MenuItem),
75 IconMenu(IconMenuItem),
76 CheckMenu(CheckMenuKind<G>),
77}
78
79impl<G> MenuControl<G> {
80 pub fn id(&self) -> &MenuId {
81 match self {
82 MenuControl::MenuItem(menu_item) => menu_item.id(),
83 MenuControl::IconMenu(icon_menu) => icon_menu.id(),
84 MenuControl::CheckMenu(check_menu_kind) => match check_menu_kind {
85 CheckMenuKind::CheckBox(check_menu, _)
86 | CheckMenuKind::Radio(check_menu, _, _)
87 | CheckMenuKind::Separate(check_menu) => check_menu.id(),
88 },
89 }
90 }
91
92 pub fn text(&self) -> String {
93 match self {
94 MenuControl::MenuItem(menu_item) => menu_item.text(),
95 MenuControl::IconMenu(icon_menu) => icon_menu.text(),
96 MenuControl::CheckMenu(check_menu_kind) => match check_menu_kind {
97 CheckMenuKind::CheckBox(check_menu, _)
98 | CheckMenuKind::Radio(check_menu, _, _)
99 | CheckMenuKind::Separate(check_menu) => check_menu.text(),
100 },
101 }
102 }
103
104 pub fn as_menu_item(&self) -> Option<&MenuItem> {
105 match self {
106 MenuControl::MenuItem(menu_item) => Some(menu_item),
107 _ => None,
108 }
109 }
110
111 pub fn as_icon_menu(&self) -> Option<&IconMenuItem> {
112 match self {
113 MenuControl::IconMenu(icon_menu) => Some(icon_menu),
114 _ => None,
115 }
116 }
117
118 pub fn as_check_menu(&self) -> Option<&CheckMenuItem> {
119 if let MenuControl::CheckMenu(check_menu) = self {
120 let check_menu = match check_menu {
121 CheckMenuKind::CheckBox(check_menu, _)
122 | CheckMenuKind::Radio(check_menu, _, _)
123 | CheckMenuKind::Separate(check_menu) => check_menu,
124 };
125 Some(check_menu)
126 } else {
127 None
128 }
129 }
130}
131
132/// Menu manager that provides centralized menu item management and group state handling
133///
134/// Core features:
135/// 1. **Menu storage**: Unified storage for `MenuItem`, `IconMenuItem`, and `CheckMenuItem`
136/// 2. **Group management**: Organizes checkbox and radio button groups, ensuring proper radio button logic
137/// 3. **Easy access**: Quick access to menu items and their properties via ID
138/// 4. **State synchronization**: Automatically updates other buttons in radio groups when one is selected
139///
140/// The type parameter `G` represents the group identifier for Radio and CheckBox menu items.
141/// Must implement: `Clone + Eq + Hash + PartialEq`
142/// Recommended to use enums or string constants for type safety and readability.
143///
144/// # Example
145/// ```
146/// use std::rc::Rc;
147/// use tray_controls::{CheckMenuKind, MenuControl, MenuManager};
148/// use tray_icon::menu::{CheckMenuItem, MenuId};
149///
150/// let mut manager = MenuManager::<&str>::new();
151///
152/// // Add a checkbox with group ID "display_group"
153/// let checkbox = CheckMenuItem::with_id("show_toolbar", "Show Toolbar", true, true, None);
154/// manager.insert(MenuControl::CheckMenu(
155/// CheckMenuKind::CheckBox(Rc::new(checkbox), "display_group")
156/// ));
157///
158/// // Add radio buttons with group ID "color_group"
159/// let radio = CheckMenuItem::with_id("red", "Red", true, true, None);
160/// manager.insert(MenuControl::CheckMenu(
161/// CheckMenuKind::Radio(Rc::new(radio), Rc::new(MenuId::new("radio default id")), "color_group")
162/// ));
163///
164/// // Handle menu clicks - radio groups are automatically synchronized
165/// let click_menu_id = MenuId::new("");
166///
167/// manager.update(&click_menu_id, |menu| {
168/// if let Some(menu) = menu {
169/// println!("Clicked menu: {}", menu.text());
170/// }
171/// });
172/// ```
173///
174/// # Example
175/// ```
176/// use std::rc::Rc;
177/// use tray_controls::{CheckMenuKind, MenuControl, MenuManager};
178/// use tray_icon::menu::{CheckMenuItem, MenuId};
179///
180/// #[derive(Clone, Eq, Hash, PartialEq)]
181/// enum MenuGroup {
182/// CheckBoxDisplay,
183/// RadioColor,
184/// }
185///
186/// let mut manager = MenuManager::<MenuGroup>::new();
187///
188/// // Add a checkbox with group ID "CheckBoxDisplay"
189/// let checkbox = CheckMenuItem::with_id("show_toolbar", "Show Toolbar", true, true, None);
190/// manager.insert(MenuControl::CheckMenu(
191/// CheckMenuKind::CheckBox(Rc::new(checkbox), MenuGroup::CheckBoxDisplay)
192/// ));
193///
194/// // Add radio buttons with group ID "RadioColor", and set the default radio menu ID
195/// let radio = CheckMenuItem::with_id("red", "Red", true, true, None);
196/// manager.insert(MenuControl::CheckMenu(
197/// CheckMenuKind::Radio(Rc::new(radio), Rc::new(MenuId::new("red")), MenuGroup::RadioColor)
198/// ));
199///
200/// // Handle menu clicks - radio groups are automatically synchronized
201/// let click_menu_id = MenuId::new("");
202///
203/// manager.update(&click_menu_id, |menu| {
204/// if let Some(menu) = menu {
205/// println!("Clicked menu: {}", menu.text());
206/// }
207/// });
208/// ```
209#[derive(Clone)]
210pub struct MenuManager<G>
211where
212 G: Clone + Eq + Hash + PartialEq,
213{
214 id_to_menu: HashMap<Rc<MenuId>, MenuControl<G>>,
215 grouped_check_items: HashMap<G, HashMap<Rc<MenuId>, Rc<CheckMenuItem>>>,
216}
217
218impl<G> Default for MenuManager<G>
219where
220 G: Clone + Eq + Hash + PartialEq,
221{
222 fn default() -> Self {
223 Self::new()
224 }
225}
226
227impl<G> MenuManager<G>
228where
229 G: Clone + Eq + Hash + PartialEq,
230{
231 pub fn new() -> Self {
232 MenuManager {
233 id_to_menu: HashMap::new(),
234 grouped_check_items: HashMap::new(),
235 }
236 }
237
238 /// Inserts a menu control from the menu manager.
239 pub fn insert(&mut self, menu_control: MenuControl<G>) {
240 match &menu_control {
241 MenuControl::MenuItem(menu_item) => {
242 self.id_to_menu
243 .insert(Rc::new(menu_item.id().clone()), menu_control);
244 }
245 MenuControl::IconMenu(icon_menu) => {
246 self.id_to_menu
247 .insert(Rc::new(icon_menu.id().clone()), menu_control);
248 }
249 MenuControl::CheckMenu(check_menu_mind) => match check_menu_mind {
250 CheckMenuKind::Separate(check_menu) => {
251 self.id_to_menu
252 .insert(Rc::new(check_menu.id().clone()), menu_control);
253 }
254 CheckMenuKind::Radio(check_menu, _default_menu_id, menu_group) => {
255 let menu_id = Rc::new(check_menu.id().clone());
256 let menu_group = menu_group.clone();
257 let check_menu = check_menu.clone();
258
259 self.id_to_menu.insert(menu_id.clone(), menu_control);
260 self.grouped_check_items
261 .entry(menu_group)
262 .or_default()
263 .insert(menu_id, check_menu);
264 }
265 CheckMenuKind::CheckBox(check_menu, menu_group) => {
266 let menu_id = Rc::new(check_menu.id().clone());
267 let menu_group = menu_group.clone();
268 let check_menu = check_menu.clone();
269
270 self.id_to_menu.insert(menu_id.clone(), menu_control);
271 self.grouped_check_items
272 .entry(menu_group)
273 .or_default()
274 .insert(menu_id, check_menu);
275 }
276 },
277 }
278 }
279
280 /// Removes a menu control from the menu manager.
281 pub fn remove(&mut self, menu_id: &MenuId) {
282 let remove_menu = self.id_to_menu.remove(menu_id);
283
284 if let Some(remove_menu) = remove_menu {
285 match &remove_menu {
286 MenuControl::MenuItem(_) | MenuControl::IconMenu(_) => {}
287 MenuControl::CheckMenu(check_menu_kind) => match check_menu_kind {
288 CheckMenuKind::Separate(_) => {}
289 CheckMenuKind::CheckBox(_, group) | CheckMenuKind::Radio(_, _, group) => {
290 if let Some(map) = self.grouped_check_items.get_mut(group) {
291 map.remove(menu_id);
292 }
293 }
294 },
295 }
296 }
297 }
298
299 /// Updates the menu control state based on the provided menu ID, and callback the menu control.
300 ///
301 /// If the menu control is a radio, it ensures that only one item in the group is checked, and callbakc the cheked menu control.
302 pub fn update(&mut self, menu_id: &MenuId, callback: impl Fn(Option<&MenuControl<G>>)) {
303 let menu_control = self.id_to_menu.get(menu_id);
304
305 if let Some(menu) = menu_control {
306 match menu {
307 MenuControl::MenuItem(_) | MenuControl::IconMenu(_) => {}
308 MenuControl::CheckMenu(check_menu_kind) => match check_menu_kind {
309 CheckMenuKind::CheckBox(_, _) | CheckMenuKind::Separate(_) => {}
310 CheckMenuKind::Radio(check_menu, default_menu_id, group) => {
311 if let Some(check_menus) = self.get_check_items_from_grouped(group) {
312 let click_menu_state = check_menu.is_checked();
313
314 let (is_checked_menu_id, is_checked_menu) = if click_menu_state {
315 (check_menu.id(), Some(menu))
316 } else {
317 let default_menu = self.get_menu_item_from_id(default_menu_id);
318
319 if let Some(MenuControl::CheckMenu(CheckMenuKind::Radio(
320 menu,
321 _,
322 _,
323 ))) = default_menu
324 {
325 menu.set_checked(true);
326 (default_menu_id.as_ref(), default_menu)
327 } else {
328 return callback(menu_control);
329 }
330 };
331
332 check_menus
333 .iter()
334 .filter(|(menu_id, _)| menu_id.as_ref().ne(is_checked_menu_id))
335 .for_each(|(_, check_menu)| check_menu.set_checked(false));
336
337 return callback(is_checked_menu);
338 }
339 }
340 },
341 }
342 }
343
344 callback(menu_control);
345 }
346
347 /// Gets a menu control from the menu manager based on the provided menu ID.
348 pub fn get_menu_item_from_id(&self, menu_id: &MenuId) -> Option<&MenuControl<G>> {
349 self.id_to_menu.get(menu_id)
350 }
351
352 /// Gets grouped check menu items from the menu manager based on the provided menu group id.
353 pub fn get_check_items_from_grouped(
354 &self,
355 group_id: &G,
356 ) -> Option<&HashMap<Rc<MenuId>, Rc<CheckMenuItem>>> {
357 self.grouped_check_items.get(group_id)
358 }
359}