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::{
25 ContextMenuMouseButton, KeyboardState, OptionVirtualKeyCodeCombo, VirtualKeyCode,
26 VirtualKeyCodeCombo,
27 },
28};
29
30/// Does `combo` describe the chord the keyboard is in, with `pressed` as the
31/// key that just went down?
32///
33/// THE shared accelerator rule, used by every platform that dispatches menu
34/// accelerators itself (Windows, X11, Wayland, headless; macOS's menu bar
35/// uses `AppKit` key equivalents, its context menus this).
36///
37/// * The combo names its modifiers with the modifier keys (`LControl`,
38/// `LShift`, `LAlt`, `LWin`; the right-hand twins are equivalent) and
39/// exactly ONE non-modifier key, which must be `pressed`.
40/// * `LWin` / `RWin` mean the platform's PRIMARY shortcut modifier — Cmd on
41/// macOS, Ctrl everywhere else — so `[LWin, S]` is Cmd+S on a Mac and
42/// Ctrl+S on Windows/Linux from one definition (the MWA-A2 rule behind
43/// `KeyboardState::primary_down`). `LControl` stays the Control key on
44/// every platform.
45/// * The match is EXACT: `Ctrl+S` does not fire while Shift is also held, so
46/// `Ctrl+S` and `Ctrl+Shift+S` can coexist in one menu.
47#[must_use]
48pub fn accelerator_matches(
49 combo: &VirtualKeyCodeCombo,
50 keyboard: &KeyboardState,
51 pressed: VirtualKeyCode,
52) -> bool {
53 let mut want_ctrl = false;
54 let mut want_shift = false;
55 let mut want_alt = false;
56 let mut want_primary = false;
57 let mut main_key: Option<VirtualKeyCode> = None;
58 for key in combo.keys.as_ref() {
59 match key {
60 VirtualKeyCode::LControl | VirtualKeyCode::RControl => want_ctrl = true,
61 VirtualKeyCode::LShift | VirtualKeyCode::RShift => want_shift = true,
62 VirtualKeyCode::LAlt | VirtualKeyCode::RAlt => want_alt = true,
63 VirtualKeyCode::LWin | VirtualKeyCode::RWin => want_primary = true,
64 other => {
65 if main_key.is_some() {
66 // Two non-modifier keys: not a chord this rule can match.
67 return false;
68 }
69 main_key = Some(*other);
70 }
71 }
72 }
73 if main_key != Some(pressed) {
74 return false;
75 }
76 if want_shift != keyboard.shift_down() || want_alt != keyboard.alt_down() {
77 return false;
78 }
79 if cfg!(target_os = "macos") {
80 want_ctrl == keyboard.ctrl_down() && want_primary == keyboard.super_down()
81 } else {
82 // Ctrl IS the primary modifier here; the Super/Windows key never
83 // takes part in an application chord.
84 (want_ctrl || want_primary) == keyboard.ctrl_down() && !keyboard.super_down()
85 }
86}
87
88/// Depth-first search of `items` for the first enabled entry whose
89/// accelerator matches the chord (see [`accelerator_matches`]).
90fn find_accelerated_in<'a>(
91 items: &'a [MenuItem],
92 keyboard: &KeyboardState,
93 pressed: VirtualKeyCode,
94) -> Option<&'a StringMenuItem> {
95 for item in items {
96 let MenuItem::String(s) = item else {
97 continue;
98 };
99 if s.menu_item_state == MenuItemState::Normal {
100 if let OptionVirtualKeyCodeCombo::Some(combo) = &s.accelerator {
101 if accelerator_matches(combo, keyboard, pressed) {
102 return Some(s);
103 }
104 }
105 }
106 if let Some(found) = find_accelerated_in(s.children.as_ref(), keyboard, pressed) {
107 return Some(found);
108 }
109 }
110 None
111}
112
113/// Represents a menu (context menu, dropdown menu, or application menu).
114///
115/// A menu consists of a list of items that can be displayed as a popup or
116/// attached to a window's menu bar. Modeled after the Windows API for
117/// cross-platform consistency.
118///
119/// # Fields
120///
121/// * `items` - The menu items to display
122/// * `position` - Where the menu should appear (for popups)
123/// * `context_mouse_btn` - Which mouse button triggers the context menu
124#[derive(Debug, Default, Clone, PartialEq, PartialOrd, Hash, Eq, Ord)]
125#[repr(C)]
126pub struct Menu {
127 pub items: MenuItemVec,
128 pub position: MenuPopupPosition,
129 pub context_mouse_btn: ContextMenuMouseButton,
130}
131
132impl_option!(
133 Menu,
134 OptionMenu,
135 copy = false,
136 [Debug, Clone, PartialEq, PartialOrd, Hash, Eq, Ord]
137);
138
139impl Menu {
140 /// The first enabled item anywhere in this menu whose accelerator
141 /// matches the chord the keyboard is in, `pressed` being the key that
142 /// just went down. Greyed / disabled items never match.
143 #[must_use]
144 pub fn find_accelerated_item<'a>(
145 &'a self,
146 keyboard: &KeyboardState,
147 pressed: VirtualKeyCode,
148 ) -> Option<&'a StringMenuItem> {
149 find_accelerated_in(self.items.as_ref(), keyboard, pressed)
150 }
151
152 /// Creates a new menu with the given items.
153 ///
154 /// Uses default position (`AutoCursor`) and right mouse button for context menus.
155 #[must_use]
156 pub const fn create(items: MenuItemVec) -> Self {
157 Self {
158 items,
159 position: MenuPopupPosition::AutoCursor,
160 context_mouse_btn: ContextMenuMouseButton::Right,
161 }
162 }
163
164 /// Builder method to set the popup position.
165 #[must_use]
166 pub const fn with_position(mut self, position: MenuPopupPosition) -> Self {
167 self.position = position;
168 self
169 }
170
171 /// Computes a 64-bit hash of this menu using the `HighwayHash` algorithm.
172 ///
173 /// This is used to detect changes in menu structure for caching and optimization.
174 #[must_use]
175 pub fn get_hash(&self) -> u64 {
176 use core::hash::Hasher;
177 let mut hasher = crate::hash::DefaultHasher::new();
178 self.hash(&mut hasher);
179 hasher.finish()
180 }
181}
182
183/// Specifies where a popup menu should appear relative to the cursor or clicked element.
184///
185/// This positioning information is ignored for application-level menus (menu bars)
186/// and only applies to context menus and dropdowns.
187#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Hash, Eq, Ord)]
188#[repr(C)]
189pub enum MenuPopupPosition {
190 /// Position menu below and to the left of the cursor
191 BottomLeftOfCursor,
192 /// Position menu below and to the right of the cursor
193 BottomRightOfCursor,
194 /// Position menu above and to the left of the cursor
195 TopLeftOfCursor,
196 /// Position menu above and to the right of the cursor
197 TopRightOfCursor,
198 /// Position menu below the rectangle that was clicked
199 BottomOfHitRect,
200 /// Position menu to the left of the rectangle that was clicked
201 LeftOfHitRect,
202 /// Position menu above the rectangle that was clicked
203 TopOfHitRect,
204 /// Position menu to the right of the rectangle that was clicked
205 RightOfHitRect,
206 /// Automatically calculate position based on available screen space near cursor
207 AutoCursor,
208 /// Automatically calculate position based on available screen space near clicked rect
209 AutoHitRect,
210}
211
212impl Default for MenuPopupPosition {
213 fn default() -> Self {
214 Self::AutoCursor
215 }
216}
217
218/// Describes the interactive state of a menu item.
219///
220/// Menu items can be in different states that affect their appearance and behavior:
221///
222/// - Normal items are clickable and render normally
223/// - Greyed items are visually disabled (greyed out) and non-clickable
224/// - Disabled items are non-clickable but retain normal appearance
225#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Hash, Eq, Ord)]
226#[repr(C)]
227pub enum MenuItemState {
228 /// Normal menu item (default)
229 Normal,
230 /// Menu item is greyed out and clicking it does nothing
231 Greyed,
232 /// Menu item is disabled, but NOT greyed out
233 Disabled,
234}
235#[allow(variant_size_differences)]
236// repr(C,u8) FFI enum: boxing the large variant would change the C ABI (api.json bindings); size disparity accepted
237/// Represents a single item in a menu.
238///
239/// Menu items can be regular text items with labels and callbacks,
240/// visual separators, or line breaks for horizontal menu layouts.
241#[derive(Debug, Clone, PartialEq, PartialOrd, Hash, Eq, Ord)]
242#[repr(C, u8)]
243#[allow(clippy::large_enum_variant)] // #[repr(C,u8)] FFI enum: boxing a variant changes the C ABI/api.json
244pub enum MenuItem {
245 /// A regular menu item with a label, optional icon, callback, and sub-items
246 String(StringMenuItem),
247 /// A visual separator line (only rendered in vertical layouts)
248 Separator,
249 /// Forces a line break when the menu is laid out horizontally
250 BreakLine,
251}
252
253impl_option!(
254 MenuItem,
255 OptionMenuItem,
256 copy = false,
257 [Debug, Clone, PartialEq, PartialOrd, Hash, Eq, Ord]
258);
259
260impl_vec!(
261 MenuItem,
262 MenuItemVec,
263 MenuItemVecDestructor,
264 MenuItemVecDestructorType,
265 MenuItemVecSlice,
266 OptionMenuItem
267);
268impl_vec_clone!(MenuItem, MenuItemVec, MenuItemVecDestructor);
269impl_vec_debug!(MenuItem, MenuItemVec);
270impl_vec_partialeq!(MenuItem, MenuItemVec);
271impl_vec_partialord!(MenuItem, MenuItemVec);
272impl_vec_hash!(MenuItem, MenuItemVec);
273impl_vec_eq!(MenuItem, MenuItemVec);
274impl_vec_ord!(MenuItem, MenuItemVec);
275
276/// A menu item with a text label and optional features.
277///
278/// `StringMenuItem` represents a clickable menu entry that can have:
279///
280/// - A text label
281/// - An optional keyboard accelerator (e.g., Ctrl+C)
282/// - An optional callback function
283/// - An optional icon (checkbox or image)
284/// - A state (normal, greyed, or disabled)
285/// - Child menu items (for sub-menus)
286#[derive(Debug, Clone, PartialEq, PartialOrd, Hash, Eq, Ord)]
287#[repr(C)]
288pub struct StringMenuItem {
289 /// Label of the menu
290 /// (ex. "File", "Edit", "View")
291 pub label: AzString,
292 /// Optional accelerator combination
293 /// (ex. "CTRL + X" = [`VirtualKeyCode::Ctrl`, `VirtualKeyCode::X`]) for keyboard shortcut
294 pub accelerator: OptionVirtualKeyCodeCombo,
295 /// Optional callback to call
296 pub callback: OptionCoreMenuCallback,
297 /// State (normal, greyed, disabled)
298 pub menu_item_state: MenuItemState,
299 /// Optional icon for the menu entry
300 pub icon: OptionMenuItemIcon,
301 /// Sub-menus of this item (separators and line-breaks can't have sub-menus)
302 pub children: MenuItemVec,
303}
304
305impl StringMenuItem {
306 /// Creates a new menu item with the given label.
307 /// All optional fields default to `None` / `Normal`.
308 #[must_use]
309 pub const fn create(label: AzString) -> Self {
310 Self {
311 label,
312 accelerator: OptionVirtualKeyCodeCombo::None,
313 callback: OptionCoreMenuCallback::None,
314 menu_item_state: MenuItemState::Normal,
315 icon: OptionMenuItemIcon::None,
316 children: MenuItemVec::from_const_slice(&[]),
317 }
318 }
319
320 /// Sets the child menu items for this item, creating a sub-menu.
321 #[must_use]
322 pub fn with_children(mut self, children: MenuItemVec) -> Self {
323 self.children = children;
324 self
325 }
326
327 /// Adds a single child menu item to this item.
328 #[must_use]
329 pub fn with_child(mut self, child: MenuItem) -> Self {
330 let mut children = self.children.into_library_owned_vec();
331 children.push(child);
332 self.children = children.into();
333 self
334 }
335
336 /// Attaches a callback function to this menu item.
337 ///
338 /// # Parameters
339 ///
340 /// * `data` - User data passed to the callback
341 /// * `callback` - Function pointer (as usize) to invoke when item is clicked
342 ///
343 /// # Note
344 ///
345 /// This uses `CoreCallbackType` (usize) instead of a real function pointer
346 /// to avoid circular dependencies. The conversion happens in azul-layout.
347 #[must_use]
348 pub fn with_callback<I: Into<CoreCallback>>(mut self, data: RefAny, callback: I) -> Self {
349 self.callback = Some(CoreMenuCallback {
350 refany: data,
351 callback: callback.into(),
352 })
353 .into();
354 self
355 }
356}
357
358/// Optional icon displayed next to a menu item.
359///
360/// Icons can be either:
361/// - A checkbox (checked or unchecked)
362/// - A custom image (typically 16x16 pixels)
363#[derive(Debug, Clone, PartialEq, PartialOrd, Hash, Eq, Ord)]
364#[repr(C, u8)]
365pub enum MenuItemIcon {
366 /// Displays a checkbox, with `true` = checked, `false` = unchecked
367 Checkbox(bool),
368 /// Displays a custom image (typically 16x16 format)
369 Image(ImageRef),
370}
371
372impl_option!(
373 MenuItemIcon,
374 OptionMenuItemIcon,
375 copy = false,
376 [Debug, Clone, PartialEq, PartialOrd, Hash, Eq, Ord]
377);
378
379// Core menu callback types (usize-based placeholders)
380//
381// Similar to CoreCallback, these use usize instead of function pointers
382// to avoid circular dependencies. Will be converted to real function
383// pointers in azul-layout.
384//
385// IMPORTANT: Memory layout must be identical to the real callback types!
386// Tests for this are in azul-layout/src/callbacks.rs
387
388/// Menu callback using usize placeholder for function pointer.
389///
390/// This type is used in `azul-core` to represent menu item callbacks without
391/// creating circular dependencies with `azul-layout`. The actual function pointer
392/// is stored as a `usize` and converted via unsafe code in `azul-layout`.
393#[derive(Debug, Clone, PartialEq, PartialOrd, Hash, Eq, Ord)]
394#[repr(C)]
395pub struct CoreMenuCallback {
396 /// User data passed to the callback when the menu item is clicked
397 pub refany: RefAny,
398 /// Callback function pointer stored as usize (converted to real fn pointer in azul-layout)
399 pub callback: CoreCallback,
400}
401
402impl_option!(
403 CoreMenuCallback,
404 OptionCoreMenuCallback,
405 copy = false,
406 [Debug, Clone, PartialEq, PartialOrd, Hash, Eq, Ord]
407);
408
409#[cfg(test)]
410#[path = "menu_test.rs"]
411mod menu_test;