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}