From 43a47df3b3781b238b50235a09a650e3290d1718 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 .character-count properties Introduce two new properties for string in .slint: - .is-empty: Checks if a string is empty. - .character-count: 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. --- api/cpp/Cargo.toml | 1 + api/cpp/lib.rs | 5 + api/rs/slint/Cargo.toml | 2 + api/rs/slint/private_unstable_api.rs | 1 + .../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 | 23 +++++ .../cases/types/string_character_count.slint | 96 +++++++++++++++++++ 13 files changed, 199 insertions(+), 7 deletions(-) create mode 100644 tests/cases/types/string_character_count.slint 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..2d906e6aa81 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_character_count(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/api/rs/slint/Cargo.toml b/api/rs/slint/Cargo.toml index 38c163413dd..53d3033e7cc 100644 --- a/api/rs/slint/Cargo.toml +++ b/api/rs/slint/Cargo.toml @@ -187,6 +187,8 @@ log = { workspace = true, optional = true } raw-window-handle-06 = { workspace = true, optional = true } +unicode-segmentation = "1.12.0" + [target.'cfg(not(target_os = "android"))'.dependencies] # FemtoVG is disabled on android because it doesn't compile without setting RUST_FONTCONFIG_DLOPEN=on # end even then wouldn't work because it can't load fonts diff --git a/api/rs/slint/private_unstable_api.rs b/api/rs/slint/private_unstable_api.rs index aa050a5d112..08feb998db2 100644 --- a/api/rs/slint/private_unstable_api.rs +++ b/api/rs/slint/private_unstable_api.rs @@ -227,5 +227,6 @@ pub mod re_exports { pub use once_cell::race::OnceBox; pub use once_cell::unsync::OnceCell; pub use pin_weak::rc::PinWeak; + pub use unicode_segmentation::UnicodeSegmentation; pub use vtable::{self, *}; } diff --git a/docs/astro/src/content/docs/reference/primitive-types.mdx b/docs/astro/src/content/docs/reference/primitive-types.mdx index 5b596151f94..ecfa15714c8 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 +} +``` + +`character-count` property returns the number of [grapheme clusters](https://www.unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries). + +```slint +export component CharacterCountOfString { + property empty: "".character-count; // 0 + property hello: "hello".character-count; // 5 + property hiragana: "あいうえお".character-count; // 5 + property surrogate-pair: "😊𩸽".character-count; // 2 + property variation-selectors: "👍🏿".character-count; // 1 + property combining-character: "パ".character-count; // 1 + property zero-width-joiner: "👨‍👩‍👧‍👦".character-count; // 1 + property region-indicator-character: "🇦🇿🇿🇦".character-count; // 2 + property emoji-tag-sequences: "🏴󠁧󠁢󠁥󠁮󠁧󠁿".character-count; // 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 24faef287f2..5436fa2df60 100644 --- a/internal/compiler/expression_tree.rs +++ b/internal/compiler/expression_tree.rs @@ -53,6 +53,10 @@ pub enum BuiltinFunction { StringToFloat, /// the "42".is_float() StringIsFloat, + /// the "42".is_empty + StringIsEmpty, + /// the "42".length + StringCharacterCount, ColorRgbaStruct, ColorHsvaStruct, ColorBrighter, @@ -167,6 +171,8 @@ declare_builtin_function_types!( ItemFontMetrics: (Type::ElementReference) -> typeregister::font_metrics_type(), StringToFloat: (Type::String) -> Type::Float32, StringIsFloat: (Type::String) -> Type::Bool, + StringIsEmpty: (Type::String) -> Type::Bool, + StringCharacterCount: (Type::String) -> Type::Int32, ImplicitLayoutInfo(..): (Type::ElementReference) -> typeregister::layout_info_type(), ColorRgbaStruct: (Type::Color) -> Type::Struct(Rc::new(Struct { fields: IntoIterator::into_iter([ @@ -281,7 +287,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::StringCharacterCount => true, BuiltinFunction::ColorRgbaStruct | BuiltinFunction::ColorHsvaStruct | BuiltinFunction::ColorBrighter @@ -352,7 +361,10 @@ impl BuiltinFunction { BuiltinFunction::SetSelectionOffsets => false, BuiltinFunction::ItemMemberFunction(..) => false, BuiltinFunction::ItemFontMetrics => true, - BuiltinFunction::StringToFloat | BuiltinFunction::StringIsFloat => true, + BuiltinFunction::StringToFloat + | BuiltinFunction::StringIsFloat + | BuiltinFunction::StringIsEmpty + | BuiltinFunction::StringCharacterCount => true, BuiltinFunction::ColorRgbaStruct | BuiltinFunction::ColorHsvaStruct | BuiltinFunction::ColorBrighter diff --git a/internal/compiler/generator/cpp.rs b/internal/compiler/generator/cpp.rs index 9675683e963..5bb94f555aa 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::StringCharacterCount => { + format!("[](const auto &a){{ return slint::cbindgen_private::slint_string_character_count(&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 22688f73ae9..e8a127bc2ba 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::StringCharacterCount => { + quote!( sp::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 ac9bee963d5..4b8b241b52f 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::StringCharacterCount => 50, BuiltinFunction::ColorRgbaStruct => 50, BuiltinFunction::ColorHsvaStruct => 50, BuiltinFunction::ColorBrighter => 50, diff --git a/internal/compiler/lookup.rs b/internal/compiler/lookup.rs index 13fc149314d..afc1db66beb 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("character-count", function_call(BuiltinFunction::StringCharacterCount))) } } struct ColorExpression<'a>(&'a Expression); diff --git a/internal/interpreter/Cargo.toml b/internal/interpreter/Cargo.toml index f66c6c6eac0..3911c3f1520 100644 --- a/internal/interpreter/Cargo.toml +++ b/internal/interpreter/Cargo.toml @@ -134,6 +134,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 5ec31a46a22..70659ff5e9b 100644 --- a/internal/interpreter/eval.rs +++ b/internal/interpreter/eval.rs @@ -911,6 +911,29 @@ 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::StringCharacterCount => { + if arguments.len() != 1 { + panic!("internal error: incorrect argument count to StringCharacterCount") + } + 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_character_count.slint b/tests/cases/types/string_character_count.slint new file mode 100644 index 00000000000..13f5c2b765d --- /dev/null +++ b/tests/cases/types/string_character_count.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; + + // character-count + out property empty-character-count: empty.character-count; + out property hello-character-count: hello.character-count; + out property hiragana-character-count: hiragana.character-count; + out property surrogate-pair-character-count: surrogate-pair.character-count; + out property variation-selectors-character-count: variation-selectors.character-count; + out property combining-character-character-count: combining-character.character-count; + out property zero-width-joiner-character-count: zero-width-joiner.character-count; + out property region-indicator-character-character-count: region-indicator-character.character-count; + out property emoji-tag-sequences-character-count: emoji-tag-sequences.character-count; + out property test_character-count: empty-character-count == 0 + && hello-character-count == 5 + && hiragana-character-count == 5 + && surrogate-pair-character-count == 2 + && variation-selectors-character-count == 1 + && combining-character-character-count == 1 + && zero-width-joiner-character-count == 1 + && region-indicator-character-character-count == 2 + && emoji-tag-sequences-character-count == 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_character_count() == 0); +assert(instance.get_hello_character_count() == 5); +assert(instance.get_hiragana_character_count() == 5); +assert(instance.get_surrogate_pair_character_count() == 2); +assert(instance.get_variation_selectors_character_count() == 1); +assert(instance.get_combining_character_character_count() == 1); +assert(instance.get_zero_width_joiner_character_count() == 1); +assert(instance.get_region_indicator_character_character_count() == 2); +assert(instance.get_emoji_tag_sequences_character_count() == 1); +assert(instance.get_test_character_count()); +``` + +```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_character_count(), 0); +assert_eq!(instance.get_hello_character_count(), 5); +assert_eq!(instance.get_hiragana_character_count(), 5); +assert_eq!(instance.get_surrogate_pair_character_count(), 2); +assert_eq!(instance.get_variation_selectors_character_count(), 1); +assert_eq!(instance.get_combining_character_character_count(), 1); +assert_eq!(instance.get_zero_width_joiner_character_count(), 1); +assert_eq!(instance.get_region_indicator_character_character_count(), 2); +assert_eq!(instance.get_emoji_tag_sequences_character_count(), 1); +assert!(instance.get_test_character_count()); +``` + +```js +var instance = new slint.TestCase({}); +assert(instance.is_empty); +assert(instance.is_not_empty); +assert(instance.test_is_empty); +assert.equal(instance.empty_character_count, 0); +assert.equal(instance.hello_character_count, 5); +assert.equal(instance.hiragana_character_count, 5); +assert.equal(instance.surrogate_pair_character_count, 2); +assert.equal(instance.variation_selectors_character_count, 1); +assert.equal(instance.combining_character_character_count, 1); +assert.equal(instance.zero_width_joiner_character_count, 1); +assert.equal(instance.region_indicator_character_character_count, 2); +assert.equal(instance.emoji_tag_sequences_character_count, 1); +assert(instance.test_character_count); +``` + +*/