From 22c4fbc35349b06a47c4c89e4a41b3c5049d1f5d Mon Sep 17 00:00:00 2001 From: AlexKnauth Date: Sat, 9 Dec 2023 14:32:47 -0500 Subject: [PATCH 01/10] Add settings gui add_file_selection --- src/runtime/settings/gui.rs | 16 ++++++++++++++++ src/runtime/sys.rs | 13 +++++++++++++ 2 files changed, 29 insertions(+) diff --git a/src/runtime/settings/gui.rs b/src/runtime/settings/gui.rs index c13acf2..861d31c 100644 --- a/src/runtime/settings/gui.rs +++ b/src/runtime/settings/gui.rs @@ -89,6 +89,22 @@ pub fn add_choice_option(key: &str, option_key: &str, option_description: &str) } } +/// Adds a new file selection setting that the user can modify. +/// This allows the user to select a file path to be stored at the key. +/// The filter can include `*` wildcards, for example `"*.txt"`. +pub fn add_file_selection(key: &str, description: &str, filter: &str) { + unsafe { + sys::user_settings_add_file_selection( + key.as_ptr(), + key.len(), + description.as_ptr(), + description.len(), + filter.as_ptr(), + filter.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] diff --git a/src/runtime/sys.rs b/src/runtime/sys.rs index f40988e..2c39e80 100644 --- a/src/runtime/sys.rs +++ b/src/runtime/sys.rs @@ -260,6 +260,19 @@ extern "C" { option_description_ptr: *const u8, option_description_len: usize, ) -> bool; + /// Adds a new file selection setting that the user can modify. + /// This allows the user to select a file path to be stored at the key. + /// The filter can include `*` wildcards, for example `"*.txt"`. + /// The pointers need to point to valid UTF-8 encoded text with the + /// respective given length. + pub fn user_settings_add_file_selection( + key_ptr: *const u8, + key_len: usize, + description_ptr: *const u8, + description_len: usize, + filter_ptr: *const u8, + filter_len: usize, + ); /// 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. From 0a2ae3fa01427b55019455fe24980f9837ad519b Mon Sep 17 00:00:00 2001 From: AlexKnauth Date: Sat, 9 Dec 2023 22:49:00 -0500 Subject: [PATCH 02/10] derive Gui attribute args --- asr-derive/src/lib.rs | 56 +++++++++++++++++++++++++++++++++---------- 1 file changed, 43 insertions(+), 13 deletions(-) diff --git a/asr-derive/src/lib.rs b/asr-derive/src/lib.rs index ef22c5e..08c9040 100644 --- a/asr-derive/src/lib.rs +++ b/asr-derive/src/lib.rs @@ -111,7 +111,7 @@ use syn::{ /// use_game_time: Pair, /// } /// ``` -#[proc_macro_derive(Gui, attributes(default, heading_level))] +#[proc_macro_derive(Gui, attributes(default, heading_level, args))] pub fn settings_macro(input: TokenStream) -> TokenStream { let ast: DeriveInput = syn::parse(input).unwrap(); @@ -189,18 +189,31 @@ fn generate_struct_settings(struct_name: Ident, struct_data: DataStruct) -> Toke .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 + match &x.meta { + Meta::NameValue(nv) => { + 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 + } + }, + Meta::List(nl) => { + if nl.path.is_ident("args") { + if let Ok(ParseArgs(args)) = syn::parse(nl.tokens.clone().into()) { + Some(args.into()) + } else { + None + } + } else { + None + } + } + _ => None, } }) .collect::>(); @@ -524,3 +537,20 @@ pub fn il2cpp_class_binding(input: TokenStream) -> TokenStream { pub fn mono_class_binding(input: TokenStream) -> TokenStream { unity::process(input, quote! { asr::game_engine::unity::mono }) } + +struct ParseArgs(TokenStream); + +impl syn::parse::Parse for ParseArgs { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let mut assignments = Vec::new(); + while !input.is_empty() { + let field: syn::Ident = input.parse()?; + input.parse::()?; + let value_expr: syn::Expr = input.parse()?; + assignments.push(quote! { args.#field = #value_expr; }); + if input.is_empty() { break; } + input.parse::()?; + } + Ok(ParseArgs(quote! { #(#assignments)* }.into())) + } +} From 7306ad3ba95a3fe44805eee01fa1fd4fbcbb2fc5 Mon Sep 17 00:00:00 2001 From: AlexKnauth Date: Sat, 9 Dec 2023 23:11:57 -0500 Subject: [PATCH 03/10] Add FileSelection Widget --- src/runtime/settings/gui.rs | 42 +++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/src/runtime/settings/gui.rs b/src/runtime/settings/gui.rs index 861d31c..0087299 100644 --- a/src/runtime/settings/gui.rs +++ b/src/runtime/settings/gui.rs @@ -219,3 +219,45 @@ impl Widget for Pair { self.current.update_from(settings_map, key, args); } } + +/// A file selection widget. +#[cfg(feature = "alloc")] +pub struct FileSelection { + /// The file path. + pub path: alloc::string::String, + /// Whether the path just changed on this update. + pub new_data: bool, +} + +/// The arguments that are needed to register a file selection widget. +/// This is an internal type that you don't need to worry about. +#[cfg(feature = "alloc")] +#[doc(hidden)] +#[derive(Default)] +#[non_exhaustive] +pub struct FileSelectionArgs { + pub filter: &'static str, +} + +#[cfg(feature = "alloc")] +impl Widget for FileSelection { + type Args = FileSelectionArgs; + + fn register(key: &str, description: &str, args: Self::Args) -> Self { + add_file_selection(key, description, args.filter); + FileSelection { + path: alloc::string::ToString::to_string(""), + new_data: false, + } + } + + fn update_from(&mut self, settings_map: &Map, key: &str, _args: Self::Args) { + let new_path = settings_map.get(key).and_then(|v| v.get_string()).unwrap_or_default(); + if self.path != new_path { + self.path = new_path; + self.new_data = true; + } else { + self.new_data = false; + } + } +} From 3fefd44e8d1c078c0498cf0f8138cbc43a89c665 Mon Sep 17 00:00:00 2001 From: AlexKnauth Date: Mon, 11 Dec 2023 21:31:09 -0500 Subject: [PATCH 04/10] cargo fmt --- asr-derive/src/lib.rs | 46 ++++++++++++++++++------------------- src/runtime/settings/gui.rs | 5 +++- 2 files changed, 27 insertions(+), 24 deletions(-) diff --git a/asr-derive/src/lib.rs b/asr-derive/src/lib.rs index 08c9040..128f153 100644 --- a/asr-derive/src/lib.rs +++ b/asr-derive/src/lib.rs @@ -188,33 +188,31 @@ fn generate_struct_settings(struct_name: Ident, struct_data: DataStruct) -> Toke let args = field .attrs .iter() - .filter_map(|x| { - match &x.meta { - Meta::NameValue(nv) => { - 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 - } - }, - Meta::List(nl) => { - if nl.path.is_ident("args") { - if let Ok(ParseArgs(args)) = syn::parse(nl.tokens.clone().into()) { - Some(args.into()) - } else { - None - } + .filter_map(|x| match &x.meta { + Meta::NameValue(nv) => { + 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 + } + } + Meta::List(nl) => { + if nl.path.is_ident("args") { + if let Ok(ParseArgs(args)) = syn::parse(nl.tokens.clone().into()) { + Some(args.into()) } else { None } + } else { + None } - _ => None, } + _ => None, }) .collect::>(); args_init.push(quote! { #(#args)* }); @@ -548,7 +546,9 @@ impl syn::parse::Parse for ParseArgs { input.parse::()?; let value_expr: syn::Expr = input.parse()?; assignments.push(quote! { args.#field = #value_expr; }); - if input.is_empty() { break; } + if input.is_empty() { + break; + } input.parse::()?; } Ok(ParseArgs(quote! { #(#assignments)* }.into())) diff --git a/src/runtime/settings/gui.rs b/src/runtime/settings/gui.rs index 0087299..59cd41e 100644 --- a/src/runtime/settings/gui.rs +++ b/src/runtime/settings/gui.rs @@ -252,7 +252,10 @@ impl Widget for FileSelection { } fn update_from(&mut self, settings_map: &Map, key: &str, _args: Self::Args) { - let new_path = settings_map.get(key).and_then(|v| v.get_string()).unwrap_or_default(); + let new_path = settings_map + .get(key) + .and_then(|v| v.get_string()) + .unwrap_or_default(); if self.path != new_path { self.path = new_path; self.new_data = true; From a3a7bd1b3fa312d462bb8529b2ecd401b2cd4ca5 Mon Sep 17 00:00:00 2001 From: AlexKnauth Date: Tue, 12 Dec 2023 21:22:58 -0500 Subject: [PATCH 05/10] replace args attribute with filter --- asr-derive/src/lib.rs | 35 ++++------------------------------- 1 file changed, 4 insertions(+), 31 deletions(-) diff --git a/asr-derive/src/lib.rs b/asr-derive/src/lib.rs index 128f153..526c84e 100644 --- a/asr-derive/src/lib.rs +++ b/asr-derive/src/lib.rs @@ -111,7 +111,7 @@ use syn::{ /// use_game_time: Pair, /// } /// ``` -#[proc_macro_derive(Gui, attributes(default, heading_level, args))] +#[proc_macro_derive(Gui, attributes(default, heading_level, filter))] pub fn settings_macro(input: TokenStream) -> TokenStream { let ast: DeriveInput = syn::parse(input).unwrap(); @@ -197,17 +197,9 @@ fn generate_struct_settings(struct_name: Ident, struct_data: DataStruct) -> Toke } else if nv.path.is_ident("heading_level") { let value = &nv.value; Some(quote_spanned! { span => args.heading_level = #value; }) - } else { - None - } - } - Meta::List(nl) => { - if nl.path.is_ident("args") { - if let Ok(ParseArgs(args)) = syn::parse(nl.tokens.clone().into()) { - Some(args.into()) - } else { - None - } + } else if nv.path.is_ident("filter") { + let value = &nv.value; + Some(quote_spanned! { span => args.filter = #value; }) } else { None } @@ -535,22 +527,3 @@ pub fn il2cpp_class_binding(input: TokenStream) -> TokenStream { pub fn mono_class_binding(input: TokenStream) -> TokenStream { unity::process(input, quote! { asr::game_engine::unity::mono }) } - -struct ParseArgs(TokenStream); - -impl syn::parse::Parse for ParseArgs { - fn parse(input: syn::parse::ParseStream) -> syn::Result { - let mut assignments = Vec::new(); - while !input.is_empty() { - let field: syn::Ident = input.parse()?; - input.parse::()?; - let value_expr: syn::Expr = input.parse()?; - assignments.push(quote! { args.#field = #value_expr; }); - if input.is_empty() { - break; - } - input.parse::()?; - } - Ok(ParseArgs(quote! { #(#assignments)* }.into())) - } -} From 32de0d9a88be0b1aaf9c5cf8275ac43516362d96 Mon Sep 17 00:00:00 2001 From: AlexKnauth Date: Tue, 12 Dec 2023 21:55:50 -0500 Subject: [PATCH 06/10] revert some args-attribute-related changes --- asr-derive/src/lib.rs | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/asr-derive/src/lib.rs b/asr-derive/src/lib.rs index 526c84e..6f22382 100644 --- a/asr-derive/src/lib.rs +++ b/asr-derive/src/lib.rs @@ -188,23 +188,23 @@ fn generate_struct_settings(struct_name: Ident, struct_data: DataStruct) -> Toke let args = field .attrs .iter() - .filter_map(|x| match &x.meta { - Meta::NameValue(nv) => { - 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 if nv.path.is_ident("filter") { - let value = &nv.value; - Some(quote_spanned! { span => args.filter = #value; }) - } else { - None - } + .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 if nv.path.is_ident("filter") { + let value = &nv.value; + Some(quote_spanned! { span => args.filter = #value; }) + } else { + None } - _ => None, }) .collect::>(); args_init.push(quote! { #(#args)* }); From e7b7dc225c23175e15b138d1b7e4ed888bbac05d Mon Sep 17 00:00:00 2001 From: AlexKnauth Date: Wed, 13 Dec 2023 18:40:32 -0500 Subject: [PATCH 07/10] Document filter `;` semicolons --- src/runtime/settings/gui.rs | 6 +++++- src/runtime/sys.rs | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/runtime/settings/gui.rs b/src/runtime/settings/gui.rs index 59cd41e..ee59bbc 100644 --- a/src/runtime/settings/gui.rs +++ b/src/runtime/settings/gui.rs @@ -91,7 +91,8 @@ pub fn add_choice_option(key: &str, option_key: &str, option_description: &str) /// Adds a new file selection setting that the user can modify. /// This allows the user to select a file path to be stored at the key. -/// The filter can include `*` wildcards, for example `"*.txt"`. +/// The filter can include `*` wildcards, for example `"*.txt"`, +/// and multiple patterns separated by `;` semicolons, like `"*.txt;*.md"`. pub fn add_file_selection(key: &str, description: &str, filter: &str) { unsafe { sys::user_settings_add_file_selection( @@ -236,6 +237,9 @@ pub struct FileSelection { #[derive(Default)] #[non_exhaustive] pub struct FileSelectionArgs { + /// A filter on which files are selectable. + /// Can include `*` wildcards, for example `"*.txt"`, + /// and multiple patterns separated by `;` semicolons, like `"*.txt;*.md"`. pub filter: &'static str, } diff --git a/src/runtime/sys.rs b/src/runtime/sys.rs index 2c39e80..4829049 100644 --- a/src/runtime/sys.rs +++ b/src/runtime/sys.rs @@ -262,7 +262,8 @@ extern "C" { ) -> bool; /// Adds a new file selection setting that the user can modify. /// This allows the user to select a file path to be stored at the key. - /// The filter can include `*` wildcards, for example `"*.txt"`. + /// The filter can include `*` wildcards, for example `"*.txt"`, + /// and multiple patterns separated by `;` semicolons, like `"*.txt;*.md"`. /// The pointers need to point to valid UTF-8 encoded text with the /// respective given length. pub fn user_settings_add_file_selection( From 53a1688ce7eb950d20d3ec698aadfaec95a234c9 Mon Sep 17 00:00:00 2001 From: AlexKnauth Date: Wed, 13 Dec 2023 20:39:54 -0500 Subject: [PATCH 08/10] Document filter attribute --- asr-derive/src/lib.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/asr-derive/src/lib.rs b/asr-derive/src/lib.rs index 6f22382..a3ede12 100644 --- a/asr-derive/src/lib.rs +++ b/asr-derive/src/lib.rs @@ -68,6 +68,16 @@ use syn::{ /// # } /// ``` /// +/// A file selection filter can include `*` wildcards, for example `"*.txt"`, +/// and multiple patterns separated by `;` semicolons, like `"*.txt;*.md"`: +/// +/// ```no_run +/// # struct Settings { +/// #[filter = "*.txt;*.md"] +/// text_file: FileSelection, +/// # } +/// ``` +/// /// # Choices /// /// You can derive `Gui` for an enum to create a choice widget. You can mark one From a3610285c40a3e7ed00b8b3ddbec7cd2e6655f92 Mon Sep 17 00:00:00 2001 From: AlexKnauth Date: Wed, 13 Dec 2023 20:48:08 -0500 Subject: [PATCH 09/10] FileSelection path is WASI path --- src/runtime/settings/gui.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/runtime/settings/gui.rs b/src/runtime/settings/gui.rs index ee59bbc..d40a165 100644 --- a/src/runtime/settings/gui.rs +++ b/src/runtime/settings/gui.rs @@ -224,7 +224,9 @@ impl Widget for Pair { /// A file selection widget. #[cfg(feature = "alloc")] pub struct FileSelection { - /// The file path. + /// The file path, as accessible through the WASI file system, + /// so a Windows path of `C:\foo\bar.exe` would be represented + /// as `/mnt/c/foo/bar.exe`. pub path: alloc::string::String, /// Whether the path just changed on this update. pub new_data: bool, From cea1f7411e20955b0f9a6d1babebb3fdfd1c489f Mon Sep 17 00:00:00 2001 From: Christopher Serr Date: Thu, 21 Dec 2023 15:57:01 +0100 Subject: [PATCH 10/10] Final cleanup --- asr-derive/Cargo.toml | 5 +- asr-derive/src/lib.rs | 216 +++++++++++++++++++++++++++++----- src/runtime/settings/gui.rs | 155 ++++++++++++++++++------ src/runtime/settings/value.rs | 30 +++++ src/runtime/sys.rs | 56 +++++++-- 5 files changed, 387 insertions(+), 75 deletions(-) diff --git a/asr-derive/Cargo.toml b/asr-derive/Cargo.toml index 78fd17d..4493e44 100644 --- a/asr-derive/Cargo.toml +++ b/asr-derive/Cargo.toml @@ -6,9 +6,10 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -syn = "2.0.1" -quote = "1.0.18" heck = "0.4.0" +proc-macro2 = "1.0.70" +quote = "1.0.18" +syn = { version = "2.0.41", features = ["full"] } [lib] proc-macro = true diff --git a/asr-derive/src/lib.rs b/asr-derive/src/lib.rs index a3ede12..78485ed 100644 --- a/asr-derive/src/lib.rs +++ b/asr-derive/src/lib.rs @@ -2,7 +2,8 @@ use heck::ToTitleCase; use proc_macro::TokenStream; use quote::{quote, quote_spanned}; use syn::{ - spanned::Spanned, Data, DataEnum, DataStruct, DeriveInput, Expr, ExprLit, Ident, Lit, Meta, + parse::Parse, punctuated::Punctuated, spanned::Spanned, token::Comma, Data, DataEnum, + DataStruct, DeriveInput, Error, Expr, ExprLit, Ident, Lit, Meta, MetaList, Result, }; // FIXME: https://github.com/rust-lang/rust/issues/117463 @@ -68,13 +69,23 @@ use syn::{ /// # } /// ``` /// -/// A file selection filter can include `*` wildcards, for example `"*.txt"`, -/// and multiple patterns separated by `;` semicolons, like `"*.txt;*.md"`: +/// A file select filter can be specified like so: /// /// ```no_run /// # struct Settings { -/// #[filter = "*.txt;*.md"] -/// text_file: FileSelection, +/// #[filter( +/// // File name patterns with names +/// ("PNG images", "*.png"), +/// // Multiple patterns separated by space +/// ("Rust files", "*.rs Cargo.*"), +/// // The name is optional +/// (_, "*.md"), +/// // MIME types +/// "text/plain", +/// // Mime types with wildcards +/// "image/*", +/// )] +/// text_file: FileSelect, /// # } /// ``` /// @@ -125,14 +136,22 @@ use syn::{ pub fn settings_macro(input: TokenStream) -> TokenStream { let ast: DeriveInput = syn::parse(input).unwrap(); - match ast.data { + let res = 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"), + _ => Err(Error::new( + ast.span(), + "Only structs and enums are supported.", + )), + }; + + match res { + Ok(v) => v, + Err(e) => e.into_compile_error().into(), } } -fn generate_struct_settings(struct_name: Ident, struct_data: DataStruct) -> TokenStream { +fn generate_struct_settings(struct_name: Ident, struct_data: DataStruct) -> Result { let mut field_names = Vec::new(); let mut field_name_strings = Vec::new(); let mut field_descs = Vec::new(); @@ -198,29 +217,33 @@ fn generate_struct_settings(struct_name: Ident, struct_data: DataStruct) -> Toke 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 if nv.path.is_ident("filter") { - let value = &nv.value; - Some(quote_spanned! { span => args.filter = #value; }) - } else { - None + .filter_map(|x| match &x.meta { + Meta::NameValue(nv) => { + let span = nv.span(); + if nv.path.is_ident("default") { + let value = &nv.value; + Some(Ok(quote_spanned! { span => args.default = #value; })) + } else if nv.path.is_ident("heading_level") { + let value = &nv.value; + Some(Ok(quote_spanned! { span => args.heading_level = #value; })) + } else { + None + } } + Meta::List(list) => { + if list.path.is_ident("filter") { + Some(parse_filter(list)) + } else { + None + } + } + _ => None, }) - .collect::>(); + .collect::>>()?; args_init.push(quote! { #(#args)* }); } - quote! { + Ok(quote! { impl asr::settings::Gui for #struct_name { fn register() -> Self { Self { @@ -247,10 +270,10 @@ fn generate_struct_settings(struct_name: Ident, struct_data: DataStruct) -> Toke } } } - .into() + .into()) } -fn generate_enum_settings(enum_name: Ident, enum_data: DataEnum) -> TokenStream { +fn generate_enum_settings(enum_name: Ident, enum_data: DataEnum) -> Result { let mut variant_names = Vec::new(); let mut variant_name_strings = Vec::new(); let mut variant_descs = Vec::new(); @@ -331,7 +354,7 @@ fn generate_enum_settings(enum_name: Ident, enum_data: DataEnum) -> TokenStream .max() .unwrap_or_default(); - quote! { + Ok(quote! { impl asr::settings::gui::Widget for #enum_name { type Args = (); @@ -358,7 +381,140 @@ fn generate_enum_settings(enum_name: Ident, enum_data: DataEnum) -> TokenStream } } } - .into() + .into()) +} + +fn parse_filter(list: &MetaList) -> Result { + let span = list.span(); + let mut filters = Vec::new(); + + struct FilterArgs { + exprs: Punctuated, + } + + impl Parse for FilterArgs { + fn parse(input: syn::parse::ParseStream) -> Result { + Ok(FilterArgs { + exprs: Punctuated::parse_terminated(input)?, + }) + } + } + + let args: FilterArgs = syn::parse(list.tokens.clone().into())?; + + for expr in args.exprs { + match expr { + Expr::Tuple(tuple) => { + let mut iter = tuple.elems.iter(); + let (Some(first), Some(second), None) = (iter.next(), iter.next(), iter.next()) + else { + return Err(Error::new( + tuple.span(), + "Expected a tuple of two elements.", + )); + }; + + let has_description = match first { + Expr::Lit(ExprLit { + lit: Lit::Str(lit), .. + }) => { + let value = lit.value(); + if value.is_empty() { + return Err(Error::new( + lit.span(), + "The description should not be empty.", + )); + } + if value.trim().len() != value.len() { + return Err(Error::new( + lit.span(), + "The description should not contain leading or trailing whitespace.", + )); + } + true + } + Expr::Infer(_) => false, + _ => { + return Err(Error::new( + first.span(), + "Expected a string literal or an underscore.", + )) + } + }; + + match second { + Expr::Lit(ExprLit { + lit: Lit::Str(lit), .. + }) => { + let value = lit.value(); + if value.is_empty() { + return Err(Error::new(lit.span(), "The pattern must not be empty.")); + } + if value.trim().len() != value.len() { + return Err(Error::new( + lit.span(), + "The pattern must not contain leading or trailing whitespace.", + )); + } + if value.contains(" ") { + return Err(Error::new( + lit.span(), + "The pattern must not contain double whitespace.", + )); + } + if value.contains("*.*") { + return Err(Error::new( + lit.span(), + "The pattern handling all files doesn't need to be specified.", + )); + } + } + _ => return Err(Error::new(second.span(), "Expected a string literal.")), + } + + filters.push(if has_description { + quote! { asr::settings::gui::FileSelectFilter::NamePattern(Some(#first), #second) } + } else { + quote! { asr::settings::gui::FileSelectFilter::NamePattern(None, #second) } + }); + } + Expr::Lit(lit) => match lit { + ExprLit { + lit: Lit::Str(lit), .. + } => { + let value = lit.value(); + if value.bytes().filter(|b| *b == b'/').count() != 1 { + return Err(Error::new( + lit.span(), + "The MIME type has to contain a single `/`.", + )); + } + if value.trim().len() != value.len() { + return Err(Error::new( + lit.span(), + "The MIME type must not contain leading or trailing whitespace.", + )); + } + if value == "*/*" { + return Err(Error::new( + lit.span(), + "The MIME type handling all files doesn't need to be specified.", + )); + } + filters.push(quote! { asr::settings::gui::FileSelectFilter::MimeType(#lit) }) + } + _ => return Err(Error::new(lit.span(), "Expected a string literal.")), + }, + _ => { + return Err(Error::new( + expr.span(), + "Expected a tuple or a string literal.", + )) + } + } + } + + Ok(quote_spanned! { span => args.filter = &[#(#filters),*]; }) } /// Generates an implementation of the `FromEndian` trait for a struct. This diff --git a/src/runtime/settings/gui.rs b/src/runtime/settings/gui.rs index d40a165..faaecb6 100644 --- a/src/runtime/settings/gui.rs +++ b/src/runtime/settings/gui.rs @@ -1,6 +1,8 @@ //! This module allows you to add settings widgets to the settings GUI that the //! user can modify. +use core::mem; + #[cfg(feature = "derive")] pub use asr_derive::Gui; @@ -89,19 +91,77 @@ pub fn add_choice_option(key: &str, option_key: &str, option_description: &str) } } -/// Adds a new file selection setting that the user can modify. -/// This allows the user to select a file path to be stored at the key. -/// The filter can include `*` wildcards, for example `"*.txt"`, -/// and multiple patterns separated by `;` semicolons, like `"*.txt;*.md"`. -pub fn add_file_selection(key: &str, description: &str, filter: &str) { +/// Adds a new file select setting that the user can modify. This allows the +/// user to choose a file from the file system. The key is used to store the +/// path of the file in the settings map and needs to be unique across all types +/// of settings. The description is what's shown to the user. The path is a path +/// that is accessible through the WASI file system, so a Windows path of +/// `C:\foo\bar.exe` would be stored as `/mnt/c/foo/bar.exe`. +#[inline] +pub fn add_file_select(key: &str, description: &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_add_file_selection( + sys::user_settings_add_file_select( key.as_ptr(), key.len(), description.as_ptr(), description.len(), - filter.as_ptr(), - filter.len(), + ) + } +} + +/// Adds a filter to a file select setting. The key needs to match the key of +/// the file select setting that it's supposed to be added to. The description +/// is what's shown to the user for the specific filter. The pattern is a [glob +/// pattern](https://en.wikipedia.org/wiki/Glob_(programming)) that is used to +/// filter the files. The pattern generally only supports `*` wildcards, not `?` +/// or brackets. This may however differ between frontends. Additionally `;` +/// can't be used in Windows's native file dialog if it's part of the pattern. +/// Multiple patterns may be specified by separating them with ASCII space +/// characters. There are operating systems where glob patterns are not +/// supported. A best effort lookup of the fitting MIME type may be used by a +/// frontend on those operating systems instead. +#[inline] +pub fn add_file_select_name_filter(key: &str, description: Option<&str>, pattern: &str) { + // SAFETY: We provide valid pointers and lengths to key, description and + // pattern. They are also guaranteed to be valid UTF-8 strings. The + // description is provided as a null pointer in case it is `None` to + // indicate that no description is provided. + unsafe { + let (desc_ptr, desc_len) = match description { + Some(desc) => (desc.as_ptr(), desc.len()), + None => (core::ptr::null(), 0), + }; + sys::user_settings_add_file_select_name_filter( + key.as_ptr(), + key.len(), + desc_ptr, + desc_len, + pattern.as_ptr(), + pattern.len(), + ) + } +} + +/// Adds a filter to a file select setting. The key needs to match the key +/// of the file select setting that it's supposed to be added to. The MIME +/// type is what's used to filter the files. Most operating systems do not +/// support MIME types, but the frontends are encouraged to look up the file +/// extensions that are associated with the MIME type and use those as a +/// filter in those cases. You may also use wildcards as part of the MIME +/// types such as `image/*`. The support likely also varies between +/// frontends however. +#[inline] +pub fn add_file_select_mime_filter(key: &str, mime_type: &str) { + // SAFETY: We provide valid pointers and lengths to key and mime_type. + // They are also guaranteed to be valid UTF-8 strings. + unsafe { + sys::user_settings_add_file_select_mime_filter( + key.as_ptr(), + key.len(), + mime_type.as_ptr(), + mime_type.len(), ) } } @@ -204,32 +264,51 @@ impl Widget for Title { fn update_from(&mut self, _settings_map: &Map, _key: &str, _args: Self::Args) {} } -impl Widget for Pair { +impl Widget for Pair { type Args = T::Args; fn register(key: &str, description: &str, args: Self::Args) -> Self { let value = T::register(key, description, args); Pair { - old: value, + old: value.clone(), current: value, } } fn update_from(&mut self, settings_map: &Map, key: &str, args: Self::Args) { - self.old = self.current; + mem::swap(&mut self.old, &mut self.current); self.current.update_from(settings_map, key, args); } } -/// A file selection widget. +/// A file select widget. +/// +/// # Example +/// +/// ```ignore +/// # struct Settings { +/// #[filter( +/// // File name patterns with names +/// ("PNG images", "*.png"), +/// // Multiple patterns separated by space +/// ("Rust files", "*.rs Cargo.*"), +/// // The name is optional +/// (_, "*.md"), +/// // MIME types +/// "text/plain", +/// // MIME types with wildcards +/// "image/*", +/// )] +/// text_file: FileSelect, +/// # } +/// ``` +#[derive(Clone, PartialEq, Eq)] #[cfg(feature = "alloc")] -pub struct FileSelection { +pub struct FileSelect { /// The file path, as accessible through the WASI file system, /// so a Windows path of `C:\foo\bar.exe` would be represented /// as `/mnt/c/foo/bar.exe`. pub path: alloc::string::String, - /// Whether the path just changed on this update. - pub new_data: bool, } /// The arguments that are needed to register a file selection widget. @@ -238,35 +317,43 @@ pub struct FileSelection { #[doc(hidden)] #[derive(Default)] #[non_exhaustive] -pub struct FileSelectionArgs { - /// A filter on which files are selectable. - /// Can include `*` wildcards, for example `"*.txt"`, - /// and multiple patterns separated by `;` semicolons, like `"*.txt;*.md"`. - pub filter: &'static str, +pub struct FileSelectArgs { + pub filter: &'static [FileSelectFilter], +} + +#[cfg(feature = "alloc")] +#[doc(hidden)] +pub enum FileSelectFilter { + NamePattern(Option<&'static str>, &'static str), + MimeType(&'static str), } #[cfg(feature = "alloc")] -impl Widget for FileSelection { - type Args = FileSelectionArgs; +impl Widget for FileSelect { + type Args = FileSelectArgs; fn register(key: &str, description: &str, args: Self::Args) -> Self { - add_file_selection(key, description, args.filter); - FileSelection { - path: alloc::string::ToString::to_string(""), - new_data: false, + add_file_select(key, description); + for filter in args.filter { + match filter { + FileSelectFilter::NamePattern(desc, pattern) => { + add_file_select_name_filter(key, *desc, pattern) + } + FileSelectFilter::MimeType(mime) => add_file_select_mime_filter(key, mime), + } } + let mut this = FileSelect { + path: alloc::string::String::new(), + }; + this.update_from(&Map::load(), key, args); + this } fn update_from(&mut self, settings_map: &Map, key: &str, _args: Self::Args) { - let new_path = settings_map - .get(key) - .and_then(|v| v.get_string()) - .unwrap_or_default(); - if self.path != new_path { - self.path = new_path; - self.new_data = true; + if let Some(value) = settings_map.get(key) { + value.get_string_into(&mut self.path); } else { - self.new_data = false; + self.path.clear(); } } } diff --git a/src/runtime/settings/value.rs b/src/runtime/settings/value.rs index 9077016..1d3ff37 100644 --- a/src/runtime/settings/value.rs +++ b/src/runtime/settings/value.rs @@ -224,6 +224,36 @@ impl Value { } } + /// Writes the value as a [`String`](alloc::string::String) into the + /// provided buffer if it is a string. Returns [`true`] if the value is a + /// string. Returns [`false`] if the value is not a string. The buffer is + /// always cleared before writing into it. + #[cfg(feature = "alloc")] + #[inline] + pub fn get_string_into(&self, buf: &mut alloc::string::String) -> bool { + // SAFETY: The handle is valid. We provide a null pointer and 0 as the + // length to get the length of the string. If it failed and the length + // is 0, then that indicates that the value is not a string and we + // return false. Otherwise we allocate a buffer of the returned length + // and call the function again with the buffer. This should now always + // succeed and we can return the string. The function also guarantees + // that the buffer is valid UTF-8. + unsafe { + let buf = buf.as_mut_vec(); + buf.clear(); + let mut len = 0; + let success = sys::setting_value_get_string(self.0, core::ptr::null_mut(), &mut len); + if len == 0 && !success { + return false; + } + buf.reserve(len); + let success = sys::setting_value_get_string(self.0, buf.as_mut_ptr(), &mut len); + assert!(success); + buf.set_len(len); + true + } + } + /// Returns the value as an [`ArrayString`] if it is a string. Returns an /// error if the string is too long. The constant `N` determines the maximum /// length of the string in bytes. diff --git a/src/runtime/sys.rs b/src/runtime/sys.rs index 4829049..4fef9ff 100644 --- a/src/runtime/sys.rs +++ b/src/runtime/sys.rs @@ -260,19 +260,57 @@ extern "C" { option_description_ptr: *const u8, option_description_len: usize, ) -> bool; - /// Adds a new file selection setting that the user can modify. - /// This allows the user to select a file path to be stored at the key. - /// The filter can include `*` wildcards, for example `"*.txt"`, - /// and multiple patterns separated by `;` semicolons, like `"*.txt;*.md"`. - /// The pointers need to point to valid UTF-8 encoded text with the - /// respective given length. - pub fn user_settings_add_file_selection( + /// Adds a new file select setting that the user can modify. This allows the + /// user to choose a file from the file system. The key is used to store the + /// path of the file in the settings map and needs to be unique across all + /// types of settings. 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. The path is a path that is accessible through the WASI + /// file system, so a Windows path of `C:\foo\bar.exe` would be stored as + /// `/mnt/c/foo/bar.exe`. + pub fn user_settings_add_file_select( key_ptr: *const u8, key_len: usize, description_ptr: *const u8, description_len: usize, - filter_ptr: *const u8, - filter_len: usize, + ); + /// Adds a filter to a file select setting. The key needs to match the key + /// of the file select setting that it's supposed to be added to. The + /// description is what's shown to the user for the specific filter. The + /// description is optional. You may provide a null pointer if you don't + /// want to specify a description. The pattern is a [glob + /// pattern](https://en.wikipedia.org/wiki/Glob_(programming)) that is used + /// to filter the files. The pattern generally only supports `*` wildcards, + /// not `?` or brackets. This may however differ between frontends. + /// Additionally `;` can't be used in Windows's native file dialog if it's + /// part of the pattern. Multiple patterns may be specified by separating + /// them with ASCII space characters. There are operating systems where glob + /// patterns are not supported. A best effort lookup of the fitting MIME + /// type may be used by a frontend on those operating systems instead. The + /// pointers need to point to valid UTF-8 encoded text with the respective + /// given length. + pub fn user_settings_add_file_select_name_filter( + key_ptr: *const u8, + key_len: usize, + description_ptr: *const u8, + description_len: usize, + pattern_ptr: *const u8, + pattern_len: usize, + ); + /// Adds a filter to a file select setting. The key needs to match the key + /// of the file select setting that it's supposed to be added to. The MIME + /// type is what's used to filter the files. Most operating systems do not + /// support MIME types, but the frontends are encouraged to look up the file + /// extensions that are associated with the MIME type and use those as a + /// filter in those cases. You may also use wildcards as part of the MIME + /// types such as `image/*`. The support likely also varies between + /// frontends however. The pointers need to point to valid UTF-8 encoded + /// text with the respective given length. + pub fn user_settings_add_file_select_mime_filter( + key_ptr: *const u8, + key_len: usize, + mime_type_ptr: *const u8, + mime_type_len: usize, ); /// 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