Skip to content

Commit

Permalink
Menu item API (#79)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
tehsmeely authored Jan 12, 2024
1 parent 4f3933a commit 26afcc3
Show file tree
Hide file tree
Showing 3 changed files with 308 additions and 5 deletions.
5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
108 changes: 108 additions & 0 deletions examples/menu_items.rs
Original file line number Diff line number Diff line change
@@ -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<RefCell<HashMap<&'static str, MenuItem>>>,
text_location: ScreenPoint,
}

impl State {
pub fn new(_playdate: &Playdate) -> Result<Box<Self>, 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<String> = 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);
200 changes: 195 additions & 5 deletions src/system.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand Down Expand Up @@ -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<dyn Fn()>;
(*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<dyn Fn()>) -> Result<MenuItem, Error> {
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<dyn Fn()>,
) -> Result<MenuItem, Error> {
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<String>,
callback: Box<dyn Fn()>,
) -> Result<MenuItem, Error> {
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<CString> = options
.iter()
.map(|s| CString::new(s.clone()).map_err(|e| anyhow!("CString::new: {}", e)))
.collect::<Result<Vec<CString>, 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<usize, Error> {
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)
}
Expand Down Expand Up @@ -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<String>),
}

pub struct MenuItemInner {
item: *mut PDMenuItem,
raw_callback_ptr: *mut Box<dyn Fn()>,
}

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<RefCell<MenuItemInner>>,
pub kind: MenuItemKind,
}

0 comments on commit 26afcc3

Please sign in to comment.