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