1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
3
4#define CATCH_CONFIG_MAIN
5#include "catch2/catch.hpp"
6
7#include <slint.h>
8#include <slint-interpreter.h>
9
10SCENARIO("Value API")
11{
12 using namespace slint::interpreter;
13 Value value;
14
15 REQUIRE(value.type() == Value::Type::Void);
16
17 SECTION("Construct a string")
18 {
19 REQUIRE(!value.to_string().has_value());
20 slint::SharedString cpp_str("Hello World");
21 value = Value(cpp_str);
22 REQUIRE(value.type() == Value::Type::String);
23
24 auto string_opt = value.to_string();
25 REQUIRE(string_opt.has_value());
26 REQUIRE(string_opt.value() == "Hello World");
27 }
28
29 SECTION("Construct a number")
30 {
31 REQUIRE(!value.to_number().has_value());
32 const double number = 42.0;
33 value = Value(number);
34 REQUIRE(value.type() == Value::Type::Number);
35
36 auto number_opt = value.to_number();
37 REQUIRE(number_opt.has_value());
38 REQUIRE(number_opt.value() == number);
39
40 Value v2 = 42;
41 REQUIRE(v2.type() == Value::Type::Number);
42 REQUIRE(v2 == value);
43 REQUIRE(*v2.to_number() == number);
44 }
45
46 SECTION("Construct a bool")
47 {
48 REQUIRE(!value.to_bool().has_value());
49 value = Value(true);
50 REQUIRE(value.type() == Value::Type::Bool);
51
52 auto bool_opt = value.to_bool();
53 REQUIRE(bool_opt.has_value());
54 REQUIRE(bool_opt.value() == true);
55 }
56
57 SECTION("Construct an array")
58 {
59 REQUIRE(!value.to_array().has_value());
60 slint::SharedVector<Value> array { Value(42.0), Value(true) };
61 value = Value(array);
62 REQUIRE(value.type() == Value::Type::Model);
63
64 auto array_opt = value.to_array();
65 REQUIRE(array_opt.has_value());
66
67 auto extracted_array = array_opt.value();
68 REQUIRE(extracted_array.size() == 2);
69 REQUIRE(extracted_array[0].to_number().value() == 42);
70 REQUIRE(extracted_array[1].to_bool().value());
71 }
72
73 SECTION("Construct a brush")
74 {
75 REQUIRE(!value.to_brush().has_value());
76 slint::Brush brush(slint::Color::from_rgb_uint8(255, 0, 255));
77 value = Value(brush);
78 REQUIRE(value.type() == Value::Type::Brush);
79
80 auto brush_opt = value.to_brush();
81 REQUIRE(brush_opt.has_value());
82 REQUIRE(brush_opt.value() == brush);
83 }
84
85 SECTION("Construct a struct")
86 {
87 REQUIRE(!value.to_struct().has_value());
88 slint::interpreter::Struct struc;
89 value = Value(struc);
90 REQUIRE(value.type() == Value::Type::Struct);
91
92 auto struct_opt = value.to_struct();
93 REQUIRE(struct_opt.has_value());
94 }
95
96 SECTION("Construct an image")
97 {
98 REQUIRE(!value.to_image().has_value());
99 slint::Image image = slint::Image::load_from_path(
100 SOURCE_DIR "/../../../logo/slint-logo-square-light-128x128.png");
101 REQUIRE(image.size().width == 128);
102 value = Value(image);
103 REQUIRE(value.type() == Value::Type::Image);
104
105 auto image2 = value.to_image();
106 REQUIRE(image2.has_value());
107 REQUIRE(image2->size().width == 128);
108 REQUIRE(image == *image2);
109 }
110
111 SECTION("Construct a model")
112 {
113 // And test that it is properly destroyed when the value is destroyed
114 struct M : slint::VectorModel<Value>
115 {
116 bool *destroyed;
117 explicit M(bool *destroyed) : destroyed(destroyed) { }
118 void play()
119 {
120 this->push_back(Value(4.));
121 this->set_row_data(0, Value(9.));
122 }
123 ~M() { *destroyed = true; }
124 };
125 bool destroyed = false;
126 auto m = std::make_shared<M>(&destroyed);
127 {
128 Value value(m);
129 REQUIRE(value.type() == Value::Type::Model);
130 REQUIRE(!destroyed);
131 m->play();
132 m = nullptr;
133 REQUIRE(!destroyed);
134 // play a bit with the value to test the copy and move
135 Value v2 = value;
136 Value v3 = std::move(v2);
137 REQUIRE(!destroyed);
138 }
139 REQUIRE(destroyed);
140 }
141
142 SECTION("Compare Values")
143 {
144 Value str1 { slint::SharedString("Hello1") };
145 Value str2 { slint::SharedString("Hello2") };
146 Value fl1 { 10. };
147 Value fl2 { 12. };
148
149 REQUIRE(str1 == str1);
150 REQUIRE(str1 != str2);
151 REQUIRE(str1 != fl2);
152 REQUIRE(fl1 == fl1);
153 REQUIRE(fl1 != fl2);
154 REQUIRE(Value() == Value());
155 REQUIRE(Value() != str1);
156 REQUIRE(str1 == slint::SharedString("Hello1"));
157 REQUIRE(str1 != slint::SharedString("Hello2"));
158 REQUIRE(slint::SharedString("Hello2") == str2);
159 REQUIRE(fl1 != slint::SharedString("Hello2"));
160 REQUIRE(fl2 == 12.);
161 }
162}
163
164SCENARIO("Struct API")
165{
166 using namespace slint::interpreter;
167 Struct struc;
168
169 REQUIRE(!struc.get_field("not_there"));
170
171 struc.set_field("field_a", Value(slint::SharedString("Hallo")));
172
173 auto value_opt = struc.get_field("field_a");
174 REQUIRE(value_opt.has_value());
175 auto value = value_opt.value();
176 REQUIRE(value.to_string().has_value());
177 REQUIRE(value.to_string().value() == "Hallo");
178
179 int count = 0;
180 for (auto [k, value] : struc) {
181 REQUIRE(count == 0);
182 count++;
183 REQUIRE(k == "field-a");
184 REQUIRE(value.to_string().value() == "Hallo");
185 }
186
187 struc.set_field("field_b", Value(slint::SharedString("World")));
188 std::map<std::string, slint::SharedString> map;
189 for (auto [k, value] : struc)
190 map[std::string(k)] = *value.to_string();
191
192 REQUIRE(map
193 == std::map<std::string, slint::SharedString> {
194 { "field-a", slint::SharedString("Hallo") },
195 { "field-b", slint::SharedString("World") } });
196}
197
198SCENARIO("Struct Iterator Constructor")
199{
200 using namespace slint::interpreter;
201
202 std::vector<std::pair<std::string_view, Value>> values = { { "field_a", Value(true) },
203 { "field_b", Value(42.0) } };
204
205 Struct struc(values.begin(), values.end());
206
207 REQUIRE(!struc.get_field("foo").has_value());
208 REQUIRE(struc.get_field("field_a").has_value());
209 REQUIRE(struc.get_field("field_a").value().to_bool().value());
210 REQUIRE(struc.get_field("field_b").value().to_number().value() == 42.0);
211}
212
213SCENARIO("Struct Initializer List Constructor")
214{
215 using namespace slint::interpreter;
216
217 Struct struc({ { "field_a", Value(true) }, { "field_b", Value(42.0) } });
218
219 REQUIRE(!struc.get_field("foo").has_value());
220 REQUIRE(struc.get_field("field_a").has_value());
221 REQUIRE(struc.get_field("field_a").value().to_bool().value());
222 REQUIRE(struc.get_field("field_b").value().to_number().value() == 42.0);
223}
224
225SCENARIO("Struct empty field iteration")
226{
227 using namespace slint::interpreter;
228 Struct struc;
229 REQUIRE(struc.begin() == struc.end());
230}
231
232SCENARIO("Struct field iteration")
233{
234 using namespace slint::interpreter;
235
236 Struct struc({ { "field_a", Value(true) }, { "field_b", Value(42.0) } });
237
238 auto it = struc.begin();
239 auto end = struc.end();
240 REQUIRE(it != end);
241
242 auto check_valid_entry = [](const auto &key, const auto &value) -> bool {
243 if (key == "field-a")
244 return value == Value(true);
245 if (key == "field-b")
246 return value == Value(42.0);
247 return false;
248 };
249
250 std::set<std::string> seen_fields;
251
252 for (; it != end; ++it) {
253 const auto [key, value] = *it;
254 REQUIRE(check_valid_entry(key, value));
255 auto value_inserted = seen_fields.insert(std::string(key)).second;
256 REQUIRE(value_inserted);
257 }
258}
259
260SCENARIO("Component Compiler")
261{
262 using namespace slint::interpreter;
263 using namespace slint;
264
265 ComponentCompiler compiler;
266
267 SECTION("configure include paths")
268 {
269 SharedVector<SharedString> in_paths;
270 in_paths.push_back("path1");
271 in_paths.push_back("path2");
272 compiler.set_include_paths(in_paths);
273
274 auto out_paths = compiler.include_paths();
275 REQUIRE(out_paths.size() == 2);
276 REQUIRE(out_paths[0] == "path1");
277 REQUIRE(out_paths[1] == "path2");
278 }
279
280 SECTION("configure style")
281 {
282 REQUIRE(compiler.style() == "");
283 compiler.set_style("fluent");
284 REQUIRE(compiler.style() == "fluent");
285 }
286
287 SECTION("configure translation domain")
288 {
289 // Make sure this compiles.
290 compiler.set_translation_domain("cpptests");
291 }
292
293 SECTION("Compile failure from source")
294 {
295 auto result = compiler.build_from_source("Syntax Error!!", "");
296 REQUIRE_FALSE(result.has_value());
297 }
298
299 SECTION("Compile from source")
300 {
301 auto result = compiler.build_from_source("export component Dummy {}", "");
302 REQUIRE(result.has_value());
303 }
304
305 SECTION("Compile failure from path")
306 {
307 auto result = compiler.build_from_path(SOURCE_DIR "/file-not-there.slint");
308 REQUIRE_FALSE(result.has_value());
309 auto diags = compiler.diagnostics();
310
311 REQUIRE(diags.size() == 1);
312 REQUIRE(diags[0].message.starts_with("Could not load"));
313 REQUIRE(diags[0].line == 0);
314 REQUIRE(diags[0].column == 0);
315 }
316
317 SECTION("Compile from path")
318 {
319 auto result = compiler.build_from_path(SOURCE_DIR "/test.slint");
320 REQUIRE(result.has_value());
321 }
322}
323
324SCENARIO("Component Definition Properties")
325{
326 using namespace slint::interpreter;
327 using namespace slint;
328
329 ComponentCompiler compiler;
330 auto comp_def = *compiler.build_from_source(
331 "export component Dummy { in property <string> test; callback dummy; }", "");
332 auto properties = comp_def.properties();
333 REQUIRE(properties.size() == 1);
334 REQUIRE(properties[0].property_name == "test");
335 REQUIRE(properties[0].property_type == Value::Type::String);
336
337 auto callback_names = comp_def.callbacks();
338 REQUIRE(callback_names.size() == 1);
339 REQUIRE(callback_names[0] == "dummy");
340
341 auto instance = comp_def.create();
342 ComponentDefinition new_comp_def = instance->definition();
343 auto new_props = new_comp_def.properties();
344 REQUIRE(new_props.size() == 1);
345 REQUIRE(new_props[0].property_name == "test");
346 REQUIRE(new_props[0].property_type == Value::Type::String);
347}
348
349SCENARIO("Component Definition Properties / Two-way bindings")
350{
351 using namespace slint::interpreter;
352 using namespace slint;
353
354 ComponentCompiler compiler;
355 auto comp_def = *compiler.build_from_source(
356 "export component Dummy { in-out property <string> test <=> sub_object.test; "
357 " sub_object := Rectangle { property <string> test; }"
358 "}",
359 "");
360 auto properties = comp_def.properties();
361 REQUIRE(properties.size() == 1);
362 REQUIRE(properties[0].property_name == "test");
363 REQUIRE(properties[0].property_type == Value::Type::String);
364}
365
366SCENARIO("Invoke callback")
367{
368 using namespace slint::interpreter;
369 using namespace slint;
370
371 ComponentCompiler compiler;
372
373 SECTION("valid")
374 {
375 auto result = compiler.build_from_source(
376 "export component Dummy { callback some_callback(string, int) -> string; }", "");
377 REQUIRE(result.has_value());
378 auto instance = result->create();
379 std::string local_string = "_string_on_the_stack_";
380 REQUIRE(instance->set_callback("some_callback", [local_string](auto args) {
381 SharedString arg1 = *args[0].to_string();
382 int arg2 = int(*args[1].to_number());
383 std::string res = std::string(arg1) + ":" + std::to_string(arg2) + local_string;
384 return Value(SharedString(res));
385 }));
386 Value args[] = { SharedString("Hello"), 42. };
387 auto res = instance->invoke("some_callback", args);
388 REQUIRE(res.has_value());
389 REQUIRE(*res->to_string() == SharedString("Hello:42_string_on_the_stack_"));
390 }
391
392 SECTION("invalid")
393 {
394 auto result = compiler.build_from_source(
395 "export component Dummy { callback foo(string, int) -> string; }", "");
396 REQUIRE(result.has_value());
397 auto instance = result->create();
398 REQUIRE(!instance->set_callback("bar", [](auto) { return Value(); }));
399 Value args[] = { SharedString("Hello"), 42. };
400 auto res = instance->invoke("bar", args);
401 REQUIRE(!res.has_value());
402 }
403}
404
405SCENARIO("Array between .slint and C++")
406{
407 using namespace slint::interpreter;
408 using namespace slint;
409
410 ComponentCompiler compiler;
411
412 auto result = compiler.build_from_source(
413 "export component Dummy { in-out property <[int]> array: [1, 2, 3]; }", "");
414 REQUIRE(result.has_value());
415 auto instance = result->create();
416
417 SECTION(".slint to C++")
418 {
419 auto maybe_array = instance->get_property("array");
420 REQUIRE(maybe_array.has_value());
421 REQUIRE(maybe_array->type() == Value::Type::Model);
422
423 auto array = *maybe_array;
424 REQUIRE(array.to_array() == slint::SharedVector<Value> { Value(1.), Value(2.), Value(3.) });
425 }
426
427 SECTION("C++ to .slint")
428 {
429 slint::SharedVector<Value> cpp_array { Value(4.), Value(5.), Value(6.) };
430
431 instance->set_property("array", Value(cpp_array));
432 auto maybe_array = instance->get_property("array");
433 REQUIRE(maybe_array.has_value());
434 REQUIRE(maybe_array->type() == Value::Type::Model);
435
436 auto actual_array = *maybe_array;
437 REQUIRE(actual_array.to_array() == cpp_array);
438 }
439}
440
441SCENARIO("Angle between .slint and C++")
442{
443 using namespace slint::interpreter;
444 using namespace slint;
445
446 ComponentCompiler compiler;
447
448 auto result = compiler.build_from_source(
449 "export component Dummy { in-out property <angle> the_angle: "
450 "0.25turn; out property <bool> test: the_angle == 0.5turn; }",
451 "");
452 REQUIRE(result.has_value());
453 auto instance = result->create();
454
455 SECTION("Read property")
456 {
457 auto angle_value = instance->get_property("the-angle");
458 REQUIRE(angle_value.has_value());
459 REQUIRE(angle_value->type() == Value::Type::Number);
460 auto angle = angle_value->to_number();
461 REQUIRE(angle.has_value());
462 REQUIRE(*angle == 90);
463 }
464 SECTION("Write property")
465 {
466 REQUIRE(!*instance->get_property("test")->to_bool());
467 bool ok = instance->set_property("the_angle", 180.);
468 REQUIRE(ok);
469 REQUIRE(*instance->get_property("the_angle")->to_number() == 180);
470 REQUIRE(*instance->get_property("test")->to_bool());
471 }
472}
473
474SCENARIO("Component Definition Name")
475{
476 using namespace slint::interpreter;
477 using namespace slint;
478
479 ComponentCompiler compiler;
480 auto comp_def = *compiler.build_from_source("export component IHaveAName { }", "");
481 REQUIRE(comp_def.name() == "IHaveAName");
482}
483
484SCENARIO("Send key events")
485{
486 using namespace slint::interpreter;
487 using namespace slint;
488
489 ComponentCompiler compiler;
490 auto comp_def = compiler.build_from_source(R"(
491 export component Dummy {
492 forward-focus: scope;
493 out property <string> result;
494 scope := FocusScope {
495 key-pressed(event) => {
496 if (event.text != Key.Shift && event.text != Key.Control) {
497 result += event.text;
498 }
499 return accept;
500 }
501 }
502 }
503 )",
504 "");
505 REQUIRE(comp_def.has_value());
506 auto instance = comp_def->create();
507 slint::testing::send_keyboard_string_sequence(&*instance, "Hello keys!");
508 REQUIRE(*instance->get_property("result")->to_string() == "Hello keys!");
509}
510
511SCENARIO("Global properties")
512{
513 using namespace slint::interpreter;
514 using namespace slint;
515
516 ComponentCompiler compiler;
517
518 auto result = compiler.build_from_source(
519 R"(
520 export global The-Global {
521 in-out property <string> the-property: "€€€";
522 pure callback to_uppercase(string)->string;
523 public function ff() -> string { return the-property; }
524 }
525 export component Dummy {
526 out property <string> result: The-Global.to_uppercase("abc");
527 }
528 )",
529 "");
530 for (auto &&x : compiler.diagnostics())
531 std::cerr << x.message << std::endl;
532 REQUIRE(result.has_value());
533 auto component_definition = *result;
534
535 SECTION("Globals introspection")
536 {
537 auto globals = component_definition.globals();
538 REQUIRE(globals.size() == 1);
539 REQUIRE(globals[0] == "The-Global");
540
541 REQUIRE(!component_definition.global_properties("not there").has_value());
542
543 REQUIRE(component_definition.global_properties("The_Global").has_value());
544 REQUIRE(component_definition.global_properties("The-Global").has_value());
545
546 auto properties = *component_definition.global_properties("The-Global");
547 REQUIRE(properties.size() == 1);
548 REQUIRE(properties[0].property_name == "the-property");
549 REQUIRE(properties[0].property_type == Value::Type::String);
550
551 auto callbacks = *component_definition.global_callbacks("The-Global");
552 REQUIRE(callbacks.size() == 1);
553 REQUIRE(callbacks[0] == "to_uppercase");
554 }
555
556 auto instance = component_definition.create();
557
558 SECTION("Invalid read")
559 {
560 REQUIRE(!instance->get_global_property("the - global", "the-property").has_value());
561 REQUIRE(!instance->get_global_property("The-Global", "the property").has_value());
562 }
563 SECTION("Invalid set")
564 {
565 REQUIRE(!instance->set_global_property("the - global", "the-property", 5.));
566 REQUIRE(!instance->set_global_property("The-Global", "the property", 5.));
567 REQUIRE(!instance->set_global_property("The-Global", "the-property", 5.));
568 }
569 SECTION("get property")
570 {
571 auto value = instance->get_global_property("The_Global", "the-property");
572 REQUIRE(value.has_value());
573 REQUIRE(value->to_string().has_value());
574 REQUIRE(value->to_string().value() == "€€€");
575 }
576 SECTION("set property")
577 {
578 REQUIRE(instance->set_global_property("The-Global", "the-property", SharedString("§§§")));
579 auto value = instance->get_global_property("The-Global", "the_property");
580 REQUIRE(value.has_value());
581 REQUIRE(value->to_string().has_value());
582 REQUIRE(value->to_string().value() == "§§§");
583 }
584 SECTION("set/invoke callback")
585 {
586 REQUIRE(instance->set_global_callback("The-Global", "to_uppercase", [](auto args) {
587 std::string arg1(*args[0].to_string());
588 std::transform(arg1.begin(), arg1.end(), arg1.begin(), toupper);
589 return SharedString(arg1);
590 }));
591 auto result = instance->get_property("result");
592 REQUIRE(result.has_value());
593 REQUIRE(result->to_string().has_value());
594 REQUIRE(result->to_string().value() == "ABC");
595
596 Value args[] = { SharedString("Hello") };
597 auto res = instance->invoke_global("The_Global", "to-uppercase", args);
598 REQUIRE(res.has_value());
599 REQUIRE(*res->to_string() == SharedString("HELLO"));
600 }
601 SECTION("callback errors")
602 {
603 REQUIRE(!instance->set_global_callback("TheGlobal", "to_uppercase",
604 [](auto) { return Value {}; }));
605 REQUIRE(!instance->set_global_callback("The-Global", "touppercase",
606 [](auto) { return Value {}; }));
607 REQUIRE(!instance->invoke_global("TheGlobal", "touppercase", {}));
608 REQUIRE(!instance->invoke_global("The-Global", "touppercase", {}));
609 }
610 SECTION("invoke function")
611 {
612 REQUIRE(instance->set_global_property("The-Global", "the-property", SharedString("&&&")));
613 auto res = instance->invoke_global("The_Global", "ff", {});
614 REQUIRE(res.has_value());
615 REQUIRE(*res->to_string() == SharedString("&&&"));
616 }
617}
618

source code of slint/api/cpp/tests/interpreter.cpp