From 26afcc3b402b4aead9a3341a7c9926fb90fcc9c8 Mon Sep 17 00:00:00 2001 From: Jonty Heiser Date: Fri, 12 Jan 2024 09:01:44 +0000 Subject: [PATCH] Menu item API (#79) * Add menu item api * MenuItemKind implementation to add some protection around the checkbox and options items * Removing unused menuItem handling constants * Resolve soundness issue with dropping menu items * clarify drop handling of menu items * `System::remove_all_menu_items` no longer makes sense, removed * remove menu_item tests from example --- Cargo.toml | 5 ++ examples/menu_items.rs | 108 ++++++++++++++++++++++ src/system.rs | 200 +++++++++++++++++++++++++++++++++++++++-- 3 files changed, 308 insertions(+), 5 deletions(-) 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/menu_items.rs b/examples/menu_items.rs new file mode 100644 index 0000000..e51ca85 --- /dev/null +++ b/examples/menu_items.rs @@ -0,0 +1,108 @@ +#![no_std] + +extern crate alloc; + +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, + geometry::ScreenPoint, + graphics::{Graphics, LCDColor, LCDSolidColor}, + log_to_console, + system::{MenuItem, System}, + Game, Playdate, + }, + euclid::point2, +}; + +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()]; + 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("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, + } + }; + 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: 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) + .unwrap(); + + System::get().draw_fps(0, 0)?; + + Ok(()) + } +} + +crankstart_game!(State); diff --git a/src/system.rs b/src/system.rs index 79489ff..b2b0674 100644 --- a/src/system.rs +++ b/src/system.rs @@ -1,12 +1,19 @@ +use alloc::boxed::Box; +use alloc::rc::Rc; +use alloc::string::String; +use alloc::vec::Vec; +use core::cell::RefCell; + +use anyhow::anyhow; + +use crankstart_sys::ctypes::{c_char, c_int}; +pub use crankstart_sys::PDButtons; +use crankstart_sys::{PDDateTime, PDLanguage, PDMenuItem, PDPeripherals}; use { - crate::pd_func_caller, alloc::format, anyhow::Error, core::ptr, crankstart_sys::ctypes::c_void, + crate::pd_func_caller, anyhow::Error, core::ptr, crankstart_sys::ctypes::c_void, cstr_core::CString, }; -use crankstart_sys::ctypes::c_int; -pub use crankstart_sys::PDButtons; -use crankstart_sys::{PDDateTime, PDLanguage, PDPeripherals}; - static mut SYSTEM: System = System(ptr::null_mut()); #[derive(Clone, Debug)] @@ -47,6 +54,160 @@ 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(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 { + 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(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 { + 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_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(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.inner.borrow().item)?; + Ok(value as usize) + } + + /// 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.inner.borrow().item, + c_text.as_ptr() as *mut c_char + ) + } + pub fn remove_menu_item(&self, item: MenuItem) -> Result<(), Error> { + // 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> { + pd_func_caller!((*self.0).removeMenuItem, item_inner.item) + } + pub fn set_peripherals_enabled(&self, peripherals: PDPeripherals) -> Result<(), Error> { pd_func_caller!((*self.0).setPeripheralsEnabled, peripherals) } @@ -180,3 +341,32 @@ impl System { pd_func_caller!((*self.0).getLanguage) } } + +/// 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, +} + +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 + let _ = Box::from_raw(self.raw_callback_ptr); + } + } +} + +pub struct MenuItem { + inner: Rc>, + pub kind: MenuItemKind, +}