[reorg]: Move api/sixtyfps-rs/sixtyfps-* into api/rs

This commit is contained in:
Tobias Hunger 2022-01-31 14:06:41 +01:00 committed by Tobias Hunger
parent 2813441cd9
commit 842f75e653
95 changed files with 65 additions and 72 deletions

296
api/cpp/CMakeLists.txt Normal file
View file

@ -0,0 +1,296 @@
# Copyright © SixtyFPS GmbH <info@sixtyfps.io>
# SPDX-License-Identifier: (GPL-3.0-only OR LicenseRef-SixtyFPS-commercial)
cmake_minimum_required(VERSION 3.19)
project(SixtyFPS HOMEPAGE_URL "https://sixtyfps.io/" LANGUAGES CXX)
include(FeatureSummary)
include(CMakeDependentOption)
include(FetchContent)
FetchContent_Declare(
Corrosion
GIT_REPOSITORY https://github.com/corrosion-rs/corrosion.git
GIT_TAG 4189aa74116340dfc4b5fee9bd17510a9f36fdef
)
FetchContent_MakeAvailable(Corrosion)
list(PREPEND CMAKE_MODULE_PATH ${Corrosion_SOURCE_DIR}/cmake)
find_package(Rust 1.56 REQUIRED MODULE)
corrosion_import_crate(MANIFEST_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../../Cargo.toml"
CRATES sixtyfps-compiler sixtyfps-cpp)
set_property(
TARGET sixtyfps-cpp
APPEND
PROPERTY CORROSION_ENVIRONMENT_VARIABLES
SIXTYFPS_GENERATED_INCLUDE_DIR="${CMAKE_CURRENT_BINARY_DIR}/generated_include/"
)
set_property(
TARGET sixtyfps-compiler
PROPERTY CORROSION_USE_HOST_BUILD 1
)
add_library(SixtyFPS INTERFACE)
add_library(SixtyFPS::SixtyFPS ALIAS SixtyFPS)
target_link_libraries(SixtyFPS INTERFACE sixtyfps-cpp)
target_compile_features(SixtyFPS INTERFACE cxx_std_20)
function(define_cargo_feature cargo-feature description default)
# turn foo-bar into SIXTYFPS_FEATURE_FOO_BAR
string(TOUPPER "${cargo-feature}" cmake_option)
string(REPLACE "-" "_" cmake_option "${cmake_option}")
set(cmake_option "SIXTYFPS_FEATURE_${cmake_option}")
option("${cmake_option}" "${description}" ${default})
if(${cmake_option})
list(APPEND features ${cargo-feature})
endif()
set(features "${features}" PARENT_SCOPE)
add_feature_info(${cmake_option} ${cmake_option} ${description})
endfunction()
function(define_cargo_dependent_feature cargo-feature description default depends_condition)
# turn foo-bar into SIXTYFPS_FEATURE_FOO_BAR
string(TOUPPER "${cargo-feature}" cmake_option)
string(REPLACE "-" "_" cmake_option "${cmake_option}")
set(cmake_option "SIXTYFPS_FEATURE_${cmake_option}")
cmake_dependent_option("${cmake_option}" "${description}" ${default} ${depends_condition} OFF)
if(${cmake_option})
list(APPEND features ${cargo-feature})
endif()
set(features "${features}" PARENT_SCOPE)
add_feature_info(${cmake_option} ${cmake_option} ${description})
endfunction()
# Features that are mapped to features in the Rust crate. These and their
# defaults need to be kept in sync with the Rust bit.
define_cargo_feature(interpreter "Enable support for the SixtyFPS interpeter to load .60 files at run-time" ON)
define_cargo_feature(backend-gl "Enable OpenGL ES 2.0 based rendering backend" ON)
define_cargo_dependent_feature(x11 "Enable X11 support when using GL backend" ON SIXTYFPS_FEATURE_BACKEND_GL OFF)
define_cargo_dependent_feature(wayland "Enable Wayland support when using the GL backend" OFF SIXTYFPS_FEATURE_BACKEND_GL OFF)
define_cargo_feature(backend-qt "Enable Qt based rendering backend" ON)
set_property(
TARGET sixtyfps-cpp
PROPERTY CORROSION_FEATURES
${features}
)
set_property(
TARGET sixtyfps-cpp
PROPERTY CORROSION_NO_DEFAULT_FEATURES
${features}
)
if (SIXTYFPS_FEATURE_BACKEND_QT)
# For the CMake build don't rely on qmake being in PATH but use CMake to locate Qt. This
# means usually CMAKE_PREFIX_PATH is set.
find_package(Qt6 6.0 QUIET COMPONENTS Core Widgets)
if (NOT TARGET Qt::qmake)
find_package(Qt5 5.15 QUIET COMPONENTS Core Widgets)
endif()
endif (SIXTYFPS_FEATURE_BACKEND_QT)
if (TARGET Qt::qmake)
set_property(
TARGET sixtyfps-cpp
APPEND
PROPERTY CORROSION_ENVIRONMENT_VARIABLES
QMAKE=$<TARGET_PROPERTY:Qt::qmake,LOCATION>
)
set(SIXTYFPS_STYLE_DEFAULT "native")
else()
set_property(
TARGET sixtyfps-cpp
APPEND
PROPERTY CORROSION_ENVIRONMENT_VARIABLES
SIXTYFPS_NO_QT=1
)
set(SIXTYFPS_STYLE_DEFAULT "fluent")
endif()
set(SIXTYFPS_STYLE ${SIXTYFPS_STYLE_DEFAULT} CACHE STRING "The SixtyFPS widget style" FORCE)
file(GLOB api_headers RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}/include/"
"${CMAKE_CURRENT_SOURCE_DIR}/include/*.h")
foreach(header IN LISTS api_headers)
set_property(TARGET SixtyFPS APPEND PROPERTY PUBLIC_HEADER include/${header})
endforeach()
set(generated_headers
${CMAKE_CURRENT_BINARY_DIR}/generated_include/sixtyfps_internal.h
${CMAKE_CURRENT_BINARY_DIR}/generated_include/sixtyfps_string_internal.h
${CMAKE_CURRENT_BINARY_DIR}/generated_include/sixtyfps_brush_internal.h
${CMAKE_CURRENT_BINARY_DIR}/generated_include/sixtyfps_sharedvector_internal.h
${CMAKE_CURRENT_BINARY_DIR}/generated_include/sixtyfps_properties_internal.h
${CMAKE_CURRENT_BINARY_DIR}/generated_include/sixtyfps_image_internal.h
${CMAKE_CURRENT_BINARY_DIR}/generated_include/sixtyfps_color_internal.h
${CMAKE_CURRENT_BINARY_DIR}/generated_include/sixtyfps_pathdata_internal.h
${CMAKE_CURRENT_BINARY_DIR}/generated_include/sixtyfps_qt_internal.h
${CMAKE_CURRENT_BINARY_DIR}/generated_include/sixtyfps_backend_internal.h
${CMAKE_CURRENT_BINARY_DIR}/generated_include/sixtyfps_generated_public.h
${CMAKE_CURRENT_BINARY_DIR}/generated_include/sixtyfps_interpreter_internal.h
${CMAKE_CURRENT_BINARY_DIR}/generated_include/sixtyfps_interpreter_generated_public.h
)
foreach(header IN LISTS generated_headers)
set_property(TARGET SixtyFPS APPEND PROPERTY PUBLIC_HEADER ${header})
endforeach()
target_include_directories(SixtyFPS INTERFACE
$<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}/generated_include>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include/sixtyfps>
)
add_executable(SixtyFPS::sixtyfps-compiler ALIAS sixtyfps-compiler)
include(${CMAKE_CURRENT_LIST_DIR}/cmake/SixtyFPSMacro.cmake)
export(TARGETS SixtyFPS sixtyfps-cpp
NAMESPACE SixtyFPS:: FILE "${CMAKE_BINARY_DIR}/lib/cmake/SixtyFPS/SixtyFPSTargets.cmake")
install(EXPORT SixtyFPSTargets NAMESPACE SixtyFPS:: DESTINATION lib/cmake/SixtyFPS)
install(TARGETS SixtyFPS sixtyfps-cpp
EXPORT SixtyFPSTargets LIBRARY DESTINATION lib PUBLIC_HEADER DESTINATION include/sixtyfps)
include(CMakePackageConfigHelpers)
include(GNUInstallDirs)
install(FILES $<TARGET_FILE:sixtyfps-cpp-shared> TYPE LIB)
if(WIN32)
install(FILES $<TARGET_LINKER_FILE:sixtyfps-cpp-shared> TYPE LIB)
# Copy the dll to the top-level bin directory, where the examples will
# will also be located, so that they can find the dll.
get_property(GENERATOR_IS_MULTI_CONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
if(GENERATOR_IS_MULTI_CONFIG)
set(config_subdir_genex "$<CONFIG>/")
endif()
add_custom_target(SixtyFPS_dll_convenience ALL DEPENDS sixtyfps-cpp-shared
COMMAND ${CMAKE_COMMAND} -E copy_if_different
$<TARGET_FILE:sixtyfps-cpp-shared>
${CMAKE_BINARY_DIR}/bin/${config_subdir_genex}$<TARGET_FILE_NAME:sixtyfps-cpp-shared>)
endif()
install(PROGRAMS $<TARGET_FILE:sixtyfps-compiler> TYPE BIN)
set(SIXTYFPS_LIB_PROPERTIES "")
foreach(prop
IMPORTED_LOCATION IMPORTED_LOCATION_DEBUG IMPORTED_LOCATION_RELEASE
IMPORTED_LOCATION_RELWITHDEBINFO IMPORTED_LOCATION_MINSIZEREL
IMPORTED_IMPLIB IMPORTED_IMPLIB_DEBUG IMPORTED_IMPLIB_RELEASE
IMPORTED_IMPLIB_RELWITHDEBINFO IMPORTED_IMPLIB_MINSIZEREL)
get_target_property(value sixtyfps-cpp-shared ${prop})
if(value)
get_filename_component(value ${value} NAME)
list(APPEND SIXTYFPS_LIB_PROPERTIES ${prop} "\${_IMPORT_PREFIX}/${CMAKE_INSTALL_LIBDIR}/${value}")
endif()
endforeach()
configure_package_config_file("cmake/SixtyFPSConfig.cmake.in" "${CMAKE_CURRENT_BINARY_DIR}/lib/cmake/SixtyFPS/SixtyFPSConfig.cmake" INSTALL_DESTINATION lib/cmake/SixtyFPS)
write_basic_package_version_file(
${CMAKE_CURRENT_BINARY_DIR}/lib/cmake/SixtyFPS/SixtyFPSConfigVersion.cmake
VERSION 0.2.0
COMPATIBILITY SameMinorVersion
)
install(FILES
"${CMAKE_CURRENT_BINARY_DIR}/lib/cmake/SixtyFPS/SixtyFPSConfig.cmake"
"${CMAKE_CURRENT_BINARY_DIR}/lib/cmake/SixtyFPS/SixtyFPSConfigVersion.cmake"
"${CMAKE_CURRENT_LIST_DIR}/cmake/SixtyFPSMacro.cmake"
DESTINATION lib/cmake/SixtyFPS
)
option(SIXTYFPS_PACKAGE_BUNDLE_QT "Internal setting to install Qt binary in the packages" OFF)
if(SIXTYFPS_PACKAGE_BUNDLE_QT)
if(WIN32)
find_package(Qt6 6.0 COMPONENTS Core Gui Widgets Svg)
install(
FILES
$<TARGET_FILE:Qt6::Core>
$<TARGET_FILE:Qt6::Gui>
$<TARGET_FILE:Qt6::Widgets>
$<TARGET_FILE:Qt6::Svg>
TYPE LIB)
install(
FILES ${Qt6_DIR}/../../../plugins/platforms/qwindows.dll
DESTINATION plugins/platforms)
install(
FILES ${Qt6_DIR}/../../../plugins/styles/qwindowsvistastyle.dll
DESTINATION plugins/styles)
install(
FILES ${Qt6_DIR}/../../../plugins/imageformats/qsvg.dll
DESTINATION plugins/imageformats)
install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/../../licenses/ DESTINATION LICENSES)
set(CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS_SKIP TRUE)
include(InstallRequiredSystemLibraries)
install(FILES ${CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS} TYPE LIB)
endif()
endif(SIXTYFPS_PACKAGE_BUNDLE_QT)
set(CPACK_PACKAGE_NAME "SixtyFPS-cpp")
set(CPACK_PACKAGE_VENDOR "SixtyFPS")
set(CPACK_VERBATIM_VARIABLES true)
set(CPACK_PACKAGE_VERSION_MAJOR 0)
set(CPACK_PACKAGE_VERSION_MINOR 2)
set(CPACK_PACKAGE_VERSION_PATCH 0)
set(CPACK_PACKAGE_HOMEPAGE_URL "https://sixtyfps.io")
set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_CURRENT_LIST_DIR}/../../LICENSE.md")
set(CPACK_RESOURCE_FILE_README "${CMAKE_CURRENT_LIST_DIR}/README.md")
set(CPACK_STRIP_FILES ON)
set(CPACK_NSIS_DEFINES "ManifestDPIAware true")
if(NOT WIN32)
set(CPACK_GENERATOR "TGZ")
set(CPACK_SYSTEM_NAME "${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR}")
endif(NOT WIN32)
include(CPack)
if(BUILD_TESTING)
FetchContent_Declare(
Catch2
GIT_REPOSITORY https://github.com/catchorg/Catch2.git
GIT_TAG v2.13.1
)
FetchContent_MakeAvailable(Catch2)
find_package(Threads REQUIRED)
macro(sixtyfps_test NAME)
add_executable(test_${NAME} tests/${NAME}.cpp)
target_link_libraries(test_${NAME} PRIVATE SixtyFPS Catch2::Catch2)
target_compile_definitions(test_${NAME} PRIVATE
SOURCE_DIR=\"${CMAKE_CURRENT_SOURCE_DIR}/\"
)
add_test(test_${NAME} ${CMAKE_CURRENT_BINARY_DIR}/test_${NAME})
# Somehow the wrong relative rpath ends up in the binary, requiring us to change the
# working directory.
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
set_property(TEST test_${NAME} PROPERTY WORKING_DIRECTORY "${CMAKE_BINARY_DIR}")
endif()
if(MSVC)
target_compile_options(test_${NAME} PRIVATE /W3)
else()
target_compile_options(test_${NAME} PRIVATE -Wall -Wextra -Werror)
endif()
if (CMAKE_CXX_COMPILER_ID STREQUAL GNU)
# that warning has false positives
target_compile_options(test_${NAME} PRIVATE -Wno-maybe-uninitialized)
endif()
endmacro(sixtyfps_test)
sixtyfps_test(datastructures)
if(SIXTYFPS_FEATURE_INTERPRETER)
sixtyfps_test(interpreter)
endif()
sixtyfps_test(eventloop)
target_link_libraries(test_eventloop PRIVATE Threads::Threads)
endif()

43
api/cpp/Cargo.toml Normal file
View file

@ -0,0 +1,43 @@
# Copyright © SixtyFPS GmbH <info@sixtyfps.io>
# SPDX-License-Identifier: (GPL-3.0-only OR LicenseRef-SixtyFPS-commercial)
[package]
name = "sixtyfps-cpp"
version = "0.2.0"
authors = ["SixtyFPS <info@sixtyfps.io>"]
edition = "2021"
build = "build.rs"
license = "(GPL-3.0-only OR LicenseRef-SixtyFPS-commercial)"
description = "SixtyFPS C++ integration"
repository = "https://github.com/sixtyfpsui/sixtyfps"
homepage = "https://sixtyfps.io"
publish = false
# prefix used to convey path to generated includes to the C++ test driver
links = "sixtyfps_cpp"
[lib]
path = "lib.rs"
crate-type = ["lib", "cdylib"]
# Note, these features need to be kept in sync (along with their defaults) in
# the C++ crate's CMakeLists.txt
[features]
backend-gl = ["sixtyfps-rendering-backend-selector/sixtyfps-rendering-backend-gl"]
backend-qt = ["sixtyfps-rendering-backend-selector/sixtyfps-rendering-backend-qt"]
interpreter = ["sixtyfps-interpreter"]
testing = ["sixtyfps-rendering-backend-testing"] # Enable some function used by the integration tests
wayland = ["sixtyfps-rendering-backend-selector/wayland"]
x11 = ["sixtyfps-rendering-backend-selector/x11"]
default = ["backend-gl", "x11", "backend-qt"]
[dependencies]
sixtyfps-corelib = { version = "=0.2.0", path="../../internal/core", features = ["ffi"] }
sixtyfps-interpreter = { version = "=0.2.0", path="../../internal/interpreter", default-features = false, features = ["ffi", "compat-0-2-0"], optional = true }
sixtyfps-rendering-backend-selector = { version = "=0.2.0", path="../../internal/backends/selector" }
sixtyfps-rendering-backend-testing = { version = "=0.2.0", path="../../internal/backends/testing", optional = true }
[build-dependencies]
anyhow = "1.0"
cbindgen = "0.20"
proc-macro2 = "1.0.11"

