mirror of
https://github.com/slint-ui/slint.git
synced 2025-08-04 10:50:00 +00:00
[reorg]: Move api/sixtyfps-rs/sixtyfps-* into api/rs
This commit is contained in:
parent
2813441cd9
commit
842f75e653
95 changed files with 65 additions and 72 deletions
207
api/cpp/tests/datastructures.cpp
Normal file
207
api/cpp/tests/datastructures.cpp
Normal file
|
@ -0,0 +1,207 @@
|
|||
// Copyright © SixtyFPS GmbH <info@sixtyfps.io>
|
||||
// SPDX-License-Identifier: (GPL-3.0-only OR LicenseRef-SixtyFPS-commercial)
|
||||
|
||||
#include <chrono>
|
||||
#define CATCH_CONFIG_MAIN
|
||||
#include "catch2/catch.hpp"
|
||||
|
||||
#include <sixtyfps.h>
|
||||
#include <sixtyfps_image.h>
|
||||
|
||||
SCENARIO("SharedString API")
|
||||
{
|
||||
sixtyfps::SharedString str;
|
||||
|
||||
REQUIRE(str.empty());
|
||||
REQUIRE(str == "");
|
||||
REQUIRE(std::string_view(str.data()) == ""); // this test null termination of data()
|
||||
|
||||
SECTION("Construct from string_view")
|
||||
{
|
||||
std::string foo("Foo");
|
||||
std::string_view foo_view(foo);
|
||||
str = foo_view;
|
||||
REQUIRE(str == "Foo");
|
||||
REQUIRE(std::string_view(str.data()) == "Foo");
|
||||
}
|
||||
|
||||
SECTION("Construct from char*")
|
||||
{
|
||||
str = "Bar";
|
||||
REQUIRE(str == "Bar");
|
||||
}
|
||||
|
||||
SECTION("concatenate")
|
||||
{
|
||||
str = "Hello";
|
||||
str += " ";
|
||||
str += sixtyfps::SharedString("🦊") + sixtyfps::SharedString("!");
|
||||
REQUIRE(str == "Hello 🦊!");
|
||||
REQUIRE(std::string_view(str.data()) == "Hello 🦊!");
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Basic SharedVector API", "[vector]")
|
||||
{
|
||||
sixtyfps::SharedVector<int> vec;
|
||||
REQUIRE(vec.empty());
|
||||
|
||||
SECTION("Initializer list")
|
||||
{
|
||||
sixtyfps::SharedVector<int> vec({ 1, 4, 10 });
|
||||
REQUIRE(vec.size() == 3);
|
||||
REQUIRE(vec[0] == 1);
|
||||
REQUIRE(vec[1] == 4);
|
||||
REQUIRE(vec[2] == 10);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Property Tracker")
|
||||
{
|
||||
using namespace sixtyfps::private_api;
|
||||
PropertyTracker tracker1;
|
||||
PropertyTracker tracker2;
|
||||
Property<int> prop(42);
|
||||
|
||||
auto r = tracker1.evaluate([&]() { return tracker2.evaluate([&]() { return prop.get(); }); });
|
||||
REQUIRE(r == 42);
|
||||
|
||||
prop.set(1);
|
||||
REQUIRE(tracker2.is_dirty());
|
||||
REQUIRE(tracker1.is_dirty());
|
||||
|
||||
r = tracker1.evaluate(
|
||||
[&]() { return tracker2.evaluate_as_dependency_root([&]() { return prop.get(); }); });
|
||||
REQUIRE(r == 1);
|
||||
prop.set(100);
|
||||
REQUIRE(tracker2.is_dirty());
|
||||
REQUIRE(!tracker1.is_dirty());
|
||||
}
|
||||
|
||||
TEST_CASE("Model row changes")
|
||||
{
|
||||
using namespace sixtyfps::private_api;
|
||||
|
||||
auto model = std::make_shared<sixtyfps::VectorModel<int>>();
|
||||
|
||||
PropertyTracker tracker;
|
||||
|
||||
REQUIRE(tracker.evaluate([&]() {
|
||||
model->track_row_count_changes();
|
||||
return model->row_count();
|
||||
}) == 0);
|
||||
REQUIRE(!tracker.is_dirty());
|
||||
model->push_back(1);
|
||||
model->push_back(2);
|
||||
REQUIRE(tracker.is_dirty());
|
||||
REQUIRE(tracker.evaluate([&]() {
|
||||
model->track_row_count_changes();
|
||||
return model->row_count();
|
||||
}) == 2);
|
||||
REQUIRE(!tracker.is_dirty());
|
||||
model->erase(0);
|
||||
REQUIRE(tracker.is_dirty());
|
||||
REQUIRE(tracker.evaluate([&]() {
|
||||
model->track_row_count_changes();
|
||||
return model->row_count();
|
||||
}) == 1);
|
||||
}
|
||||
|
||||
TEST_CASE("Track model row data changes")
|
||||
{
|
||||
using namespace sixtyfps::private_api;
|
||||
|
||||
auto model = std::make_shared<sixtyfps::VectorModel<int>>(std::vector<int> { 0, 1, 2, 3, 4 });
|
||||
|
||||
PropertyTracker tracker;
|
||||
|
||||
REQUIRE(tracker.evaluate([&]() {
|
||||
model->track_row_data_changes(1);
|
||||
return model->row_data(1);
|
||||
}) == 1);
|
||||
REQUIRE(!tracker.is_dirty());
|
||||
|
||||
model->set_row_data(2, 42);
|
||||
REQUIRE(!tracker.is_dirty());
|
||||
model->set_row_data(1, 100);
|
||||
REQUIRE(tracker.is_dirty());
|
||||
|
||||
REQUIRE(tracker.evaluate([&]() {
|
||||
model->track_row_data_changes(1);
|
||||
return model->row_data(1);
|
||||
}) == 100);
|
||||
REQUIRE(!tracker.is_dirty());
|
||||
|
||||
// Any changes to rows (even if after tracked rows) for now also marks watched rows as dirty, to
|
||||
// keep the logic simple.
|
||||
model->push_back(200);
|
||||
REQUIRE(tracker.is_dirty());
|
||||
|
||||
REQUIRE(tracker.evaluate([&]() {
|
||||
model->track_row_data_changes(1);
|
||||
return model->row_data(1);
|
||||
}) == 100);
|
||||
REQUIRE(!tracker.is_dirty());
|
||||
|
||||
model->insert(0, 255);
|
||||
REQUIRE(tracker.is_dirty());
|
||||
}
|
||||
|
||||
TEST_CASE("Image")
|
||||
{
|
||||
using namespace sixtyfps;
|
||||
|
||||
// ensure a backend exists, using private api
|
||||
private_api::WindowRc wnd;
|
||||
|
||||
Image img;
|
||||
{
|
||||
auto size = img.size();
|
||||
REQUIRE(size.width == 0.);
|
||||
REQUIRE(size.height == 0.);
|
||||
}
|
||||
{
|
||||
REQUIRE(!img.path().has_value());
|
||||
}
|
||||
|
||||
img = Image::load_from_path(SOURCE_DIR "/../../vscode_extension/extension-logo.png");
|
||||
{
|
||||
auto size = img.size();
|
||||
REQUIRE(size.width == 128.);
|
||||
REQUIRE(size.height == 128.);
|
||||
}
|
||||
{
|
||||
auto actual_path = img.path();
|
||||
REQUIRE(actual_path.has_value());
|
||||
REQUIRE(*actual_path == SOURCE_DIR "/../../vscode_extension/extension-logo.png");
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("SharedVector")
|
||||
{
|
||||
using namespace sixtyfps;
|
||||
|
||||
SharedVector<SharedString> vec;
|
||||
vec.clear();
|
||||
vec.push_back("Hello");
|
||||
vec.push_back("World");
|
||||
vec.push_back("of");
|
||||
vec.push_back("Vectors");
|
||||
|
||||
auto copy = vec;
|
||||
|
||||
REQUIRE(vec.size() == 4);
|
||||
auto orig_cap = vec.capacity();
|
||||
REQUIRE(orig_cap >= vec.size());
|
||||
vec.clear();
|
||||
REQUIRE(vec.size() == 0);
|
||||
REQUIRE(vec.capacity() == 0); // vec was shared, so start with new empty vector.
|
||||
vec.push_back("Welcome back");
|
||||
REQUIRE(vec.size() == 1);
|
||||
REQUIRE(vec.capacity() >= vec.size());
|
||||
|
||||
REQUIRE(copy.size() == 4);
|
||||
REQUIRE(copy.capacity() == orig_cap);
|
||||
copy.clear(); // copy is not shared (anymore), retain capacity.
|
||||
REQUIRE(copy.capacity() == orig_cap);
|
||||
}
|
178
api/cpp/tests/eventloop.cpp
Normal file
178
api/cpp/tests/eventloop.cpp
Normal file
|
@ -0,0 +1,178 @@
|
|||
// Copyright © SixtyFPS GmbH <info@sixtyfps.io>
|
||||
// SPDX-License-Identifier: (GPL-3.0-only OR LicenseRef-SixtyFPS-commercial)
|
||||
|
||||
#define CATCH_CONFIG_MAIN
|
||||
#include "catch2/catch.hpp"
|
||||
|
||||
#include <sixtyfps.h>
|
||||
#include <thread>
|
||||
|
||||
TEST_CASE("C++ Singleshot Timers")
|
||||
{
|
||||
using namespace sixtyfps;
|
||||
int called = 0;
|
||||
Timer testTimer(std::chrono::milliseconds(16), [&]() {
|
||||
sixtyfps::quit_event_loop();
|
||||
called += 10;
|
||||
});
|
||||
REQUIRE(called == 0);
|
||||
sixtyfps::run_event_loop();
|
||||
REQUIRE(called == 10);
|
||||
}
|
||||
|
||||
TEST_CASE("C++ Repeated Timer")
|
||||
{
|
||||
int timer_triggered = 0;
|
||||
sixtyfps::Timer timer;
|
||||
|
||||
timer.start(sixtyfps::TimerMode::Repeated, std::chrono::milliseconds(30),
|
||||
[&]() { timer_triggered++; });
|
||||
|
||||
REQUIRE(timer_triggered == 0);
|
||||
|
||||
bool timer_was_running = false;
|
||||
|
||||
sixtyfps::Timer::single_shot(std::chrono::milliseconds(500), [&]() {
|
||||
timer_was_running = timer.running();
|
||||
sixtyfps::quit_event_loop();
|
||||
});
|
||||
|
||||
sixtyfps::run_event_loop();
|
||||
|
||||
REQUIRE(timer_triggered > 1);
|
||||
REQUIRE(timer_was_running);
|
||||
}
|
||||
|
||||
TEST_CASE("C++ Restart Singleshot Timer")
|
||||
{
|
||||
int timer_triggered = 0;
|
||||
sixtyfps::Timer timer;
|
||||
|
||||
timer.start(sixtyfps::TimerMode::SingleShot, std::chrono::milliseconds(30),
|
||||
[&]() { timer_triggered++; });
|
||||
|
||||
REQUIRE(timer_triggered == 0);
|
||||
|
||||
bool timer_was_running = false;
|
||||
|
||||
sixtyfps::Timer::single_shot(std::chrono::milliseconds(500), [&]() {
|
||||
timer_was_running = timer.running();
|
||||
sixtyfps::quit_event_loop();
|
||||
});
|
||||
|
||||
sixtyfps::run_event_loop();
|
||||
|
||||
REQUIRE(timer_triggered == 1);
|
||||
REQUIRE(timer_was_running);
|
||||
timer_triggered = 0;
|
||||
timer.restart();
|
||||
sixtyfps::Timer::single_shot(std::chrono::milliseconds(500), [&]() {
|
||||
timer_was_running = timer.running();
|
||||
sixtyfps::quit_event_loop();
|
||||
});
|
||||
|
||||
sixtyfps::run_event_loop();
|
||||
|
||||
REQUIRE(timer_triggered == 1);
|
||||
REQUIRE(timer_was_running);
|
||||
}
|
||||
|
||||
TEST_CASE("C++ Restart Repeated Timer")
|
||||
{
|
||||
int timer_triggered = 0;
|
||||
sixtyfps::Timer timer;
|
||||
|
||||
timer.start(sixtyfps::TimerMode::Repeated, std::chrono::milliseconds(30),
|
||||
[&]() { timer_triggered++; });
|
||||
|
||||
REQUIRE(timer_triggered == 0);
|
||||
|
||||
bool timer_was_running = false;
|
||||
|
||||
sixtyfps::Timer::single_shot(std::chrono::milliseconds(500), [&]() {
|
||||
timer_was_running = timer.running();
|
||||
sixtyfps::quit_event_loop();
|
||||
});
|
||||
|
||||
sixtyfps::run_event_loop();
|
||||
|
||||
REQUIRE(timer_triggered > 1);
|
||||
REQUIRE(timer_was_running);
|
||||
|
||||
timer_was_running = false;
|
||||
timer_triggered = 0;
|
||||
timer.stop();
|
||||
sixtyfps::Timer::single_shot(std::chrono::milliseconds(500), [&]() {
|
||||
timer_was_running = timer.running();
|
||||
sixtyfps::quit_event_loop();
|
||||
});
|
||||
|
||||
sixtyfps::run_event_loop();
|
||||
|
||||
REQUIRE(timer_triggered == 0);
|
||||
REQUIRE(!timer_was_running);
|
||||
|
||||
timer_was_running = false;
|
||||
timer_triggered = 0;
|
||||
|
||||
timer.restart();
|
||||
|
||||
sixtyfps::Timer::single_shot(std::chrono::milliseconds(500), [&]() {
|
||||
timer_was_running = timer.running();
|
||||
sixtyfps::quit_event_loop();
|
||||
});
|
||||
|
||||
sixtyfps::run_event_loop();
|
||||
|
||||
REQUIRE(timer_triggered > 1);
|
||||
REQUIRE(timer_was_running);
|
||||
}
|
||||
|
||||
TEST_CASE("Quit from event")
|
||||
{
|
||||
int called = 0;
|
||||
sixtyfps::invoke_from_event_loop([&] {
|
||||
sixtyfps::quit_event_loop();
|
||||
called += 10;
|
||||
});
|
||||
REQUIRE(called == 0);
|
||||
sixtyfps::run_event_loop();
|
||||
REQUIRE(called == 10);
|
||||
}
|
||||
|
||||
TEST_CASE("Event from thread")
|
||||
{
|
||||
std::atomic<int> called = 0;
|
||||
auto t = std::thread([&] {
|
||||
called += 10;
|
||||
sixtyfps::invoke_from_event_loop([&] {
|
||||
called += 100;
|
||||
sixtyfps::quit_event_loop();
|
||||
});
|
||||
});
|
||||
|
||||
sixtyfps::run_event_loop();
|
||||
REQUIRE(called == 110);
|
||||
t.join();
|
||||
}
|
||||
|
||||
TEST_CASE("Blocking Event from thread")
|
||||
{
|
||||
std::atomic<int> called = 0;
|
||||
auto t = std::thread([&] {
|
||||
// test returning a, unique_ptr because it is movable-only
|
||||
std::unique_ptr foo = sixtyfps::blocking_invoke_from_event_loop(
|
||||
[&] { return std::make_unique<int>(42); });
|
||||
called = *foo;
|
||||
int xxx = 123;
|
||||
sixtyfps::blocking_invoke_from_event_loop([&] {
|
||||
sixtyfps::quit_event_loop();
|
||||
xxx = 888999;
|
||||
});
|
||||
REQUIRE(xxx == 888999);
|
||||
});
|
||||
|
||||
sixtyfps::run_event_loop();
|
||||
REQUIRE(called == 42);
|
||||
t.join();
|
||||
}
|
589
api/cpp/tests/interpreter.cpp
Normal file
589
api/cpp/tests/interpreter.cpp
Normal file
|
@ -0,0 +1,589 @@
|
|||
// Copyright © SixtyFPS GmbH <info@sixtyfps.io>
|
||||
// SPDX-License-Identifier: (GPL-3.0-only OR LicenseRef-SixtyFPS-commercial)
|
||||
|
||||
#define CATCH_CONFIG_MAIN
|
||||
#include "catch2/catch.hpp"
|
||||
|
||||
#include <sixtyfps.h>
|
||||
#include <sixtyfps_interpreter.h>
|
||||
|
||||
SCENARIO("Value API")
|
||||
{
|
||||
using namespace sixtyfps::interpreter;
|
||||
Value value;
|
||||
|
||||
REQUIRE(value.type() == Value::Type::Void);
|
||||
|
||||
SECTION("Construct a string")
|
||||
{
|
||||
REQUIRE(!value.to_string().has_value());
|
||||
sixtyfps::SharedString cpp_str("Hello World");
|
||||
value = Value(cpp_str);
|
||||
REQUIRE(value.type() == Value::Type::String);
|
||||
|
||||
auto string_opt = value.to_string();
|
||||
REQUIRE(string_opt.has_value());
|
||||
REQUIRE(string_opt.value() == "Hello World");
|
||||
}
|
||||
|
||||
SECTION("Construct a number")
|
||||
{
|
||||
REQUIRE(!value.to_number().has_value());
|
||||
const double number = 42.0;
|
||||
value = Value(number);
|
||||
REQUIRE(value.type() == Value::Type::Number);
|
||||
|
||||
auto number_opt = value.to_number();
|
||||
REQUIRE(number_opt.has_value());
|
||||
REQUIRE(number_opt.value() == number);
|
||||
}
|
||||
|
||||
SECTION("Construct a bool")
|
||||
{
|
||||
REQUIRE(!value.to_bool().has_value());
|
||||
value = Value(true);
|
||||
REQUIRE(value.type() == Value::Type::Bool);
|
||||
|
||||
auto bool_opt = value.to_bool();
|
||||
REQUIRE(bool_opt.has_value());
|
||||
REQUIRE(bool_opt.value() == true);
|
||||
}
|
||||
|
||||
SECTION("Construct an array")
|
||||
{
|
||||
REQUIRE(!value.to_array().has_value());
|
||||
sixtyfps::SharedVector<Value> array { Value(42.0), Value(true) };
|
||||
value = Value(array);
|
||||
REQUIRE(value.type() == Value::Type::Model);
|
||||
|
||||
auto array_opt = value.to_array();
|
||||
REQUIRE(array_opt.has_value());
|
||||
|
||||
auto extracted_array = array_opt.value();
|
||||
REQUIRE(extracted_array.size() == 2);
|
||||
REQUIRE(extracted_array[0].to_number().value() == 42);
|
||||
REQUIRE(extracted_array[1].to_bool().value());
|
||||
}
|
||||
|
||||
SECTION("Construct a brush")
|
||||
{
|
||||
REQUIRE(!value.to_brush().has_value());
|
||||
sixtyfps::Brush brush(sixtyfps::Color::from_rgb_uint8(255, 0, 255));
|
||||
value = Value(brush);
|
||||
REQUIRE(value.type() == Value::Type::Brush);
|
||||
|
||||
auto brush_opt = value.to_brush();
|
||||
REQUIRE(brush_opt.has_value());
|
||||
REQUIRE(brush_opt.value() == brush);
|
||||
}
|
||||
|
||||
SECTION("Construct a struct")
|
||||
{
|
||||
REQUIRE(!value.to_struct().has_value());
|
||||
sixtyfps::interpreter::Struct struc;
|
||||
value = Value(struc);
|
||||
REQUIRE(value.type() == Value::Type::Struct);
|
||||
|
||||
auto struct_opt = value.to_struct();
|
||||
REQUIRE(struct_opt.has_value());
|
||||
}
|
||||
|
||||
SECTION("Construct an image")
|
||||
{
|
||||
// ensure a backend exists, using private api
|
||||
sixtyfps::private_api::WindowRc wnd;
|
||||
|
||||
REQUIRE(!value.to_image().has_value());
|
||||
sixtyfps::Image image = sixtyfps::Image::load_from_path(
|
||||
SOURCE_DIR "/../../vscode_extension/extension-logo.png");
|
||||
REQUIRE(image.size().width == 128);
|
||||
value = Value(image);
|
||||
REQUIRE(value.type() == Value::Type::Image);
|
||||
|
||||
auto image2 = value.to_image();
|
||||
REQUIRE(image2.has_value());
|
||||
REQUIRE(image2->size().width == 128);
|
||||
REQUIRE(image == *image2);
|
||||
}
|
||||
|
||||
SECTION("Construct a model")
|
||||
{
|
||||
// And test that it is properly destroyed when the value is destroyed
|
||||
struct M : sixtyfps::VectorModel<Value>
|
||||
{
|
||||
bool *destroyed;
|
||||
explicit M(bool *destroyed) : destroyed(destroyed) { }
|
||||
void play()
|
||||
{
|
||||
this->push_back(Value(4.));
|
||||
this->set_row_data(0, Value(9.));
|
||||
}
|
||||
~M() { *destroyed = true; }
|
||||
};
|
||||
bool destroyed = false;
|
||||
auto m = std::make_shared<M>(&destroyed);
|
||||
{
|
||||
Value value(m);
|
||||
REQUIRE(value.type() == Value::Type::Model);
|
||||
REQUIRE(!destroyed);
|
||||
m->play();
|
||||
m = nullptr;
|
||||
REQUIRE(!destroyed);
|
||||
// play a bit with the value to test the copy and move
|
||||
Value v2 = value;
|
||||
Value v3 = std::move(v2);
|
||||
REQUIRE(!destroyed);
|
||||
}
|
||||
REQUIRE(destroyed);
|
||||
}
|
||||
|
||||
SECTION("Compare Values")
|
||||
{
|
||||
Value str1 { sixtyfps::SharedString("Hello1") };
|
||||
Value str2 { sixtyfps::SharedString("Hello2") };
|
||||
Value fl1 { 10. };
|
||||
Value fl2 { 12. };
|
||||
|
||||
REQUIRE(str1 == str1);
|
||||
REQUIRE(str1 != str2);
|
||||
REQUIRE(str1 != fl2);
|
||||
REQUIRE(fl1 == fl1);
|
||||
REQUIRE(fl1 != fl2);
|
||||
REQUIRE(Value() == Value());
|
||||
REQUIRE(Value() != str1);
|
||||
REQUIRE(str1 == sixtyfps::SharedString("Hello1"));
|
||||
REQUIRE(str1 != sixtyfps::SharedString("Hello2"));
|
||||
REQUIRE(sixtyfps::SharedString("Hello2") == str2);
|
||||
REQUIRE(fl1 != sixtyfps::SharedString("Hello2"));
|
||||
REQUIRE(fl2 == 12.);
|
||||
}
|
||||
}
|
||||
|
||||
SCENARIO("Struct API")
|
||||
{
|
||||
using namespace sixtyfps::interpreter;
|
||||
Struct struc;
|
||||
|
||||
REQUIRE(!struc.get_field("not_there"));
|
||||
|
||||
struc.set_field("field_a", Value(sixtyfps::SharedString("Hallo")));
|
||||
|
||||
auto value_opt = struc.get_field("field_a");
|
||||
REQUIRE(value_opt.has_value());
|
||||
auto value = value_opt.value();
|
||||
REQUIRE(value.to_string().has_value());
|
||||
REQUIRE(value.to_string().value() == "Hallo");
|
||||
|
||||
int count = 0;
|
||||
for (auto [k, value] : struc) {
|
||||
REQUIRE(count == 0);
|
||||
count++;
|
||||
REQUIRE(k == "field-a");
|
||||
REQUIRE(value.to_string().value() == "Hallo");
|
||||
}
|
||||
|
||||
struc.set_field("field_b", Value(sixtyfps::SharedString("World")));
|
||||
std::map<std::string, sixtyfps::SharedString> map;
|
||||
for (auto [k, value] : struc)
|
||||
map[std::string(k)] = *value.to_string();
|
||||
|
||||
REQUIRE(map
|
||||
== std::map<std::string, sixtyfps::SharedString> {
|
||||
{ "field-a", sixtyfps::SharedString("Hallo") },
|
||||
{ "field-b", sixtyfps::SharedString("World") } });
|
||||
}
|
||||
|
||||
SCENARIO("Struct Iterator Constructor")
|
||||
{
|
||||
using namespace sixtyfps::interpreter;
|
||||
|
||||
std::vector<std::pair<std::string_view, Value>> values = { { "field_a", Value(true) },
|
||||
{ "field_b", Value(42.0) } };
|
||||
|
||||
Struct struc(values.begin(), values.end());
|
||||
|
||||
REQUIRE(!struc.get_field("foo").has_value());
|
||||
REQUIRE(struc.get_field("field_a").has_value());
|
||||
REQUIRE(struc.get_field("field_a").value().to_bool().value());
|
||||
REQUIRE(struc.get_field("field_b").value().to_number().value() == 42.0);
|
||||
}
|
||||
|
||||
SCENARIO("Struct Initializer List Constructor")
|
||||
{
|
||||
using namespace sixtyfps::interpreter;
|
||||
|
||||
Struct struc({ { "field_a", Value(true) }, { "field_b", Value(42.0) } });
|
||||
|
||||
REQUIRE(!struc.get_field("foo").has_value());
|
||||
REQUIRE(struc.get_field("field_a").has_value());
|
||||
REQUIRE(struc.get_field("field_a").value().to_bool().value());
|
||||
REQUIRE(struc.get_field("field_b").value().to_number().value() == 42.0);
|
||||
}
|
||||
|
||||
SCENARIO("Struct empty field iteration")
|
||||
{
|
||||
using namespace sixtyfps::interpreter;
|
||||
Struct struc;
|
||||
REQUIRE(struc.begin() == struc.end());
|
||||
}
|
||||
|
||||
SCENARIO("Struct field iteration")
|
||||
{
|
||||
using namespace sixtyfps::interpreter;
|
||||
|
||||
Struct struc({ { "field_a", Value(true) }, { "field_b", Value(42.0) } });
|
||||
|
||||
auto it = struc.begin();
|
||||
auto end = struc.end();
|
||||
REQUIRE(it != end);
|
||||
|
||||
auto check_valid_entry = [](const auto &key, const auto &value) -> bool {
|
||||
if (key == "field-a")
|
||||
return value == Value(true);
|
||||
if (key == "field-b")
|
||||
return value == Value(42.0);
|
||||
return false;
|
||||
};
|
||||
|
||||
std::set<std::string> seen_fields;
|
||||
|
||||
for (; it != end; ++it) {
|
||||
const auto [key, value] = *it;
|
||||
REQUIRE(check_valid_entry(key, value));
|
||||
auto value_inserted = seen_fields.insert(std::string(key)).second;
|
||||
REQUIRE(value_inserted);
|
||||
}
|
||||
}
|
||||
|
||||
SCENARIO("Component Compiler")
|
||||
{
|
||||
using namespace sixtyfps::interpreter;
|
||||
using namespace sixtyfps;
|
||||
|
||||
ComponentCompiler compiler;
|
||||
|
||||
SECTION("configure include paths")
|
||||
{
|
||||
SharedVector<SharedString> in_paths;
|
||||
in_paths.push_back("path1");
|
||||
in_paths.push_back("path2");
|
||||
compiler.set_include_paths(in_paths);
|
||||
|
||||
auto out_paths = compiler.include_paths();
|
||||
REQUIRE(out_paths.size() == 2);
|
||||
REQUIRE(out_paths[0] == "path1");
|
||||
REQUIRE(out_paths[1] == "path2");
|
||||
}
|
||||
|
||||
SECTION("configure style")
|
||||
{
|
||||
REQUIRE(compiler.style() == "");
|
||||
compiler.set_style("ugly");
|
||||
REQUIRE(compiler.style() == "ugly");
|
||||
}
|
||||
|
||||
SECTION("Compile failure from source")
|
||||
{
|
||||
auto result = compiler.build_from_source("Syntax Error!!", "");
|
||||
REQUIRE_FALSE(result.has_value());
|
||||
}
|
||||
|
||||
SECTION("Compile from source")
|
||||
{
|
||||
auto result = compiler.build_from_source("export Dummy := Rectangle {}", "");
|
||||
REQUIRE(result.has_value());
|
||||
}
|
||||
|
||||
SECTION("Compile failure from path")
|
||||
{
|
||||
auto result = compiler.build_from_path(SOURCE_DIR "/file-not-there.60");
|
||||
REQUIRE_FALSE(result.has_value());
|
||||
auto diags = compiler.diagnostics();
|
||||
|
||||
REQUIRE(diags.size() == 1);
|
||||
REQUIRE(diags[0].message.starts_with("Could not load"));
|
||||
}
|
||||
|
||||
SECTION("Compile from path")
|
||||
{
|
||||
auto result = compiler.build_from_path(SOURCE_DIR "/tests/test.60");
|
||||
REQUIRE(result.has_value());
|
||||
}
|
||||
}
|
||||
|
||||
SCENARIO("Component Definition Properties")
|
||||
{
|
||||
using namespace sixtyfps::interpreter;
|
||||
using namespace sixtyfps;
|
||||
|
||||
ComponentCompiler compiler;
|
||||
auto comp_def = *compiler.build_from_source(
|
||||
"export Dummy := Rectangle { property <string> test; callback dummy; }", "");
|
||||
auto properties = comp_def.properties();
|
||||
REQUIRE(properties.size() == 1);
|
||||
REQUIRE(properties[0].property_name == "test");
|
||||
REQUIRE(properties[0].property_type == Value::Type::String);
|
||||
|
||||
auto callback_names = comp_def.callbacks();
|
||||
REQUIRE(callback_names.size() == 1);
|
||||
REQUIRE(callback_names[0] == "dummy");
|
||||
}
|
||||
|
||||
SCENARIO("Component Definition Properties / Two-way bindings")
|
||||
{
|
||||
using namespace sixtyfps::interpreter;
|
||||
using namespace sixtyfps;
|
||||
|
||||
ComponentCompiler compiler;
|
||||
auto comp_def = *compiler.build_from_source(
|
||||
"export Dummy := Rectangle { property <string> test <=> sub_object.test; "
|
||||
" sub_object := Rectangle { property <string> test; }"
|
||||
"}",
|
||||
"");
|
||||
auto properties = comp_def.properties();
|
||||
REQUIRE(properties.size() == 1);
|
||||
REQUIRE(properties[0].property_name == "test");
|
||||
REQUIRE(properties[0].property_type == Value::Type::String);
|
||||
}
|
||||
|
||||
SCENARIO("Invoke callback")
|
||||
{
|
||||
using namespace sixtyfps::interpreter;
|
||||
using namespace sixtyfps;
|
||||
|
||||
ComponentCompiler compiler;
|
||||
|
||||
SECTION("valid")
|
||||
{
|
||||
auto result = compiler.build_from_source(
|
||||
"export Dummy := Rectangle { callback some_callback(string, int) -> string; }", "");
|
||||
REQUIRE(result.has_value());
|
||||
auto instance = result->create();
|
||||
REQUIRE(instance->set_callback("some_callback", [](auto args) {
|
||||
SharedString arg1 = *args[0].to_string();
|
||||
int arg2 = int(*args[1].to_number());
|
||||
std::string res = std::string(arg1) + ":" + std::to_string(arg2);
|
||||
return Value(SharedString(res));
|
||||
}));
|
||||
Value args[] = { SharedString("Hello"), 42. };
|
||||
auto res = instance->invoke_callback("some_callback", args);
|
||||
REQUIRE(res.has_value());
|
||||
REQUIRE(*res->to_string() == SharedString("Hello:42"));
|
||||
}
|
||||
|
||||
SECTION("invalid")
|
||||
{
|
||||
auto result = compiler.build_from_source(
|
||||
"export Dummy := Rectangle { callback foo(string, int) -> string; }", "");
|
||||
REQUIRE(result.has_value());
|
||||
auto instance = result->create();
|
||||
REQUIRE(!instance->set_callback("bar", [](auto) { return Value(); }));
|
||||
Value args[] = { SharedString("Hello"), 42. };
|
||||
auto res = instance->invoke_callback("bar", args);
|
||||
REQUIRE(!res.has_value());
|
||||
}
|
||||
}
|
||||
|
||||
SCENARIO("Array between .60 and C++")
|
||||
{
|
||||
using namespace sixtyfps::interpreter;
|
||||
using namespace sixtyfps;
|
||||
|
||||
ComponentCompiler compiler;
|
||||
|
||||
auto result = compiler.build_from_source(
|
||||
"export Dummy := Rectangle { property <[int]> array: [1, 2, 3]; }", "");
|
||||
REQUIRE(result.has_value());
|
||||
auto instance = result->create();
|
||||
|
||||
SECTION(".60 to C++")
|
||||
{
|
||||
auto maybe_array = instance->get_property("array");
|
||||
REQUIRE(maybe_array.has_value());
|
||||
REQUIRE(maybe_array->type() == Value::Type::Model);
|
||||
|
||||
auto array = *maybe_array;
|
||||
REQUIRE(array.to_array() == sixtyfps::SharedVector<Value> { Value(1.), Value(2.), Value(3.) });
|
||||
}
|
||||
|
||||
SECTION("C++ to .60")
|
||||
{
|
||||
sixtyfps::SharedVector<Value> cpp_array { Value(4.), Value(5.), Value(6.) };
|
||||
|
||||
instance->set_property("array", Value(cpp_array));
|
||||
auto maybe_array = instance->get_property("array");
|
||||
REQUIRE(maybe_array.has_value());
|
||||
REQUIRE(maybe_array->type() == Value::Type::Model);
|
||||
|
||||
auto actual_array = *maybe_array;
|
||||
REQUIRE(actual_array.to_array() == cpp_array);
|
||||
}
|
||||
}
|
||||
|
||||
SCENARIO("Angle between .60 and C++")
|
||||
{
|
||||
using namespace sixtyfps::interpreter;
|
||||
using namespace sixtyfps;
|
||||
|
||||
ComponentCompiler compiler;
|
||||
|
||||
auto result =
|
||||
compiler.build_from_source("export Dummy := Rectangle { property <angle> the_angle: "
|
||||
"0.25turn; property <bool> test: the_angle == 0.5turn; }",
|
||||
"");
|
||||
REQUIRE(result.has_value());
|
||||
auto instance = result->create();
|
||||
|
||||
SECTION("Read property")
|
||||
{
|
||||
auto angle_value = instance->get_property("the-angle");
|
||||
REQUIRE(angle_value.has_value());
|
||||
REQUIRE(angle_value->type() == Value::Type::Number);
|
||||
auto angle = angle_value->to_number();
|
||||
REQUIRE(angle.has_value());
|
||||
REQUIRE(*angle == 90);
|
||||
}
|
||||
SECTION("Write property")
|
||||
{
|
||||
REQUIRE(!*instance->get_property("test")->to_bool());
|
||||
bool ok = instance->set_property("the_angle", 180.);
|
||||
REQUIRE(ok);
|
||||
REQUIRE(*instance->get_property("the_angle")->to_number() == 180);
|
||||
REQUIRE(*instance->get_property("test")->to_bool());
|
||||
}
|
||||
}
|
||||
|
||||
SCENARIO("Component Definition Name")
|
||||
{
|
||||
using namespace sixtyfps::interpreter;
|
||||
using namespace sixtyfps;
|
||||
|
||||
ComponentCompiler compiler;
|
||||
auto comp_def = *compiler.build_from_source("export IHaveAName := Rectangle { }", "");
|
||||
REQUIRE(comp_def.name() == "IHaveAName");
|
||||
}
|
||||
|
||||
SCENARIO("Send key events")
|
||||
{
|
||||
using namespace sixtyfps::interpreter;
|
||||
using namespace sixtyfps;
|
||||
|
||||
ComponentCompiler compiler;
|
||||
auto comp_def = compiler.build_from_source(R"(
|
||||
export Dummy := Rectangle {
|
||||
forward-focus: scope;
|
||||
property <string> result;
|
||||
scope := FocusScope {
|
||||
key-pressed(event) => {
|
||||
result += event.text;
|
||||
return accept;
|
||||
}
|
||||
}
|
||||
}
|
||||
)",
|
||||
"");
|
||||
REQUIRE(comp_def.has_value());
|
||||
auto instance = comp_def->create();
|
||||
sixtyfps::testing::send_keyboard_string_sequence(&*instance, "Hello keys!", {});
|
||||
REQUIRE(*instance->get_property("result")->to_string() == "Hello keys!");
|
||||
}
|
||||
|
||||
SCENARIO("Global properties")
|
||||
{
|
||||
using namespace sixtyfps::interpreter;
|
||||
using namespace sixtyfps;
|
||||
|
||||
ComponentCompiler compiler;
|
||||
|
||||
auto result = compiler.build_from_source(
|
||||
R"(
|
||||
export global The-Global := {
|
||||
property <string> the-property: "€€€";
|
||||
callback to_uppercase(string)->string;
|
||||
}
|
||||
export Dummy := Rectangle {
|
||||
property <string> result: The-Global.to_uppercase("abc");
|
||||
}
|
||||
)",
|
||||
"");
|
||||
for (auto &&x : compiler.diagnostics())
|
||||
std::cerr << x.message << std::endl;
|
||||
REQUIRE(result.has_value());
|
||||
auto component_definition = *result;
|
||||
|
||||
SECTION("Globals introspection")
|
||||
{
|
||||
auto globals = component_definition.globals();
|
||||
REQUIRE(globals.size() == 1);
|
||||
REQUIRE(globals[0] == "The-Global");
|
||||
|
||||
REQUIRE(!component_definition.global_properties("not there").has_value());
|
||||
|
||||
REQUIRE(component_definition.global_properties("The_Global").has_value());
|
||||
REQUIRE(component_definition.global_properties("The-Global").has_value());
|
||||
|
||||
auto properties = *component_definition.global_properties("The-Global");
|
||||
REQUIRE(properties.size() == 1);
|
||||
REQUIRE(properties[0].property_name == "the-property");
|
||||
REQUIRE(properties[0].property_type == Value::Type::String);
|
||||
|
||||
auto callbacks = *component_definition.global_callbacks("The-Global");
|
||||
REQUIRE(callbacks.size() == 1);
|
||||
REQUIRE(callbacks[0] == "to_uppercase");
|
||||
}
|
||||
|
||||
auto instance = component_definition.create();
|
||||
|
||||
SECTION("Invalid read")
|
||||
{
|
||||
REQUIRE(!instance->get_global_property("the - global", "the-property").has_value());
|
||||
REQUIRE(!instance->get_global_property("The-Global", "the property").has_value());
|
||||
}
|
||||
SECTION("Invalid set")
|
||||
{
|
||||
REQUIRE(!instance->set_global_property("the - global", "the-property", 5.));
|
||||
REQUIRE(!instance->set_global_property("The-Global", "the property", 5.));
|
||||
REQUIRE(!instance->set_global_property("The-Global", "the-property", 5.));
|
||||
}
|
||||
SECTION("get property")
|
||||
{
|
||||
auto value = instance->get_global_property("The_Global", "the-property");
|
||||
REQUIRE(value.has_value());
|
||||
REQUIRE(value->to_string().has_value());
|
||||
REQUIRE(value->to_string().value() == "€€€");
|
||||
}
|
||||
SECTION("set property")
|
||||
{
|
||||
REQUIRE(instance->set_global_property("The-Global", "the-property", SharedString("§§§")));
|
||||
auto value = instance->get_global_property("The-Global", "the_property");
|
||||
REQUIRE(value.has_value());
|
||||
REQUIRE(value->to_string().has_value());
|
||||
REQUIRE(value->to_string().value() == "§§§");
|
||||
}
|
||||
SECTION("set/invoke callback")
|
||||
{
|
||||
REQUIRE(instance->set_global_callback("The-Global", "to_uppercase", [](auto args) {
|
||||
std::string arg1(*args[0].to_string());
|
||||
std::transform(arg1.begin(), arg1.end(), arg1.begin(), toupper);
|
||||
return SharedString(arg1);
|
||||
}));
|
||||
auto result = instance->get_property("result");
|
||||
REQUIRE(result.has_value());
|
||||
REQUIRE(result->to_string().has_value());
|
||||
REQUIRE(result->to_string().value() == "ABC");
|
||||
|
||||
Value args[] = { SharedString("Hello") };
|
||||
auto res = instance->invoke_global_callback("The_Global", "to-uppercase", args);
|
||||
REQUIRE(res.has_value());
|
||||
REQUIRE(*res->to_string() == SharedString("HELLO"));
|
||||
}
|
||||
SECTION("callback errors")
|
||||
{
|
||||
REQUIRE(!instance->set_global_callback("TheGlobal", "to_uppercase",
|
||||
[](auto) { return Value {}; }));
|
||||
REQUIRE(!instance->set_global_callback("The-Global", "touppercase",
|
||||
[](auto) { return Value {}; }));
|
||||
REQUIRE(!instance->invoke_global_callback("TheGlobal", "touppercase", {}));
|
||||
REQUIRE(!instance->invoke_global_callback("The-Global", "touppercase", {}));
|
||||
}
|
||||
}
|
4
api/cpp/tests/test.60
Normal file
4
api/cpp/tests/test.60
Normal file
|
@ -0,0 +1,4 @@
|
|||
// Copyright © SixtyFPS GmbH <info@sixtyfps.io>
|
||||
// SPDX-License-Identifier: (GPL-3.0-only OR LicenseRef-SixtyFPS-commercial)
|
||||
|
||||
export Test := Rectangle {}
|
Loading…
Add table
Add a link
Reference in a new issue