Skip to content

Commit

Permalink
Add Tooltips and Titles to the User Settings (#45)
Browse files Browse the repository at this point in the history
Auto Splitters can now specify tooltips for the user settings.
Additionally there's a new type of user setting. Titles allow users to
group settings together and give them a name. This is useful for
settings that are related to each other. Heading levels can also be
specified to create a hierarchy of settings. Unlike ASL, the titles do
not by themselves have a value. A user interface could however still
allow toggling the grouped settings by clicking on the title.
  • Loading branch information
CryZe committed Jul 9, 2023
1 parent 62d912e commit c42e0d4
Show file tree
Hide file tree
Showing 8 changed files with 205 additions and 73 deletions.
7 changes: 6 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,15 @@ jobs:
run: |
cargo build --all-features --target ${{ matrix.target }}
- name: Test (All Features)
- name: Test (Target, All Features)
run: |
cargo test --all-features
# Test on the host to also run the doc tests
- name: Test (Host, All Features)
run: |
cargo test --target x86_64-unknown-linux-gnu --all-features
clippy:
name: Check clippy lints
runs-on: ubuntu-latest
Expand Down
127 changes: 81 additions & 46 deletions asr-derive/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use heck::ToTitleCase;
use proc_macro::TokenStream;
use quote::quote;
use syn::{Data, DeriveInput, Expr, ExprLit, Lit, LitStr, Meta};
use quote::{quote, quote_spanned};
use syn::{spanned::Spanned, Data, DeriveInput, Expr, ExprLit, Lit, Meta};

/// Generates a `register` method for a struct that automatically registers its
/// fields as settings and returns the struct with the user's settings applied.
Expand All @@ -11,7 +11,11 @@ use syn::{Data, DeriveInput, Expr, ExprLit, Lit, LitStr, Meta};
/// ```no_run
/// #[derive(Settings)]
/// struct MySettings {
/// /// General Settings
/// _general_settings: Title,
/// /// Use Game Time
/// ///
/// /// This is the tooltip.
/// use_game_time: bool,
/// }
/// ```
Expand All @@ -21,12 +25,14 @@ use syn::{Data, DeriveInput, Expr, ExprLit, Lit, LitStr, Meta};
/// ```no_run
/// impl MySettings {
/// pub fn register() -> Self {
/// let use_game_time = asr::Setting::register("use_game_time", "Use Game Time", false);
/// asr::user_settings::add_title("_general_settings", "General Settings", 0);
/// let use_game_time = asr::user_settings::add_bool("use_game_time", "Use Game Time", false);
/// asr::user_settings::set_tooltip("use_game_time", "This is the tooltip.");
/// Self { use_game_time }
/// }
/// }
/// ```
#[proc_macro_derive(Settings, attributes(default))]
#[proc_macro_derive(Settings, attributes(default, heading_level))]
pub fn settings_macro(input: TokenStream) -> TokenStream {
let ast: DeriveInput = syn::parse(input).unwrap();

Expand All @@ -40,60 +46,89 @@ pub fn settings_macro(input: TokenStream) -> TokenStream {
let mut field_names = Vec::new();
let mut field_name_strings = Vec::new();
let mut field_descs = Vec::new();
let mut field_defaults = Vec::new();
let mut field_tooltips = Vec::new();
let mut field_tys = Vec::new();
let mut args_init = Vec::new();
for field in struct_data.fields {
let ident = field.ident.clone().unwrap();
let ident_name = ident.to_string();
let ident_span = ident.span();
field_names.push(ident);
field_descs.push(
field
.attrs
.iter()
.find_map(|x| {
let Meta::NameValue(nv) = &x.meta else { return None };
if nv.path.get_ident()? != "doc" {
return None;
field_tys.push(field.ty);
let mut doc_string = String::new();
let mut tooltip_string = String::new();
let mut is_in_tooltip = false;
for attr in &field.attrs {
let Meta::NameValue(nv) = &attr.meta else { continue };
let Some(ident) = nv.path.get_ident() else { continue };
if ident != "doc" {
continue;
}
let Expr::Lit(ExprLit {
lit: Lit::Str(s), ..
}) = &nv.value else { continue };
let value = s.value();
let value = value.trim();
let target_string = if is_in_tooltip {
&mut tooltip_string
} else {
&mut doc_string
};
if !target_string.is_empty() {
if value.is_empty() {
if !is_in_tooltip {
is_in_tooltip = true;
continue;
}
let Expr::Lit(ExprLit {
lit: Lit::Str(s), ..
}) = &nv.value else { return None };
let lit = LitStr::new(s.value().trim(), s.span());
Some(Expr::Lit(ExprLit {
attrs: Vec::new(),
lit: Lit::Str(lit),
}))
})
.unwrap_or_else(|| {
Expr::Lit(ExprLit {
attrs: Vec::new(),
lit: Lit::Str(LitStr::new(&ident_name.to_title_case(), ident_span)),
})
}),
);
target_string.push('\n');
} else if !target_string.ends_with(|c: char| c.is_whitespace()) {
target_string.push(' ');
}
}
target_string.push_str(&value);
}
if doc_string.is_empty() {
doc_string = ident_name.to_title_case();
}

field_descs.push(doc_string);
field_tooltips.push(if tooltip_string.is_empty() {
quote! {}
} else {
quote! { asr::user_settings::set_tooltip(#ident_name, #tooltip_string); }
});
field_name_strings.push(ident_name);
field_defaults.push(
field
.attrs
.iter()
.find_map(|x| {
let Meta::NameValue(nv) = &x.meta else { return None };
if !nv.path.is_ident("default") {
return None;
}
Some(nv.value.clone())
})
.unwrap_or_else(|| {
syn::parse(quote! { ::core::default::Default::default() }.into()).unwrap()
}),
);

let args = field
.attrs
.iter()
.filter_map(|x| {
let Meta::NameValue(nv) = &x.meta else { return None };
let span = nv.span();
if nv.path.is_ident("default") {
let value = &nv.value;
Some(quote_spanned! { span => args.default = #value; })
} else if nv.path.is_ident("heading_level") {
let value = &nv.value;
Some(quote_spanned! { span => args.heading_level = #value; })
} else {
None
}
})
.collect::<Vec<_>>();
args_init.push(quote! { #(#args)* });
}

quote! {
impl #struct_name {
pub fn register() -> Self {
Self {
#(#field_names: asr::Setting::register(#field_name_strings, #field_descs, #field_defaults),)*
#(#field_names: {
let mut args = <#field_tys as asr::user_settings::Setting>::Args::default();
#args_init
let mut value = asr::user_settings::Setting::register(#field_name_strings, #field_descs, args);
#field_tooltips
value
},)*
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ pub mod sync;
pub mod time_util;
pub mod watcher;

pub use self::{primitives::*, runtime::*};
pub use self::{primitives::*, runtime::*, user_settings::Setting};
pub use arrayvec;
pub use time;

Expand Down
16 changes: 1 addition & 15 deletions src/runtime/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
pub use memory_range::*;
pub use process::Process;
pub use process::*;

mod memory_range;
mod process;
Expand All @@ -13,20 +13,6 @@ pub mod user_settings;
#[non_exhaustive]
pub struct Error {}

/// A type that can be registered as a user setting.
pub trait Setting {
/// Registers the setting with the given key, description and default value.
/// Returns the value that the user has set or the default value if the user
/// did not set a value.
fn register(key: &str, description: &str, default_value: Self) -> Self;
}

impl Setting for bool {
fn register(key: &str, description: &str, default_value: Self) -> Self {
user_settings::add_bool(key, description, default_value)
}
}

/// Sets the tick rate of the runtime. This influences how many times per second
/// the `update` function is called. The default tick rate is 120 ticks per
/// second.
Expand Down
25 changes: 23 additions & 2 deletions src/runtime/sys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,13 +121,34 @@ extern "C" {
/// Example values: `x86`, `x86_64`, `arm`, `aarch64`
pub fn runtime_get_arch(buf_ptr: *mut u8, buf_len_ptr: *mut usize) -> bool;

/// Adds a new setting that the user can modify. This will return either
/// the specified default value or the value that the user has set.
/// Adds a new boolean setting that the user can modify. This will return
/// either the specified default value or the value that the user has set.
/// The key is used to store the setting and needs to be unique across all
/// types of settings.
pub fn user_settings_add_bool(
key_ptr: *const u8,
key_len: usize,
description_ptr: *const u8,
description_len: usize,
default_value: bool,
) -> bool;
/// Adds a new title to the user settings. This is used to group settings
/// together. The heading level determines the size of the title. The top
/// level titles use a heading level of 0. The key needs to be unique across
/// all types of settings.
pub fn user_settings_add_title(
key_ptr: *const u8,
key_len: usize,
description_ptr: *const u8,
description_len: usize,
heading_level: u32,
);
/// Adds a tooltip to a setting based on its key. A tooltip is useful for
/// explaining the purpose of a setting to the user.
pub fn user_settings_set_tooltip(
key_ptr: *const u8,
key_len: usize,
tooltip_ptr: *const u8,
tooltip_len: usize,
);
}
88 changes: 86 additions & 2 deletions src/runtime/user_settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

use super::sys;

/// Adds a new setting that the user can modify. This will return either the
/// specified default value or the value that the user has set.
/// Adds a new boolean setting that the user can modify. This will return either
/// the specified default value or the value that the user has set. The key is
/// used to store the setting and needs to be unique across all types of
/// settings.
#[inline]
pub fn add_bool(key: &str, description: &str, default_value: bool) -> bool {
// SAFETY: We provide valid pointers and lengths to key and description.
Expand All @@ -18,3 +20,85 @@ pub fn add_bool(key: &str, description: &str, default_value: bool) -> bool {
)
}
}

/// Adds a new title to the user settings. This is used to group settings
/// together. The heading level determines the size of the title. The top level
/// titles use a heading level of 0. The key needs to be unique across all types
/// of settings.
#[inline]
pub fn add_title(key: &str, description: &str, heading_level: u32) {
// 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_title(
key.as_ptr(),
key.len(),
description.as_ptr(),
description.len(),
heading_level,
)
}
}

/// Adds a tooltip to a setting based on its key. A tooltip is useful for
/// explaining the purpose of a setting to the user.
#[inline]
pub fn set_tooltip(key: &str, tooltip: &str) {
// 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_set_tooltip(key.as_ptr(), key.len(), tooltip.as_ptr(), tooltip.len())
}
}

/// A type that can be registered as a user setting. This is an internal trait
/// that you don't need to worry about.
pub trait Setting {
/// The arguments that are needed to register the setting.
type Args: Default;
/// Registers the setting with the given key, description and default value.
/// Returns the value that the user has set or the default value if the user
/// did not set a value.
fn register(key: &str, description: &str, args: Self::Args) -> Self;
}

/// The arguments that are needed to register a boolean setting. This is an
/// internal type that you don't need to worry about.
#[derive(Default)]
#[non_exhaustive]
pub struct BoolArgs {
/// The default value of the setting, in case the user didn't set it yet.
pub default: bool,
}

impl Setting for bool {
type Args = BoolArgs;

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

/// A title that can be used to group settings together.
pub struct Title;

/// The arguments that are needed to register a title. This is an internal type
/// that you don't need to worry about.
#[derive(Default)]
#[non_exhaustive]
pub struct TitleArgs {
/// The heading level of the title. The top level titles use a heading level
/// of 0.
pub heading_level: u32,
}

impl Setting for Title {
type Args = TitleArgs;

#[inline]
fn register(key: &str, description: &str, args: Self::Args) -> Self {
add_title(key, description, args.heading_level);
Self
}
}
9 changes: 5 additions & 4 deletions src/string.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,8 @@ impl<const N: usize> ArrayWString<N> {
}

/// Returns the 16-bit characters of the string up until (but excluding) the
/// nul-terminator. If there is no nul-terminator, all bytes are returned.
/// nul-terminator. If there is no nul-terminator, all characters are
/// returned.
pub fn as_slice(&self) -> &[u16] {
let len = self.0.iter().position(|&b| b == 0).unwrap_or(N);
&self.0[..len]
Expand All @@ -101,9 +102,9 @@ impl<const N: usize> ArrayWString<N> {
/// calling [`as_slice`](Self::as_slice) and then comparing, because it can
/// use the length information of the parameter.
pub fn matches(&self, text: impl AsRef<[u16]>) -> bool {
let bytes = text.as_ref();
!self.0.get(bytes.len()).is_some_and(|&b| b != 0)
&& self.0.get(..bytes.len()).is_some_and(|s| s == bytes)
let chars = text.as_ref();
!self.0.get(chars.len()).is_some_and(|&b| b != 0)
&& self.0.get(..chars.len()).is_some_and(|s| s == chars)
}

/// Checks whether the string matches the given text. This dynamically
Expand Down
4 changes: 2 additions & 2 deletions src/time_util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ mod instant {
}
}

/// A version of [`std::time::Instant`] using WASI that doesn't need the
/// standard library.
/// A version of the standard library's `Instant` using WASI that doesn't
/// need the standard library.
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
#[repr(transparent)]
pub struct Instant(pub(crate) Timestamp);
Expand Down

0 comments on commit c42e0d4

Please sign in to comment.