tray-controls 0.2.2

An enhanced menu management tool designed for the tray-icon library
Documentation

tray-controls

Licence Crates.io version

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:

[dependencies]
tray-control = "0.2.0"
tray-icon = ">=0.20.0"

Example using winit + tray-icon + tray-control:

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:
#[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,
}
  1. Create a menu registry (tray_controls::MenuRegistry<G>)
use tray_controls::MenuRegistry;

let mut menu_registry = MenuRegistry::<MenuGroup>::new();
  1. 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:
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:
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);
  1. After all menu items are created and registered, add the menu registry into global state management, such as inside a winit App:
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
  1. Handle menu events, for example in winit by setting an event handler for menu events:
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.
                    }
                }
            }
        }
    }
}