use std::cell::RefCell;
use std::ffi::{c_void, CString};
use std::os::raw::c_int;
use std::rc::Rc;
use xplm_sys::{
xplm_Menu_Checked, xplm_Menu_NoCheck, xplm_Menu_Unchecked, XPLMAppendMenuItem,
XPLMAppendMenuSeparator, XPLMCheckMenuItem, XPLMCheckMenuItemState, XPLMClearAllMenuItems,
XPLMCreateMenu, XPLMDestroyMenu, XPLMEnableMenuItem, XPLMFindPluginsMenu, XPLMMenuCheck,
XPLMMenuID, XPLMSetMenuItemName,
};
#[cfg(feature = "XPLM210")]
use xplm_sys::XPLMRemoveMenuItem;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum MenuCheckState {
NoCheck,
Unchecked,
Checked,
}
impl From<MenuCheckState> for XPLMMenuCheck {
fn from(state: MenuCheckState) -> Self {
match state {
MenuCheckState::NoCheck => xplm_Menu_NoCheck,
MenuCheckState::Unchecked => xplm_Menu_Unchecked,
MenuCheckState::Checked => xplm_Menu_Checked,
}
}
}
impl From<XPLMMenuCheck> for MenuCheckState {
fn from(raw: XPLMMenuCheck) -> Self {
if raw == xplm_Menu_Checked {
MenuCheckState::Checked
} else if raw == xplm_Menu_Unchecked {
MenuCheckState::Unchecked
} else {
MenuCheckState::NoCheck
}
}
}
struct ItemToken;
struct MenuState {
handler: RefCell<Box<dyn FnMut(i32) + 'static>>,
items: RefCell<Vec<Option<Rc<ItemToken>>>>,
}
pub struct Menu {
id: XPLMMenuID,
state: *mut MenuState,
}
unsafe impl Send for Menu {}
impl Menu {
pub fn new_submenu(
parent: &Menu,
parent_item: &MenuItem<'_>,
name: &str,
handler: impl FnMut(i32) + 'static,
) -> Option<Self> {
let index = parent_item.index()?;
Self::create(parent.id, index, name, handler)
}
pub fn new_in_plugins_menu(name: &str, handler: impl FnMut(i32) + 'static) -> Option<Self> {
let plugins_menu = unsafe { XPLMFindPluginsMenu() };
let c_name = CString::new(name).ok()?;
let item_index =
unsafe { XPLMAppendMenuItem(plugins_menu, c_name.as_ptr(), std::ptr::null_mut(), 0) };
if item_index < 0 {
return None;
}
Self::create(plugins_menu, item_index, name, handler)
}
fn create(
parent_id: XPLMMenuID,
parent_item_index: i32,
name: &str,
handler: impl FnMut(i32) + 'static,
) -> Option<Self> {
let c_name = CString::new(name).ok()?;
let state = Box::into_raw(Box::new(MenuState {
handler: RefCell::new(Box::new(handler)),
items: RefCell::new(Vec::new()),
}));
let id = unsafe {
XPLMCreateMenu(
c_name.as_ptr(),
parent_id,
parent_item_index,
Some(trampoline),
state as *mut c_void,
)
};
if id.is_null() {
unsafe { drop(Box::from_raw(state)) };
return None;
}
Some(Self { id, state })
}
pub fn add_item(&self, name: &str) -> Option<MenuItem<'_>> {
let c_name = CString::new(name).ok()?;
let state = unsafe { &*self.state };
let token = Rc::new(ItemToken);
let item_ref = Rc::as_ptr(&token) as *mut c_void;
#[cfg(debug_assertions)]
let predicted_index = state.items.borrow().len() as i32;
let actual_index = unsafe { XPLMAppendMenuItem(self.id, c_name.as_ptr(), item_ref, 0) };
if actual_index < 0 {
return None;
}
#[cfg(debug_assertions)]
debug_assert_eq!(
actual_index, predicted_index,
"XPLMAppendMenuItem index didn't match our tracked item count"
);
std::mem::forget(token.clone());
state.items.borrow_mut().push(Some(token.clone()));
Some(MenuItem { menu: self, token })
}
pub fn add_separator(&self) {
unsafe { XPLMAppendMenuSeparator(self.id) };
let state = unsafe { &*self.state };
state.items.borrow_mut().push(None);
}
pub fn clear_items(&self) {
unsafe { XPLMClearAllMenuItems(self.id) };
let state = unsafe { &*self.state };
for slot in state.items.borrow_mut().drain(..) {
if let Some(token) = slot {
reclaim_leaked_count(&token);
}
}
}
}
fn reclaim_leaked_count(token: &Rc<ItemToken>) {
unsafe { drop(Rc::from_raw(Rc::as_ptr(token))) };
}
impl Drop for Menu {
fn drop(&mut self) {
let state = unsafe { &*self.state };
for slot in state.items.borrow().iter() {
if let Some(token) = slot {
reclaim_leaked_count(token);
}
}
unsafe {
XPLMDestroyMenu(self.id);
drop(Box::from_raw(self.state));
}
}
}
pub struct MenuItem<'a> {
menu: &'a Menu,
token: Rc<ItemToken>,
}
impl MenuItem<'_> {
pub fn index(&self) -> Option<i32> {
let state = unsafe { &*self.menu.state };
state
.items
.borrow()
.iter()
.position(|slot| matches!(slot, Some(t) if Rc::ptr_eq(t, &self.token)))
.map(|i| i as i32)
}
pub fn set_checked(&self, state: MenuCheckState) {
if let Some(index) = self.index() {
unsafe { XPLMCheckMenuItem(self.menu.id, index, state.into()) }
}
}
pub fn checked(&self) -> Option<MenuCheckState> {
let index = self.index()?;
let mut raw: XPLMMenuCheck = 0;
unsafe { XPLMCheckMenuItemState(self.menu.id, index, &mut raw) };
Some(raw.into())
}
pub fn set_enabled(&self, enabled: bool) {
if let Some(index) = self.index() {
unsafe { XPLMEnableMenuItem(self.menu.id, index, enabled as c_int) }
}
}
pub fn set_name(&self, name: &str) {
let (Some(index), Ok(c_name)) = (self.index(), CString::new(name)) else {
return;
};
unsafe { XPLMSetMenuItemName(self.menu.id, index, c_name.as_ptr(), 0) }
}
#[cfg(feature = "XPLM210")]
pub fn remove(self) {
let state = unsafe { &*self.menu.state };
let Some(index) = self.index() else {
return;
};
unsafe { XPLMRemoveMenuItem(self.menu.id, index) };
let mut items = state.items.borrow_mut();
if let Some(slot) = items.get_mut(index as usize) {
if let Some(token) = slot.take() {
reclaim_leaked_count(&token);
}
}
}
}
unsafe extern "C" fn trampoline(in_menu_ref: *mut c_void, in_item_ref: *mut c_void) {
crate::guard(|| {
let state: &MenuState = unsafe { &*(in_menu_ref as *const MenuState) };
let index = state.items.borrow().iter().position(
|slot| matches!(slot, Some(t) if Rc::as_ptr(t) as *mut c_void == in_item_ref),
);
if let Some(index) = index {
(state.handler.borrow_mut())(index as i32);
}
});
}