From ccf95629267de7c2d6ba2d2629958a8d875c2159 Mon Sep 17 00:00:00 2001 From: Jonty Date: Mon, 1 Jan 2024 17:40:45 +0000 Subject: [PATCH 01/10] Add menu item api --- Cargo.toml | 5 ++ examples/hello_world.rs | 19 +++++++ examples/menu_items.rs | 106 +++++++++++++++++++++++++++++++++++ src/system.rs | 120 +++++++++++++++++++++++++++++++++++++++- 4 files changed, 249 insertions(+), 1 deletion(-) create mode 100644 examples/menu_items.rs diff --git a/Cargo.toml b/Cargo.toml index f6c77e9..865836f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,11 @@ name = "hello_world" path = "examples/hello_world.rs" crate-type = ["staticlib", "cdylib"] +[[example]] +name = "menu_items" +path = "examples/menu_items.rs" +crate-type = ["staticlib", "cdylib"] + [[example]] name = "life" path = "examples/life.rs" diff --git a/examples/hello_world.rs b/examples/hello_world.rs index 8928c90..ebffc33 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -9,6 +9,7 @@ use { crankstart_game, geometry::{ScreenPoint, ScreenVector}, graphics::{Graphics, LCDColor, LCDSolidColor}, + log_to_console, system::System, Game, Playdate, }, @@ -24,6 +25,24 @@ struct State { impl State { pub fn new(_playdate: &Playdate) -> Result, Error> { crankstart::display::Display::get().set_refresh_rate(20.0)?; + System::get().add_menu_item( + "Say Hello", + Box::new(|| { + log_to_console!("Hello"); + }), + )?; + System::get().add_menu_item( + "Say Goodbye", + Box::new(|| { + log_to_console!("Goodbye"); + }), + )?; + System::get().add_menu_item( + "Sausage", + Box::new(|| { + log_to_console!("Sausage"); + }), + )?; Ok(Box::new(Self { location: point2(INITIAL_X, INITIAL_Y), velocity: vec2(1, 2), diff --git a/examples/menu_items.rs b/examples/menu_items.rs new file mode 100644 index 0000000..a33cc59 --- /dev/null +++ b/examples/menu_items.rs @@ -0,0 +1,106 @@ +#![no_std] + +extern crate alloc; + +use alloc::{format, vec}; +use alloc::rc::Rc; +use alloc::string::String; +use alloc::vec::Vec; +use core::cell::RefCell; +use hashbrown::HashMap; +use { + alloc::boxed::Box, + anyhow::Error, + crankstart::{ + crankstart_game, + geometry::{ScreenPoint, ScreenVector}, + graphics::{Graphics, LCDColor, LCDSolidColor}, + log_to_console, + system::{System, MenuItem}, + Game, Playdate, + }, + crankstart_sys::{LCD_COLUMNS, LCD_ROWS}, + euclid::{point2, vec2}, +}; + +struct State { + menu_items: Rc>>, + text_location: ScreenPoint, +} + +impl State { + pub fn new(_playdate: &Playdate) -> Result, Error> { + crankstart::display::Display::get().set_refresh_rate(20.0)?; + let menu_items = Rc::new(RefCell::new(HashMap::new())); + let system = System::get(); + let normal_item = { + system.add_menu_item( + "Select Me", + Box::new(|| { + log_to_console!("Normal option picked"); + } + ), + )? + }; + let checkmark_item = { + let ref_menu_items = menu_items.clone(); + system.add_checkmark_menu_item( + "Toggle Me", + false, + Box::new(move || { + let value_of_item = { + let menu_items = ref_menu_items.borrow(); + let this_menu_item = menu_items.get("checkmark").unwrap(); + System::get().get_menu_item_value(this_menu_item).unwrap() != 0 + }; + log_to_console!("Checked option picked: Value is now: {}", value_of_item); + }), + )? + }; + let options_item = { + let ref_menu_items = menu_items.clone(); + let options : Vec= vec!["Small".into(), "Medium".into(), "Large".into()]; + let options_copy = options.clone(); + system.add_options_menu_item( + "Size", + options, + Box::new(move || { + let value_of_item = { + let menu_items = ref_menu_items.borrow(); + let this_menu_item = menu_items.get("checkmark").unwrap(); + let idx = System::get().get_menu_item_value(this_menu_item).unwrap(); + options_copy.get(idx as usize) + }; + log_to_console!("Checked option picked: Value is now {:?}", value_of_item); + }), + )? + }; + { + let mut menu_items = menu_items.borrow_mut(); + menu_items.insert("normal", normal_item); + menu_items.insert("checkmark", checkmark_item); + menu_items.insert("options", options_item); + } + Ok(Box::new(Self { + menu_items, + text_location: point2(100, 100), + + } + )) + } +} + +impl Game for State { + fn update(&mut self, _playdate: &mut Playdate) -> Result<(), Error> { + let graphics = Graphics::get(); + graphics.clear(LCDColor::Solid(LCDSolidColor::kColorWhite))?; + graphics.draw_text("Menu Items", self.text_location); + + System::get().draw_fps(0, 0)?; + + Ok(()) + } +} + + +crankstart_game!(State); diff --git a/src/system.rs b/src/system.rs index 4ded03a..9c9a96b 100644 --- a/src/system.rs +++ b/src/system.rs @@ -1,13 +1,25 @@ +use alloc::boxed::Box; +use alloc::rc::Rc; +use alloc::string::String; +use alloc::vec::Vec; +use core::cell::RefCell; +use anyhow::anyhow; use { crate::pd_func_caller, alloc::format, anyhow::Error, core::ptr, crankstart_sys::ctypes::c_void, cstr_core::CString, }; -use crankstart_sys::ctypes::c_int; +use crankstart_sys::ctypes::{c_char, c_int}; pub use crankstart_sys::PDButtons; +use crankstart_sys::PDMenuItem; +use crate::log_to_console; static mut SYSTEM: System = System(ptr::null_mut()); +static mut MENU_ITEM_CALLBACKS: Vec> = Vec::new(); +const MENU_ITEM_CALLBACKS_MAX: usize = 10; +const MENU_ITEM_INDICES: [c_int; MENU_ITEM_CALLBACKS_MAX] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; + #[derive(Clone, Debug)] pub struct System(*const crankstart_sys::playdate_sys); @@ -46,6 +58,95 @@ impl System { Ok((current, pushed, released)) } + extern "C" fn menu_item_callback(user_data: *mut core::ffi::c_void) { + unsafe { + let callback = user_data as *mut Box; + (*callback)() + } + } + + /// Adds a option to the menu. The callback is called when the option is selected. + pub fn add_menu_item(&self, title: &str, callback: Box) -> Result { + let c_text = CString::new(title).map_err(|e| anyhow!("CString::new: {}", e))?; + let wrapped_callback = Box::new(callback); + let raw_callback_ptr = Box::into_raw(wrapped_callback); + let raw_menu_item = pd_func_caller!((*self.0).addMenuItem, c_text.as_ptr() as *mut core::ffi::c_char, Some(Self::menu_item_callback), raw_callback_ptr as *mut c_void)?; + Ok(Rc::new(RefCell::new(MenuItemInner { + item: raw_menu_item, + raw_callback_ptr, + }))) + } + + /// Adds a option to the menu that has a checkbox. The initial_checked_state is the initial + /// state of the checkbox. Callback will only be called when the menu is closed, not when the + /// option is toggled. Use `System::get_menu_item_value` to get the state of the checkbox when + /// the callback is called. + pub fn add_checkmark_menu_item(&self, title: &str, initial_checked_state: bool, callback: Box) -> Result { + let c_text = CString::new(title).map_err(|e| anyhow!("CString::new: {}", e))?; + let wrapped_callback = Box::new(callback); + let raw_callback_ptr = Box::into_raw(wrapped_callback); + let raw_menu_item = pd_func_caller!((*self.0).addCheckmarkMenuItem, + c_text.as_ptr() as *mut core::ffi::c_char, + initial_checked_state as c_int, + Some(Self::menu_item_callback), + raw_callback_ptr as *mut c_void)?; + Ok(Rc::new(RefCell::new(MenuItemInner { + item: raw_menu_item, + raw_callback_ptr, + }))) + } + + /// Adds a option to the menu that has multiple values that can be cycled through. The initial + /// value is the first element in `options`. Callback will only be called when the menu is + /// closed, not when the option is toggled. Use `System::get_menu_item_value` to get the index + /// of the options list when the callback is called, which can be used to lookup the value. + pub fn add_options_menu_item(&self, title: &str, options: Vec, callback: Box) -> Result { + let c_text = CString::new(title).map_err(|e| anyhow!("CString::new: {}", e))?; + let options_count = options.len() as c_int; + let c_options: Vec = options.into_iter().map(|s| CString::new(s).map_err(|e| anyhow!("CString::new: {}", e))).collect::, Error>>()?; + let c_options_ptrs: Vec<*const i8> = c_options.iter().map(|c| c.as_ptr()).collect(); + let c_options_ptrs_ptr = c_options_ptrs.as_ptr(); + let option_titles = c_options_ptrs_ptr as *mut *const c_char; + let wrapped_callback = Box::new(callback); + let raw_callback_ptr = Box::into_raw(wrapped_callback); + let raw_menu_item = pd_func_caller!((*self.0).addOptionsMenuItem, + c_text.as_ptr() as *mut core::ffi::c_char, + option_titles, + options_count, + Some(Self::menu_item_callback), + raw_callback_ptr as *mut c_void)?; + Ok(Rc::new(RefCell::new(MenuItemInner { + item: raw_menu_item, + raw_callback_ptr, + }))) + } + + /// Returns the state of a given menu item. The meaning depends on the type of menu item. + /// If it is the checkbox, the int represents the boolean checked state. If it's a option the + /// int represents the index of the option array. + pub fn get_menu_item_value(&self, item: &MenuItem) -> Result { + let value = pd_func_caller!((*self.0).getMenuItemValue, item.borrow().item)?; + Ok(value as i32) + } + + /// Set the title of a given menu item + // TODO: Determine what happens if you go out of range .... + pub fn set_menu_item_value(&self, item: &MenuItem, new_value: i32) -> Result<(), Error> { + pd_func_caller!((*self.0).setMenuItemValue, item.borrow().item, new_value as c_int) + } + + + pub fn set_menu_item_title(&self, item: &MenuItem, new_title: &str) -> Result<(), Error> { + let c_text = CString::new(new_title).map_err(|e| anyhow!("CString::new: {}", e))?; + pd_func_caller!((*self.0).setMenuItemTitle, item.borrow().item, c_text.as_ptr() as *mut c_char) + } + pub fn remove_menu_item(&self, item: MenuItem) -> Result<(), Error> { + pd_func_caller!((*self.0).removeMenuItem, item.borrow().item) + } + pub fn remove_all_menu_items(&self) -> Result<(), Error> { + pd_func_caller!((*self.0).removeAllMenuItems) + } + pub fn is_crank_docked(&self) -> Result { let docked: bool = pd_func_caller!((*self.0).isCrankDocked)? != 0; Ok(docked) @@ -129,3 +230,20 @@ impl System { pd_func_caller!((*self.0).drawFPS, x, y) } } + +pub struct MenuItemInner { + item: *mut PDMenuItem, + raw_callback_ptr: *mut Box, +} + +impl Drop for MenuItemInner { + fn drop(&mut self) { + unsafe { + // Recast into box to let Box deal with freeing the right memory + let _ = Box::from_raw(self.raw_callback_ptr); + } + } +} + +pub type MenuItem = Rc>; + From 4f0e27948877071874b6c401523bcaca14abe2a1 Mon Sep 17 00:00:00 2001 From: Jonty Date: Mon, 1 Jan 2024 19:41:56 +0000 Subject: [PATCH 02/10] MenuItemKind implementation to add some protection around the checkbox and options items --- examples/menu_items.rs | 36 +++++++++-------- src/system.rs | 87 ++++++++++++++++++++++++++++++------------ 2 files changed, 83 insertions(+), 40 deletions(-) diff --git a/examples/menu_items.rs b/examples/menu_items.rs index a33cc59..0dc5e5a 100644 --- a/examples/menu_items.rs +++ b/examples/menu_items.rs @@ -2,29 +2,31 @@ extern crate alloc; -use alloc::{format, vec}; +use alloc::vec; use alloc::rc::Rc; use alloc::string::String; use alloc::vec::Vec; use core::cell::RefCell; + use hashbrown::HashMap; + use { alloc::boxed::Box, anyhow::Error, crankstart::{ crankstart_game, - geometry::{ScreenPoint, ScreenVector}, + Game, + geometry::ScreenPoint, graphics::{Graphics, LCDColor, LCDSolidColor}, log_to_console, - system::{System, MenuItem}, - Game, Playdate, + Playdate, system::{MenuItem, System}, }, - crankstart_sys::{LCD_COLUMNS, LCD_ROWS}, - euclid::{point2, vec2}, + euclid::point2, }; +use crankstart::system::MenuItemKind; struct State { - menu_items: Rc>>, + _menu_items: Rc>>, text_location: ScreenPoint, } @@ -59,17 +61,21 @@ impl State { }; let options_item = { let ref_menu_items = menu_items.clone(); - let options : Vec= vec!["Small".into(), "Medium".into(), "Large".into()]; - let options_copy = options.clone(); + let options: Vec = vec!["Small".into(), "Medium".into(), "Large".into()]; system.add_options_menu_item( "Size", options, Box::new(move || { let value_of_item = { let menu_items = ref_menu_items.borrow(); - let this_menu_item = menu_items.get("checkmark").unwrap(); + let this_menu_item = menu_items.get("options").unwrap(); let idx = System::get().get_menu_item_value(this_menu_item).unwrap(); - options_copy.get(idx as usize) + match &this_menu_item.kind { + MenuItemKind::Options(opts) => { + opts.get(idx ).map(|s| s.clone()) + } + _ => None + } }; log_to_console!("Checked option picked: Value is now {:?}", value_of_item); }), @@ -82,11 +88,9 @@ impl State { menu_items.insert("options", options_item); } Ok(Box::new(Self { - menu_items, + _menu_items: menu_items, text_location: point2(100, 100), - - } - )) + })) } } @@ -94,7 +98,7 @@ impl Game for State { fn update(&mut self, _playdate: &mut Playdate) -> Result<(), Error> { let graphics = Graphics::get(); graphics.clear(LCDColor::Solid(LCDSolidColor::kColorWhite))?; - graphics.draw_text("Menu Items", self.text_location); + graphics.draw_text("Menu Items", self.text_location).unwrap(); System::get().draw_fps(0, 0)?; diff --git a/src/system.rs b/src/system.rs index 9c9a96b..3026585 100644 --- a/src/system.rs +++ b/src/system.rs @@ -10,9 +10,10 @@ use { }; use crankstart_sys::ctypes::{c_char, c_int}; +use crankstart_sys::l_valtype::kInt; pub use crankstart_sys::PDButtons; use crankstart_sys::PDMenuItem; -use crate::log_to_console; +use crate::{log_to_console, panic}; static mut SYSTEM: System = System(ptr::null_mut()); @@ -71,10 +72,13 @@ impl System { let wrapped_callback = Box::new(callback); let raw_callback_ptr = Box::into_raw(wrapped_callback); let raw_menu_item = pd_func_caller!((*self.0).addMenuItem, c_text.as_ptr() as *mut core::ffi::c_char, Some(Self::menu_item_callback), raw_callback_ptr as *mut c_void)?; - Ok(Rc::new(RefCell::new(MenuItemInner { - item: raw_menu_item, - raw_callback_ptr, - }))) + Ok(MenuItem { + inner: Rc::new(RefCell::new(MenuItemInner { + item: raw_menu_item, + raw_callback_ptr, + })), + kind: MenuItemKind::Normal, + }) } /// Adds a option to the menu that has a checkbox. The initial_checked_state is the initial @@ -90,10 +94,14 @@ impl System { initial_checked_state as c_int, Some(Self::menu_item_callback), raw_callback_ptr as *mut c_void)?; - Ok(Rc::new(RefCell::new(MenuItemInner { - item: raw_menu_item, - raw_callback_ptr, - }))) + Ok( + MenuItem { + inner: Rc::new(RefCell::new(MenuItemInner { + item: raw_menu_item, + raw_callback_ptr, + })), + kind: MenuItemKind::Checkmark, + }) } /// Adds a option to the menu that has multiple values that can be cycled through. The initial @@ -103,7 +111,7 @@ impl System { pub fn add_options_menu_item(&self, title: &str, options: Vec, callback: Box) -> Result { let c_text = CString::new(title).map_err(|e| anyhow!("CString::new: {}", e))?; let options_count = options.len() as c_int; - let c_options: Vec = options.into_iter().map(|s| CString::new(s).map_err(|e| anyhow!("CString::new: {}", e))).collect::, Error>>()?; + let c_options: Vec = options.iter().map(|s| CString::new(s.clone()).map_err(|e| anyhow!("CString::new: {}", e))).collect::, Error>>()?; let c_options_ptrs: Vec<*const i8> = c_options.iter().map(|c| c.as_ptr()).collect(); let c_options_ptrs_ptr = c_options_ptrs.as_ptr(); let option_titles = c_options_ptrs_ptr as *mut *const c_char; @@ -115,33 +123,54 @@ impl System { options_count, Some(Self::menu_item_callback), raw_callback_ptr as *mut c_void)?; - Ok(Rc::new(RefCell::new(MenuItemInner { - item: raw_menu_item, - raw_callback_ptr, - }))) + Ok( + MenuItem { + inner: Rc + ::new(RefCell::new(MenuItemInner { + item: raw_menu_item, + raw_callback_ptr, + })), + kind: MenuItemKind::Options(options), + + }) } /// Returns the state of a given menu item. The meaning depends on the type of menu item. /// If it is the checkbox, the int represents the boolean checked state. If it's a option the /// int represents the index of the option array. - pub fn get_menu_item_value(&self, item: &MenuItem) -> Result { - let value = pd_func_caller!((*self.0).getMenuItemValue, item.borrow().item)?; - Ok(value as i32) + pub fn get_menu_item_value(&self, item: &MenuItem) -> Result { + let value = pd_func_caller!((*self.0).getMenuItemValue, item.inner.borrow().item)?; + Ok(value as usize) } - /// Set the title of a given menu item - // TODO: Determine what happens if you go out of range .... - pub fn set_menu_item_value(&self, item: &MenuItem, new_value: i32) -> Result<(), Error> { - pd_func_caller!((*self.0).setMenuItemValue, item.borrow().item, new_value as c_int) + /// set the value of a given menu item. The meaning depends on the type of menu item. Picking + /// the right value is left up to the caller, but is protected by the `MenuItemKind` of the + /// `item` passed + pub fn set_menu_item_value(&self, item: &MenuItem, new_value: usize) -> Result<(), Error> { + match &item.kind { + MenuItemKind::Normal => {} + MenuItemKind::Checkmark => { + if new_value > 1 { + return Err(anyhow!("Invalid value ({}) for checkmark menu item", new_value)) + } + } + MenuItemKind::Options(opts) => { + if new_value >= opts.len() { + return Err(anyhow!("Invalid value ({}) for options menu item, must be between 0 and {}", new_value, opts.len() - 1)) + } + } + } + pd_func_caller!((*self.0).setMenuItemValue, item.inner.borrow().item, new_value as c_int) } + /// Set the title of a given menu item pub fn set_menu_item_title(&self, item: &MenuItem, new_title: &str) -> Result<(), Error> { let c_text = CString::new(new_title).map_err(|e| anyhow!("CString::new: {}", e))?; - pd_func_caller!((*self.0).setMenuItemTitle, item.borrow().item, c_text.as_ptr() as *mut c_char) + pd_func_caller!((*self.0).setMenuItemTitle, item.inner.borrow().item, c_text.as_ptr() as *mut c_char) } pub fn remove_menu_item(&self, item: MenuItem) -> Result<(), Error> { - pd_func_caller!((*self.0).removeMenuItem, item.borrow().item) + pd_func_caller!((*self.0).removeMenuItem, item.inner.borrow().item) } pub fn remove_all_menu_items(&self) -> Result<(), Error> { pd_func_caller!((*self.0).removeAllMenuItems) @@ -231,6 +260,12 @@ impl System { } } +/// The kind of menu item. See `System::add_{,checkmark_,options_}menu_item` for more details. +pub enum MenuItemKind { + Normal, + Checkmark, + Options(Vec), +} pub struct MenuItemInner { item: *mut PDMenuItem, raw_callback_ptr: *mut Box, @@ -244,6 +279,10 @@ impl Drop for MenuItemInner { } } } +pub struct MenuItem { + inner: Rc>, + pub kind: MenuItemKind, +} + -pub type MenuItem = Rc>; From 0049acf714979ec7393f7ed8339de186cd7f5262 Mon Sep 17 00:00:00 2001 From: Jonty Date: Mon, 1 Jan 2024 19:46:28 +0000 Subject: [PATCH 03/10] MenuItemKind implementation to add some protection around the checkbox and options items --- src/system.rs | 125 +++++++++++++++++++++++++++++++------------------- 1 file changed, 78 insertions(+), 47 deletions(-) diff --git a/src/system.rs b/src/system.rs index 3026585..51fe55d 100644 --- a/src/system.rs +++ b/src/system.rs @@ -3,17 +3,16 @@ use alloc::rc::Rc; use alloc::string::String; use alloc::vec::Vec; use core::cell::RefCell; + use anyhow::anyhow; -use { - crate::pd_func_caller, alloc::format, anyhow::Error, core::ptr, crankstart_sys::ctypes::c_void, - cstr_core::CString, -}; use crankstart_sys::ctypes::{c_char, c_int}; -use crankstart_sys::l_valtype::kInt; pub use crankstart_sys::PDButtons; use crankstart_sys::PDMenuItem; -use crate::{log_to_console, panic}; +use { + crate::pd_func_caller, anyhow::Error, core::ptr, crankstart_sys::ctypes::c_void, + cstr_core::CString, +}; static mut SYSTEM: System = System(ptr::null_mut()); @@ -71,68 +70,87 @@ impl System { let c_text = CString::new(title).map_err(|e| anyhow!("CString::new: {}", e))?; let wrapped_callback = Box::new(callback); let raw_callback_ptr = Box::into_raw(wrapped_callback); - let raw_menu_item = pd_func_caller!((*self.0).addMenuItem, c_text.as_ptr() as *mut core::ffi::c_char, Some(Self::menu_item_callback), raw_callback_ptr as *mut c_void)?; - Ok(MenuItem { - inner: Rc::new(RefCell::new(MenuItemInner { - item: raw_menu_item, - raw_callback_ptr, - })), - kind: MenuItemKind::Normal, - }) + let raw_menu_item = pd_func_caller!( + (*self.0).addMenuItem, + c_text.as_ptr() as *mut core::ffi::c_char, + Some(Self::menu_item_callback), + raw_callback_ptr as *mut c_void + )?; + Ok(MenuItem { + inner: Rc::new(RefCell::new(MenuItemInner { + item: raw_menu_item, + raw_callback_ptr, + })), + kind: MenuItemKind::Normal, + }) } /// Adds a option to the menu that has a checkbox. The initial_checked_state is the initial /// state of the checkbox. Callback will only be called when the menu is closed, not when the /// option is toggled. Use `System::get_menu_item_value` to get the state of the checkbox when /// the callback is called. - pub fn add_checkmark_menu_item(&self, title: &str, initial_checked_state: bool, callback: Box) -> Result { + pub fn add_checkmark_menu_item( + &self, + title: &str, + initial_checked_state: bool, + callback: Box, + ) -> Result { let c_text = CString::new(title).map_err(|e| anyhow!("CString::new: {}", e))?; let wrapped_callback = Box::new(callback); let raw_callback_ptr = Box::into_raw(wrapped_callback); - let raw_menu_item = pd_func_caller!((*self.0).addCheckmarkMenuItem, + let raw_menu_item = pd_func_caller!( + (*self.0).addCheckmarkMenuItem, c_text.as_ptr() as *mut core::ffi::c_char, initial_checked_state as c_int, Some(Self::menu_item_callback), - raw_callback_ptr as *mut c_void)?; - Ok( - MenuItem { - inner: Rc::new(RefCell::new(MenuItemInner { - item: raw_menu_item, - raw_callback_ptr, - })), - kind: MenuItemKind::Checkmark, - }) + raw_callback_ptr as *mut c_void + )?; + + Ok(MenuItem { + inner: Rc::new(RefCell::new(MenuItemInner { + item: raw_menu_item, + raw_callback_ptr, + })), + kind: MenuItemKind::Checkmark, + }) } /// Adds a option to the menu that has multiple values that can be cycled through. The initial /// value is the first element in `options`. Callback will only be called when the menu is /// closed, not when the option is toggled. Use `System::get_menu_item_value` to get the index /// of the options list when the callback is called, which can be used to lookup the value. - pub fn add_options_menu_item(&self, title: &str, options: Vec, callback: Box) -> Result { + pub fn add_options_menu_item( + &self, + title: &str, + options: Vec, + callback: Box, + ) -> Result { let c_text = CString::new(title).map_err(|e| anyhow!("CString::new: {}", e))?; let options_count = options.len() as c_int; - let c_options: Vec = options.iter().map(|s| CString::new(s.clone()).map_err(|e| anyhow!("CString::new: {}", e))).collect::, Error>>()?; + let c_options: Vec = options + .iter() + .map(|s| CString::new(s.clone()).map_err(|e| anyhow!("CString::new: {}", e))) + .collect::, Error>>()?; let c_options_ptrs: Vec<*const i8> = c_options.iter().map(|c| c.as_ptr()).collect(); let c_options_ptrs_ptr = c_options_ptrs.as_ptr(); let option_titles = c_options_ptrs_ptr as *mut *const c_char; let wrapped_callback = Box::new(callback); let raw_callback_ptr = Box::into_raw(wrapped_callback); - let raw_menu_item = pd_func_caller!((*self.0).addOptionsMenuItem, + let raw_menu_item = pd_func_caller!( + (*self.0).addOptionsMenuItem, c_text.as_ptr() as *mut core::ffi::c_char, option_titles, options_count, Some(Self::menu_item_callback), - raw_callback_ptr as *mut c_void)?; - Ok( - MenuItem { - inner: Rc - ::new(RefCell::new(MenuItemInner { - item: raw_menu_item, - raw_callback_ptr, - })), - kind: MenuItemKind::Options(options), - - }) + raw_callback_ptr as *mut c_void + )?; + Ok(MenuItem { + inner: Rc::new(RefCell::new(MenuItemInner { + item: raw_menu_item, + raw_callback_ptr, + })), + kind: MenuItemKind::Options(options), + }) } /// Returns the state of a given menu item. The meaning depends on the type of menu item. @@ -151,23 +169,37 @@ impl System { MenuItemKind::Normal => {} MenuItemKind::Checkmark => { if new_value > 1 { - return Err(anyhow!("Invalid value ({}) for checkmark menu item", new_value)) + return Err(anyhow!( + "Invalid value ({}) for checkmark menu item", + new_value + )); } } MenuItemKind::Options(opts) => { if new_value >= opts.len() { - return Err(anyhow!("Invalid value ({}) for options menu item, must be between 0 and {}", new_value, opts.len() - 1)) + return Err(anyhow!( + "Invalid value ({}) for options menu item, must be between 0 and {}", + new_value, + opts.len() - 1 + )); } } } - pd_func_caller!((*self.0).setMenuItemValue, item.inner.borrow().item, new_value as c_int) + pd_func_caller!( + (*self.0).setMenuItemValue, + item.inner.borrow().item, + new_value as c_int + ) } - /// Set the title of a given menu item pub fn set_menu_item_title(&self, item: &MenuItem, new_title: &str) -> Result<(), Error> { let c_text = CString::new(new_title).map_err(|e| anyhow!("CString::new: {}", e))?; - pd_func_caller!((*self.0).setMenuItemTitle, item.inner.borrow().item, c_text.as_ptr() as *mut c_char) + pd_func_caller!( + (*self.0).setMenuItemTitle, + item.inner.borrow().item, + c_text.as_ptr() as *mut c_char + ) } pub fn remove_menu_item(&self, item: MenuItem) -> Result<(), Error> { pd_func_caller!((*self.0).removeMenuItem, item.inner.borrow().item) @@ -266,6 +298,7 @@ pub enum MenuItemKind { Checkmark, Options(Vec), } + pub struct MenuItemInner { item: *mut PDMenuItem, raw_callback_ptr: *mut Box, @@ -279,10 +312,8 @@ impl Drop for MenuItemInner { } } } + pub struct MenuItem { inner: Rc>, pub kind: MenuItemKind, } - - - From 7e376a1050fcb415e94c361511042cc36dd40354 Mon Sep 17 00:00:00 2001 From: Jonty Date: Thu, 4 Jan 2024 22:28:40 +0000 Subject: [PATCH 04/10] Removing unused menuItem handling constants --- src/system.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/system.rs b/src/system.rs index 51fe55d..35404bc 100644 --- a/src/system.rs +++ b/src/system.rs @@ -16,10 +16,6 @@ use { static mut SYSTEM: System = System(ptr::null_mut()); -static mut MENU_ITEM_CALLBACKS: Vec> = Vec::new(); -const MENU_ITEM_CALLBACKS_MAX: usize = 10; -const MENU_ITEM_INDICES: [c_int; MENU_ITEM_CALLBACKS_MAX] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; - #[derive(Clone, Debug)] pub struct System(*const crankstart_sys::playdate_sys); From 2ea446566e35359a3035c02cea5d77e7e6d4d6f3 Mon Sep 17 00:00:00 2001 From: Jonty Date: Thu, 4 Jan 2024 22:29:59 +0000 Subject: [PATCH 05/10] rustfmt the menu_item example source --- examples/menu_items.rs | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/examples/menu_items.rs b/examples/menu_items.rs index 0dc5e5a..e51ca85 100644 --- a/examples/menu_items.rs +++ b/examples/menu_items.rs @@ -2,28 +2,28 @@ extern crate alloc; -use alloc::vec; use alloc::rc::Rc; use alloc::string::String; +use alloc::vec; use alloc::vec::Vec; use core::cell::RefCell; use hashbrown::HashMap; +use crankstart::system::MenuItemKind; use { alloc::boxed::Box, anyhow::Error, crankstart::{ crankstart_game, - Game, geometry::ScreenPoint, graphics::{Graphics, LCDColor, LCDSolidColor}, log_to_console, - Playdate, system::{MenuItem, System}, + system::{MenuItem, System}, + Game, Playdate, }, euclid::point2, }; -use crankstart::system::MenuItemKind; struct State { _menu_items: Rc>>, @@ -40,8 +40,7 @@ impl State { "Select Me", Box::new(|| { log_to_console!("Normal option picked"); - } - ), + }), )? }; let checkmark_item = { @@ -71,10 +70,8 @@ impl State { let this_menu_item = menu_items.get("options").unwrap(); let idx = System::get().get_menu_item_value(this_menu_item).unwrap(); match &this_menu_item.kind { - MenuItemKind::Options(opts) => { - opts.get(idx ).map(|s| s.clone()) - } - _ => None + MenuItemKind::Options(opts) => opts.get(idx).map(|s| s.clone()), + _ => None, } }; log_to_console!("Checked option picked: Value is now {:?}", value_of_item); @@ -98,7 +95,9 @@ impl Game for State { fn update(&mut self, _playdate: &mut Playdate) -> Result<(), Error> { let graphics = Graphics::get(); graphics.clear(LCDColor::Solid(LCDSolidColor::kColorWhite))?; - graphics.draw_text("Menu Items", self.text_location).unwrap(); + graphics + .draw_text("Menu Items", self.text_location) + .unwrap(); System::get().draw_fps(0, 0)?; @@ -106,5 +105,4 @@ impl Game for State { } } - crankstart_game!(State); From 4b73806745469ef6e168a1b74f8b80550b4f84d8 Mon Sep 17 00:00:00 2001 From: Jonty Date: Fri, 5 Jan 2024 16:12:03 +0000 Subject: [PATCH 06/10] fix some missing imports --- src/system.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/system.rs b/src/system.rs index aa6bc7b..ec22692 100644 --- a/src/system.rs +++ b/src/system.rs @@ -8,7 +8,7 @@ use anyhow::anyhow; use crankstart_sys::ctypes::{c_char, c_int}; pub use crankstart_sys::PDButtons; -use crankstart_sys::PDMenuItem; +use crankstart_sys::{PDDateTime, PDLanguage, PDMenuItem, PDPeripherals}; use { crate::pd_func_caller, anyhow::Error, core::ptr, crankstart_sys::ctypes::c_void, cstr_core::CString, @@ -53,7 +53,7 @@ impl System { )?; Ok((current, pushed, released)) } - + extern "C" fn menu_item_callback(user_data: *mut core::ffi::c_void) { unsafe { let callback = user_data as *mut Box; From e466b024c47d46add1c947745d67370f311aecd9 Mon Sep 17 00:00:00 2001 From: Jonty Date: Fri, 5 Jan 2024 16:40:15 +0000 Subject: [PATCH 07/10] Resolve soundness issue with dropping menu items --- src/system.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/system.rs b/src/system.rs index ec22692..0ed7e7b 100644 --- a/src/system.rs +++ b/src/system.rs @@ -198,7 +198,11 @@ impl System { ) } pub fn remove_menu_item(&self, item: MenuItem) -> Result<(), Error> { - pd_func_caller!((*self.0).removeMenuItem, item.inner.borrow().item) + self.remove_menu_item_internal(&item.inner.borrow()) + } + fn remove_menu_item_internal(&self, item_inner: &MenuItemInner) -> Result<(), Error> { + // Empirically it's been shown this is safe to call multiple times for the same item + pd_func_caller!((*self.0).removeMenuItem, item_inner.item) } pub fn remove_all_menu_items(&self) -> Result<(), Error> { pd_func_caller!((*self.0).removeAllMenuItems) @@ -352,6 +356,7 @@ pub struct MenuItemInner { impl Drop for MenuItemInner { fn drop(&mut self) { + System::get().remove_menu_item_internal(self).unwrap(); unsafe { // Recast into box to let Box deal with freeing the right memory let _ = Box::from_raw(self.raw_callback_ptr); From 78298956a8b8af8fc4633fdbaf07b4415f272fb7 Mon Sep 17 00:00:00 2001 From: Jonty Date: Fri, 5 Jan 2024 16:53:13 +0000 Subject: [PATCH 08/10] clarify drop handling of menu items --- src/system.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/system.rs b/src/system.rs index 0ed7e7b..5339ce7 100644 --- a/src/system.rs +++ b/src/system.rs @@ -198,10 +198,13 @@ impl System { ) } pub fn remove_menu_item(&self, item: MenuItem) -> Result<(), Error> { - self.remove_menu_item_internal(&item.inner.borrow()) + // Explicitly drops item. The actual calling of the removeMenuItem + // (via `remove_menu_item_internal`) is done in the drop impl to avoid calling it multiple + // times, even though that's been experimentally shown to be safe. + drop(item); + Ok(()) } fn remove_menu_item_internal(&self, item_inner: &MenuItemInner) -> Result<(), Error> { - // Empirically it's been shown this is safe to call multiple times for the same item pd_func_caller!((*self.0).removeMenuItem, item_inner.item) } pub fn remove_all_menu_items(&self) -> Result<(), Error> { @@ -356,6 +359,8 @@ pub struct MenuItemInner { impl Drop for MenuItemInner { fn drop(&mut self) { + // We must remove the menu item on drop to avoid a memory or having the firmware read + // unmanaged memory. System::get().remove_menu_item_internal(self).unwrap(); unsafe { // Recast into box to let Box deal with freeing the right memory From c97678ad8e79b05b15568ca6d2ecb94da3946f24 Mon Sep 17 00:00:00 2001 From: Jonty Date: Fri, 5 Jan 2024 16:54:55 +0000 Subject: [PATCH 09/10] `System::remove_all_menu_items` no longer makes sense, removed --- src/system.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/system.rs b/src/system.rs index 5339ce7..b2b0674 100644 --- a/src/system.rs +++ b/src/system.rs @@ -207,9 +207,6 @@ impl System { fn remove_menu_item_internal(&self, item_inner: &MenuItemInner) -> Result<(), Error> { pd_func_caller!((*self.0).removeMenuItem, item_inner.item) } - pub fn remove_all_menu_items(&self) -> Result<(), Error> { - pd_func_caller!((*self.0).removeAllMenuItems) - } pub fn set_peripherals_enabled(&self, peripherals: PDPeripherals) -> Result<(), Error> { pd_func_caller!((*self.0).setPeripheralsEnabled, peripherals) From b8902d88ea91a10a8bf18b7378755784575c8cd5 Mon Sep 17 00:00:00 2001 From: Jonty Date: Fri, 5 Jan 2024 16:59:22 +0000 Subject: [PATCH 10/10] remove menu_item tests from example --- examples/hello_world.rs | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/examples/hello_world.rs b/examples/hello_world.rs index ebffc33..8928c90 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -9,7 +9,6 @@ use { crankstart_game, geometry::{ScreenPoint, ScreenVector}, graphics::{Graphics, LCDColor, LCDSolidColor}, - log_to_console, system::System, Game, Playdate, }, @@ -25,24 +24,6 @@ struct State { impl State { pub fn new(_playdate: &Playdate) -> Result, Error> { crankstart::display::Display::get().set_refresh_rate(20.0)?; - System::get().add_menu_item( - "Say Hello", - Box::new(|| { - log_to_console!("Hello"); - }), - )?; - System::get().add_menu_item( - "Say Goodbye", - Box::new(|| { - log_to_console!("Goodbye"); - }), - )?; - System::get().add_menu_item( - "Sausage", - Box::new(|| { - log_to_console!("Sausage"); - }), - )?; Ok(Box::new(Self { location: point2(INITIAL_X, INITIAL_Y), velocity: vec2(1, 2),