Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Menu item API #79

Merged
merged 12 commits into from
Jan 12, 2024
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,
}