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}