# tray-controls
[](LICENSE) [](https://crates.io/crates/tray-controls)
An enhanced menu management utility designed for the **tray-icon** crate.
It provides grouped management for Radio, CheckBox, and other menu item types, making it especially suitable for projects requiring single-selection menus (Radio) and complex tray menu systems.
# Features
## 🎯 Core Advantages
- Menu Management: Easily manage multiple types of menu items
- Group Management: Automatically manages Radio menu groups to ensure correct single-selection behavior
- Convenient Access: Directly access or modify any menu item and its properties by ID
- Multi-Tray Menu Management: Supports grouped registration for menus from different tray icons, making it easier to manage multiple tray menus
## 🔧 Problems This Crate Solves
When using the `tray-icon` crate, menu event handlers only return the target menu ID instead of the actual menu item object. This makes it:
- Difficult to directly access the target menu object
- Inconvenient to modify menu properties (such as text or checked state)
- Hard to synchronize grouped menu states (such as Radio menus)
- Difficult to manage menus when multiple tray icons exist
This crate solves these problems through a unified menu manager.
# Usage
Add the dependency to your `Cargo.toml`:
```toml
[dependencies]
tray-control = "0.2.0"
tray-icon = ">=0.20.0"
```
Example using **winit + tray-icon + tray-control**:
* [`examples/winit.rs`](examples/winit.rs)
Steps:
1. Create a generic type `G` that implements `Clone + Copy + Eq + Hash + PartialEq + std::fmt::Debug`.
This generic type `G` is used for grouping **Radio menus** or **CheckBox menus**, making it easier to handle logic inside `tray_icon::MenuEvent::set_event_handler`, for example:
```rust
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum MenuGroup {
RadioColor, // Only one color can be selected
RadioLanguage, // Only one language can be selected
CheckBoxA,
CheckBoxB,
}
```
2. Create a menu registry (`tray_controls::MenuRegistry<G>`)
```rust
use tray_controls::MenuRegistry;
let mut menu_registry = MenuRegistry::<MenuGroup>::new();
```
3. Create tray menu items, convert them into `tray_icon::menu::MenuItemKind`, and register them into the menu registry (`tray_controls::MenuRegistry<G>`).
Items can optionally be grouped.
- Standard menu items:
```rust
use tray_icon::menu::MenuItem;
let mut menu_registry = MenuRegistry::<MenuGroup>::new();
let tray_menu = Menu::new();
let quit_menu_item = MenuItem::with_id("quit", "Quit", true, None);
// Register as a normal menu item
menu_registry.register_normal(quit_menu_item.kind());
let icon_menu_item = IconMenuItem::with_id(
"icon",
"Icon",
true,
Some(tray_icon::menu::Icon),
None,
);
// Register as a normal menu item
menu_registry.register_normal(icon_menu_item.kind());
tray_menu.append(&quit_menu_item as &dyn IsMenuItem);
tray_menu.append(&icon_menu_item as &dyn IsMenuItem);
```
- Radio menu items:
```rust
use tray_icon::menu::{Menu, MenuId, CheckMenuItem, IsMenuItem, Submenu};
use tray_controls::MenuRegistry;
let tray_menu = Menu::new();
let language_sub_menu_item = {
let english_menu_id = MenuId::new("english");
let chinese_menu_id = MenuId::new("chinese");
let japanese_menu_id = MenuId::new("japanese");
let english_menu_item =
CheckMenuItem::with_id(english_menu_id.clone(), "English", true, true, None);
let chinese_menu_item =
CheckMenuItem::with_id(chinese_menu_id, "Chinese", true, false, None);
let japanese_menu_item =
CheckMenuItem::with_id(japanese_menu_id, "Japanese", true, false, None);
let menu_items = [english_menu_item, chinese_menu_item, japanese_menu_item];
let menu_items: Vec<&dyn IsMenuItem> = menu_items
.iter()
.map(|check_menu_item| {
// Register as a radio menu with a default selected item
menu_registry.register_radio(
check_menu_item.kind(),
MenuGroup::RadioLanguage,
Some(english_menu_id.clone()),
);
check_menu_item as &dyn IsMenuItem
})
.collect();
Submenu::with_items("Language", true, &menu_items)?
};
// Append submenu to tray menu
tray_menu.append(&language_sub_menu_item as &dyn IsMenuItem);
```
4. After all menu items are created and registered, add the menu registry into global state management, such as inside a `winit` App:
```rust
struct App {
menu_registry: MenuRegistry<MenuGroup>,
// ...
}
let mut menu_registry = MenuRegistry::<MenuGroup>::new();
let tray_menu = create_register_menu(&mut menu_registry)?;
let tray = create_tray(tray_menu);
let mut app = App {
menu_registry,
// ...
};
event_loop.run_app(&mut app);
// fn create_registry_menu(menu_registry: &mut MenuRegistry<MenuGroup>)
// fn create_tray(tray_menu: Menu) -> tray_icon::TrayIcon
```
5. Handle menu events, for example in `winit` by setting an event handler for menu events:
```rust
use tray_controls::MenuRegistry;
use tray_icon::menu::MenuItemKind;
struct App {
menu_registry: MenuRegistry<MenuGroup>,
// ...
}
UserEvent::MenuEvent(event) => {
match self.menu_registry.handle_event(event.id()) {
Err(err) => {
println!("Failed to handle menu event: {err}");
}
Ok(return_menu_meta) => {
let return_menu_group = return_menu_meta.group();
let return_menu_kind = return_menu_meta.kind();
let return_menu_id = return_menu_kind.id();
match return_menu_group {
// Normal menu items
None => match return_menu_kind {
MenuItemKind::MenuItem(_menu_item) => {
// If there are only a few ungrouped normal menus,
// you can directly match menu IDs here.
match return_menu_id.0.as_str() {
"quit" => {
// TODO: do something
}
_ => {
// TODO: do something
}
}
}
MenuItemKind::Check(_check_menu_item) => {
// TODO: do something
}
MenuItemKind::Icon(_icon_menu_item) => {
// TODO: do something
}
MenuItemKind::Predefined(_predefined_menu_item) => {
// TODO: do something
}
_ => {
// Submenu not supported
}
},
// Grouped menus
Some(group) => {
match group {
// Handle radio menus
MenuGroup::RadioColor => {
// TODO: do something
}
MenuGroup::RadioLanguage => {
// TODO: do something
}
// Handle checkbox menus
MenuGroup::CheckBoxA => {
// TODO: do something
}
MenuGroup::CheckBoxB => {
// TODO: do something
}
// If multiple tray icons exist,
// normal menus can also be grouped for management.
}
}
}
}
}
}
```