Skip to content

Commit

Permalink
Implement choice settings widgets (#81)
Browse files Browse the repository at this point in the history
This adds choice settings widgets to the settings GUI. You can derive
your own enum to describe the choices.
  • Loading branch information
CryZe committed Dec 1, 2023
1 parent 35b9731 commit 57a1a5d
Show file tree
Hide file tree
Showing 3 changed files with 219 additions and 7 deletions.
156 changes: 149 additions & 7 deletions asr-derive/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use heck::ToTitleCase;
use proc_macro::TokenStream;
use quote::{quote, quote_spanned};
use syn::{spanned::Spanned, Data, DeriveInput, Expr, ExprLit, Lit, Meta};
use syn::{
spanned::Spanned, Data, DataEnum, DataStruct, DeriveInput, Expr, ExprLit, Ident, Lit, Meta,
};

// FIXME: https://github.com/rust-lang/rust/issues/117463
#[allow(rustdoc::redundant_explicit_links)]
Expand Down Expand Up @@ -66,6 +68,34 @@ use syn::{spanned::Spanned, Data, DeriveInput, Expr, ExprLit, Lit, Meta};
/// # }
/// ```
///
/// # Choices
///
/// You can derive `Gui` for an enum to create a choice widget. You can mark one
/// of the variants as the default by adding the `#[default]` attribute to it.
///
/// ```no_run
/// #[derive(Gui)]
/// enum Category {
/// /// Any%
/// AnyPercent,
/// /// Glitchless
/// Glitchless,
/// /// 100%
/// #[default]
/// HundredPercent,
/// }
/// ```
///
/// You can then use it as a widget like so:
///
/// ```no_run
/// #[derive(Gui)]
/// struct Settings {
/// /// Category
/// category: Category,
/// }
/// ```
///
/// # Tracking changes
///
/// You can track changes to a setting by wrapping the widget type in a `Pair`.
Expand All @@ -85,13 +115,14 @@ use syn::{spanned::Spanned, Data, DeriveInput, Expr, ExprLit, Lit, Meta};
pub fn settings_macro(input: TokenStream) -> TokenStream {
let ast: DeriveInput = syn::parse(input).unwrap();

let struct_data = match ast.data {
Data::Struct(s) => s,
_ => panic!("Only structs are supported"),
};

let struct_name = ast.ident;
match ast.data {
Data::Struct(s) => generate_struct_settings(ast.ident, s),
Data::Enum(e) => generate_enum_settings(ast.ident, e),
_ => panic!("Only structs and enums are supported"),
}
}

fn generate_struct_settings(struct_name: Ident, struct_data: DataStruct) -> TokenStream {
let mut field_names = Vec::new();
let mut field_name_strings = Vec::new();
let mut field_descs = Vec::new();
Expand Down Expand Up @@ -206,6 +237,117 @@ pub fn settings_macro(input: TokenStream) -> TokenStream {
.into()
}

fn generate_enum_settings(enum_name: Ident, enum_data: DataEnum) -> TokenStream {
let mut variant_names = Vec::new();
let mut variant_name_strings = Vec::new();
let mut variant_descs = Vec::new();
let mut default_index = None;
for (index, variant) in enum_data.variants.into_iter().enumerate() {
let ident = variant.ident.clone();
let ident_name = ident.to_string();
variant_names.push(ident);
let mut doc_string = String::new();
let mut tooltip_string = String::new();
let mut is_in_tooltip = false;
for attr in &variant.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;
}
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();
}

variant_descs.push(doc_string);
variant_name_strings.push(ident_name);

let is_default = variant.attrs.iter().any(|x| {
let Meta::Path(path) = &x.meta else {
return false;
};
path.is_ident("default")
});

if is_default {
if default_index.is_some() {
panic!("Only one variant can be marked as default");
}
default_index = Some(index);
}
}

let default_index = default_index.unwrap_or_default();

let default_option = &variant_names[default_index];
let default_option_key = &variant_name_strings[default_index];

let longest_string = variant_name_strings
.iter()
.map(|x| x.len())
.max()
.unwrap_or_default();

quote! {
impl asr::settings::gui::Widget for #enum_name {
type Args = ();

#[inline]
fn register(key: &str, description: &str, args: Self::Args) -> Self {
asr::settings::gui::add_choice(key, description, #default_option_key);
let mut v = Self::#default_option;
#(if asr::settings::gui::add_choice_option(key, #variant_name_strings, #variant_descs) {
v = Self::#variant_names;
})*
v
}

#[inline]
fn update_from(&mut self, settings_map: &asr::settings::Map, key: &str, args: Self::Args) {
let Some(option_key) = settings_map.get(key).and_then(|v| v.get_array_string::<#longest_string>()?.ok()) else {
*self = Self::#default_option;
return;
};
*self = match &*option_key {
#(#variant_name_strings => Self::#variant_names,)*
_ => Self::#default_option,
};
}
}
}
.into()
}

/// Generates an implementation of the `FromEndian` trait for a struct. This
/// allows converting values from a given endianness to the host's endianness.
///
Expand Down
42 changes: 42 additions & 0 deletions src/runtime/settings/gui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,48 @@ pub fn add_title(key: &str, description: &str, heading_level: u32) {
}
}

/// 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
/// of settings. The description is what's shown to the user. The key of the
/// default option to show needs to be specified.
#[inline]
pub fn add_choice(key: &str, description: &str, default_item_key: &str) {
// SAFETY: We provide valid pointers and lengths to key, description and
// default_item_key. They are also guaranteed to be valid UTF-8 strings.
unsafe {
sys::user_settings_add_choice(
key.as_ptr(),
key.len(),
description.as_ptr(),
description.len(),
default_item_key.as_ptr(),
default_item_key.len(),
)
}
}

/// Adds a new option to a choice setting widget. The key needs to match the key
/// of the choice setting widget 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. Returns [`true`] if the option is
/// at this point in time chosen by the user.
#[inline]
pub fn add_choice_option(key: &str, option_key: &str, option_description: &str) -> bool {
// SAFETY: We provide valid pointers and lengths to key, option_key and
// option_description. They are also guaranteed to be valid UTF-8 strings.
unsafe {
sys::user_settings_add_choice_option(
key.as_ptr(),
key.len(),
option_key.as_ptr(),
option_key.len(),
option_description.as_ptr(),
option_description.len(),
)
}
}

/// Adds a tooltip to a setting widget based on its key. A tooltip is useful for
/// explaining the purpose of a setting to the user.
#[inline]
Expand Down
28 changes: 28 additions & 0 deletions src/runtime/sys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,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

0 comments on commit 57a1a5d

Please sign in to comment.