Skip to main content

azul_core/
menu.rs

1//! Menu system for context menus, dropdown menus, and application menus.
2//!
3//! This module provides a cross-platform menu abstraction modeled after the Windows API,
4//! supporting hierarchical menus with separators, icons, keyboard accelerators, and callbacks.
5//!
6//! # Core vs Layout Types
7//!
8//! This module uses `CoreMenuCallback` with `usize` placeholders instead of function pointers
9//! to avoid circular dependencies between `azul-core` and `azul-layout`. The actual function
10//! pointers are stored in `azul-layout` and converted via unsafe code with identical memory
11//! layout.
12
13extern crate alloc;
14
15use alloc::vec::Vec;
16use core::hash::Hash;
17
18use azul_css::AzString;
19
20use crate::{
21    callbacks::{CoreCallback, CoreCallbackType},
22    refany::RefAny,
23    resources::ImageRef,
24    window::{ContextMenuMouseButton, OptionVirtualKeyCodeCombo},
25};
26
27/// Represents a menu (context menu, dropdown menu, or application menu).
28///
29/// A menu consists of a list of items that can be displayed as a popup or
30/// attached to a window's menu bar. Modeled after the Windows API for
31/// cross-platform consistency.
32///
33/// # Fields
34///
35/// * `items` - The menu items to display
36/// * `position` - Where the menu should appear (for popups)
37/// * `context_mouse_btn` - Which mouse button triggers the context menu
38#[derive(Debug, Default, Clone, PartialEq, PartialOrd, Hash, Eq, Ord)]
39#[repr(C)]
40pub struct Menu {
41    pub items: MenuItemVec,
42    pub position: MenuPopupPosition,
43    pub context_mouse_btn: ContextMenuMouseButton,
44}
45
46impl_option!(
47    Menu,
48    OptionMenu,
49    copy = false,
50    [Debug, Clone, PartialEq, PartialOrd, Hash, Eq, Ord]
51);
52
53impl Menu {
54    /// Creates a new menu with the given items.
55    ///
56    /// Uses default position (AutoCursor) and right mouse button for context menus.
57    pub fn create(items: MenuItemVec) -> Self {
58        Self {
59            items,
60            position: MenuPopupPosition::AutoCursor,
61            context_mouse_btn: ContextMenuMouseButton::Right,
62        }
63    }
64
65    /// Builder method to set the popup position.
66    pub fn with_position(mut self, position: MenuPopupPosition) -> Self {
67        self.position = position;
68        self
69    }
70}
71
72impl Menu {
73    /// Swaps this menu with a default menu and returns the previous contents.
74    ///
75    /// This is useful for taking ownership of the menu's contents without cloning.
76    pub fn swap_with_default(&mut self) -> Self {
77        let mut new = Self::default();
78        core::mem::swap(&mut new, self);
79        new
80    }
81}
82
83/// Specifies where a popup menu should appear relative to the cursor or clicked element.
84///
85/// This positioning information is ignored for application-level menus (menu bars)
86/// and only applies to context menus and dropdowns.
87#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Hash, Eq, Ord)]
88#[repr(C)]
89pub enum MenuPopupPosition {
90    /// Position menu below and to the left of the cursor
91    BottomLeftOfCursor,
92    /// Position menu below and to the right of the cursor
93    BottomRightOfCursor,
94    /// Position menu above and to the left of the cursor
95    TopLeftOfCursor,
96    /// Position menu above and to the right of the cursor
97    TopRightOfCursor,
98    /// Position menu below the rectangle that was clicked
99    BottomOfHitRect,
100    /// Position menu to the left of the rectangle that was clicked
101    LeftOfHitRect,
102    /// Position menu above the rectangle that was clicked
103    TopOfHitRect,
104    /// Position menu to the right of the rectangle that was clicked
105    RightOfHitRect,
106    /// Automatically calculate position based on available screen space near cursor
107    AutoCursor,
108    /// Automatically calculate position based on available screen space near clicked rect
109    AutoHitRect,
110}
111
112impl Default for MenuPopupPosition {
113    fn default() -> Self {
114        Self::AutoCursor
115    }
116}
117
118impl Menu {
119    /// Computes a 64-bit hash of this menu using the HighwayHash algorithm.
120    ///
121    /// This is used to detect changes in menu structure for caching and optimization.
122    pub fn get_hash(&self) -> u64 {
123        use highway::{HighwayHash, HighwayHasher, Key};
124        let mut hasher = HighwayHasher::new(Key([0; 4]));
125        self.hash(&mut hasher);
126        hasher.finalize64()
127    }
128}
129
130/// Describes the interactive state of a menu item.
131///
132/// Menu items can be in different states that affect their appearance and behavior:
133///
134/// - Normal items are clickable and render normally
135/// - Greyed items are visually disabled (greyed out) and non-clickable
136/// - Disabled items are non-clickable but retain normal appearance
137#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Hash, Eq, Ord)]
138#[repr(C)]
139pub enum MenuItemState {
140    /// Normal menu item (default)
141    Normal,
142    /// Menu item is greyed out and clicking it does nothing
143    Greyed,
144    /// Menu item is disabled, but NOT greyed out
145    Disabled,
146}
147
148/// Represents a single item in a menu.
149///
150/// Menu items can be regular text items with labels and callbacks,
151/// visual separators, or line breaks for horizontal menu layouts.
152#[derive(Debug, Clone, PartialEq, PartialOrd, Hash, Eq, Ord)]
153#[repr(C, u8)]
154pub enum MenuItem {
155    /// A regular menu item with a label, optional icon, callback, and sub-items
156    String(StringMenuItem),
157    /// A visual separator line (only rendered in vertical layouts)
158    Separator,
159    /// Forces a line break when the menu is laid out horizontally
160    BreakLine,
161}
162
163impl_option!(
164    MenuItem,
165    OptionMenuItem,
166    copy = false,
167    [Debug, Clone, PartialEq, PartialOrd, Hash, Eq, Ord]
168);
169
170impl_vec!(MenuItem, MenuItemVec, MenuItemVecDestructor, MenuItemVecDestructorType, MenuItemVecSlice, OptionMenuItem);
171impl_vec_clone!(MenuItem, MenuItemVec, MenuItemVecDestructor);
172impl_vec_debug!(MenuItem, MenuItemVec);
173impl_vec_partialeq!(MenuItem, MenuItemVec);
174impl_vec_partialord!(MenuItem, MenuItemVec);
175impl_vec_hash!(MenuItem, MenuItemVec);
176impl_vec_eq!(MenuItem, MenuItemVec);
177impl_vec_ord!(MenuItem, MenuItemVec);
178
179/// A menu item with a text label and optional features.
180///
181/// `StringMenuItem` represents a clickable menu entry that can have:
182///
183/// - A text label
184/// - An optional keyboard accelerator (e.g., Ctrl+C)
185/// - An optional callback function
186/// - An optional icon (checkbox or image)
187/// - A state (normal, greyed, or disabled)
188/// - Child menu items (for sub-menus)
189///
190#[derive(Debug, Clone, PartialEq, PartialOrd, Hash, Eq, Ord)]
191#[repr(C)]
192pub struct StringMenuItem {
193    /// Label of the menu
194    /// (ex. "File", "Edit", "View")
195    pub label: AzString,
196    /// Optional accelerator combination
197    /// (ex. "CTRL + X" = [VirtualKeyCode::Ctrl, VirtualKeyCode::X]) for keyboard shortcut
198    pub accelerator: OptionVirtualKeyCodeCombo,
199    /// Optional callback to call
200    pub callback: OptionCoreMenuCallback,
201    /// State (normal, greyed, disabled)
202    pub menu_item_state: MenuItemState,
203    /// Optional icon for the menu entry
204    pub icon: OptionMenuItemIcon,
205    /// Sub-menus of this item (separators and line-breaks can't have sub-menus)
206    pub children: MenuItemVec,
207}
208
209impl StringMenuItem {
210    /// Creates a new menu item with the given label and default values.
211    ///
212    /// Default values:
213    ///
214    /// - No accelerator
215    /// - No callback
216    /// - Normal state
217    /// - No icon
218    /// - No children
219    pub const fn create(label: AzString) -> Self {
220        StringMenuItem {
221            label,
222            accelerator: OptionVirtualKeyCodeCombo::None,
223            callback: OptionCoreMenuCallback::None,
224            menu_item_state: MenuItemState::Normal,
225            icon: OptionMenuItemIcon::None,
226            children: MenuItemVec::from_const_slice(&[]),
227        }
228    }
229
230    /// Swaps this menu item with a default item and returns the previous contents.
231    ///
232    /// This is useful for taking ownership without cloning.
233    pub fn swap_with_default(&mut self) -> Self {
234        let mut default = Self {
235            label: AzString::from_const_str(""),
236            accelerator: None.into(),
237            callback: None.into(),
238            menu_item_state: MenuItemState::Normal,
239            icon: None.into(),
240            children: Vec::new().into(),
241        };
242        core::mem::swap(&mut default, self);
243        default
244    }
245
246    /// Sets the child menu items for this item, creating a sub-menu.
247    pub fn with_children(mut self, children: MenuItemVec) -> Self {
248        self.children = children;
249        self
250    }
251
252    /// Adds a single child menu item to this item.
253    pub fn with_child(mut self, child: MenuItem) -> Self {
254        let mut children = self.children.into_library_owned_vec();
255        children.push(child);
256        self.children = children.into();
257        self
258    }
259
260    /// Attaches a callback function to this menu item.
261    ///
262    /// # Parameters
263    ///
264    /// * `data` - User data passed to the callback
265    /// * `callback` - Function pointer (as usize) to invoke when item is clicked
266    ///
267    /// # Note
268    ///
269    /// This uses `CoreCallbackType` (usize) instead of a real function pointer
270    /// to avoid circular dependencies. The conversion happens in azul-layout.
271    pub fn with_callback<I: Into<CoreCallback>>(mut self, data: RefAny, callback: I) -> Self {
272        self.callback = Some(CoreMenuCallback {
273            refany: data,
274            callback: callback.into(),
275        })
276        .into();
277        self
278    }
279}
280
281/// Optional icon displayed next to a menu item.
282///
283/// Icons can be either:
284/// - A checkbox (checked or unchecked)
285/// - A custom image (typically 16x16 pixels)
286#[derive(Debug, Clone, PartialEq, PartialOrd, Hash, Eq, Ord)]
287#[repr(C, u8)]
288pub enum MenuItemIcon {
289    /// Displays a checkbox, with `true` = checked, `false` = unchecked
290    Checkbox(bool),
291    /// Displays a custom image (typically 16x16 format)
292    Image(ImageRef),
293}
294
295impl_option!(
296    MenuItemIcon,
297    OptionMenuItemIcon,
298    copy = false,
299    [Debug, Clone, PartialEq, PartialOrd, Hash, Eq, Ord]
300);
301
302// Core menu callback types (usize-based placeholders)
303//
304// Similar to CoreCallback, these use usize instead of function pointers
305// to avoid circular dependencies. Will be converted to real function
306// pointers in azul-layout.
307//
308// IMPORTANT: Memory layout must be identical to the real callback types!
309// Tests for this are in azul-layout/src/callbacks.rs
310
311/// Menu callback using usize placeholder for function pointer.
312///
313/// This type is used in `azul-core` to represent menu item callbacks without
314/// creating circular dependencies with `azul-layout`. The actual function pointer
315/// is stored as a `usize` and converted via unsafe code in `azul-layout`.
316#[derive(Debug, Clone, PartialEq, PartialOrd, Hash, Eq, Ord)]
317#[repr(C)]
318pub struct CoreMenuCallback {
319    pub refany: RefAny,
320    pub callback: CoreCallback,
321}
322
323impl_option!(
324    CoreMenuCallback,
325    OptionCoreMenuCallback,
326    copy = false,
327    [Debug, Clone, PartialEq, PartialOrd, Hash, Eq, Ord]
328);