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.
This commit is contained in:
Tasuku Suzuki 2024-12-21 23:40:57 +09:00 committed by Simon Hausmann
parent 12fe2bb36d
commit 68b9dfc247
13 changed files with 200 additions and 8 deletions

View file

@ -60,6 +60,7 @@ raw-window-handle = { version = "0.6", optional = true }
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"

View file

@ -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::<usize>() {

View file

@ -195,6 +195,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

View file

@ -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, *};
}

View file

@ -20,6 +20,11 @@ boolean whose value can be either `true` or `false`.
<SlintProperty propName="string" typeName="string" defaultValue='""'>
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.
```slint
export component Example inherits Text {
text: "hello";
}
```
:::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 LengthOfString {
property<bool> empty: "".is-empty; // true
property<bool> 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<int> empty: "".character-count; // 0
property<int> hello: "hello".character-count; // 5
property<int> hiragana: "あいうえお".character-count; // 5
property<int> surrogate-pair: "😊𩸽".character-count; // 2
property<int> variation-selectors: "👍🏿".character-count; // 1
property<int> combining-character: "パ".character-count; // 1
property<int> zero-width-joiner: "👨‍👩‍👧‍👦".character-count; // 1
property<int> region-indicator-character: "🇦🇿🇿🇦".character-count; // 2
property<int> emoji-tag-sequences: "🏴󠁧󠁢󠁥󠁮󠁧󠁿".character-count; // 1
}
```
</SlintProperty>
## Numeric Types

View file

@ -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

View file

@ -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())
}

View file

@ -2929,6 +2929,10 @@ fn compile_builtin_function_call(
quote!(#(#a)*.as_str().parse::<f64>().unwrap_or_default())
}
BuiltinFunction::StringIsFloat => quote!(#(#a)*.as_str().parse::<f64>().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 => {

View file

@ -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,

View file

@ -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);

View file

@ -142,6 +142,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 }

View file

@ -927,6 +927,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")

View file

@ -0,0 +1,96 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
export component TestCase {
property<string> empty;
property<string> hello: "hello";
property<string> hiragana: "あいうえお";
property<string> surrogate-pair: "😊𩸽";
property<string> variation-selectors: "👍🏿";
property<string> combining-character: "パ";
property<string> zero-width-joiner: "👨‍👩‍👧‍👦";
property<string> region-indicator-character: "🇦🇿🇿🇦";
property<string> emoji-tag-sequences: "🏴󠁧󠁢󠁥󠁮󠁧󠁿";
// is-empty
out property<bool> is-empty: empty.is-empty;
out property<bool> is-not_empty: !hello.is-empty;
out property<bool> test-is_empty: is_empty && is_not_empty;
// character-count
out property<int> empty-character-count: empty.character-count;
out property<int> hello-character-count: hello.character-count;
out property<int> hiragana-character-count: hiragana.character-count;
out property<int> surrogate-pair-character-count: surrogate-pair.character-count;
out property<int> variation-selectors-character-count: variation-selectors.character-count;
out property<int> combining-character-character-count: combining-character.character-count;
out property<int> zero-width-joiner-character-count: zero-width-joiner.character-count;
out property<int> region-indicator-character-character-count: region-indicator-character.character-count;
out property<int> emoji-tag-sequences-character-count: emoji-tag-sequences.character-count;
out property<bool> 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);
```
*/