mirror of
https://github.com/slint-ui/slint.git
synced 2025-08-04 18:58:36 +00:00
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:
parent
12fe2bb36d
commit
68b9dfc247
13 changed files with 200 additions and 8 deletions
|
@ -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"
|
||||
|
|
|
@ -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>() {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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, *};
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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")
|
||||
|
|
96
tests/cases/types/string_character_count.slint
Normal file
96
tests/cases/types/string_character_count.slint
Normal 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);
|
||||
```
|
||||
|
||||
*/
|
Loading…
Add table
Add a link
Reference in a new issue