Skip to content

Commit

Permalink
String: Add .is-empty and .length properties
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
task-jp committed Dec 24, 2024
1 parent 9db8027 commit bedc413
Show file tree
Hide file tree
Showing 13 changed files with 197 additions and 7 deletions.
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions api/cpp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
5 changes: 5 additions & 0 deletions api/cpp/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<usize>() {
Expand Down
36 changes: 31 additions & 5 deletions docs/astro/src/content/docs/reference/primitive-types.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -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<bool> empty "".is-empty; // true
property<bool> 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<int> empty: "".length; // 0
property<int> hello: "hello".length; // 5
property<int> hiragana: "あいうえお".length; // 5
property<int> surrogate-pair: "😊𩸽".length; // 2
property<int> variation-selectors: "👍🏿".length; // 1
property<int> combining-character: "パ".length; // 1
property<int> zero-width-joiner: "👨‍👩‍👧‍👦".length; // 1
property<int> region-indicator-character: "🇦🇿🇿🇦".length; // 2
property<int> emoji-tag-sequences: "🏴󠁧󠁢󠁥󠁮󠁧󠁿".length; // 1
}
```

:::note[Note]
The `\{...}` syntax is not valid within the `slint!` macro in Rust.
:::
</SlintProperty>

## Numeric Types
Expand Down
16 changes: 14 additions & 2 deletions internal/compiler/expression_tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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([
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions internal/compiler/generator/cpp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
Expand Down
4 changes: 4 additions & 0 deletions internal/compiler/generator/rust.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::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 => {
Expand Down
2 changes: 2 additions & 0 deletions internal/compiler/llr/optim_passes/inline_expressions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
13 changes: 13 additions & 0 deletions internal/compiler/lookup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions internal/interpreter/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
20 changes: 20 additions & 0 deletions internal/interpreter/eval.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
96 changes: 96 additions & 0 deletions tests/cases/types/string_length.slint
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// Copyright © SixtyFPS GmbH <[email protected]>
// 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;

// length
out property<int> empty-length: empty.length;
out property<int> hello-length: hello.length;
out property<int> hiragana-length: hiragana.length;
out property<int> surrogate-pair-length: surrogate-pair.length;
out property<int> variation-selectors-length: variation-selectors.length;
out property<int> combining-character-length: combining-character.length;
out property<int> zero-width-joiner-length: zero-width-joiner.length;
out property<int> region-indicator-character-length: region-indicator-character.length;
out property<int> emoji-tag-sequences-length: emoji-tag-sequences.length;
out property<bool> 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);
```
*/
1 change: 1 addition & 0 deletions tests/driver/rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down

0 comments on commit bedc413

Please sign in to comment.