Skip to content

Commit

Permalink
Add Choice Settings to the Auto Splitting Runtime (#745)
Browse files Browse the repository at this point in the history
This allows auto splitters to define settings widgets where the user can
choose between a set of options. These settings widgets could then be
visualized as for example dropdowns or radio buttons. Each option has a
string based key that is stored in the settings map as the value of the
setting when it is chosen.
  • Loading branch information
CryZe authored Dec 1, 2023
1 parent dc7f862 commit 14cc805
Show file tree
Hide file tree
Showing 8 changed files with 184 additions and 14 deletions.
12 changes: 5 additions & 7 deletions capi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,21 +120,19 @@ fn with_vec<F, R>(f: F) -> R
where
F: FnOnce(&mut Vec<u8>) -> R,
{
OUTPUT_VEC.with(|output| {
let mut output = output.borrow_mut();
OUTPUT_VEC.with_borrow_mut(|output| {
output.clear();
f(&mut output)
f(output)
})
}

fn output_vec<F>(f: F) -> *const c_char
where
F: FnOnce(&mut Vec<u8>),
{
OUTPUT_VEC.with(|output| {
let mut output = output.borrow_mut();
OUTPUT_VEC.with_borrow_mut(|output| {
output.clear();
f(&mut output);
f(output);
output.push(0);
output.as_ptr() as *const c_char
})
Expand Down Expand Up @@ -207,5 +205,5 @@ pub extern "C" fn dealloc(ptr: *mut u8, cap: usize) {
/// current thread. The length excludes the nul-terminator.
#[no_mangle]
pub extern "C" fn get_buf_len() -> usize {
OUTPUT_VEC.with(|v| v.borrow().len() - 1)
OUTPUT_VEC.with_borrow(|v| v.len() - 1)
}
28 changes: 28 additions & 0 deletions crates/livesplit-auto-splitting/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,34 @@ extern "C" {
description_len: usize,
heading_level: u32,
);
/// 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.
/// The description is what's shown to the user. The key of the default
/// option to show needs to be specified. The pointers need to point to
/// valid UTF-8 encoded text with the respective given length.
pub fn user_settings_add_choice(
key_ptr: *const u8,
key_len: usize,
description_ptr: *const u8,
description_len: usize,
default_option_key_ptr: *const u8,
default_option_key_len: usize,
);
/// Adds a new option to a choice setting. The key needs to match the key of
/// the choice setting that it's supposed to be added to. The option key is
/// used as the value to store when the user chooses this option. The
/// description is what's shown to the user. The pointers need to point to
/// valid UTF-8 encoded text with the respective given length. Returns
/// `true` if the option is at this point in time chosen by the user.
pub fn user_settings_add_choice_option(
key_ptr: *const u8,
key_len: usize,
option_key_ptr: *const u8,
option_key_len: usize,
option_description_ptr: *const u8,
option_description_len: usize,
) -> bool;
/// Adds a tooltip to a setting based on its key. A tooltip is useful for
/// explaining the purpose of a setting to the user. The pointers need to
/// point to valid UTF-8 encoded text with the respective given length.
Expand Down
28 changes: 28 additions & 0 deletions crates/livesplit-auto-splitting/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,34 @@
//! description_len: usize,
//! heading_level: u32,
//! );
//! /// 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.
//! /// The description is what's shown to the user. The key of the default
//! /// option to show needs to be specified. The pointers need to point to
//! /// valid UTF-8 encoded text with the respective given length.
//! pub fn user_settings_add_choice(
//! key_ptr: *const u8,
//! key_len: usize,
//! description_ptr: *const u8,
//! description_len: usize,
//! default_option_key_ptr: *const u8,
//! default_option_key_len: usize,
//! );
//! /// Adds a new option to a choice setting. The key needs to match the key of
//! /// the choice setting that it's supposed to be added to. The option key is
//! /// used as the value to store when the user chooses this option. The
//! /// description is what's shown to the user. The pointers need to point to
//! /// valid UTF-8 encoded text with the respective given length. Returns
//! /// `true` if the option is at this point in time chosen by the user.
//! pub fn user_settings_add_choice_option(
//! key_ptr: *const u8,
//! key_len: usize,
//! option_key_ptr: *const u8,
//! option_key_len: usize,
//! option_description_ptr: *const u8,
//! option_description_len: usize,
//! ) -> bool;
//! /// Adds a tooltip to a setting based on its key. A tooltip is useful for
//! /// explaining the purpose of a setting to the user. The pointers need to
//! /// point to valid UTF-8 encoded text with the respective given length.
Expand Down
66 changes: 65 additions & 1 deletion crates/livesplit-auto-splitting/src/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use crate::{
timer::Timer,
};

use anyhow::{ensure, format_err, Context as _, Result};
use anyhow::{bail, ensure, format_err, Context as _, Result};
use slotmap::{Key, KeyData, SlotMap};
use snafu::Snafu;
use std::{
Expand Down Expand Up @@ -1127,6 +1127,70 @@ fn bind_interface<T: Timer>(linker: &mut Linker<Context<T>>) -> Result<(), Creat
source,
name: "user_settings_add_title",
})?
.func_wrap("env", "user_settings_add_choice", {
|mut caller: Caller<'_, Context<T>>,
key_ptr: u32,
key_len: u32,
description_ptr: u32,
description_len: u32,
default_option_key_ptr: u32,
default_option_key_len: u32| {
let (memory, context) = memory_and_context(&mut caller);
let key = get_str(memory, key_ptr, key_len)?.into();
let description = get_str(memory, description_ptr, description_len)?.into();
let default_option_key =
get_str(memory, default_option_key_ptr, default_option_key_len)?.into();
Arc::make_mut(&mut context.settings_widgets).push(settings::Widget {
key,
description,
tooltip: None,
kind: settings::WidgetKind::Choice {
default_option_key,
options: Arc::new(Vec::new()),
},
});
Ok(())
}
})
.map_err(|source| CreationError::LinkFunction {
source,
name: "user_settings_add_choice",
})?
.func_wrap("env", "user_settings_add_choice_option", {
|mut caller: Caller<'_, Context<T>>,
key_ptr: u32,
key_len: u32,
option_key_ptr: u32,
option_key_len: u32,
option_description_ptr: u32,
option_description_len: u32| {
let (memory, context) = memory_and_context(&mut caller);
let key = get_str(memory, key_ptr, key_len)?.into();
let option_key = get_str(memory, option_key_ptr, option_key_len)?.into();
let option_description =
get_str(memory, option_description_ptr, option_description_len)?.into();
let setting = Arc::make_mut(&mut context.settings_widgets)
.iter_mut()
.find(|s| s.key == key)
.context("There is no setting with the provided key.")?;
let (options, is_chosen) = match &mut setting.kind {
settings::WidgetKind::Choice {
options,
default_option_key,
} => (options, *default_option_key == option_key),
_ => bail!("The setting is not a choice."),
};
Arc::make_mut(options).push(settings::ChoiceOption {
key: option_key,
description: option_description,
});
Ok(is_chosen as u32)
}
})
.map_err(|source| CreationError::LinkFunction {
source,
name: "user_settings_add_choice_option",
})?
.func_wrap("env", "user_settings_set_tooltip", {
|mut caller: Caller<'_, Context<T>>,
key_ptr: u32,
Expand Down
20 changes: 20 additions & 0 deletions crates/livesplit-auto-splitting/src/settings/gui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,24 @@ pub enum WidgetKind {
/// settings [`Map`](super::Map) yet.
default_value: bool,
},
/// A choice setting. This could be shown as a dropdown or radio buttons.
Choice {
/// The default value of the setting, if it's not available in the
/// settings [`Map`](super::Map) yet.
default_option_key: Arc<str>,
/// The available options for the setting.
options: Arc<Vec<ChoiceOption>>,
},
}

/// An option for a choice setting.
#[derive(Clone)]
pub struct ChoiceOption {
/// The unique identifier of the option. This is not meant to be shown to
/// the user and is only used to keep track of the option. This key is used
/// to store and retrieve the value of the option from the main settings
/// [`Map`](super::Map).
pub key: Arc<str>,
/// The name of the option that is shown to the user.
pub description: Arc<str>,
}
3 changes: 1 addition & 2 deletions crates/livesplit-hotkey/src/windows/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -267,8 +267,7 @@ const fn parse_scan_code(value: u32) -> Option<KeyCode> {
}

unsafe extern "system" fn callback_proc(code: i32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
let hook = STATE.with(|state| {
let mut state = state.borrow_mut();
let hook = STATE.with_borrow_mut(|state| {
let state = state.as_mut().expect("State should be initialized by now");

if code >= 0 {
Expand Down
39 changes: 36 additions & 3 deletions src/auto_splitting/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,34 @@
//! description_len: usize,
//! heading_level: u32,
//! );
//! /// 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.
//! /// The description is what's shown to the user. The key of the default
//! /// option to show needs to be specified. The pointers need to point to
//! /// valid UTF-8 encoded text with the respective given length.
//! pub fn user_settings_add_choice(
//! key_ptr: *const u8,
//! key_len: usize,
//! description_ptr: *const u8,
//! description_len: usize,
//! default_option_key_ptr: *const u8,
//! default_option_key_len: usize,
//! );
//! /// Adds a new option to a choice setting. The key needs to match the key of
//! /// the choice setting that it's supposed to be added to. The option key is
//! /// used as the value to store when the user chooses this option. The
//! /// description is what's shown to the user. The pointers need to point to
//! /// valid UTF-8 encoded text with the respective given length. Returns
//! /// `true` if the option is at this point in time chosen by the user.
//! pub fn user_settings_add_choice_option(
//! key_ptr: *const u8,
//! key_len: usize,
//! option_key_ptr: *const u8,
//! option_key_len: usize,
//! option_description_ptr: *const u8,
//! option_description_len: usize,
//! ) -> bool;
//! /// Adds a tooltip to a setting based on its key. A tooltip is useful for
//! /// explaining the purpose of a setting to the user. The pointers need to
//! /// point to valid UTF-8 encoded text with the respective given length.
Expand Down Expand Up @@ -882,11 +910,16 @@ async fn run(
} else {
let widget_value =
match runtime.settings_widgets().iter().find(|x| *x.key == key) {
Some(widget) => match widget.kind {
Some(widget) => match &widget.kind {
settings::WidgetKind::Bool { default_value } => {
Some(settings::Value::Bool(default_value))
Some(settings::Value::Bool(*default_value))
}
settings::WidgetKind::Title { heading_level: _ } => None,
settings::WidgetKind::Title { .. } => None,
settings::WidgetKind::Choice {
default_option_key, ..
} => Some(settings::Value::String(
default_option_key.clone(),
)),
},
None => None,
};
Expand Down
2 changes: 1 addition & 1 deletion tests/rendering.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ fn font_fallback() {
let build_number: u64 = system.kernel_version().unwrap().parse().unwrap();
let expected_hash = if build_number >= 22000 {
// Windows 11
"d16b447322881767"
"04fd5c64e5ca85f5"
} else {
// Windows 10
"f4bffc6bc6fab953"
Expand Down

0 comments on commit 14cc805

Please sign in to comment.