Skip to content

Commit

Permalink
String: Add .is-empty and .character-count 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.
- .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.
  • Loading branch information
task-jp committed Jan 6, 2025
1 parent d6f83a2 commit ebb5d3f
Show file tree
Hide file tree
Showing 13 changed files with 196 additions and 7 deletions.
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_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>() {
Expand Down
2 changes: 2 additions & 0 deletions api/rs/slint/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions api/rs/slint/private_unstable_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -230,5 +230,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, *};
}
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
StringCharacterCount,
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,
StringCharacterCount: (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::StringCharacterCount => 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::StringCharacterCount => 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::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())
}
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::StringCharacterCount => {
quote!( slint::private_unstable_api::re_exports::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::StringCharacterCount => 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("character-count", function_call(BuiltinFunction::StringCharacterCount)))
}
}
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::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")
Expand Down
96 changes: 96 additions & 0 deletions tests/cases/types/string_character_count.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;

// 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);
```
*/

0 comments on commit ebb5d3f

Please sign in to comment.