From bedc413c24ab4b66443df78e4dd5d47827238e0e Mon Sep 17 00:00:00 2001 From: Tasuku Suzuki Date: Sat, 21 Dec 2024 23:40:57 +0900 Subject: [PATCH] String: Add .is-empty and .length properties Introduce two new properties for string in .slint: - .is-empty: Checks if a string is empty. - .length: Retrieves the number of grapheme clusters https://www.unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries These additions enhance functionality and improve convenience when working with string properties. --- Cargo.lock | 3 + api/cpp/Cargo.toml | 1 + api/cpp/lib.rs | 5 + .../docs/reference/primitive-types.mdx | 36 ++++++- internal/compiler/expression_tree.rs | 16 +++- internal/compiler/generator/cpp.rs | 6 ++ internal/compiler/generator/rust.rs | 4 + .../llr/optim_passes/inline_expressions.rs | 2 + internal/compiler/lookup.rs | 13 +++ internal/interpreter/Cargo.toml | 1 + internal/interpreter/eval.rs | 20 ++++ tests/cases/types/string_length.slint | 96 +++++++++++++++++++ tests/driver/rust/Cargo.toml | 1 + 13 files changed, 197 insertions(+), 7 deletions(-) create mode 100644 tests/cases/types/string_length.slint diff --git a/Cargo.lock b/Cargo.lock index c8ca360ae41..7879ee14c8f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7749,6 +7749,7 @@ dependencies = [ "proc-macro2", "raw-window-handle 0.6.2", "slint-interpreter", + "unicode-segmentation", ] [[package]] @@ -7787,6 +7788,7 @@ dependencies = [ "smol_str 0.3.2", "spin_on", "thiserror 1.0.69", + "unicode-segmentation", "vtable", "web-sys", ] @@ -8412,6 +8414,7 @@ dependencies = [ "slint-interpreter", "spin_on", "test_driver_lib", + "unicode-segmentation", ] [[package]] diff --git a/api/cpp/Cargo.toml b/api/cpp/Cargo.toml index 5ccd5aa808d..a93fa06ac04 100644 --- a/api/cpp/Cargo.toml +++ b/api/cpp/Cargo.toml @@ -62,6 +62,7 @@ image = { workspace = true, optional = true, features = ["default"] } esp-backtrace = { version = "0.14.0", features = ["panic-handler", "println"], optional = true } esp-println = { version = "0.12.0", default-features = false, features = ["uart"], optional = true } +unicode-segmentation = "1.12.0" [build-dependencies] anyhow = "1.0" diff --git a/api/cpp/lib.rs b/api/cpp/lib.rs index d49dfd8ce28..bfa6b99c628 100644 --- a/api/cpp/lib.rs +++ b/api/cpp/lib.rs @@ -152,6 +152,11 @@ pub extern "C" fn slint_string_to_float(string: &SharedString, value: &mut f32) } } +#[no_mangle] +pub extern "C" fn slint_string_length(string: &SharedString) -> usize { + unicode_segmentation::UnicodeSegmentation::graphemes(string.as_str(), true).count() +} + #[no_mangle] pub extern "C" fn slint_string_to_usize(string: &SharedString, value: &mut usize) -> bool { match string.as_str().parse::() { diff --git a/docs/astro/src/content/docs/reference/primitive-types.mdx b/docs/astro/src/content/docs/reference/primitive-types.mdx index 5988db4e1e9..36918a8916a 100644 --- a/docs/astro/src/content/docs/reference/primitive-types.mdx +++ b/docs/astro/src/content/docs/reference/primitive-types.mdx @@ -20,6 +20,11 @@ boolean whose value can be either `true` or `false`. Any sequence of utf-8 encoded characters surrounded by quotes is a `string`: `"foo"`. +```slint +export component Example inherits Text { + text: "hello"; +} +``` Escape sequences may be embedded into strings to insert characters that would be hard to insert otherwise: @@ -33,15 +38,36 @@ be hard to insert otherwise: Anything else following an unescaped `\` is an error. +:::note[Note] + The `\{...}` syntax is not valid within the `slint!` macro in Rust. +::: + + +`is-empty` property is true when `string` doesn't contain anything. + ```slint -export component Example inherits Text { - text: "hello"; +export component LengthOfString { + property empty "".is-empty; // true + property not-empty: "hello".is-empty; // false +} +``` + +`length` property returns number of [grapheme clusters](https://www.unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries). + +```slint +export component LengthOfString { + property empty: "".length; // 0 + property hello: "hello".length; // 5 + property hiragana: "あいうえお".length; // 5 + property surrogate-pair: "😊𩸽".length; // 2 + property variation-selectors: "👍🏿".length; // 1 + property combining-character: "パ".length; // 1 + property zero-width-joiner: "👨‍👩‍👧‍👦".length; // 1 + property region-indicator-character: "🇦🇿🇿🇦".length; // 2 + property emoji-tag-sequences: "🏴󠁧󠁢󠁥󠁮󠁧󠁿".length; // 1 } ``` -:::note[Note] - The `\{...}` syntax is not valid within the `slint!` macro in Rust. -::: ## Numeric Types diff --git a/internal/compiler/expression_tree.rs b/internal/compiler/expression_tree.rs index 74076ab248d..aeb210b6eca 100644 --- a/internal/compiler/expression_tree.rs +++ b/internal/compiler/expression_tree.rs @@ -52,6 +52,10 @@ pub enum BuiltinFunction { StringToFloat, /// the "42".is_float() StringIsFloat, + /// the "42".is_empty + StringIsEmpty, + /// the "42".length + StringLength, ColorRgbaStruct, ColorHsvaStruct, ColorBrighter, @@ -164,6 +168,8 @@ declare_builtin_function_types!( ItemFontMetrics: (Type::ElementReference) -> crate::typeregister::font_metrics_type(), StringToFloat: (Type::String) -> Type::Float32, StringIsFloat: (Type::String) -> Type::Bool, + StringIsEmpty: (Type::String) -> Type::Bool, + StringLength: (Type::String) -> Type::Int32, ImplicitLayoutInfo(..): (Type::ElementReference) -> crate::typeregister::layout_info_type(), ColorRgbaStruct: (Type::Color) -> Type::Struct(Rc::new(Struct { fields: IntoIterator::into_iter([ @@ -273,7 +279,10 @@ impl BuiltinFunction { BuiltinFunction::SetSelectionOffsets => false, BuiltinFunction::ItemMemberFunction(..) => false, BuiltinFunction::ItemFontMetrics => false, // depends also on Window's font properties - BuiltinFunction::StringToFloat | BuiltinFunction::StringIsFloat => true, + BuiltinFunction::StringToFloat + | BuiltinFunction::StringIsFloat + | BuiltinFunction::StringIsEmpty + | BuiltinFunction::StringLength => true, BuiltinFunction::ColorRgbaStruct | BuiltinFunction::ColorHsvaStruct | BuiltinFunction::ColorBrighter @@ -342,7 +351,10 @@ impl BuiltinFunction { BuiltinFunction::SetSelectionOffsets => false, BuiltinFunction::ItemMemberFunction(..) => false, BuiltinFunction::ItemFontMetrics => true, - BuiltinFunction::StringToFloat | BuiltinFunction::StringIsFloat => true, + BuiltinFunction::StringToFloat + | BuiltinFunction::StringIsFloat + | BuiltinFunction::StringIsEmpty + | BuiltinFunction::StringLength => true, BuiltinFunction::ColorRgbaStruct | BuiltinFunction::ColorHsvaStruct | BuiltinFunction::ColorBrighter diff --git a/internal/compiler/generator/cpp.rs b/internal/compiler/generator/cpp.rs index 9ec1d723e9d..134ccf5c6e4 100644 --- a/internal/compiler/generator/cpp.rs +++ b/internal/compiler/generator/cpp.rs @@ -3553,6 +3553,12 @@ fn compile_builtin_function_call( ctx.generator_state.conditional_includes.cstdlib.set(true); format!("[](const auto &a){{ float res = 0; slint::cbindgen_private::slint_string_to_float(&a, &res); return res; }}({})", a.next().unwrap()) } + BuiltinFunction::StringIsEmpty => { + format!("{}.empty()", a.next().unwrap()) + } + BuiltinFunction::StringLength => { + format!("[](const auto &a){{ return slint::cbindgen_private::slint_string_length(&a); }}({})", a.next().unwrap()) + } BuiltinFunction::ColorRgbaStruct => { format!("{}.to_argb_uint()", a.next().unwrap()) } diff --git a/internal/compiler/generator/rust.rs b/internal/compiler/generator/rust.rs index 744e7e22961..c338dbffca4 100644 --- a/internal/compiler/generator/rust.rs +++ b/internal/compiler/generator/rust.rs @@ -2929,6 +2929,10 @@ fn compile_builtin_function_call( quote!(#(#a)*.as_str().parse::().unwrap_or_default()) } BuiltinFunction::StringIsFloat => quote!(#(#a)*.as_str().parse::().is_ok()), + BuiltinFunction::StringIsEmpty => quote!(#(#a)*.is_empty()), + BuiltinFunction::StringLength => { + quote!( unicode_segmentation::UnicodeSegmentation::graphemes(#(#a)*.as_str(), true).count() as i32 ) + } BuiltinFunction::ColorRgbaStruct => quote!( #(#a)*.to_argb_u8()), BuiltinFunction::ColorHsvaStruct => quote!( #(#a)*.to_hsva()), BuiltinFunction::ColorBrighter => { diff --git a/internal/compiler/llr/optim_passes/inline_expressions.rs b/internal/compiler/llr/optim_passes/inline_expressions.rs index 9a25f8e2db4..9be00ab4821 100644 --- a/internal/compiler/llr/optim_passes/inline_expressions.rs +++ b/internal/compiler/llr/optim_passes/inline_expressions.rs @@ -111,6 +111,8 @@ fn builtin_function_cost(function: &BuiltinFunction) -> isize { BuiltinFunction::ItemFontMetrics => PROPERTY_ACCESS_COST, BuiltinFunction::StringToFloat => 50, BuiltinFunction::StringIsFloat => 50, + BuiltinFunction::StringIsEmpty => 50, + BuiltinFunction::StringLength => 50, BuiltinFunction::ColorRgbaStruct => 50, BuiltinFunction::ColorHsvaStruct => 50, BuiltinFunction::ColorBrighter => 50, diff --git a/internal/compiler/lookup.rs b/internal/compiler/lookup.rs index 13fc149314d..372c053e0e5 100644 --- a/internal/compiler/lookup.rs +++ b/internal/compiler/lookup.rs @@ -977,9 +977,22 @@ impl<'a> LookupObject for StringExpression<'a> { )), }) }; + let function_call = |f: BuiltinFunction| { + LookupResult::from(Expression::FunctionCall { + function: Box::new(Expression::BuiltinFunctionReference( + f, + ctx.current_token.as_ref().map(|t| t.to_source_location()), + )), + source_location: ctx.current_token.as_ref().map(|t| t.to_source_location()), + arguments: vec![self.0.clone()], + }) + }; + let mut f = |s, res| f(&SmolStr::new_static(s), res); None.or_else(|| f("is-float", member_function(BuiltinFunction::StringIsFloat))) .or_else(|| f("to-float", member_function(BuiltinFunction::StringToFloat))) + .or_else(|| f("is-empty", function_call(BuiltinFunction::StringIsEmpty))) + .or_else(|| f("length", function_call(BuiltinFunction::StringLength))) } } struct ColorExpression<'a>(&'a Expression); diff --git a/internal/interpreter/Cargo.toml b/internal/interpreter/Cargo.toml index 5c94711f9fc..a519f9e9647 100644 --- a/internal/interpreter/Cargo.toml +++ b/internal/interpreter/Cargo.toml @@ -135,6 +135,7 @@ spin_on = { workspace = true, optional = true } raw-window-handle-06 = { workspace = true, optional = true } itertools = { workspace = true } smol_str = { workspace = true } +unicode-segmentation = "1.12.0" [target.'cfg(target_arch = "wasm32")'.dependencies] i-slint-backend-winit = { workspace = true } diff --git a/internal/interpreter/eval.rs b/internal/interpreter/eval.rs index 7fdc3ecd25c..2c2dd667e38 100644 --- a/internal/interpreter/eval.rs +++ b/internal/interpreter/eval.rs @@ -909,6 +909,26 @@ fn call_builtin_function( panic!("Argument not a string"); } } + BuiltinFunction::StringIsEmpty => { + if arguments.len() != 1 { + panic!("internal error: incorrect argument count to StringIsEmpty") + } + if let Value::String(s) = eval_expression(&arguments[0], local_context) { + Value::Bool(s.is_empty()) + } else { + panic!("Argument not a string"); + } + } + BuiltinFunction::StringLength => { + if arguments.len() != 1 { + panic!("internal error: incorrect argument count to StringLength") + } + if let Value::String(s) = eval_expression(&arguments[0], local_context) { + Value::Number(unicode_segmentation::UnicodeSegmentation::graphemes(s.as_str(), true).count() as f64) + } else { + panic!("Argument not a string"); + } + } BuiltinFunction::ColorRgbaStruct => { if arguments.len() != 1 { panic!("internal error: incorrect argument count to ColorRGBAComponents") diff --git a/tests/cases/types/string_length.slint b/tests/cases/types/string_length.slint new file mode 100644 index 00000000000..9fca8ec0a2d --- /dev/null +++ b/tests/cases/types/string_length.slint @@ -0,0 +1,96 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + +export component TestCase { + property empty; + property hello: "hello"; + property hiragana: "あいうえお"; + property surrogate-pair: "😊𩸽"; + property variation-selectors: "👍🏿"; + property combining-character: "パ"; + property zero-width-joiner: "👨‍👩‍👧‍👦"; + property region-indicator-character: "🇦🇿🇿🇦"; + property emoji-tag-sequences: "🏴󠁧󠁢󠁥󠁮󠁧󠁿"; + + // is-empty + out property is-empty: empty.is-empty; + out property is-not_empty: !hello.is-empty; + out property test-is_empty: is_empty && is_not_empty; + + // length + out property empty-length: empty.length; + out property hello-length: hello.length; + out property hiragana-length: hiragana.length; + out property surrogate-pair-length: surrogate-pair.length; + out property variation-selectors-length: variation-selectors.length; + out property combining-character-length: combining-character.length; + out property zero-width-joiner-length: zero-width-joiner.length; + out property region-indicator-character-length: region-indicator-character.length; + out property emoji-tag-sequences-length: emoji-tag-sequences.length; + out property test_length: empty-length == 0 + && hello-length == 5 + && hiragana-length == 5 + && surrogate-pair-length == 2 + && variation-selectors-length == 1 + && combining-character-length == 1 + && zero-width-joiner-length == 1 + && region-indicator-character-length == 2 + && emoji-tag-sequences-length == 1; +} + + +/* + +```cpp +auto handle = TestCase::create(); +const TestCase &instance = *handle; +assert(instance.get_is_empty()); +assert(instance.get_is_not_empty()); +assert(instance.get_test_is_empty()); +assert(instance.get_empty_length() == 0); +assert(instance.get_hello_length() == 5); +assert(instance.get_hiragana_length() == 5); +assert(instance.get_surrogate_pair_length() == 2); +assert(instance.get_variation_selectors_length() == 1); +assert(instance.get_combining_character_length() == 1); +assert(instance.get_zero_width_joiner_length() == 1); +assert(instance.get_region_indicator_character_length() == 2); +assert(instance.get_emoji_tag_sequences_length() == 1); +assert(instance.get_test_length()); +``` + +```rust +let instance = TestCase::new().unwrap(); +assert!(instance.get_is_empty()); +assert!(instance.get_is_not_empty()); +assert!(instance.get_test_is_empty()); +assert_eq!(instance.get_empty_length(), 0); +assert_eq!(instance.get_hello_length(), 5); +assert_eq!(instance.get_hiragana_length(), 5); +assert_eq!(instance.get_surrogate_pair_length(), 2); +assert_eq!(instance.get_variation_selectors_length(), 1); +assert_eq!(instance.get_combining_character_length(), 1); +assert_eq!(instance.get_zero_width_joiner_length(), 1); +assert_eq!(instance.get_region_indicator_character_length(), 2); +assert_eq!(instance.get_emoji_tag_sequences_length(), 1); +assert!(instance.get_test_length()); +``` + +```js +var instance = new slint.TestCase({}); +assert(instance.is_empty); +assert(instance.is_not_empty); +assert(instance.test_is_empty); +assert.equal(instance.empty_length, 0); +assert.equal(instance.hello_length, 5); +assert.equal(instance.hiragana_length, 5); +assert.equal(instance.surrogate_pair_length, 2); +assert.equal(instance.variation_selectors_length, 1); +assert.equal(instance.combining_character_length, 1); +assert.equal(instance.zero_width_joiner_length, 1); +assert.equal(instance.region_indicator_character_length, 2); +assert.equal(instance.emoji_tag_sequences_length, 1); +assert(instance.test_length); +``` + +*/ diff --git a/tests/driver/rust/Cargo.toml b/tests/driver/rust/Cargo.toml index 01d4f13d6df..d79e0959a51 100644 --- a/tests/driver/rust/Cargo.toml +++ b/tests/driver/rust/Cargo.toml @@ -25,6 +25,7 @@ slint = { workspace = true, features = ["std", "compat-1-2"] } i-slint-backend-testing = { workspace = true, features = ["internal"] } slint-interpreter = { workspace = true, features = ["std", "compat-1-2", "internal"] } spin_on = { workspace = true } +unicode-segmentation = "1.12.0" [build-dependencies] i-slint-compiler = { workspace = true, features = ["default", "rust", "display-diagnostics", "bundle-translations"], optional = true}