244
api/cpp/README.md Normal file
View file

@ -0,0 +1,244 @@
# SixtyFPS-cpp
## A C++ UI toolkit
[SixtyFPS](https://sixtyfps.io/) is a UI toolkit that supports different programming languages.
SixtyFPS.cpp is the C++ API to interact with a SixtyFPS UI from C++.
The complete C++ documentation can be viewed online at https://sixtyfps.io/docs/cpp/.
If you are new to SixtyFPS, you might also consider going through our [Walk-through tutorial](https://sixtyfps.io/docs/tutorial/cpp).
## Installing or Building SixtyFPS
SixtyFPS comes with a CMake integration that automates the compilation step of the `.60` markup language files and
offers a CMake target for convenient linkage.
*Note*: We recommend using the Ninja generator of CMake for the most efficient build and `.60` dependency tracking.
You can select the CMake Ninja backend by passing `-GNinja` or setting the `CMAKE_GENERATOR` environment variable to `Ninja`.
### Building from Sources
The recommended and most flexible way to use the C++ API is to build SixtyFPS from sources.
First you need to install the prerequisites:
* Install Rust by following the [Rust Getting Started Guide](https://www.rust-lang.org/learn/get-started). Once this is done,
you should have the ```rustc``` compiler and the ```cargo``` build system installed in your path.
* **[cmake](https://cmake.org/download/)** (3.19 or newer)
* A C++ compiler that supports C++20 (e.g., **MSVC 2019 16.6** on Windows)
You can include SixtyFPS in your CMake project using CMake's `FetchContent` feature. Insert the following snippet into your
`CMakeLists.txt` to make CMake download the latest release, compile it and make the CMake integration available:
```cmake
include(FetchContent)
FetchContent_Declare(
SixtyFPS
GIT_REPOSITORY https://github.com/sixtyfpsui/sixtyfps.git
GIT_TAG v0.1.6
SOURCE_SUBDIR api/cpp
)
FetchContent_MakeAvailable(SixtyFPS)
```
If you prefer to treat SixtyFPS as an external CMake package, then you can also build SixtyFPS from source like a regular
CMake project, install it into a prefix directory of your choice and use `find_package(SixtyFPS)` in your `CMakeLists.txt`.
#### Cross-compiling
It is possible to cross-compile SixtyFPS to a different target architecture when building with CMake. In order to complete
that, you need to make sure that your CMake setup is ready for cross-compilation. You can find more information about
how to set this up in the [upstream CMake documentation](https://cmake.org/cmake/help/latest/manual/cmake-toolchains.7.html#cross-compiling).
If you are building against a Yocto SDK, it is sufficient to source the SDK's environment setup file.
Since SixtyFPS is implemented using the Rust programming language, you need to determine which Rust target
matches the target architecture that you're compiling to. Please consult the [upstream Rust documentation](https://doc.rust-lang.org/nightly/rustc/platform-support.html) to find the correct target name. Now you need to install the Rust toolchain:
```sh
rustup target add <target-name>
```
Then you're ready to invoke CMake and you need to add `-DRust_CARGO_TARGET=<target name>` to the CMake command line.
This ensures that the SixtyFPS library is built for the correct architecture.
For example if you are building against an embedded Linux Yocto SDK targeting an ARM64 board, the following commands
show how to compile:
Install the Rust targe toolchain once:
```sh
rustup target add aarch64-unknown-linux-gnu
```
Set up the environment and build:
```sh
. /path/to/yocto/sdk/environment-setup-cortexa53-crypto-poky-linux
cd sixtyfps
mkdir build
cd build
cmake -DRust_CARGO_TARGET=aarch64-unknown-linux-gnu -DCMAKE_INSTALL_PREFIX=/sixtyfps/install/path ..
cmake --build .
cmake --install .
```
### Binary Packages
We also provide binary packages of SixtyFPS for use with C++, which eliminates the need to have Rust installed in your development environment.
You can download one of our pre-built binaries for Linux or Windows on x86-64 architectures:
1. Open <https://github.com/sixtyfpsui/sixtyfps/releases>
2. Click on the latest release
3. From "Assets" download either `sixtyfps-cpp-XXX-Linux-x86_64.tar.gz` for a Linux x86-64 archive
or `sixtyfps-cpp-XXX-win64.exe` for a Windows x86-64 installer. ("XXX" refers to the version of the latest release)
4. Uncompress the downloaded archive or run the installer.
After extracting the artifact or running the installer, you can place the `lib` sub-directory into your `CMAKE_PREFIX_PATH` and `find_package(SixtyFPS)` should succeed in locating the package.
## Usage via CMake
A typical example looks like this:
```cmake
cmake_minimum_required(VERSION 3.19)
project(my_application LANGUAGES CXX)
# Note: Use find_package(SixtyFPS) instead of the following three commands, if you prefer the package
# approach.
include(FetchContent)
FetchContent_Declare(
SixtyFPS
GIT_REPOSITORY https://github.com/sixtyfpsui/sixtyfps.git
GIT_TAG v0.1.6
SOURCE_SUBDIR api/cpp
)
FetchContent_MakeAvailable(SixtyFPS)
add_executable(my_application main.cpp)
target_link_libraries(my_application PRIVATE SixtyFPS::SixtyFPS)
sixtyfps_target_60_sources(my_application my_application_ui.60)
```
The `sixtyfps_target_60_sources` cmake command allows you to add .60 files to your build. Finally it is
necessary to link your executable or library against the `SixtyFPS::SixtyFPS` target.
## Tutorial
Let's make a UI for a todo list application using the SixtyFPS UI description language.
Hopefully this should be self explanatory. Check out the documentation of the language for help
```60
// file: my_application_ui.60
import { CheckBox, Button, ListView, LineEdit } from "sixtyfps_widgets.60";
export struct TodoItem := {
title: string,
checked: bool,
}
export MainWindow := Window {
callback todo_added(string);
property <[TodoItem]> todo_model;
GridLayout {
Row {
text_edit := LineEdit {
accepted(text) => { todo_added(text); }
}
Button {
text: "Add Todo";
clicked => {
todo_added(text_edit.text);
}
}
}
list_view := ListView {
rowspan: 2;
row: 2;
for todo in todo_model: Rectangle {
height: 20px;
GridLayout {
CheckBox {
text: todo.title;
checked: todo.checked;
toggled => {
todo.checked = checked;
}
}
}
}
}
}
}
```
We can compile this code using the `sixtyfps-compiler` binary:
```sh
sixtyfps-compiler my_application_ui.60 > my_application_ui.h
```
Note: You would usually not type this command yourself, this is done automatically by the build system.
(that's what the `sixtyfps_target_60_sources` cmake function does)
This will generate a `my_application_ui.h` header file. It basically contains the following code
(edited for brevity)
```C++
#include <sixtyfps>
struct TodoItem {
bool checked;
sixtyfps::SharedString title;
};
struct MainWindow {
public:
inline auto create () -> sixtyfps::ComponentHandle<MainWindow>;
inline auto get_todo_model () const -> std::shared_ptr<sixtyfps::Model<TodoItem>>;
inline void set_todo_model (const std::shared_ptr<sixtyfps::Model<TodoItem>> &value) const;
inline void invoke_todo_added (sixtyfps::SharedString arg_0) const;
template<typename Functor> inline void on_todo_added (Functor && callback_handler) const;
//...
}
```
We can then use this from out .cpp file
```C++
// include the generated file
#include "my_application_ui.h"
int main() {
// Let's instantiate our window
auto todo_app = MainWindow::create();
// let's create a model:
auto todo_model = std::make_shared<sixtyfps::VectorModel<TodoItem>>(std::vector {
TodoItem { false, "Write documentation" },
});
// set the model as the model of our view
todo_app->set_todo_model(todo_model);
// let's connect our "add" button to add an item in the model
todo_app->on_todo_added([todo_model](const sixtyfps::SharedString &s) {
todo_model->push_back(TodoItem { false, s} );
});
// Show the window and run the event loop
todo_app->run();
}
```
That's it.
For more details, check the [Online documentation](https://sixtyfps.io/docs/cpp) and the full
[Walk-through tutorial](https://sixtyfps.io/docs/tutorial/cpp).
We also have a [Getting Started Template](https://github.com/sixtyfpsui/sixtyfps-cpp-template) repository with
the code of a minimal C++ application using SixtyFPS that can be used as a starting point to your program.

28
api/cpp/build.rs Normal file
View file

@ -0,0 +1,28 @@
// Copyright © SixtyFPS GmbH <info@sixtyfps.io>
// SPDX-License-Identifier: (GPL-3.0-only OR LicenseRef-SixtyFPS-commercial)
use std::path::Path;
mod cbindgen;
fn main() -> Result<(), anyhow::Error> {
let manifest_dir = std::env::var_os("CARGO_MANIFEST_DIR").unwrap();
// Go from $root/api/cpp down to $root
let root_dir = Path::new(&manifest_dir).ancestors().nth(2).expect(&format!(
"Failed to locate root directory, relative to {}",
manifest_dir.to_string_lossy()
));
let output_dir = std::env::var_os("SIXTYFPS_GENERATED_INCLUDE_DIR").unwrap_or_else(|| {
Path::new(&std::env::var_os("OUT_DIR").unwrap()).join("generated_include").into()
});
let output_dir = Path::new(&output_dir);
println!("cargo:GENERATED_INCLUDE_DIR={}", output_dir.display());
let dependencies = cbindgen::gen_all(&root_dir, &output_dir)?;
for path in dependencies {
println!("cargo:rerun-if-changed={}", path.display());
}
Ok(())
}

564
api/cpp/cbindgen.rs Normal file
View file

@ -0,0 +1,564 @@
// Copyright © SixtyFPS GmbH <info@sixtyfps.io>
// SPDX-License-Identifier: (GPL-3.0-only OR LicenseRef-SixtyFPS-commercial)
use anyhow::Context;
use std::iter::Extend;
use std::path::{Path, PathBuf};
// cspell::ignore compat constexpr corelib sharedvector pathdata
fn ensure_cargo_rerun_for_crate(
crate_dir: &Path,
dependencies: &mut Vec<PathBuf>,
) -> anyhow::Result<()> {
dependencies.push(crate_dir.to_path_buf());
for entry in std::fs::read_dir(crate_dir)? {
let entry = entry?;
if entry.path().extension().map_or(false, |e| e == "rs") {
dependencies.push(entry.path());
}
}
Ok(())
}
fn default_config() -> cbindgen::Config {
cbindgen::Config {
pragma_once: true,
include_version: true,
namespaces: Some(vec!["sixtyfps".into(), "cbindgen_private".into()]),
line_length: 100,
tab_width: 4,
// Note: we might need to switch to C if we need to generate bindings for language that needs C headers
language: cbindgen::Language::Cxx,
cpp_compat: true,
documentation: true,
export: cbindgen::ExportConfig {
rename: [
("Callback".into(), "private_api::CallbackHelper".into()),
("VoidArg".into(), "void".into()),
("KeyEventArg".into(), "KeyEvent".into()),
("PointerEventArg".into(), "PointerEvent".into()),
("PointArg".into(), "Point".into()),
]
.iter()
.cloned()
.collect(),
..Default::default()
},
defines: [
("target_pointer_width = 64".into(), "SIXTYFPS_TARGET_64".into()),
("target_pointer_width = 32".into(), "SIXTYFPS_TARGET_32".into()),
]
.iter()
.cloned()
.collect(),
..Default::default()
}
}
fn gen_item_declarations(items: &[&str]) -> String {
format!(
r#"
namespace sixtyfps::private_api {{
#define SIXTYFPS_DECL_ITEM(ItemName) \
extern const cbindgen_private::ItemVTable ItemName##VTable; \
extern SIXTYFPS_DLL_IMPORT const cbindgen_private::ItemVTable* sixtyfps_get_##ItemName##VTable();
extern "C" {{
{}
}}
#undef SIXTYFPS_DECL_ITEM
}}
"#,
items
.iter()
.map(|item_name| format!("SIXTYFPS_DECL_ITEM({});", item_name))
.collect::<Vec<_>>()
.join("\n")
)
}
fn gen_corelib(
root_dir: &Path,
include_dir: &Path,
dependencies: &mut Vec<PathBuf>,
) -> anyhow::Result<()> {
let mut config = default_config();
let items = [
"Rectangle",
"BorderRectangle",
"ImageItem",
"ClippedImage",
"TouchArea",
"FocusScope",
"Flickable",
"Text",
"Path",
"WindowItem",
"TextInput",
"Clip",
"BoxShadow",
"Rotate",
"Opacity",
];
config.export.include = [
"ComponentVTable",
"Slice",
"WindowRcOpaque",
"PropertyAnimation",
"EasingCurve",
"TextHorizontalAlignment",
"TextVerticalAlignment",
"TextOverflow",
"TextWrap",
"ImageFit",
"FillRule",
"MouseCursor",
"StandardButtonKind",
"DialogButtonRole",
"PointerEventKind",
"PointerEventButton",
"PointerEvent",
]
.iter()
.chain(items.iter())
.map(|x| x.to_string())
.collect();
config.export.exclude = [
"SharedString",
"SharedVector",
"ImageInner",
"Image",
"Color",
"PathData",
"PathElement",
"Brush",
"sixtyfps_new_path_elements",
"sixtyfps_new_path_events",
"Property",
"Slice",
"PropertyHandleOpaque",
"Callback",
"sixtyfps_property_listener_scope_evaluate",
"sixtyfps_property_listener_scope_is_dirty",
"PropertyTrackerOpaque",
"CallbackOpaque",
"WindowRc",
"VoidArg",
"KeyEventArg",
"PointerEventArg",
"PointArg",
"Point",
"sixtyfps_color_brighter",
"sixtyfps_color_darker",
"sixtyfps_image_size",
"sixtyfps_image_path",
"TimerMode", // included in generated_public.h
"IntSize", // included in generated_public.h
]
.iter()
.map(|x| x.to_string())
.collect();
let mut crate_dir = root_dir.to_owned();
crate_dir.extend(["internal", "core"].iter());
ensure_cargo_rerun_for_crate(&crate_dir, dependencies)?;
let mut string_config = config.clone();
string_config.export.exclude = vec!["SharedString".into()];
string_config.export.body.insert(
"Slice".to_owned(),
" const T &operator[](int i) const { return ptr[i]; }".to_owned(),
);
cbindgen::Builder::new()
.with_config(string_config)
.with_src(crate_dir.join("string.rs"))
.with_src(crate_dir.join("slice.rs"))
.with_after_include("namespace sixtyfps { struct SharedString; }")
.generate()
.context("Unable to generate bindings for sixtyfps_string_internal.h")?
.write_to_file(include_dir.join("sixtyfps_string_internal.h"));
cbindgen::Builder::new()
.with_config(config.clone())
.with_src(crate_dir.join("sharedvector.rs"))
.with_after_include("namespace sixtyfps { template<typename T> struct SharedVector; }")
.generate()
.context("Unable to generate bindings for sixtyfps_sharedvector_internal.h")?
.write_to_file(include_dir.join("sixtyfps_sharedvector_internal.h"));
let mut properties_config = config.clone();
properties_config.export.exclude.clear();
properties_config.export.include.push("StateInfo".into());
properties_config
.export
.pre_body
.insert("StateInfo".to_owned(), " using Instant = uint64_t;".into());
properties_config.structure.derive_eq = true;
properties_config.structure.derive_neq = true;
cbindgen::Builder::new()
.with_config(properties_config)
.with_src(crate_dir.join("properties.rs"))
.with_src(crate_dir.join("callbacks.rs"))
.with_after_include("namespace sixtyfps { class Color; class Brush; }")
.generate()
.context("Unable to generate bindings for sixtyfps_properties_internal.h")?
.write_to_file(include_dir.join("sixtyfps_properties_internal.h"));
for (rust_types, extra_excluded_types, internal_header) in [
(
vec![
"ImageInner",
"Image",
"Size",
"sixtyfps_image_size",
"sixtyfps_image_path",
"SharedPixelBuffer",
"SharedImageBuffer",
],
vec!["Color"],
"sixtyfps_image_internal.h",
),
(
vec!["Color", "sixtyfps_color_brighter", "sixtyfps_color_darker"],
vec![],
"sixtyfps_color_internal.h",
),
(
vec![
"PathData",
"PathElement",
"sixtyfps_new_path_elements",
"sixtyfps_new_path_events",
],
vec![],
"sixtyfps_pathdata_internal.h",
),
(
vec!["Brush", "LinearGradient", "GradientStop"],
vec!["Color"],
"sixtyfps_brush_internal.h",
),
]
.iter()
{
let mut special_config = config.clone();
special_config.export.include = rust_types.iter().map(|s| s.to_string()).collect();
special_config.export.exclude = [
"sixtyfps_visit_item_tree",
"sixtyfps_windowrc_drop",
"sixtyfps_windowrc_clone",
"sixtyfps_windowrc_show",
"sixtyfps_windowrc_hide",
"sixtyfps_windowrc_get_scale_factor",
"sixtyfps_windowrc_set_scale_factor",
"sixtyfps_windowrc_free_graphics_resources",
"sixtyfps_windowrc_set_focus_item",
"sixtyfps_windowrc_set_component",
"sixtyfps_windowrc_show_popup",
"sixtyfps_new_path_elements",
"sixtyfps_new_path_events",
"sixtyfps_color_brighter",
"sixtyfps_color_darker",
"sixtyfps_image_size",
"sixtyfps_image_path",
"IntSize",
]
.iter()
.filter(|exclusion| !rust_types.iter().any(|inclusion| inclusion == *exclusion))
.chain(extra_excluded_types.iter())
.map(|s| s.to_string())
.collect();
special_config.enumeration = cbindgen::EnumConfig {
derive_tagged_enum_copy_assignment: true,
derive_tagged_enum_copy_constructor: true,
derive_tagged_enum_destructor: true,
derive_helper_methods: true,
private_default_tagged_enum_constructor: true,
..Default::default()
};
special_config.structure.derive_eq = true;
special_config.structure.derive_neq = true;
// Put the rust type in a deeper "types" namespace, so the use of same type in for example generated
// Property<> fields uses the public `sixtyfps::Blah` type
special_config.namespaces =
Some(vec!["sixtyfps".into(), "cbindgen_private".into(), "types".into()]);
cbindgen::Builder::new()
.with_config(special_config)
.with_src(crate_dir.join("graphics.rs"))
.with_src(crate_dir.join("graphics/color.rs"))
.with_src(crate_dir.join("graphics/path.rs"))
.with_src(crate_dir.join("graphics/brush.rs"))
.with_src(crate_dir.join("graphics/image.rs"))
.with_src(crate_dir.join("animations.rs"))
// .with_src(crate_dir.join("input.rs"))
.with_src(crate_dir.join("item_rendering.rs"))
.with_src(crate_dir.join("window.rs"))
.generate()
.with_context(|| format!("Unable to generate bindings for {}", internal_header))?
.write_to_file(include_dir.join(internal_header));
}
// Generate a header file with some public API (enums, etc.)
let mut public_config = config.clone();
public_config.namespaces = Some(vec!["sixtyfps".into()]);
public_config.export.item_types = vec![cbindgen::ItemType::Enums, cbindgen::ItemType::Structs];
public_config.export.include = vec!["TimerMode".into(), "IntSize".into()];
public_config.export.exclude.clear();
public_config.export.body.insert(
"IntSize".to_owned(),
r#"
/// Compares this IntSize with \a other and returns true if they are equal; false otherwise.
bool operator==(const IntSize &other) const = default;"#
.to_owned(),
);
cbindgen::Builder::new()
.with_config(public_config)
.with_src(crate_dir.join("timers.rs"))
.with_src(crate_dir.join("graphics.rs"))
.with_after_include(format!(
r"
/// This macro expands to the to the numeric value of the major version of SixtyFPS you're
/// developing against. For example if you're using version 1.5.2, this macro will expand to 1.
#define SIXTYFPS_VERSION_MAJOR {}
/// This macro expands to the to the numeric value of the minor version of SixtyFPS you're
/// developing against. For example if you're using version 1.5.2, this macro will expand to 5.
#define SIXTYFPS_VERSION_MINOR {}
/// This macro expands to the to the numeric value of the patch version of SixtyFPS you're
/// developing against. For example if you're using version 1.5.2, this macro will expand to 2.
#define SIXTYFPS_VERSION_PATCH {}
",
env!("CARGO_PKG_VERSION_MAJOR"),
env!("CARGO_PKG_VERSION_MINOR"),
env!("CARGO_PKG_VERSION_PATCH"),
))
.generate()
.context("Unable to generate bindings for sixtyfps_generated_public.h")?
.write_to_file(include_dir.join("sixtyfps_generated_public.h"));
config.export.body.insert(
"ItemTreeNode".to_owned(),
" constexpr ItemTreeNode(Item_Body x) : item {x} {}
constexpr ItemTreeNode(DynamicTree_Body x) : dynamic_tree{x} {}"
.to_owned(),
);
config.export.body.insert(
"CachedRenderingData".to_owned(),
" constexpr CachedRenderingData() : cache_index{}, cache_generation{} {}".to_owned(),
);
config.export.body.insert(
"EasingCurve".to_owned(),
" constexpr EasingCurve() : tag(Tag::Linear), cubic_bezier{{0,0,1,1}} {}
constexpr explicit EasingCurve(EasingCurve::Tag tag, float a, float b, float c, float d) : tag(tag), cubic_bezier{{a,b,c,d}} {}".into()
);
config.export.body.insert(
"LayoutInfo".to_owned(),
" inline LayoutInfo merge(const LayoutInfo &other) const;
friend inline LayoutInfo operator+(const LayoutInfo &a, const LayoutInfo &b) { return a.merge(b); }
friend bool operator==(const LayoutInfo&, const LayoutInfo&) = default;".into(),
);
config.export.body.insert(
"StandardListViewItem".to_owned(),
"friend bool operator==(const StandardListViewItem&, const StandardListViewItem&) = default;".into(),
);
config
.export
.body
.insert("Flickable".to_owned(), " inline Flickable(); inline ~Flickable();".into());
config.export.pre_body.insert("FlickableDataBox".to_owned(), "struct FlickableData;".into());
config.export.include.push("StandardListViewItem".into());
cbindgen::Builder::new()
.with_config(config)
.with_src(crate_dir.join("lib.rs"))
.with_include("sixtyfps_config.h")
.with_include("vtable.h")
.with_include("sixtyfps_string.h")
.with_include("sixtyfps_sharedvector.h")
.with_include("sixtyfps_properties.h")
.with_include("sixtyfps_callbacks.h")
.with_include("sixtyfps_color.h")
.with_include("sixtyfps_image.h")
.with_include("sixtyfps_pathdata.h")
.with_include("sixtyfps_brush.h")
.with_include("sixtyfps_generated_public.h")
.with_after_include(
r"
namespace sixtyfps {
namespace private_api { class WindowRc; }
namespace cbindgen_private {
using sixtyfps::private_api::WindowRc;
using namespace vtable;
struct KeyEvent; struct PointerEvent;
using private_api::Property;
using private_api::PathData;
using private_api::Point;
}
}",
)
.with_trailer(gen_item_declarations(&items))
.generate()
.expect("Unable to generate bindings")
.write_to_file(include_dir.join("sixtyfps_internal.h"));
Ok(())
}
fn gen_backend_qt(
root_dir: &Path,
include_dir: &Path,
dependencies: &mut Vec<PathBuf>,
) -> anyhow::Result<()> {
let mut config = default_config();
let items = [
"NativeButton",
"NativeSpinBox",
"NativeCheckBox",
"NativeSlider",
"NativeGroupBox",
"NativeLineEdit",
"NativeScrollView",
"NativeStandardListViewItem",
"NativeComboBox",
"NativeComboBoxPopup",
"NativeTabWidget",
"NativeTab",
];
config.export.include = items.iter().map(|x| x.to_string()).collect();
config.export.body.insert(
"NativeStyleMetrics".to_owned(),
" inline NativeStyleMetrics(); inline ~NativeStyleMetrics();".to_owned(),
);
let mut crate_dir = root_dir.to_owned();
crate_dir.extend(["internal", "backends", "qt"].iter());
ensure_cargo_rerun_for_crate(&crate_dir, dependencies)?;
cbindgen::Builder::new()
.with_config(config)
.with_crate(crate_dir)
.with_include("sixtyfps_internal.h")
.with_trailer(gen_item_declarations(&items))
.generate()
.context("Unable to generate bindings for sixtyfps_qt_internal.h")?
.write_to_file(include_dir.join("sixtyfps_qt_internal.h"));
Ok(())
}
fn gen_backend(
root_dir: &Path,
include_dir: &Path,
dependencies: &mut Vec<PathBuf>,
) -> anyhow::Result<()> {
let config = default_config();
let mut crate_dir = root_dir.to_owned();
crate_dir.extend(["api", "cpp"].iter());
ensure_cargo_rerun_for_crate(&crate_dir, dependencies)?;
cbindgen::Builder::new()
.with_config(config)
.with_crate(crate_dir)
.with_header("#include <sixtyfps_internal.h>")
.generate()
.context("Unable to generate bindings for sixtyfps_backend_internal.h")?
.write_to_file(include_dir.join("sixtyfps_backend_internal.h"));
Ok(())
}
fn gen_interpreter(
root_dir: &Path,
include_dir: &Path,
dependencies: &mut Vec<PathBuf>,
) -> anyhow::Result<()> {
let mut config = default_config();
// Avoid Value, just export ValueOpaque.
config.export.exclude = IntoIterator::into_iter([
"Value",
"ValueType",
"PropertyDescriptor",
"Diagnostic",
"PropertyDescriptor",
])
.map(String::from)
.collect();
let mut crate_dir = root_dir.to_owned();
crate_dir.extend(["internal", "interpreter"].iter());
ensure_cargo_rerun_for_crate(&crate_dir, dependencies)?;
// Generate a header file with some public API (enums, etc.)
let mut public_config = config.clone();
public_config.namespaces = Some(vec!["sixtyfps".into(), "interpreter".into()]);
public_config.export.item_types = vec![cbindgen::ItemType::Enums, cbindgen::ItemType::Structs];
public_config.export.exclude = IntoIterator::into_iter([
"ComponentCompilerOpaque",
"ComponentDefinitionOpaque",
"ModelAdaptorVTable",
"StructIteratorOpaque",
"ComponentInstance",
"StructIteratorResult",
"ValueOpaque",
"StructOpaque",
"ModelNotifyOpaque",
])
.map(String::from)
.collect();
cbindgen::Builder::new()
.with_config(public_config)
.with_crate(crate_dir.clone())
.generate()
.context("Unable to generate bindings for sixtyfps_interpreter_generated_public.h")?
.write_to_file(include_dir.join("sixtyfps_interpreter_generated_public.h"));
cbindgen::Builder::new()
.with_config(config)
.with_crate(crate_dir)
.with_include("sixtyfps_internal.h")
.with_include("sixtyfps_interpreter_generated_public.h")
.with_after_include(
r"
namespace sixtyfps::cbindgen_private {
struct Value;
using sixtyfps::interpreter::ValueType;
using sixtyfps::interpreter::PropertyDescriptor;
using sixtyfps::interpreter::Diagnostic;
}",
)
.generate()
.context("Unable to generate bindings for sixtyfps_interpreter_internal.h")?
.write_to_file(include_dir.join("sixtyfps_interpreter_internal.h"));
Ok(())
}
/// Generate the headers.
/// `root_dir` is the root directory of the sixtyfps git repo
/// `include_dir` is the output directory
/// Returns the list of all paths that contain dependencies to the generated output. If you call this
/// function from build.rs, feed each entry to stdout prefixed with `cargo:rerun-if-changed=`.
pub fn gen_all(root_dir: &Path, include_dir: &Path) -> anyhow::Result<Vec<PathBuf>> {
proc_macro2::fallback::force(); // avoid a abort if panic=abort is set
std::fs::create_dir_all(include_dir).context("Could not create the include directory")?;
let mut deps = Vec::new();
gen_corelib(root_dir, include_dir, &mut deps)?;
gen_backend_qt(root_dir, include_dir, &mut deps)?;
gen_backend(root_dir, include_dir, &mut deps)?;
gen_interpreter(root_dir, include_dir, &mut deps)?;
Ok(deps)
}

View file

@ -0,0 +1,25 @@
# Copyright © SixtyFPS GmbH <info@sixtyfps.io>
# SPDX-License-Identifier: (GPL-3.0-only OR LicenseRef-SixtyFPS-commercial)
@PACKAGE_INIT@
get_filename_component(_IMPORT_PREFIX "${CMAKE_CURRENT_LIST_FILE}" PATH)
get_filename_component(_IMPORT_PREFIX "${_IMPORT_PREFIX}" PATH)
get_filename_component(_IMPORT_PREFIX "${_IMPORT_PREFIX}" PATH)
get_filename_component(_IMPORT_PREFIX "${_IMPORT_PREFIX}" PATH)
if(_IMPORT_PREFIX STREQUAL "/")
set(_IMPORT_PREFIX "")
endif()
add_library(sixtyfps-cpp-shared SHARED IMPORTED)
set_target_properties(sixtyfps-cpp-shared PROPERTIES @SIXTYFPS_LIB_PROPERTIES@)
add_executable(SixtyFPS::sixtyfps-compiler IMPORTED GLOBAL)
set_target_properties(SixtyFPS::sixtyfps-compiler PROPERTIES IMPORTED_LOCATION "${_IMPORT_PREFIX}/@CMAKE_INSTALL_BINDIR@/sixtyfps-compiler${CMAKE_EXECUTABLE_SUFFIX}")
set(_IMPORT_PREFIX)
include("${CMAKE_CURRENT_LIST_DIR}/SixtyFPSTargets.cmake")
include("${CMAKE_CURRENT_LIST_DIR}/SixtyFPSMacro.cmake")
set(SIXTYFPS_STYLE @SIXTYFPS_STYLE_DEFAULT@ CACHE STRING "The SixtyFPS widget style")

View file

@ -0,0 +1,41 @@
# Copyright © SixtyFPS GmbH <info@sixtyfps.io>
# SPDX-License-Identifier: (GPL-3.0-only OR LicenseRef-SixtyFPS-commercial)
function(SIXTYFPS_TARGET_60_SOURCES target)
foreach (it IN ITEMS ${ARGN})
get_filename_component(_60_BASE_NAME ${it} NAME_WE)
get_filename_component(_60_ABSOLUTE ${it} REALPATH BASE_DIR ${CMAKE_CURRENT_SOURCE_DIR})
if(CMAKE_GENERATOR STREQUAL "Ninja")
# this code is inspired from the llvm source
# https://github.com/llvm/llvm-project/blob/a00290ed10a6b4e9f6e9be44ceec367562f270c6/llvm/cmake/modules/TableGen.cmake#L13
file(RELATIVE_PATH _60_BASE_NAME_REL ${CMAKE_BINARY_DIR} ${CMAKE_CURRENT_BINARY_DIR}/${_60_BASE_NAME})
add_custom_command(
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${_60_BASE_NAME}.h
COMMAND SixtyFPS::sixtyfps-compiler ${_60_ABSOLUTE}
-o ${_60_BASE_NAME_REL}.h --depfile ${_60_BASE_NAME_REL}.d
--style ${SIXTYFPS_STYLE}
DEPENDS SixtyFPS::sixtyfps-compiler ${_60_ABSOLUTE}
COMMENT "Generating ${_60_BASE_NAME}.h"
DEPFILE ${CMAKE_CURRENT_BINARY_DIR}/${_60_BASE_NAME}.d
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
)
else(CMAKE_GENERATOR STREQUAL "Ninja")
get_filename_component(_60_DIR ${_60_ABSOLUTE} DIRECTORY )
file(GLOB ALL_60S "${_60_DIR}/*.60")
add_custom_command(
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${_60_BASE_NAME}.h
COMMAND SixtyFPS::sixtyfps-compiler ${_60_ABSOLUTE}
-o ${CMAKE_CURRENT_BINARY_DIR}/${_60_BASE_NAME}.h
--style ${SIXTYFPS_STYLE}
DEPENDS SixtyFPS::sixtyfps-compiler ${_60_ABSOLUTE} ${ALL_60S}
COMMENT "Generating ${_60_BASE_NAME}.h"
)
endif(CMAKE_GENERATOR STREQUAL "Ninja")
target_sources(${target} PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/${_60_BASE_NAME}.h)
endforeach()
target_include_directories(${target} PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
endfunction()

4
api/cpp/docs/.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
# Copyright © 2021 SixtyFPS GmbH <info@sixtyfps.io>
# SPDX-License-Identifier: (GPL-3.0-only OR LicenseRef-SixtyFPS-commercial)
Pipfile.lock

20
api/cpp/docs/Pipfile Normal file
View file

@ -0,0 +1,20 @@
# Copyright © 2021 SixtyFPS GmbH <info@sixtyfps.io>
# SPDX-License-Identifier: (GPL-3.0-only OR LicenseRef-SixtyFPS-commercial)
[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true
[dev-packages]
[packages]
breathe = "*"
sphinx-rtd-theme = "*"
sphinx = "*"
exhale = "*"
myst_parser = "*"
sphinx-markdown-tables = "*"
[requires]
python_version = "3.8"

14
api/cpp/docs/_static/theme_tweak.css vendored Normal file
View file

@ -0,0 +1,14 @@
.wy-table-responsive table td, .wy-table-responsive table th {
white-space: normal;
}
.wy-table-responsive {
margin-bottom: 24px;
max-width: 100%;
overflow: visible;
}
img.logo {
max-height: 70px;
border-radius: 12% !important;
}

6
api/cpp/docs/_templates/layout.html vendored Normal file
View file

@ -0,0 +1,6 @@
{% extends "!layout.html" %}
{% block scripts %}
{{ super() }}
{% include "../../../../docs/resources/sixtyfps-docs-preview.html" %}
{% include "../../../../docs/resources/sixtyfps-docs-highlight.html" %}
{% endblock %}

112
api/cpp/docs/cmake.md Normal file
View file

@ -0,0 +1,112 @@
# Installing or Building with CMake
SixtyFPS comes with a CMake integration that automates the compilation step of the `.60` markup language files and
offers a CMake target for convenient linkage.
*Note*: We recommend using the Ninja generator of CMake for the most efficient build and `.60` dependency tracking.
You can select the CMake Ninja backend by passing `-GNinja` or setting the `CMAKE_GENERATOR` environment variable to `Ninja`.
## Binary Packages
We also provide binary packages of SixtyFPS for use with C++, which eliminates the need to have Rust installed in your development environment.
You can download one of our pre-built binaries for Linux or Windows on x86-64 architectures:
1. Open <https://github.com/sixtyfpsui/sixtyfps/releases>
2. Click on the latest release
3. From "Assets" download either `sixtyfps-cpp-XXX-Linux-x86_64.tar.gz` for a Linux archive
or `sixtyfps-cpp-XXX-win64.exe` for a Windows installer. ("XXX" refers to the version of the latest release)
4. Uncompress the downloaded archive or run the installer.
After extracting the artifact or running the installer, you can place the `lib` sub-directory into your `CMAKE_PREFIX_PATH` and `find_package(SixtyFPS)` should succeed in locating the package.
In the next section you will learn how to use the installed library in your application
and load `.60` UI files.
## Building from Sources
The recommended and most flexible way to use the C++ API is to build SixtyFPS from sources.
First you need to install the prerequisites:
* Install Rust by following the [Rust Getting Started Guide](https://www.rust-lang.org/learn/get-started). If you already
have Rust installed, make sure that it's at least version 1.56 or newer. You can check which version you have installed
by running `rustc --version`. Once this is done, you should have the ```rustc``` compiler and the ```cargo``` build system installed in your path.
* **[cmake](https://cmake.org/download/)** (3.19 or newer)
* A C++ compiler that supports C++20 (e.g., **MSVC 2019 16.6** on Windows)
You can include SixtyFPS in your CMake project using CMake's [`FetchContent`](https://cmake.org/cmake/help/latest/module/FetchContent.html) feature.
Insert the following snippet into your `CMakeLists.txt` to make CMake download the latest release, compile it and make the CMake integration available:
```cmake
include(FetchContent)
FetchContent_Declare(
SixtyFPS
GIT_REPOSITORY https://github.com/sixtyfpsui/sixtyfps.git
GIT_TAG v0.1.6
SOURCE_SUBDIR api/cpp
)
FetchContent_MakeAvailable(SixtyFPS)
```
If you prefer to treat SixtyFPS as an external CMake package, then you can also build SixtyFPS from source like a regular
CMake project, install it into a prefix directory of your choice and use `find_package(SixtyFPS)` in your `CMakeLists.txt`.
### Features
The SixtyFPS run-time library supports different features that can be toggled. You might want to enable a feature that is
not enabled by default but that is revelant for you, or you may want to disable a feature that you know you do not need and
therefore reduce the size of the resulting library.
The CMake configure step offers CMake options for various feature that are all prefixed with `SIXTYFPS_FEATURE_`. For example
you can enable support for the Wayland windowing system on Linux by enabling the `SIXTYFPS_FEATURE_WAYLAND` feature. There are
different ways of toggling CMake options. For example on the command line using the `-D` parameter:
`cmake -DSIXTYFPS_FEATURE_WAYLAND=ON ...`
Alternatively, after the configure step you can use `cmake-gui` or `ccmake` on the build directory for a list of all features
and their description.
This works when compiling SixtyFPS as a package, using `cmake --build` and `cmake --install`, or when including SixtyFPS
using `FetchContent`.
### Cross-compiling
It is possible to cross-compile SixtyFPS to a different target architecture when building with CMake. In order to complete
that, you need to make sure that your CMake setup is ready for cross-compilation. You can find more information about
how to set this up in the [upstream CMake documentation](https://cmake.org/cmake/help/latest/manual/cmake-toolchains.7.html#cross-compiling).
If you are building against a Yocto SDK, it is sufficient to source the SDK's environment setup file.
Since SixtyFPS is implemented using the Rust programming language, you need to determine which Rust target
matches the target architecture that you're compiling to. Please consult the [upstream Rust documentation](https://doc.rust-lang.org/nightly/rustc/platform-support.html) to find the correct target name. Now you need to install the Rust toolchain:
```sh
rustup target add <target-name>
```
Then you're ready to invoke CMake and you need to add `-DRust_CARGO_TARGET=<target name>` to the CMake command line.
This ensures that the SixtyFPS library is built for the correct architecture.
For example if you are building against an embedded Linux Yocto SDK targeting an ARM64 board, the following commands
show how to compile:
Install the Rust targe toolchain once:
<!-- cSpell:disable -->
```sh
rustup target add aarch64-unknown-linux-gnu
```
<!-- cSpell:enable -->
Set up the environment and build:
<!-- cSpell:disable -->
```sh
. /path/to/yocto/sdk/environment-setup-cortexa53-crypto-poky-linux
cd sixtyfps
mkdir build
cd build
cmake -DRust_CARGO_TARGET=aarch64-unknown-linux-gnu -DCMAKE_INSTALL_PREFIX=/sixtyfps/install/path ..
cmake --build .
cmake --install .
```

124
api/cpp/docs/conf.py Normal file
View file

@ -0,0 +1,124 @@
# Copyright © 2021 SixtyFPS GmbH <info@sixtyfps.io>
# SPDX-License-Identifier: (GPL-3.0-only OR LicenseRef-SixtyFPS-commercial)
# Configuration file for the Sphinx documentation builder.
#
# This file only contains a selection of the most common options. For a full
# list see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Path setup --------------------------------------------------------------
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))
import textwrap
# -- Project information -----------------------------------------------------
project = "SixtyFPS C++"
copyright = "2021, info@sixtyfps.io"
author = "info@sixtyfps.io"
# The full version, including alpha/beta/rc tags
version = "0.2.0"
cpp_index_common_prefix = ["sixtyfps::", "sixtyfps::interpreter::"]
# -- General configuration ---------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = ["breathe", "myst_parser", "exhale", "sphinx_markdown_tables"]
breathe_projects = {"SixtyFPS": "./docs/xml"}
breathe_default_project = "SixtyFPS"
exhale_args = {
"containmentFolder": "./api",
"rootFileName": "library_root.rst",
"rootFileTitle": "C++ API Reference",
"afterTitleDescription": textwrap.dedent(
"""
The following sections present the C++ API Reference. All types are
within the :ref:`sixtyfps<namespace_sixtyfps>` namespace and are accessible by including
the :code:`sixtyfps.h` header file.
If you choose to load :code:`.60` files dynamically at run-time, then
you can use the classes in :ref:`sixtyfps::interpreter<namespace_sixtyfps__interpreter>`, starting at
:cpp:class:`sixtyfps::interpreter::ComponentCompiler`. You need to include
the :code:`sixtyfps_interpreter.h` header file.
"""
),
"doxygenStripFromPath": "..",
"createTreeView": True,
"exhaleExecutesDoxygen": True,
"exhaleDoxygenStdin": """INPUT = ../../api/cpp/include generated_include
EXCLUDE_SYMBOLS = sixtyfps::cbindgen_private* sixtyfps::private_api* vtable* SIXTYFPS_DECL_ITEM
EXCLUDE = ../../api/cpp/include/vtable.h ../../api/sixtyfps-cpp/include/sixtyfps_testing.h
ENABLE_PREPROCESSING = YES
PREDEFINED += DOXYGEN
WARN_AS_ERROR = YES""",
}
# Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"]
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = [
"_build",
"html/_static/collapsible-lists/LICENSE.md",
"Thumbs.db",
".DS_Store",
"markdown/tutorial",
"markdown/building.md",
"markdown/development.md",
"markdown/install_qt.md",
"markdown/README.md",
"README.md",
]
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = "sphinx_rtd_theme"
html_theme_options = {"collapse_navigation": False}
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ["_static"]
html_show_sourcelink = False
html_logo = "logo.drawio.svg"
myst_enable_extensions = [
"html_image",
]
# Annotate h1/h2 elements with anchors
myst_heading_anchors = 2
rst_epilog = """
.. |ListView| replace:: :code:`ListView`
.. _ListView: ../markdown/widgets.html#listview
.. |Repetition| replace:: :code:`for` - :code:`in`
.. _Repetition: ../markdown/langref.html#repetition
"""
def setup(app):
app.add_css_file("theme_tweak.css")

View file

@ -0,0 +1,60 @@
# Migrating from Older Versions
The C++ library is versioned according to the principles of [Semantic Versioning](https://semver.org). We define that the left-most non-zero component of the version is the major version, followed by the minor and optionally patch version. That means releases in the "0.y.z" series treat changes in "y" as a major release, which can contain incompatible API changes, while changes in just "z" are minor. For example the release 0.1.6 is fully backwards compatible to 0.1.5, but it contains new functionality. The release 0.2.0 however is a new major version compared to 0.1.x and may contain API incompatible changes.
This guide lists all API incompatible changes between major versions and describes how you can migrate your application's source code.
## Migrating from Version 0.1.x to 0.2.0
In version 0.2.0 we have increased the minimum version of C++. You need to have a C++ compiler installed that supports C++ 20 or newer.
If you are building SixtyFPS from source, you need to make sure that your Rust installation is up-to-date. If you have installed Rust using `rustup`, then you can upgrade to the latest Version of Rust by running `rustup update`.
### Models
`Model::row_data` returns now a `std::optional<ModelData>` and can thus be used with indices that are out of bounds.
This also means that `Model`s must handle invalid indices and may not crash when a invalid index is passed in.
Old code:
```cpp
float value = another_model->row_data(2);
do_something(value)
```
New code:
```cpp
// `another_model` is a model that contains floats.
std::optional<float> value = another_model->row_data(2);
if (value.has_value()) {
do_something(*value);
} else {
// row index 2 is out of bounds
}
```
### C++ Interpreter API
#### Callbacks
Callbacks declared in `.60` markup can be invoked from C++ using {cpp:func}`sixtyfps::interpreter::ComponentInstance::invoke_callback()` or {cpp:func}`sixtyfps::interpreter::ComponentInstance::invoke_global_callback()`. The arguments to the callback at invocation time used to require the use of `sixtyfps::Slice` type. This was changed to use the C++ 20 [`std::span`](https://en.cppreference.com/w/cpp/container/span) type, for easier passing.
Old code:
```cpp
sixtyfps::Value args[] = { SharedString("Hello"), 42. };
instance->invoke_callback("foo", sixtyfps::Slice{ args, 2 });
```
New code:
```cpp
sixtyfps::Value args[] = { SharedString("Hello"), 42. };
instance->invoke_callback("foo", args);
```
#### Models
The `Value::Type::Array` has been replaced by `Value::Type::Model`

View file

@ -0,0 +1,122 @@
# Generated code
The SixtyFPS compiler called by the build system will generate a header file for the root `.60`
file. This header file will contain a `class` with the same name as the component.
This class will have the following public member functions:
* A `create` constructor function and a destructor.
* A `show` function, which will show the component on the screen. Note that in order to render
and react to user input, it's still necessary to spin the event loop, by calling {cpp:func}`sixtyfps::run_event_loop()`
or using the convenience `fun` function in this class.
* A `hide` function, which de-registers the component from the windowing system.
* A `window` function that provides access to the {cpp:class}`sixtyfps::Window`, allow for further customization
towards the windowing system.
* A `run` convenience function, which will show the component and starts the event loop.
* for each properties:
* A getter `get_<property_name>` returning the property type.
* A setter `set_<property_name>` taking the new value of the property by const reference
* for each callbacks:
* `invoke_<callback_name>` function which takes the callback argument as parameter and call the callback.
* `on_<callback_name>` function which takes a functor as an argument and sets the callback handler
for this callback. the functor must accept the type parameter of the callback
* A `global` function, to provide access to any exported global singletons.
The class is instantiated with the `create` function, which returns the type wrapped in {cpp:class}`sixtyfps::ComponentHandle`.
This is a smart pointer that owns the actual instance and keeps it alive as long as at least one {cpp:class}`sixtyfps::ComponentHandle`
is in scope, similar to `std::shared_ptr<T>`.
For more complex UIs it is common to supply data in the form of an abstract data model, that is used with
[`for` - `in`](markdown/langref.md#repetition) repetitions or [`ListView`](markdown/widgets.md#listview) elements in the `.60` language.
All models in C++ are sub-classes of the {cpp:class}`sixtyfps::Model` and you can sub-class it yourself. For convenience,
the {cpp:class}`sixtyfps::VectorModel` provides an implementation that is backed by a `std::vector<T>`.
## Example
Let's assume we have this code in our `.60` file
```60
SampleComponent := Window {
property<int> counter;
property<string> user_name;
callback hello;
// ... maybe more elements here
}
```
This will generate a header with the following contents (edited for documentation purpose)
```cpp
#include <array>
#include <limits>
#include <sixtyfps.h>
class SampleComponent {
public:
/// Constructor function
inline auto create () -> sixtyfps::ComponentHandle<MainWindow>;
/// Destructor
inline ~SampleComponent ();
/// Show this component, and runs the event loop
inline void run () const;
/// Show the window that renders this component. Call `sixtyfps::run_event_loop()`
/// to continuously render the contents and react to user input.
inline void show () const;
/// Hide the window that renders this component.
inline void hide () const;
/// Getter for the `counter` property
inline int get_counter () const;
/// Setter for the `counter` property
inline void set_counter (const int &value) const;
/// Getter for the `user_name` property
inline sixtyfps::SharedString get_user_name () const;
/// Setter for the `user_name` property
inline void set_user_name (const sixtyfps::SharedString &value) const;
/// Call this function to call the `hello` callback
inline void invoke_hello () const;
/// Sets the callback handler for the `hello` callback.
template<typename Functor> inline void on_hello (Functor && callback_handler) const;
// Returns a reference to a global singleton that's exported.
inline template<typename T>
const T &global() const;
private:
/// private fields omitted
};
```
## Global Singletons
In `.60` files it is possible to declare [singletons that are globally available](markdown/langref.md#global-singletons).
You can access them from to your C++ code by exporting them and using the `global()` getter function in the
C++ class generated for your entry component. Each global singleton creates a class that has getter/setter functions
for properties and callbacks, similar to API that's created for your `.60` component, as demonstrated in the previous section.
For example the following `.60` markup defines a global `Logic` singleton that's also exported:
```60,ignore
export global Logic := {
callback to_uppercase(string) -> string;
}
```
If this were used together with the `SampleComponent` from the previous section, then you can access it
like this:
```cpp
auto app = SampleComponent::create();
// ...
app->global<Logic>().on_to_uppercase([](SharedString str) -> SharedString {
std::string arg(str);
std::transform(arg.begin(), arg.end(), arg.begin(), toupper);
return SharedString(arg);
});
```

View file

@ -0,0 +1,6 @@
.. Copyright © SixtyFPS GmbH <info@sixtyfps.io>
.. SPDX-License-Identifier: (GPL-3.0-only OR LicenseRef-SixtyFPS-commercial)
===========
Index (C++)
===========

View file

@ -0,0 +1,81 @@
# Getting Started
Once SixtyFPS is built, you can use it in your CMake application or library target in two steps:
1. Associate the `.60` files that you'd like to use by calling the `sixtyfps_target_60_sources` cmake command. The first parameter is
your application (or library) CMake target, and the parameters following are the names of the `.60` files. This will result in the
`.60` files to be compiled into C++ source code.
2. The generated C++ source code also needs the SixtyFPS run-time library. This dependency is satisfied by linking `SixtyFPS::SixtyFPS`
into your target with the `target_link_libraries` command.
A typical example looks like this:
```cmake
cmake_minimum_required(VERSION 3.19)
project(my_application LANGUAGES CXX)
# Note: Use find_package(SixtyFPS) instead of the following three commands,
# if you prefer the package approach.
include(FetchContent)
FetchContent_Declare(
SixtyFPS
GIT_REPOSITORY https://github.com/sixtyfpsui/sixtyfps.git
GIT_TAG v0.1.6
SOURCE_SUBDIR api/cpp
)
FetchContent_MakeAvailable(SixtyFPS)
add_executable(my_application main.cpp)
sixtyfps_target_60_sources(my_application my_application_ui.60)
target_link_libraries(my_application PRIVATE SixtyFPS::SixtyFPS)
```
Suppose `my_application_ui.60` was a "Hello World" like this:
```60,ignore
HelloWorld := Window {
width: 400px;
height: 400px;
// Declare an alias that exposes the label's text property to C++
property my_label <=> label.text;
label := Text {
y: parent.width / 2;
x: parent.x + 200px;
text: "Hello, world";
color: blue;
}
}
```
then you can use the following code in you `main` function to show the [`Window`](markdown/builtin_elements.md#window)
and change the text:
```cpp
#include "my_application_ui.h"
int main(int argc, char **argv)
{
auto hello_world = HelloWorld::create();
hello_world->set_my_label("Hello from C++");
// Show the window and spin the event loop until the window is closed.
hello_world->run();
return 0;
}
```
This works because the SixtyFPS compiler translated `my_application_ui.60` to C++ code, in the `my_application_ui.h`
header file. That generated code has a C++ class that corresponds to the `HelloWorld` element and has API to create
the ui, read or write properties or set callbacks. You can learn more about how this API looks like in general in the
[](generated_code.md) section.
## Tutorial
For an in-depth walk-through, you may be interested in reading our walk-through <a href="../tutorial/cpp">SixtyFPS Memory Game Tutorial Tutorial</a>.
It will guide you through the `.60` mark-up language and the C++ API by building a little memory game.
## Template
You can clone the [Template Repository](https://github.com/sixtyfpsui/sixtyfps-cpp-template) repository with
the code of a minimal C++ application using SixtyFPS that can be used as a starting point to your program.

96
api/cpp/docs/index.rst Normal file
View file

@ -0,0 +1,96 @@
.. Copyright © SixtyFPS GmbH <info@sixtyfps.io>
.. SPDX-License-Identifier: (GPL-3.0-only OR LicenseRef-SixtyFPS-commercial)
.. SixtyFPS C++ documentation master file
Welcome to SixtyFPS C++'s documentation!
========================================
.. toctree::
:maxdepth: 2
:hidden:
:caption: Getting Started
cmake.md
First Steps <getting_started.md>
.. toctree::
:maxdepth: 2
:hidden:
:caption: C++ / .60 Integration
Overview <overview.md>
Type Mapping to C++ <types.md>
Example Generated Code <generated_code.md>
.. toctree::
:maxdepth: 2
:hidden:
:caption: Reference
api/library_root
genindex
language.rst
markdown/debugging_techniques.md
cpp_migration.md
.. image:: https://github.com/sixtyfpsui/sixtyfps/workflows/CI/badge.svg
:target: https://github.com/sixtyfpsui/sixtyfps/actions
:alt: GitHub CI Build Status
.. image:: https://img.shields.io/github/discussions/sixtyfpsui/sixtyfps
:target: https://github.com/sixtyfpsui/sixtyfps/discussions
:alt: GitHub Discussions
`SixtyFPS <https://sixtyfps.io/>`_ is a toolkit to efficiently develop fluid graphical user interfaces for any display: embedded devices and desktop applications.
SixtyFPS C++ is the C++ API to interact with a SixtyFPS UI from C++.
The .60 Markup Language
=======================
SixtyFPS comes with a markup language that is specifically designed for user interfaces. This language provides a
powerful way to describe graphical elements, their placement, and the flow of data through the different states. It is a familiar syntax to describe the hierarchy
of elements and property bindings. Here's the obligatory "Hello World":
.. code-block:: 60-no-preview
HelloWorld := Window {
width: 400px;
height: 400px;
Text {
y: parent.width / 2;
x: parent.x + 200px;
text: "Hello, world";
color: blue;
}
}
Check out the `language reference <markdown/langref.html>`_ for more details.
Architecture
============
An application is composed of the business logic written in C++ and the `.60` user interface design markup, which
is compiled to native code.
.. image:: https://sixtyfps.io/resources/architecture.drawio.svg
:alt: Architecture Overview
Developing
==========
You can create and edit `.60` files using our `SixtyFPS Visual Studio Code Extension <https://marketplace.visualstudio.com/items?itemName=SixtyFPS.sixtyfps-vscode>`_,
which features syntax highlighting and live design preview.
For a quick edit and preview cycle, you can also use the :code:`sixtyfps-viewer` command line tool, which can be installed using :code:`cargo install sixtyfps-viewer`,
if you have `Cargo <https://marketplace.visualstudio.com/items?itemName=SixtyFPS.sixtyfps-vscode>`_ installed.
In the next section you will learn how to install the SixtyFPS C++ library and the CMake build system integration.

12
api/cpp/docs/language.rst Normal file
View file

@ -0,0 +1,12 @@
.. Copyright © SixtyFPS GmbH <info@sixtyfps.io>
.. SPDX-License-Identifier: (GPL-3.0-only OR LicenseRef-SixtyFPS-commercial)
The .60 UI Design Language
==========================
.. toctree::
Language Reference<markdown/langref.md>
markdown/builtin_elements.md
markdown/widgets.md
markdown/layouting.md

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 10 KiB

View file

@ -0,0 +1,3 @@
Copyright © SixtyFPS GmbH <info@sixtyfps.io>
SPDX-License-Identifier: CC-BY-ND-4.0

56
api/cpp/docs/overview.md Normal file
View file

@ -0,0 +1,56 @@
# Overview
The following two sections explain how you can integrate your `.60` designs into your
C++ application. The entry point is a `.60` file that contains your primary component
that you instantiate from C++.
There are two ways in that you can instantiate your `.60` designs in your C++ application,
either by compiling them ahead of time or by dynamically loading them at run-time.
Once instantiated you feed data into it, for example by setting properties, populating
data models or setting up callbacks that are invoked when the user activates certain elements.
## Compiled `.60` designs
You can choose to compile a `.60` file to C++, which provides the best performance
and lowest memory consumption.
The `sixtyfps_target_60_sources` cmake command makes the translation automatic
and [generated code](generated_code.md) has an API that allows setting and getting
property values, etc. That API will use types from the {ref}`sixtyfps <namespace_sixtyfps>`
namespace, for example {cpp:class}`sixtyfps::SharedString` or {cpp:class}`sixtyfps::Color`.
## Run-time interpreted `.60` designs
Instead of compiling `.60` designs to C++, you can also choose to dynamically load `.60`
files at run-time. This is slower than compiling them ahead of time and requires more memory,
however it provides more flexibility in your application design.
The entry point to loading a `.60` file is the {cpp:class}`sixtyfps::interpreter::ComponentCompiler`
class in the {ref}`sixtyfps::interpreter <namespace_sixtyfps__interpreter>` namespace.
With the help of {cpp:class}`sixtyfps::interpreter::ComponentCompiler` you create a {cpp:class}`sixtyfps::interpreter::ComponentDefinition`,
which provides you with information about properties and callbacks that are common to all instances. The
{cpp:func}`sixtyfps::interpreter::ComponentDefinition::create()` function creates new instances, which
are wrapped in {cpp:class}`sixtyfps::ComponentHandle`. This is a smart pointer that owns the actual instance
and keeps it alive as long as at least one {cpp:class}`sixtyfps::ComponentHandle` is in scope, similar to `std::shared_ptr<T>`.
All property values in `.60` are mapped to {cpp:class}`sixtyfps::interpreter::Value` in C++. This is a
polymorphic data type that can hold different kinds of values, such as numbers, strings or even data models.
For more complex UIs it is common to supply data in the form of an abstract data model, that is used with
[`for` - `in`](markdown/langref.md#repetition) repetitions or [`ListView`](markdown/widgets.md#listview) elements in the `.60` language.
All models in C++ with the interpreter API are sub-classes of the {cpp:class}`sixtyfps::Model` where the template
parameter is {cpp:class}`sixtyfps::interpreter::Value`. Therefore to provide your own data model, you can subclass
`sixtyfps::Model<sixtyfps::interpreter::Value>`.
In `.60` files it is possible to declare [singletons that are globally available](markdown/langref.md#global-singletons).
You can access them from to your C++ code by exporting them and using the getter and setter functions on
{cpp:class}`sixtyfps::interpreter::ComponentInstance` to change properties and callbacks:
1. {cpp:func}`sixtyfps::interpreter::ComponentInstance::set_global_property()`
1. {cpp:func}`sixtyfps::interpreter::ComponentInstance::get_global_property()`
1. {cpp:func}`sixtyfps::interpreter::ComponentInstance::set_global_callback()`
1. {cpp:func}`sixtyfps::interpreter::ComponentInstance::invoke_global_callback()`

43
api/cpp/docs/types.md Normal file
View file

@ -0,0 +1,43 @@
# Type Mappings
The types used for properties in `.60` design markup each translate to specific types in C++.
The follow table summarizes the entire mapping:
| `.60` Type | C++ Type | Note |
| --- | --- | --- |
| `int` | `int` | |
| `float` | `float` | |
| `bool` | `bool` | |
| `string` | [`sixtyfps::SharedString`](api/structsixtyfps_1_1_shared_string.html) | A reference-counted string type that uses UTF-8 encoding and can be easily converted to a std::string_view or a const char *. |
| `color` | [`sixtyfps::Color`](api/classsixtyfps_1_1_color.html) | |
| `brush` | [`sixtyfps::Brush`](api/classsixtyfps_1_1_brush.html) | |
| `image` | [`sixtyfps::Image`](api/structsixtyfps_1_1_image.html) | |
| `physical_length` | `float` | The unit are physical pixels. |
| `length` | `float` | At run-time, logical lengths are automatically translated to physical pixels using the device pixel ratio. |
| `duration` | `std::int64_t` | At run-time, durations are always represented as signed 64-bit integers with millisecond precision. |
| `angle` | `float` | The value in degrees. |
| structure | A `class` of the same name | The order of the data member are in the lexicographic order of their name |
## Structures
For user-defined structures in the .60 code, a `class` of the same name is generated with data member
in lexicographic order.
For example, if you have this structure in the .60 file
```60,ignore
export struct MyStruct := {
foo: int,
bar: string,
}
```
It would result in the following type being generated:
```cpp
class MyStruct {
public:
sixtyfps::SharedString bar;
int foo;
};
```

1020
api/cpp/include/sixtyfps.h Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,116 @@
// Copyright © SixtyFPS GmbH <info@sixtyfps.io>
// SPDX-License-Identifier: (GPL-3.0-only OR LicenseRef-SixtyFPS-commercial)
#pragma once
#include <string_view>
#include "sixtyfps_color.h"
#include "sixtyfps_brush_internal.h"
#include "sixtyfps_string.h"
namespace sixtyfps {
namespace private_api {
using cbindgen_private::types::GradientStop;
/// \private
/// LinearGradientBrush represents a gradient for a brush that is a linear sequence of color stops,
/// that are aligned at a specific angle.
class LinearGradientBrush
{
public:
/// Constructs an empty linear gradient with no color stops.
LinearGradientBrush() = default;
/// Constructs a new linear gradient with the specified \a angle. The color stops will be
/// constructed from the stops array pointed to be \a firstStop, with the length \a stopCount.
LinearGradientBrush(float angle, const GradientStop *firstStop, int stopCount)
: inner(make_linear_gradient(angle, firstStop, stopCount))
{
}
/// Returns the linear gradient's angle in degrees.
float angle() const
{
// The gradient's first stop is a fake stop to store the angle
return inner[0].position;
}
/// Returns the number of gradient stops.
int stopCount() const { return int(inner.size()) - 1; }
/// Returns a pointer to the first gradient stop; undefined if the gradient has not stops.
const GradientStop *stopsBegin() const { return inner.begin() + 1; }
/// Returns a pointer past the last gradient stop. The returned pointer cannot be dereferenced,
/// it can only be used for comparison.
const GradientStop *stopsEnd() const { return inner.end(); }
private:
cbindgen_private::types::LinearGradientBrush inner;
friend class sixtyfps::Brush;
static SharedVector<private_api::GradientStop>
make_linear_gradient(float angle, const GradientStop *firstStop, int stopCount)
{
SharedVector<private_api::GradientStop> gradient;
gradient.push_back({ Color::from_argb_encoded(0).inner, angle });
for (int i = 0; i < stopCount; ++i, ++firstStop)
gradient.push_back(*firstStop);
return gradient;
}
};
}
/// Brush is used to declare how to fill or outline shapes, such as rectangles, paths or text. A
/// brush is either a solid color or a linear gradient.
class Brush
{
public:
/// Constructs a new brush that is a transparent color.
Brush() : Brush(Color {}) { }
/// Constructs a new brush that is of color \a color.
Brush(const Color &color) : data(Inner::SolidColor(color.inner)) { }
/// \private
/// Constructs a new brush that is the gradient \a gradient.
Brush(const private_api::LinearGradientBrush &gradient)
: data(Inner::LinearGradient(gradient.inner))
{
}
/// Returns the color of the brush. If the brush is a gradient, this function returns the color
/// of the first stop.
inline Color color() const;
/// Returns true if \a a is equal to \a b. If \a a holds a color, then \a b must also hold a
/// color that is identical to \a a's color. If it holds a gradient, then the gradients must be
/// identical. Returns false if the brushes differ in what they hold or their respective color
/// or gradient are not equal.
friend bool operator==(const Brush &a, const Brush &b) { return a.data == b.data; }
/// Returns false if \a is not equal to \a b; true otherwise.
friend bool operator!=(const Brush &a, const Brush &b) { return a.data != b.data; }
private:
using Tag = cbindgen_private::types::Brush::Tag;
using Inner = cbindgen_private::types::Brush;
Inner data;
};
Color Brush::color() const
{
Color result;
switch (data.tag) {
case Tag::SolidColor: {
result.inner = data.solid_color._0;
break;
}
case Tag::LinearGradient:
if (data.linear_gradient._0.size() > 1) {
result.inner = data.linear_gradient._0[1].color;
}
break;
}
return result;
}
}

View file

@ -0,0 +1,112 @@
// Copyright © SixtyFPS GmbH <info@sixtyfps.io>
// SPDX-License-Identifier: (GPL-3.0-only OR LicenseRef-SixtyFPS-commercial)
#pragma once
#include <tuple>
#include "sixtyfps_properties_internal.h"
namespace sixtyfps::private_api {
/// A Callback stores a function pointer with no parameters and no return value.
/// It's possible to set that pointer via set_handler() and it can be invoked via call(). This is
/// used to implement callbacks in the `.60` language.
template<typename = void()>
struct Callback;
/// A Callback stores a function pointer with \a Arg parameters and a return value of type \a Ret.
/// It's possible to set that pointer via set_handler() and it can be invoked via call(). This is
/// used to implement callbacks in the `.60` language.
template<typename Ret, typename... Arg>
struct Callback<Ret(Arg...)>
{
/// Constructs an empty callback that contains no handler.
Callback() { cbindgen_private::sixtyfps_callback_init(&inner); }
/// Destructs the callback.
~Callback() { cbindgen_private::sixtyfps_callback_drop(&inner); }
Callback(const Callback &) = delete;
Callback(Callback &&) = delete;
Callback &operator=(const Callback &) = delete;
/// Sets a new handler \a binding for this callback, that will be invoked when call() is called.
template<typename F>
void set_handler(F binding) const
{
cbindgen_private::sixtyfps_callback_set_handler(
&inner,
[](void *user_data, const void *arg, void *ret) {
*reinterpret_cast<Ret *>(ret) =
std::apply(*reinterpret_cast<F *>(user_data),
*reinterpret_cast<const Tuple *>(arg));
},
new F(std::move(binding)),
[](void *user_data) { delete reinterpret_cast<F *>(user_data); });
}
/// Invokes a previously set handler with the parameters \a arg and returns the return value of
/// the handler.
Ret call(const Arg &...arg) const
{
Ret r {};
Tuple tuple { arg... };
cbindgen_private::sixtyfps_callback_call(&inner, &tuple, &r);
return r;
}
private:
using Tuple = std::tuple<Arg...>;
cbindgen_private::CallbackOpaque inner;
};
/// A Callback stores a function pointer with \a Arg parameters and no return value.
/// It's possible to set that pointer via set_handler() and it can be invoked via call(). This is
/// used to implement callbacks in the `.60` language.
template<typename... Arg>
struct Callback<void(Arg...)>
{
/// Constructs an empty callback that contains no handler.
Callback() { cbindgen_private::sixtyfps_callback_init(&inner); }
/// Destructs the callback.
~Callback() { cbindgen_private::sixtyfps_callback_drop(&inner); }
Callback(const Callback &) = delete;
Callback(Callback &&) = delete;
Callback &operator=(const Callback &) = delete;
/// Sets a new handler \a binding for this callback, that will be invoked when call() is called.
template<typename F>
void set_handler(F binding) const
{
cbindgen_private::sixtyfps_callback_set_handler(
&inner,
[](void *user_data, const void *arg, void *) {
std::apply(*reinterpret_cast<F *>(user_data),
*reinterpret_cast<const Tuple *>(arg));
},
new F(std::move(binding)),
[](void *user_data) { delete reinterpret_cast<F *>(user_data); });
}
/// Invokes a previously set handler with the parameters \a arg.
void call(const Arg &...arg) const
{
Tuple tuple { arg... };
cbindgen_private::sixtyfps_callback_call(&inner, &tuple, reinterpret_cast<void *>(0x1));
}
private:
using Tuple = std::tuple<Arg...>;
cbindgen_private::CallbackOpaque inner;
};
template<typename A, typename R>
struct CallbackSignatureHelper
{
using Result = R(A);
};
template<typename R>
struct CallbackSignatureHelper<void, R>
{
using Result = R();
};
template<typename A, typename R = void>
using CallbackHelper = Callback<typename CallbackSignatureHelper<A, R>::Result>;
} // namespace sixtyfps

View file

@ -0,0 +1,232 @@
// Copyright © SixtyFPS GmbH <info@sixtyfps.io>
// SPDX-License-Identifier: (GPL-3.0-only OR LicenseRef-SixtyFPS-commercial)
#pragma once
#include "sixtyfps_color_internal.h"
#include "sixtyfps_properties.h"
#include <stdint.h>
namespace sixtyfps {
namespace private_api {
class LinearGradientBrush;
}
class Color;
/// RgbaColor stores the red, green, blue and alpha components of a color
/// with the precision of the template parameter T. For example if T is float,
/// the values are normalized between 0 and 1. If T is uint8_t, they values range
/// is 0 to 255.
template<typename T>
struct RgbaColor
{
/// The alpha component.
T alpha;
/// The red component.
T red;
/// The green component.
T green;
/// The blue component.
T blue;
/// Creates a new RgbaColor instance from a given color. This template function is
/// specialized and thus implemented for T == uint8_t and T == float.
RgbaColor(const Color &col);
};
/// Color represents a color in the SixtyFPS run-time, represented using 8-bit channels for
/// red, green, blue and the alpha (opacity).
class Color
{
public:
/// Default constructs a new color that is entirely transparent.
Color() { inner.red = inner.green = inner.blue = inner.alpha = 0; }
/// Constructs a new color from the given RgbaColor<uint8_t> \a col.
Color(const RgbaColor<uint8_t> &col)
{
inner.red = col.red;
inner.green = col.green;
inner.blue = col.blue;
inner.alpha = col.alpha;
}
/// Constructs a new color from the given RgbaColor<float> \a col.
Color(const RgbaColor<float> &col)
{
inner.red = uint8_t(col.red * 255);
inner.green = uint8_t(col.green * 255);
inner.blue = uint8_t(col.blue * 255);
inner.alpha = uint8_t(col.alpha * 255);
}
/// Construct a color from an integer encoded as `0xAARRGGBB`
static Color from_argb_encoded(uint32_t argb_encoded)
{
Color col;
col.inner.red = (argb_encoded >> 16) & 0xff;
col.inner.green = (argb_encoded >> 8) & 0xff;
col.inner.blue = argb_encoded & 0xff;
col.inner.alpha = (argb_encoded >> 24) & 0xff;
return col;
}
/// Returns `(alpha, red, green, blue)` encoded as uint32_t.
uint32_t as_argb_encoded() const
{
return (uint32_t(inner.red) << 16) | (uint32_t(inner.green) << 8) | uint32_t(inner.blue)
| (uint32_t(inner.alpha) << 24);
}
/// Construct a color from the alpha, red, green and blue color channel parameters.
static Color from_argb_uint8(uint8_t alpha, uint8_t red, uint8_t green, uint8_t blue)
{
Color col;
col.inner.alpha = alpha;
col.inner.red = red;
col.inner.green = green;
col.inner.blue = blue;
return col;
}
/// Construct a color from the red, green and blue color channel parameters. The alpha
/// channel will have the value 255.
static Color from_rgb_uint8(uint8_t red, uint8_t green, uint8_t blue)
{
return from_argb_uint8(255, red, green, blue);
}
/// Construct a color from the alpha, red, green and blue color channel parameters.
static Color from_argb_float(float alpha, float red, float green, float blue)
{
Color col;
col.inner.alpha = uint8_t(alpha * 255);
col.inner.red = uint8_t(red * 255);
col.inner.green = uint8_t(green * 255);
col.inner.blue = uint8_t(blue * 255);
return col;
}
/// Construct a color from the red, green and blue color channel parameters. The alpha
/// channel will have the value 255.
static Color from_rgb_float(float red, float green, float blue)
{
return Color::from_argb_float(1.0, red, green, blue);
}
/// Converts this color to an RgbaColor struct for easy destructuring.
inline RgbaColor<uint8_t> to_argb_uint() const;
/// Converts this color to an RgbaColor struct for easy destructuring.
inline RgbaColor<float> to_argb_float() const;
/// Returns the red channel of the color as u8 in the range 0..255.
uint8_t red() const { return inner.red; }
/// Returns the green channel of the color as u8 in the range 0..255.
uint8_t green() const { return inner.green; }
/// Returns the blue channel of the color as u8 in the range 0..255.
uint8_t blue() const { return inner.blue; }
/// Returns the alpha channel of the color as u8 in the range 0..255.
uint8_t alpha() const { return inner.alpha; }
/// Returns a new version of this color that has the brightness increased
/// by the specified factor. This is done by converting the color to the HSV
/// color space and multiplying the brightness (value) with (1 + factor).
/// The result is converted back to RGB and the alpha channel is unchanged.
/// So for example `brighter(0.2)` will increase the brightness by 20%, and
/// calling `brighter(-0.5)` will return a color that's 50% darker.
inline Color brighter(float factor) const;
/// Returns a new version of this color that has the brightness decreased
/// by the specified factor. This is done by converting the color to the HSV
/// color space and dividing the brightness (value) by (1 + factor). The
/// result is converted back to RGB and the alpha channel is unchanged.
/// So for example `darker(0.3)` will decrease the brightness by 30%.
inline Color darker(float factor) const;
/// Returns true if \a lhs has the same values for the individual color channels as \a rhs;
/// false otherwise.
friend bool operator==(const Color &lhs, const Color &rhs) = default;
/// Writes the \a color to the specified \a stream and returns a reference to the
/// stream.
friend std::ostream &operator<<(std::ostream &stream, const Color &color)
{
// Cast to uint32_t to avoid the components being interpreted as char.
return stream << "argb(" << uint32_t(color.inner.alpha) << ", " << uint32_t(color.inner.red)
<< ", " << uint32_t(color.inner.green) << ", " << uint32_t(color.inner.blue)
<< ")";
}
#if !defined(DOXYGEN)
// FIXME: we need this to create GradientStop
operator const cbindgen_private::types::Color &() const { return inner; }
#endif
private:
cbindgen_private::types::Color inner;
friend class private_api::LinearGradientBrush;
friend class Brush;
};
inline Color Color::brighter(float factor) const
{
Color result;
cbindgen_private::types::sixtyfps_color_brighter(&inner, factor, &result.inner);
return result;
}
inline Color Color::darker(float factor) const
{
Color result;
cbindgen_private::types::sixtyfps_color_darker(&inner, factor, &result.inner);
return result;
}
/// Constructs a new RgbaColor<uint8_t> from the color \a color.
template<>
inline RgbaColor<uint8_t>::RgbaColor(const Color &color)
{
red = color.red();
green = color.green();
blue = color.blue();
alpha = color.alpha();
}
/// Constructs a new RgbaColor<float> from the color \a color.
template<>
inline RgbaColor<float>::RgbaColor(const Color &color)
{
red = float(color.red()) / 255.f;
green = float(color.green()) / 255.f;
blue = float(color.blue()) / 255.f;
alpha = float(color.alpha()) / 255.f;
}
RgbaColor<uint8_t> Color::to_argb_uint() const
{
return RgbaColor<uint8_t>(*this);
}
RgbaColor<float> Color::to_argb_float() const
{
return RgbaColor<float>(*this);
}
namespace private_api {
template<>
inline void
Property<Color>::set_animated_value(const Color &new_value,
const cbindgen_private::PropertyAnimation &animation_data) const
{
cbindgen_private::sixtyfps_property_set_animated_value_color(&inner, value, new_value,
&animation_data);
}
} // namespace private_api
} // namespace sixtyfps

View file

@ -0,0 +1,26 @@
// Copyright © SixtyFPS GmbH <info@sixtyfps.io>
// SPDX-License-Identifier: (GPL-3.0-only OR LicenseRef-SixtyFPS-commercial)
#pragma once
#include <cstdint>
#if UINTPTR_MAX == 0xFFFFFFFF
# define SIXTYFPS_TARGET_32
#elif UINTPTR_MAX == 0xFFFFFFFFFFFFFFFFu
# define SIXTYFPS_TARGET_64
#endif
#if !defined(DOXYGEN)
# if defined(_MSC_VER)
# define SIXTYFPS_DLL_IMPORT __declspec(dllimport)
# elif defined(__GNUC__)
# if defined(_WIN32) || defined(_WIN64)
# define SIXTYFPS_DLL_IMPORT __declspec(dllimport)
# else
# define SIXTYFPS_DLL_IMPORT __attribute__((visibility("default")))
# endif
# else
# define SIXTYFPS_DLL_IMPORT
# endif
#endif // !defined(DOXYGEN)

View file

@ -0,0 +1,62 @@
// Copyright © SixtyFPS GmbH <info@sixtyfps.io>
// SPDX-License-Identifier: (GPL-3.0-only OR LicenseRef-SixtyFPS-commercial)
#pragma once
#include <string_view>
#include "sixtyfps_generated_public.h"
#include "sixtyfps_image_internal.h"
#include "sixtyfps_string.h"
#include "sixtyfps_sharedvector.h"
namespace sixtyfps {
/// An image type that can be displayed by the Image element
struct Image
{
public:
Image() : data(Data::None()) { }
/// Load an image from an image file
static Image load_from_path(const SharedString &file_path)
{
Image img;
img.data = Data::AbsoluteFilePath(file_path);
return img;
}
/*
static Image load_from_argb(int width, int height, const SharedVector<uint32_t> &data) {
Image img;
img.data = Data::EmbeddedRgbaImage(width, height, data);
return img;
}
*/
/// Returns the size of the Image in pixels.
IntSize size() const { return cbindgen_private::types::sixtyfps_image_size(&data); }
/// Returns the path of the image on disk, if it was constructed via Image::load_from_path().
std::optional<sixtyfps::SharedString> path() const
{
if (auto *str = cbindgen_private::types::sixtyfps_image_path(&data)) {
return *str;
} else {
return {};
}
}
/// Returns true if \a a refers to the same image as \a b; false otherwise.
friend bool operator==(const Image &a, const Image &b) { return a.data == b.data; }
/// Returns false if \a a refers to the same image as \a b; true otherwise.
friend bool operator!=(const Image &a, const Image &b) { return a.data != b.data; }
/// \private
explicit Image(cbindgen_private::types::Image inner) : data(inner) { }
private:
using Tag = cbindgen_private::types::ImageInner::Tag;
using Data = cbindgen_private::types::Image;
Data data;
};
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,72 @@
// Copyright © SixtyFPS GmbH <info@sixtyfps.io>
// SPDX-License-Identifier: (GPL-3.0-only OR LicenseRef-SixtyFPS-commercial)
#pragma once
#include <initializer_list>
#include <string_view>
#include "sixtyfps_pathdata_internal.h"
// The C++ code generator assumes that enums are in the cbindgen_private namespace
namespace sixtyfps::cbindgen_private {
using cbindgen_private::types::PathEvent;
}
namespace sixtyfps::private_api {
using cbindgen_private::types::PathArcTo;
using cbindgen_private::types::PathCubicTo;
using cbindgen_private::types::PathElement;
using cbindgen_private::types::PathEvent;
using cbindgen_private::types::PathLineTo;
using cbindgen_private::types::PathMoveTo;
using cbindgen_private::types::PathQuadraticTo;
using cbindgen_private::types::Point;
struct PathData
{
public:
using Tag = cbindgen_private::types::PathData::Tag;
PathData() : data(Data::None()) { }
PathData(const PathElement *firstElement, size_t count)
: data(Data::Elements(elements_from_array(firstElement, count)))
{
}
PathData(const PathEvent *firstEvent, size_t event_count, const Point *firstCoordinate,
size_t coordinate_count)
: data(events_from_array(firstEvent, event_count, firstCoordinate, coordinate_count))
{
}
PathData(const SharedString &commands)
: data(cbindgen_private::types::PathData::Commands(commands))
{
}
friend bool operator==(const PathData &a, const PathData &b) = default;
private:
static SharedVector<PathElement> elements_from_array(const PathElement *firstElement,
size_t count)
{
SharedVector<PathElement> tmp;
sixtyfps_new_path_elements(&tmp, firstElement, count);
return tmp;
}
static cbindgen_private::types::PathData events_from_array(const PathEvent *firstEvent,
size_t event_count,
const Point *firstCoordinate,
size_t coordinate_count)
{
SharedVector<PathEvent> events;
SharedVector<Point> coordinates;
sixtyfps_new_path_events(&events, &coordinates, firstEvent, event_count, firstCoordinate,
coordinate_count);
return Data::Events(events, coordinates);
}
using Data = cbindgen_private::types::PathData;
Data data;
};
}

View file

@ -0,0 +1,295 @@
// Copyright © SixtyFPS GmbH <info@sixtyfps.io>
// SPDX-License-Identifier: (GPL-3.0-only OR LicenseRef-SixtyFPS-commercial)
#pragma once
#include <string_view>
#include <memory>
namespace sixtyfps::cbindgen_private {
struct PropertyAnimation;
}
#include "sixtyfps_properties_internal.h"
namespace sixtyfps::private_api {
using cbindgen_private::StateInfo;
inline void sixtyfps_property_set_animated_binding_helper(
const cbindgen_private::PropertyHandleOpaque *handle, void (*binding)(void *, int32_t *),
void *user_data, void (*drop_user_data)(void *),
const cbindgen_private::PropertyAnimation *animation_data,
cbindgen_private::PropertyAnimation (*transition_data)(void *, uint64_t *))
{
cbindgen_private::sixtyfps_property_set_animated_binding_int(
handle, binding, user_data, drop_user_data, animation_data, transition_data);
}
inline void sixtyfps_property_set_animated_binding_helper(
const cbindgen_private::PropertyHandleOpaque *handle, void (*binding)(void *, float *),
void *user_data, void (*drop_user_data)(void *),
const cbindgen_private::PropertyAnimation *animation_data,
cbindgen_private::PropertyAnimation (*transition_data)(void *, uint64_t *))
{
cbindgen_private::sixtyfps_property_set_animated_binding_float(
handle, binding, user_data, drop_user_data, animation_data, transition_data);
}
inline void sixtyfps_property_set_animated_binding_helper(
const cbindgen_private::PropertyHandleOpaque *handle, void (*binding)(void *, Color *),
void *user_data, void (*drop_user_data)(void *),
const cbindgen_private::PropertyAnimation *animation_data,
cbindgen_private::PropertyAnimation (*transition_data)(void *, uint64_t *))
{
cbindgen_private::sixtyfps_property_set_animated_binding_color(
handle, binding, user_data, drop_user_data, animation_data, transition_data);
}
inline void sixtyfps_property_set_animated_binding_helper(
const cbindgen_private::PropertyHandleOpaque *handle, void (*binding)(void *, Brush *),
void *user_data, void (*drop_user_data)(void *),
const cbindgen_private::PropertyAnimation *animation_data,
cbindgen_private::PropertyAnimation (*transition_data)(void *, uint64_t *))
{
cbindgen_private::sixtyfps_property_set_animated_binding_brush(
handle, binding, user_data, drop_user_data, animation_data, transition_data);
}
template<typename T>
struct Property
{
Property() { cbindgen_private::sixtyfps_property_init(&inner); }
~Property() { cbindgen_private::sixtyfps_property_drop(&inner); }
Property(const Property &) = delete;
Property(Property &&) = delete;
Property &operator=(const Property &) = delete;
explicit Property(const T &value) : value(value)
{
cbindgen_private::sixtyfps_property_init(&inner);
}
/* Should it be implicit?
void operator=(const T &value) {
set(value);
}*/
void set(const T &value) const
{
if (this->value != value) {
this->value = value;
cbindgen_private::sixtyfps_property_set_changed(&inner, &this->value);
}
}
const T &get() const
{
cbindgen_private::sixtyfps_property_update(&inner, &value);
return value;
}
template<typename F>
void set_binding(F binding) const
{
cbindgen_private::sixtyfps_property_set_binding(
&inner,
[](void *user_data, void *value) {
*reinterpret_cast<T *>(value) = (*reinterpret_cast<F *>(user_data))();
},
new F(binding), [](void *user_data) { delete reinterpret_cast<F *>(user_data); },
nullptr, nullptr);
}
inline void set_animated_value(const T &value,
const cbindgen_private::PropertyAnimation &animation_data) const;
template<typename F>
inline void
set_animated_binding(F binding, const cbindgen_private::PropertyAnimation &animation_data) const
{
private_api::sixtyfps_property_set_animated_binding_helper(
&inner,
[](void *user_data, T *value) {
*reinterpret_cast<T *>(value) = (*reinterpret_cast<F *>(user_data))();
},
new F(binding), [](void *user_data) { delete reinterpret_cast<F *>(user_data); },
&animation_data, nullptr);
}
template<typename F, typename Trans>
inline void set_animated_binding_for_transition(F binding, Trans animation) const
{
struct UserData
{
F binding;
Trans animation;
};
private_api::sixtyfps_property_set_animated_binding_helper(
&inner,
[](void *user_data, T *value) {
*reinterpret_cast<T *>(value) =
reinterpret_cast<UserData *>(user_data)->binding();
},
new UserData { binding, animation },
[](void *user_data) { delete reinterpret_cast<UserData *>(user_data); }, nullptr,
[](void *user_data, uint64_t *instant) {
return reinterpret_cast<UserData *>(user_data)->animation(instant);
});
}
bool is_dirty() const { return cbindgen_private::sixtyfps_property_is_dirty(&inner); }
void mark_dirty() const { cbindgen_private::sixtyfps_property_mark_dirty(&inner); }
static void link_two_way(const Property<T> *p1, const Property<T> *p2)
{
auto value = p2->get();
cbindgen_private::PropertyHandleOpaque handle {};
if ((p2->inner._0 & 0b10) == 0b10) {
std::swap(handle, const_cast<Property<T> *>(p2)->inner);
}
auto common_property = std::make_shared<Property<T>>(handle, std::move(value));
struct TwoWayBinding
{
std::shared_ptr<Property<T>> common_property;
};
auto del_fn = [](void *user_data) { delete reinterpret_cast<TwoWayBinding *>(user_data); };
auto call_fn = [](void *user_data, void *value) {
*reinterpret_cast<T *>(value) =
reinterpret_cast<TwoWayBinding *>(user_data)->common_property->get();
};
auto intercept_fn = [](void *user_data, const void *value) {
reinterpret_cast<TwoWayBinding *>(user_data)->common_property->set(
*reinterpret_cast<const T *>(value));
return true;
};
auto intercept_binding_fn = [](void *user_data, void *value) {
cbindgen_private::sixtyfps_property_set_binding_internal(
&reinterpret_cast<TwoWayBinding *>(user_data)->common_property->inner, value);
return true;
};
cbindgen_private::sixtyfps_property_set_binding(&p1->inner, call_fn,
new TwoWayBinding { common_property },
del_fn, intercept_fn, intercept_binding_fn);
cbindgen_private::sixtyfps_property_set_binding(&p2->inner, call_fn,
new TwoWayBinding { common_property },
del_fn, intercept_fn, intercept_binding_fn);
}
/// Internal (private) constructor used by link_two_way
explicit Property(cbindgen_private::PropertyHandleOpaque inner, T value)
: inner(inner), value(std::move(value))
{
}
private:
cbindgen_private::PropertyHandleOpaque inner;
mutable T value {};
template<typename F>
friend void set_state_binding(const Property<StateInfo> &property, F binding);
};
template<>
inline void Property<int32_t>::set_animated_value(
const int32_t &new_value, const cbindgen_private::PropertyAnimation &animation_data) const
{
cbindgen_private::sixtyfps_property_set_animated_value_int(&inner, value, new_value,
&animation_data);
}
template<>
inline void
Property<float>::set_animated_value(const float &new_value,
const cbindgen_private::PropertyAnimation &animation_data) const
{
cbindgen_private::sixtyfps_property_set_animated_value_float(&inner, value, new_value,
&animation_data);
}
template<typename F>
void set_state_binding(const Property<StateInfo> &property, F binding)
{
cbindgen_private::sixtyfps_property_set_state_binding(
&property.inner,
[](void *user_data) -> int32_t { return (*reinterpret_cast<F *>(user_data))(); },
new F(binding), [](void *user_data) { delete reinterpret_cast<F *>(user_data); });
}
/// PropertyTracker allows keeping track of when properties change and lazily evaluate code
/// if necessary.
/// Once constructed, you can call evaluate() with a functor that will be invoked. Any
/// Property<T> types that have their value read from within the invoked functor or any code that's
/// reached from there are added to internal book-keeping. When after returning from evaluate(),
/// any of these accessed properties change their value, the property tracker's is_dirt() function
/// will return true.
///
/// PropertyTracker instances nest, so if during the evaluation of one tracker, another tracker's
/// evaluate() function gets called and properties from within that evaluation change their value
/// later, both tracker instances will report true for is_dirty(). If you would like to disable the
/// nesting, use the evaluate_as_dependency_root() function instead.
struct PropertyTracker
{
/// Constructs a new property tracker instance.
PropertyTracker() { cbindgen_private::sixtyfps_property_tracker_init(&inner); }
/// Destroys the property tracker.
~PropertyTracker() { cbindgen_private::sixtyfps_property_tracker_drop(&inner); }
/// The copy constructor is intentionally deleted, property trackers cannot be copied.
PropertyTracker(const PropertyTracker &) = delete;
/// The assignment operator is intentionally deleted, property trackers cannot be copied.
PropertyTracker &operator=(const PropertyTracker &) = delete;
/// Returns true if any properties accessed during the last evaluate() call have changed their
/// value since then.
bool is_dirty() const { return cbindgen_private::sixtyfps_property_tracker_is_dirty(&inner); }
/// Invokes the provided functor \a f and tracks accessed to any properties during that
/// invocation.
template<typename F>
auto evaluate(const F &f) const -> std::enable_if_t<std::is_same_v<decltype(f()), void>>
{
cbindgen_private::sixtyfps_property_tracker_evaluate(
&inner, [](void *f) { (*reinterpret_cast<const F *>(f))(); }, const_cast<F *>(&f));
}
/// Invokes the provided functor \a f and tracks accessed to any properties during that
/// invocation. Use this overload if your functor returns a value, as evaluate() will pass it on
/// and return it.
template<typename F>
auto evaluate(const F &f) const
-> std::enable_if_t<!std::is_same_v<decltype(f()), void>, decltype(f())>
{
decltype(f()) result;
this->evaluate([&] { result = f(); });
return result;
}
/// Invokes the provided functor \a f and tracks accessed to any properties during that
/// invocation.
///
/// This starts a new dependency chain and if called during the evaluation of another
/// property tracker, the outer tracker will not be notified if any accessed properties change.
template<typename F>
auto evaluate_as_dependency_root(const F &f) const
-> std::enable_if_t<std::is_same_v<decltype(f()), void>>
{
cbindgen_private::sixtyfps_property_tracker_evaluate_as_dependency_root(
&inner, [](void *f) { (*reinterpret_cast<const F *>(f))(); }, const_cast<F *>(&f));
}
/// Invokes the provided functor \a f and tracks accessed to any properties during that
/// invocation. Use this overload if your functor returns a value, as evaluate() will pass it on
/// and return it.
///
/// This starts a new dependency chain and if called during the evaluation of another
/// property tracker, the outer tracker will not be notified if any accessed properties change.
template<typename F>
auto evaluate_as_dependency_root(const F &f) const
-> std::enable_if_t<!std::is_same_v<decltype(f()), void>, decltype(f())>
{
decltype(f()) result;
this->evaluate_as_dependency_root([&] { result = f(); });
return result;
}
private:
cbindgen_private::PropertyTrackerOpaque inner;
};
} // namespace sixtyfps::private_api

View file

@ -0,0 +1,220 @@
// Copyright © SixtyFPS GmbH <info@sixtyfps.io>
// SPDX-License-Identifier: (GPL-3.0-only OR LicenseRef-SixtyFPS-commercial)
#pragma once
#include "sixtyfps_sharedvector_internal.h"
#include <atomic>
#include <algorithm>
#include <initializer_list>
namespace sixtyfps {
/// SharedVector is a vector template class similar to std::vector that's primarily used for passing
/// data in and out of the SixtyFPS run-time library. It uses implicit-sharing to make creating
/// copies cheap. Only when a function changes the vector's data, a copy is is made.
template<typename T>
struct SharedVector
{
/// Creates a new, empty vector.
SharedVector()
: inner(const_cast<SharedVectorHeader *>(reinterpret_cast<const SharedVectorHeader *>(
cbindgen_private::sixtyfps_shared_vector_empty())))
{
}
/// Creates a new vector that holds all the elements of the given std::initializer_list \a args.
SharedVector(std::initializer_list<T> args) : SharedVector()
{
auto new_array = SharedVector::with_capacity(args.size());
auto new_data = reinterpret_cast<T *>(new_array.inner + 1);
auto input_it = args.begin();
for (std::size_t i = 0; i < args.size(); ++i, ++input_it) {
new (new_data + i) T(*input_it);
new_array.inner->size++;
}
*this = std::move(new_array);
}
/// Creates a new vector that is a copy of \a other.
SharedVector(const SharedVector &other) : inner(other.inner)
{
if (inner->refcount > 0) {
++inner->refcount;
}
}
/// Destroys this vector. The underlying data is destroyed if no other
/// vector references it.
~SharedVector() { drop(); }
/// Assigns the data of \a other to this vector and returns a reference to this vector.
SharedVector &operator=(const SharedVector &other)
{
if (other.inner == inner) {
return *this;
}
drop();
inner = other.inner;
if (inner->refcount > 0) {
++inner->refcount;
}
return *this;
}
/// Move-assign's \a other to this vector and returns a reference to this vector.
SharedVector &operator=(SharedVector &&other)
{
std::swap(inner, other.inner);
return *this;
}
/// Returns a const pointer to the first element of this vector.
const T *cbegin() const { return reinterpret_cast<const T *>(inner + 1); }
/// Returns a const pointer that points past the last element of this vector. The
/// pointer cannot be dereferenced, it can only be used for comparison.
const T *cend() const { return cbegin() + inner->size; }
/// Returns a const pointer to the first element of this vector.
const T *begin() const { return cbegin(); }
/// Returns a const pointer that points past the last element of this vector. The
/// pointer cannot be dereferenced, it can only be used for comparison.
const T *end() const { return cend(); }
/// Returns a pointer to the first element of this vector.
T *begin()
{
detach(inner->size);
return reinterpret_cast<T *>(inner + 1);
}
/// Returns a pointer that points past the last element of this vector. The
/// pointer cannot be dereferenced, it can only be used for comparison.
T *end()
{
detach(inner->size);
return begin() + inner->size;
}
/// Returns the number of elements in this vector.
std::size_t size() const { return inner->size; }
/// Returns true if there are no elements on this vector; false otherwise.
bool empty() const { return inner->size == 0; }
/// This indexing operator returns a reference to the \a `index`th element of this vector.
T &operator[](std::size_t index) { return begin()[index]; }
/// This indexing operator returns a const reference to the \a `index`th element of this vector.
const T &operator[](std::size_t index) const { return begin()[index]; }
/// Returns a reference to the \a `index`th element of this vector.
const T &at(std::size_t index) const { return begin()[index]; }
/// Appends the \a value as a new element to the end of this vector.
void push_back(const T &value)
{
detach(inner->size + 1);
new (end()) T(value);
inner->size++;
}
/// Moves the \a value as a new element to the end of this vector.
void push_back(T &&value)
{
detach(inner->size + 1);
new (end()) T(std::move(value));
inner->size++;
}
/// Clears the vector and removes all elements. The capacity remains unaffected.
void clear()
{
if (inner->refcount != 1) {
*this = SharedVector();
} else {
auto b = cbegin(), e = cend();
inner->size = 0;
for (auto it = b; it < e; ++it) {
it->~T();
}
}
}
/// Returns true if the vector \a a has the same number of elements as \a b
/// and all the elements also compare equal; false otherwise.
friend bool operator==(const SharedVector &a, const SharedVector &b)
{
if (a.size() != a.size())
return false;
return std::equal(a.cbegin(), a.cend(), b.cbegin());
}
/// Returns false if the vector \a a has the same number of elements as \a b
/// and all the elements also compare equal; true otherwise.
friend bool operator!=(const SharedVector &a, const SharedVector &b) { return !(a == b); }
/// \private
std::size_t capacity() const { return inner->capacity; }
private:
void detach(std::size_t expected_capacity)
{
if (inner->refcount == 1 && expected_capacity <= inner->capacity) {
return;
}
auto new_array = SharedVector::with_capacity(expected_capacity);
auto old_data = reinterpret_cast<const T *>(inner + 1);
auto new_data = reinterpret_cast<T *>(new_array.inner + 1);
for (std::size_t i = 0; i < inner->size; ++i) {
new (new_data + i) T(old_data[i]);
new_array.inner->size++;
}
*this = std::move(new_array);
}
void drop()
{
if (inner->refcount > 0 && (--inner->refcount) == 0) {
auto b = cbegin(), e = cend();
for (auto it = b; it < e; ++it) {
it->~T();
}
cbindgen_private::sixtyfps_shared_vector_free(reinterpret_cast<uint8_t *>(inner),
sizeof(SharedVectorHeader)
+ inner->capacity * sizeof(T),
alignof(SharedVectorHeader));
}
}
static SharedVector with_capacity(std::size_t capacity)
{
auto mem = cbindgen_private::sixtyfps_shared_vector_allocate(
sizeof(SharedVectorHeader) + capacity * sizeof(T), alignof(SharedVectorHeader));
return SharedVector(new (mem) SharedVectorHeader { { 1 }, 0, capacity });
}
#if !defined(DOXYGEN)
// Unfortunately, this cannot be generated by cbindgen because std::atomic is not understood
struct SharedVectorHeader
{
std::atomic<std::intptr_t> refcount;
std::size_t size;
std::size_t capacity;
};
static_assert(alignof(T) <= alignof(SharedVectorHeader),
"Not yet supported because we would need to add padding");
SharedVectorHeader *inner;
explicit SharedVector(SharedVectorHeader *inner) : inner(inner) { }
#endif
};
#if !defined(DOXYGEN) // Hide these from Doxygen as Slice is private API
template<typename T>
bool operator==(cbindgen_private::Slice<T> a, cbindgen_private::Slice<T> b)
{
if (a.len != b.len)
return false;
return std::equal(a.ptr, a.ptr + a.len, b.ptr);
}
template<typename T>
bool operator!=(cbindgen_private::Slice<T> a, cbindgen_private::Slice<T> b)
{
return !(a != b);
}
#endif // !defined(DOXYGEN)
}

View file

@ -0,0 +1,210 @@
// Copyright © SixtyFPS GmbH <info@sixtyfps.io>
// SPDX-License-Identifier: (GPL-3.0-only OR LicenseRef-SixtyFPS-commercial)
#pragma once
#include <string_view>
#include "sixtyfps_string_internal.h"
namespace sixtyfps {
/// A string type used by the SixtyFPS run-time.
///
/// SharedString uses implicit data sharing to make it efficient to pass around copies. When
/// copying, a reference to the data is cloned, not the data itself.
///
/// The class provides constructors from std::string_view as well as the automatic conversion to
/// a std::string_view.
///
/// For convenience, it's also possible to convert a number to a string using
/// SharedString::from_number(double).
///
/// Under the hood the string data is UTF-8 encoded and it is always terminated with a null
/// character.
struct SharedString
{
/// Creates an empty default constructed string.
SharedString() { cbindgen_private::sixtyfps_shared_string_from_bytes(this, "", 0); }
/// Creates a new SharedString from the string view \a s. The underlying string data
/// is copied.
SharedString(std::string_view s)
{
cbindgen_private::sixtyfps_shared_string_from_bytes(this, s.data(), s.size());
}
/// Creates a new SharedString from the null-terminated string pointer \a s. The underlying
/// string data is copied. It is assumed that the string is UTF-8 encoded.
SharedString(const char *s) : SharedString(std::string_view(s)) { }
#if defined(__cpp_char8_t) || __cplusplus >= 202002L
/// Creates a new SharedString from the null-terminated string pointer \a s. The underlying
/// string data is copied.
SharedString(const char8_t *s) : SharedString(reinterpret_cast<const char*>(s)) { }
#endif
#ifdef __cpp_lib_char8_t
/// Creates a new SharedString from the string view \a s. The underlying string data is copied.
SharedString(std::u8string_view s)
{
cbindgen_private::sixtyfps_shared_string_from_bytes(
this, reinterpret_cast<const char*>(s.data()), s.size());
}
#endif
/// Creates a new SharedString from \a other.
SharedString(const SharedString &other)
{
cbindgen_private::sixtyfps_shared_string_clone(this, &other);
}
/// Destroys this SharedString and frees the memory if this is the last instance
/// referencing it.
~SharedString() { cbindgen_private::sixtyfps_shared_string_drop(this); }
/// Assigns \a other to this string and returns a reference to this string.
SharedString &operator=(const SharedString &other)
{
cbindgen_private::sixtyfps_shared_string_drop(this);
cbindgen_private::sixtyfps_shared_string_clone(this, &other);
return *this;
}
/// Assigns the string view \a s to this string and returns a reference to this string.
/// The underlying string data is copied. It is assumed that the string is UTF-8 encoded.
SharedString &operator=(std::string_view s)
{
cbindgen_private::sixtyfps_shared_string_drop(this);
cbindgen_private::sixtyfps_shared_string_from_bytes(this, s.data(), s.size());
return *this;
}
/// Assigns null-terminated string pointer \a s to this string and returns a reference
/// to this string. The underlying string data is copied. It is assumed that the string
/// is UTF-8 encoded.
SharedString &operator=(const char *s) { return *this = std::string_view(s); }
/// Move-assigns \a other to this SharedString instance.
SharedString &operator=(SharedString &&other)
{
std::swap(inner, other.inner);
return *this;
}
/// Provides a view to the string data. The returned view is only valid as long as at
/// least this SharedString exists.
operator std::string_view() const
{
return cbindgen_private::sixtyfps_shared_string_bytes(this);
}
/// Provides a raw pointer to the string data. The returned pointer is only valid as long as at
/// least this SharedString exists.
auto data() const -> const char *
{
return cbindgen_private::sixtyfps_shared_string_bytes(this);
}
/// Returns a pointer to the first character. It is only safe to dereference the pointer if the
/// string contains at least one character.
const char *begin() const { return data(); }
/// Returns a point past the last character of the string. It is not safe to dereference the
/// pointer, but it is suitable for comparison.
const char *end() const { return &*std::string_view(*this).end(); }
/// \return true if the string contains no characters; false otherwise.
bool empty() const { return std::string_view(*this).empty(); }
/// \return true if the string starts with the specified prefix string; false otherwise
bool starts_with(std::string_view prefix) const
{
return std::string_view(*this).substr(0, prefix.size()) == prefix;
}
/// \return true if the string ends with the specified prefix string; false otherwise
bool ends_with(std::string_view prefix) const
{
std::string_view self_view(*this);
return self_view.size() >= prefix.size()
&& self_view.compare(self_view.size() - prefix.size(), std::string_view::npos,
prefix)
== 0;
}
/// Creates a new SharedString from the given number \a n. The string representation of the
/// number uses a minimal formatting scheme: If \a n has no fractional part, the number will be
/// formatted as an integer.
///
/// For example:
/// \code
/// auto str = sixtyfps::SharedString::from_number(42); // creates "42"
/// auto str2 = sixtyfps::SharedString::from_number(100.5) // creates "100.5"
/// \endcode
static SharedString from_number(double n) { return SharedString(n); }
/// Returns true if \a a is equal to \a b; otherwise returns false.
friend bool operator==(const SharedString &a, const SharedString &b)
{
return std::string_view(a) == std::string_view(b);
}
/// Returns true if \a a is not equal to \a b; otherwise returns false.
friend bool operator!=(const SharedString &a, const SharedString &b)
{
return std::string_view(a) != std::string_view(b);
}
/// Returns true if \a a is lexicographically less than \a b; false otherwise.
friend bool operator<(const SharedString &a, const SharedString &b)
{
return std::string_view(a) < std::string_view(b);
}
/// Returns true if \a a is lexicographically less or equal than \a b; false otherwise.
friend bool operator<=(const SharedString &a, const SharedString &b)
{
return std::string_view(a) <= std::string_view(b);
}
/// Returns true if \a a is lexicographically greater than \a b; false otherwise.
friend bool operator>(const SharedString &a, const SharedString &b)
{
return std::string_view(a) > std::string_view(b);
}
/// Returns true if \a a is lexicographically greater or equal than \a b; false otherwise.
friend bool operator>=(const SharedString &a, const SharedString &b)
{
return std::string_view(a) >= std::string_view(b);
}
/// Writes the \a shared_string to the specified \a stream and returns a reference to the
/// stream.
friend std::ostream &operator<<(std::ostream &stream, const SharedString &shared_string)
{
return stream << std::string_view(shared_string);
}
/// Concatenates \a a and \a and returns the result as a new SharedString.
friend SharedString operator+(const SharedString &a, std::string_view b)
{
SharedString a2 = a;
return a2 += b;
}
/// Move-concatenates \a b to \a and returns a reference to \a a.
friend SharedString operator+(SharedString &&a, std::string_view b)
{
a += b;
return a;
}
/// Appends \a other to this string and returns a reference to this.
SharedString &operator+=(std::string_view other)
{
cbindgen_private::sixtyfps_shared_string_append(this, other.data(), other.size());
return *this;
}
private:
/// Use SharedString::from_number
explicit SharedString(double n)
{
cbindgen_private::sixtyfps_shared_string_from_number(this, n);
}
void *inner; // opaque
};
namespace private_api {
inline cbindgen_private::Slice<uint8_t> string_to_slice(std::string_view str)
{
return cbindgen_private::Slice<uint8_t> {
const_cast<unsigned char *>(reinterpret_cast<const unsigned char *>(str.data())), str.size()
};
}
}
}

View file

@ -0,0 +1,52 @@
// Copyright © SixtyFPS GmbH <info@sixtyfps.io>
// SPDX-License-Identifier: (GPL-3.0-only OR LicenseRef-SixtyFPS-commercial)
#pragma once
#include "sixtyfps.h"
#include <iostream>
namespace sixtyfps::testing {
inline void init()
{
cbindgen_private::sixtyfps_testing_init_backend();
}
inline void mock_elapsed_time(int64_t time_in_ms)
{
cbindgen_private::sixtyfps_mock_elapsed_time(time_in_ms);
}
template<typename Component>
inline void send_mouse_click(const Component *component, float x, float y)
{
auto crc = *component->self_weak.into_dyn().lock();
cbindgen_private::sixtyfps_send_mouse_click(&crc, x, y, &component->m_window.window_handle());
}
template<typename Component>
inline void send_keyboard_string_sequence(const Component *component,
const sixtyfps::SharedString &str,
cbindgen_private::KeyboardModifiers modifiers = {})
{
cbindgen_private::send_keyboard_string_sequence(&str, modifiers,
&component->m_window.window_handle());
}
#define assert_eq(A, B) \
sixtyfps::testing::private_api::assert_eq_impl(A, B, #A, #B, __FILE__, __LINE__)
namespace private_api {
template<typename A, typename B>
void assert_eq_impl(const A &a, const B &b, const char *a_str, const char *b_str, const char *file,
int line)
{
if (a != b) {
std::cerr << file << ":" << line << ": assert_eq FAILED!\n"
<< a_str << ": " << a << "\n"
<< b_str << ": " << b << std::endl;
std::abort();
}
}
}
} // namespace sixtyfps

186
api/cpp/include/vtable.h Normal file
View file

@ -0,0 +1,186 @@
// Copyright © SixtyFPS GmbH <info@sixtyfps.io>
// SPDX-License-Identifier: (GPL-3.0-only OR LicenseRef-SixtyFPS-commercial)
#pragma once
#include <cstddef>
#include <new>
#include <algorithm>
#include <optional>
#include <atomic>
namespace vtable {
template<typename T>
struct VRefMut
{
const T *vtable;
void *instance;
};
struct Layout {
std::size_t size;
std::size_t align;
};
// For the C++'s purpose, they are all the same
template<typename T>
using VRef = VRefMut<T>;
template<typename T>
using VBox = VRefMut<T>;
template<typename T>
using Pin = T;
/*
template<typename T>
struct VBox {
const T *vtable;
void *instance;
};
template<typename T>
struct VRef {
const T *vtable;
const void *instance;
};
*/
struct AllowPin;
template<typename Base, typename T, typename Flag = void>
struct VOffset
{
const T *vtable;
std::uintptr_t offset;
};
template<typename VTable, typename X>
struct VRcInner {
template<typename VTable_, typename X_> friend class VRc;
template<typename VTable_, typename X_> friend class VWeak;
private:
VRcInner() : layout {} {}
const VTable *vtable = &X::static_vtable;
std::atomic<int> strong_ref = 1;
std::atomic<int> weak_ref = 1;
std::uint16_t data_offset = offsetof(VRcInner, data);
union {
X data;
Layout layout;
};
void *data_ptr() { return reinterpret_cast<char *>(this) + data_offset; }
~VRcInner() = delete;
};
struct Dyn {};
template<typename VTable, typename X = Dyn>
class VRc {
VRcInner<VTable, X> *inner;
VRc(VRcInner<VTable, X> *inner) : inner(inner) {}
template<typename VTable_, typename X_> friend class VWeak;
public:
~VRc() {
if (!--inner->strong_ref) {
Layout layout = inner->vtable->drop_in_place({inner->vtable, &inner->data});
layout.size += inner->data_offset;
layout.align = std::max<size_t>(layout.align, alignof(VRcInner<VTable, Dyn>));
inner->layout = layout;
if (!--inner->weak_ref) {
inner->vtable->dealloc(inner->vtable, reinterpret_cast<uint8_t*>(inner), layout);
}
}
}
VRc(const VRc &other): inner(other.inner) {
inner->strong_ref++;
}
VRc &operator=(const VRc &other) {
if (inner == other.inner)
return *this;
this->~VRc();
new(this) VRc(other);
return *this;
}
/// Construct a new VRc holding an X.
///
/// The type X must have a static member `static_vtable` of type VTable
template<typename ...Args> static VRc make(Args... args) {
auto mem = ::operator new(sizeof(VRcInner<VTable, X>), static_cast<std::align_val_t>(alignof(VRcInner<VTable, X>)));
auto inner = new (mem) VRcInner<VTable, X>;
new (&inner->data) X(args...);
return VRc(inner);
}
const X* operator->() const {
return &inner->data;
}
const X& operator*() const {
return inner->data;
}
X* operator->() {
return &inner->data;
}
X& operator*() {
return inner->data;
}
VRc<VTable, Dyn> into_dyn() const { return *reinterpret_cast<const VRc<VTable, Dyn> *>(this); }
VRef<VTable> borrow() const { return { inner->vtable, inner->data_ptr() }; }
friend bool operator==(const VRc &a, const VRc &b) {
return a.inner == b.inner;
}
friend bool operator!=(const VRc &a, const VRc &b) {
return a.inner != b.inner;
}
const VTable *vtable() const { return inner->vtable; }
};
template<typename VTable, typename X = Dyn>
class VWeak {
VRcInner<VTable, X> *inner = nullptr;
public:
VWeak() = default;
~VWeak() {
if (inner && !--inner->weak_ref) {
inner->vtable->dealloc(inner->vtable, reinterpret_cast<uint8_t*>(inner), inner->layout);
}
}
VWeak(const VWeak &other): inner(other.inner) {
inner && inner->weak_ref++;
}
VWeak(const VRc<VTable, X> &other): inner(other.inner) {
inner && inner->weak_ref++;
}
VWeak &operator=(const VWeak &other) {
if (inner == other.inner)
return *this;
this->~VWeak();
new(this) VWeak(other);
return *this;
}
std::optional<VRc<VTable, X>> lock() const {
if (!inner || inner->strong_ref == 0)
return {};
inner->strong_ref++;
return { VRc<VTable, X>(inner) };
}
VWeak<VTable, Dyn> into_dyn() const { return *reinterpret_cast<const VWeak<VTable, Dyn> *>(this); }
friend bool operator==(const VWeak &a, const VWeak &b) {
return a.inner == b.inner;
}
friend bool operator!=(const VWeak &a, const VWeak &b) {
return a.inner != b.inner;
}
const VTable *vtable() const { return inner ? inner->vtable : nullptr; }
};
} //namespace vtable

96
api/cpp/lib.rs Normal file
View file

@ -0,0 +1,96 @@
// Copyright © SixtyFPS GmbH <info@sixtyfps.io>
// SPDX-License-Identifier: (GPL-3.0-only OR LicenseRef-SixtyFPS-commercial)
/*! This crate just expose the function used by the C++ integration */
use core::ffi::c_void;
use sixtyfps_corelib::window::ffi::WindowRcOpaque;
use sixtyfps_corelib::window::WindowRc;
use sixtyfps_rendering_backend_selector::backend;
#[doc(hidden)]
#[cold]
pub fn use_modules() -> usize {
#[cfg(feature = "sixtyfps-interpreter")]
sixtyfps_interpreter::use_modules();
sixtyfps_rendering_backend_selector::use_modules();
sixtyfps_corelib::use_modules()
}
#[no_mangle]
pub unsafe extern "C" fn sixtyfps_windowrc_init(out: *mut WindowRcOpaque) {
assert_eq!(core::mem::size_of::<WindowRc>(), core::mem::size_of::<WindowRcOpaque>());
core::ptr::write(out as *mut WindowRc, crate::backend().create_window());
}
#[no_mangle]
pub unsafe extern "C" fn sixtyfps_run_event_loop() {
crate::backend()
.run_event_loop(sixtyfps_corelib::backend::EventLoopQuitBehavior::QuitOnLastWindowClosed);
}
/// Will execute the given functor in the main thread
#[no_mangle]
pub unsafe extern "C" fn sixtyfps_post_event(
event: extern "C" fn(user_data: *mut c_void),
user_data: *mut c_void,
drop_user_data: Option<extern "C" fn(*mut c_void)>,
) {
struct UserData {
user_data: *mut c_void,
drop_user_data: Option<extern "C" fn(*mut c_void)>,
}
impl Drop for UserData {
fn drop(&mut self) {
if let Some(x) = self.drop_user_data {
x(self.user_data)
}
}
}
unsafe impl Send for UserData {}
let ud = UserData { user_data, drop_user_data };
crate::backend().post_event(Box::new(move || {
let ud = &ud;
event(ud.user_data);
}));
}
#[no_mangle]
pub unsafe extern "C" fn sixtyfps_quit_event_loop() {
crate::backend().quit_event_loop();
}
#[no_mangle]
pub unsafe extern "C" fn sixtyfps_register_font_from_path(
path: &sixtyfps_corelib::SharedString,
error_str: *mut sixtyfps_corelib::SharedString,
) {
core::ptr::write(
error_str,
match crate::backend().register_font_from_path(std::path::Path::new(path.as_str())) {
Ok(()) => Default::default(),
Err(err) => err.to_string().into(),
},
)
}
#[no_mangle]
pub unsafe extern "C" fn sixtyfps_register_font_from_data(
data: sixtyfps_corelib::slice::Slice<'static, u8>,
error_str: *mut sixtyfps_corelib::SharedString,
) {
core::ptr::write(
error_str,
match crate::backend().register_font_from_memory(data.as_slice()) {
Ok(()) => Default::default(),
Err(err) => err.to_string().into(),
},
)
}
#[cfg(feature = "testing")]
#[no_mangle]
pub unsafe extern "C" fn sixtyfps_testing_init_backend() {
sixtyfps_rendering_backend_testing::init();
}

View 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
View 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();
}

View 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
View 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 {}