Skip to main content

par_term/menu/
bridge.rs

1//! Delivery channel for menu actions raised inside the process.
2//!
3//! The native menu delivers activations through muda's process-global
4//! [`muda::MenuEvent`] channel, which `MenuManager::poll_events` drains once per
5//! event-loop iteration. The in-app egui menu ([`super::egui_menu`]) is drawn
6//! deep inside a window's egui pass, far from the [`super::MenuManager`] that
7//! owns the dispatch table, so it needs the same shape of channel — this module
8//! is it. Both queues are drained together by
9//! `WindowManager::process_menu_events`, so an action behaves identically no
10//! matter which menu raised it.
11
12use super::MenuAction;
13use std::sync::Mutex;
14use std::sync::atomic::{AtomicBool, Ordering};
15
16/// Actions raised by the in-app menu, waiting to be dispatched.
17static PENDING: Mutex<Vec<MenuAction>> = Mutex::new(Vec::new());
18
19/// Set when something outside the egui pass asks the menu to open or close.
20static TOGGLE_REQUEST: AtomicBool = AtomicBool::new(false);
21
22/// Queue an action raised by the in-app menu.
23pub fn dispatch(action: MenuAction) {
24    match PENDING.lock() {
25        Ok(mut pending) => pending.push(action),
26        Err(poisoned) => poisoned.into_inner().push(action),
27    }
28}
29
30/// Take every queued action, leaving the queue empty.
31pub fn drain_pending_actions() -> Vec<MenuAction> {
32    match PENDING.lock() {
33        Ok(mut pending) => std::mem::take(&mut *pending),
34        Err(poisoned) => std::mem::take(&mut *poisoned.into_inner()),
35    }
36}
37
38/// Ask the in-app menu to open (or close, if already open).
39///
40/// The request is applied by the next egui pass that draws the menu, so this is
41/// safe to call from the input handler — for example from a `toggle_menu`
42/// keybinding action.
43pub fn request_toggle() {
44    TOGGLE_REQUEST.store(true, Ordering::Relaxed);
45}
46
47/// Consume a pending toggle request, if any.
48pub fn take_toggle_request() -> bool {
49    TOGGLE_REQUEST.swap(false, Ordering::Relaxed)
50}
51
52/// Serialises the tests that exercise these process-global queues.
53///
54/// Without it, `cargo test`'s parallelism lets one test drain the other's
55/// action or toggle request.
56#[cfg(test)]
57pub(super) static TEST_LOCK: Mutex<()> = Mutex::new(());
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62
63    #[test]
64    fn queue_and_toggle_round_trip() {
65        let _guard = TEST_LOCK.lock();
66        let _ = drain_pending_actions();
67        let _ = take_toggle_request();
68
69        assert!(drain_pending_actions().is_empty());
70        dispatch(MenuAction::Quit);
71        dispatch(MenuAction::NewWindow);
72        assert_eq!(
73            drain_pending_actions(),
74            vec![MenuAction::Quit, MenuAction::NewWindow]
75        );
76        assert!(drain_pending_actions().is_empty());
77
78        assert!(!take_toggle_request());
79        request_toggle();
80        assert!(take_toggle_request());
81        assert!(!take_toggle_request());
82    }
83}