Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 47 additions & 1 deletion asr-derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,17 @@ use syn::{
/// # }
/// ```
///
/// A settings button requires an `on_click` handler and the `alloc` feature.
/// You also need to call `asr::export_settings_buttons!()` so the runtime can
/// invoke it. Without that macro, the host ignores clicks.
///
/// ```no_run
/// # struct Settings {
/// #[button(on_click = clear_counters)]
/// clear_counters: Button,
/// # }
/// ```
///
/// # Choices
///
/// You can derive `Gui` for an enum to create a choice widget. You can mark one
Expand Down Expand Up @@ -132,7 +143,7 @@ use syn::{
/// use_game_time: Pair<bool>,
/// }
/// ```
#[proc_macro_derive(Gui, attributes(default, heading_level, filter))]
#[proc_macro_derive(Gui, attributes(default, heading_level, filter, button))]
pub fn settings_macro(input: TokenStream) -> TokenStream {
let ast: DeriveInput = syn::parse(input).unwrap();

Expand Down Expand Up @@ -162,6 +173,17 @@ fn generate_struct_settings(struct_name: Ident, struct_data: DataStruct) -> Resu
let ident = field.ident.clone().unwrap();
let ident_name = ident.to_string();
field_names.push(ident);
if is_button_type(&field.ty)
&& !field
.attrs
.iter()
.any(|attr| attr.path().is_ident("button"))
{
return Err(Error::new(
field.ty.span(),
"Button fields require #[button(on_click = ...)]",
));
}
field_tys.push(field.ty);
let mut doc_string = String::new();
let mut tooltip_string = String::new();
Expand Down Expand Up @@ -233,6 +255,8 @@ fn generate_struct_settings(struct_name: Ident, struct_data: DataStruct) -> Resu
Meta::List(list) => {
if list.path.is_ident("filter") {
Some(parse_filter(list))
} else if list.path.is_ident("button") {
Some(parse_button(list))
} else {
None
}
Expand Down Expand Up @@ -384,6 +408,28 @@ fn generate_enum_settings(enum_name: Ident, enum_data: DataEnum) -> Result<Token
.into())
}

fn is_button_type(ty: &syn::Type) -> bool {
let syn::Type::Path(path) = ty else {
return false;
};
path.path
.segments
.last()
.is_some_and(|segment| segment.ident == "Button" && segment.arguments.is_empty())
}

fn parse_button(list: &MetaList) -> Result<proc_macro2::TokenStream> {
let span = list.span();
let nv: syn::MetaNameValue = list
.parse_args()
.map_err(|_| Error::new(span, "expected `#[button(on_click = ...)]`"))?;
if !nv.path.is_ident("on_click") {
return Err(Error::new(nv.path.span(), "expected `on_click`"));
}
let value = &nv.value;
Ok(quote_spanned! { span => args.on_click = #value; })
}

fn parse_filter(list: &MetaList) -> Result<proc_macro2::TokenStream> {
let span = list.span();
let mut filters = Vec::new();
Expand Down
214 changes: 214 additions & 0 deletions src/runtime/settings/gui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ pub use asr_derive::Gui;

use crate::{runtime::sys, watcher::Pair};

#[cfg(feature = "alloc")]
use crate::sync::RacyCell;

use super::map::Map;

/// Adds a new boolean setting widget to the settings GUI that the user can
Expand Down Expand Up @@ -49,6 +52,152 @@ pub fn add_title(key: &str, description: &str, heading_level: u32) {
}
}

#[cfg(feature = "alloc")]
type ButtonHandler = (alloc::string::String, fn());

#[cfg(feature = "alloc")]
static BUTTON_HANDLERS: RacyCell<alloc::vec::Vec<ButtonHandler>> =
RacyCell::new(alloc::vec::Vec::new());

#[cfg(feature = "alloc")]
fn register_button_handler(key: &str, on_click: fn()) {
// SAFETY: The auto splitter runtime is single-threaded, so there are no
// other references to the handler list while we mutate it.
let handlers = unsafe { &mut *BUTTON_HANDLERS.get_mut() };
if let Some((_, slot)) = handlers.iter_mut().find(|(k, _)| k == key) {
*slot = on_click;
return;
}
handlers.push((key.into(), on_click));
}

/// Adds a new button to the settings GUI. The key needs to be unique across
/// all types of settings and is not persisted in the settings
/// [`Map`](super::Map). The description is what's shown to the user. When the
/// user clicks the button, `on_click` is called.
///
/// Click handlers are synchronous. They may load and store the settings
/// [`Map`](super::Map), but they cannot `.await`.
///
/// Clicks are only delivered if you export the host entry point with
/// [`export_settings_buttons`](macro@crate::export_settings_buttons). Without that macro, the host ignores clicks.
///
/// This requires the `alloc` feature.
///
/// # Example
///
/// ```ignore
/// fn clear_counters() {
/// let mut map = asr::settings::Map::load();
/// map.insert("deaths", 0i64);
/// map.store();
/// }
///
/// asr::settings::gui::add_button("clear_counters", "Clear Counters", clear_counters);
///
/// asr::export_settings_buttons!();
/// ```
#[cfg(feature = "alloc")]
#[inline]
pub fn add_button(key: &str, description: &str, on_click: fn()) {
// SAFETY: We provide valid pointers and lengths to key and description.
// They are also guaranteed to be valid UTF-8 strings.
unsafe {
sys::user_settings_add_button(
key.as_ptr(),
key.len(),
description.as_ptr(),
description.len(),
)
}
register_button_handler(key, on_click);
}

/// Dispatches a settings button click to the handler registered for `key`.
/// This is called by [`export_settings_buttons`](macro@crate::export_settings_buttons).
#[cfg(feature = "alloc")]
pub fn dispatch_button(key: &str) {
// SAFETY: The auto splitter runtime is single-threaded. The function
// pointer is copied out so the handler can run without aliasing the
// handler list, including if it registers further buttons.
let on_click = unsafe {
(*BUTTON_HANDLERS.get())
.iter()
.find(|(k, _)| k == key)
.map(|(_, f)| *f)
};
if let Some(on_click) = on_click {
on_click();
}
}

/// Reads the key of the settings button that is currently being invoked.
#[cfg(feature = "alloc")]
#[doc(hidden)]
pub fn get_button_key() -> Option<alloc::string::String> {
// SAFETY: Calling with a null pointer and 0 length returns the required
// length. We then allocate a buffer and call again. After a successful
// call, the buffer contains `len` bytes of valid UTF-8.
unsafe {
let mut len = 0;
let success = sys::user_settings_get_button_key(core::ptr::null_mut(), &mut len);
if len == 0 && !success {
return None;
}
let mut buf = alloc::vec::Vec::with_capacity(len);
let success = sys::user_settings_get_button_key(buf.as_mut_ptr(), &mut len);
if !success {
return None;
}
buf.set_len(len);
Some(alloc::string::String::from_utf8_unchecked(buf))
}
}

/// Exports the `on_settings_button` entry point so the runtime can deliver
/// settings button clicks to handlers registered with [`add_button`] or
/// `#[button(on_click = ...)]`.
///
/// The generated export takes no arguments. It pulls the clicked button's key
/// through `user_settings_get_button_key` and then calls [`dispatch_button`].
/// Authors never see `key_ptr` / `key_len` on the export. This is the
/// import-pull ABI used by `livesplit-auto-splitting`.
///
/// Without this macro, the host ignores button clicks. Requires the `alloc`
/// feature.
///
/// # Example
///
/// ```ignore
/// asr::export_settings_buttons!();
///
/// fn clear_counters() {
/// let mut map = asr::settings::Map::load();
/// map.insert("deaths", 0i64);
/// map.store();
/// }
///
/// #[no_mangle]
/// pub extern "C" fn update() {
/// asr::settings::gui::add_button("clear_counters", "Clear Counters", clear_counters);
/// }
/// ```
#[macro_export]
macro_rules! export_settings_buttons {
() => {
/// Called by the runtime when the user clicks a settings button.
///
/// # Safety
/// This is invoked by the auto splitting runtime.
#[no_mangle]
pub unsafe extern "C" fn on_settings_button() {
if let Some(key) = $crate::settings::gui::get_button_key() {
$crate::settings::gui::dispatch_button(&key);
}
}
};
}

/// Adds a new choice setting widget that the user can modify. This allows the
/// user to choose between various options. The key is used to store the setting
/// in the settings [`Map`](super::Map) and needs to be unique across all types
Expand Down Expand Up @@ -264,6 +413,71 @@ impl Widget for Title {
fn update_from(&mut self, _settings_map: &Map, _key: &str, _args: Self::Args) {}
}

/// A button that the user can click. Buttons are not persisted in the settings
/// [`Map`](super::Map).
///
/// Click handlers are synchronous. They may load and store the settings
/// [`Map`](super::Map), but they cannot `.await`.
///
/// The field requires `#[button(on_click = ...)]`. Clicks are only delivered if
/// you also call [`export_settings_buttons`](macro@crate::export_settings_buttons). Without that macro, the host
/// ignores clicks.
///
/// This requires the `alloc` feature.
///
/// # Example
///
/// ```ignore
/// fn clear_counters() {
/// let mut map = asr::settings::Map::load();
/// map.insert("deaths", 0i64);
/// map.store();
/// }
///
/// #[derive(Gui)]
/// struct Settings {
/// /// Clear Counters
/// #[button(on_click = clear_counters)]
/// clear_counters: Button,
/// }
///
/// asr::export_settings_buttons!();
/// ```
#[cfg(feature = "alloc")]
pub struct Button;

/// The arguments that are needed to register a button. This is an internal type
/// that you don't need to worry about.
#[cfg(feature = "alloc")]
#[doc(hidden)]
#[non_exhaustive]
pub struct ButtonArgs {
/// The function to call when the user clicks the button.
pub on_click: fn(),
}

#[cfg(feature = "alloc")]
impl Default for ButtonArgs {
#[inline]
fn default() -> Self {
Self { on_click: || {} }
}
}

#[cfg(feature = "alloc")]
impl Widget for Button {
type Args = ButtonArgs;

#[inline]
fn register(key: &str, description: &str, args: Self::Args) -> Self {
add_button(key, description, args.on_click);
Self
}

#[inline]
fn update_from(&mut self, _settings_map: &Map, _key: &str, _args: Self::Args) {}
}

impl<T: Clone + Widget> Widget for Pair<T> {
type Args = T::Args;

Expand Down
31 changes: 31 additions & 0 deletions src/runtime/settings/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,37 @@
//! Check the [`Gui`](macro@Gui) derive macro and the [`Gui`](trait@Gui) trait
//! for more information.
//!
//! # Settings buttons
//!
//! Buttons run a callback when clicked. They are not stored in the settings
//! [`Map`]. Register them with [`gui::add_button`] or with
//! `#[button(on_click = ...)]` on a [`gui::Button`] field:
//!
//! ```ignore
//! fn clear_counters() {
//! let mut map = asr::settings::Map::load();
//! map.insert("deaths", 0i64);
//! map.store();
//! }
//!
//! asr::settings::gui::add_button("clear_counters", "Clear Counters", clear_counters);
//!
//! // or:
//! #[derive(Gui)]
//! struct Settings {
//! /// Clear Counters
//! #[button(on_click = clear_counters)]
//! clear_counters: asr::settings::gui::Button,
//! }
//!
//! asr::export_settings_buttons!();
//! ```
//!
//! Click handlers are synchronous and may mutate the settings [`Map`], but they
//! cannot `.await`. The [`export_settings_buttons`](macro@crate::export_settings_buttons)
//! macro is required for clicks to run; without it, the host ignores clicks.
//! Buttons require the `alloc` feature.
//!
//! # Modifying the global settings map
//!
//! ```no_run
Expand Down
23 changes: 23 additions & 0 deletions src/runtime/sys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,19 @@ extern "C" {
description_len: usize,
heading_level: u32,
);
/// Adds a new button to the user settings. This is used to trigger an
/// action in the auto splitter. The key needs to be unique across all
/// types of settings and is not persisted in the settings map. The
/// pointers need to point to valid UTF-8 encoded text with the respective
/// given length. When the user clicks the button, the auto splitter's
/// `on_settings_button` export is called.
#[cfg(feature = "alloc")]
pub fn user_settings_add_button(
key_ptr: *const u8,
key_len: usize,
description_ptr: *const u8,
description_len: usize,
);
/// Adds a new choice setting that the user can modify. This allows the user
/// to choose between various options. The key is used to store the setting
/// in the settings map and needs to be unique across all types of settings.
Expand Down Expand Up @@ -334,6 +347,16 @@ extern "C" {
tooltip_ptr: *const u8,
tooltip_len: usize,
);
/// Stores the key of the settings button that is currently being invoked
/// in the buffer given. Returns `false` if the buffer is too small or if
/// no settings button is currently being invoked. After this call, no
/// matter whether it was successful or not, the `buf_len_ptr` will be set
/// to the required buffer size. If `false` is returned and the
/// `buf_len_ptr` got set to 0, no settings button is currently being
/// invoked. The key is guaranteed to be valid UTF-8 and is not
/// nul-terminated.
#[cfg(feature = "alloc")]
pub fn user_settings_get_button_key(buf_ptr: *mut u8, buf_len_ptr: *mut usize) -> bool;

/// Creates a new settings map. You own the settings map and are responsible
/// for freeing it.
Expand Down