Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

String: Add .is-empty and .character-count properties #7192

Merged
merged 1 commit into from
Jan 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After #7310 this could be:

Suggested change
unicode-segmentation = "1.12.0"
unicode-segmentation = { workspace = true }

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR. I'll update all of them once #7310 is merged.


[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"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
unicode-segmentation = "1.12.0"
unicode-segmentation = { workspace = true }


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

`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
}
```

:::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 @@ -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,
Expand Down Expand Up @@ -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([
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
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())
task-jp marked this conversation as resolved.
Show resolved Hide resolved
}
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!( 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 => {
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 @@ -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"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
unicode-segmentation = "1.12.0"
unicode-segmentation = { workspace = true }


[target.'cfg(target_arch = "wasm32")'.dependencies]
i-slint-backend-winit = { workspace = true }
Expand Down
23 changes: 23 additions & 0 deletions internal/interpreter/eval.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
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);
```

*/
Loading