Skip to content

Commit

Permalink
Add Tooltips and Titles to the User Settings
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 748b34e
Show file tree
Hide file tree
Showing 8 changed files with 206 additions and 74 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
4 changes: 2 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#![no_std]
#![cfg_attr(not(feature = "std"), no_std)]
#![warn(
clippy::complexity,
clippy::correctness,
Expand Down 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
Loading

0 comments on commit 748b34e

Please sign in to comment.