WIP: live-reload for C++

Missing feature:
 - conversion between Value and enums
 - conversion from value to Model
 - Compatibility with the testing framework (get the `VRc<ItemTreeTable>` from an instance)
This commit is contained in:
Olivier Goffart 2025-07-06 14:29:34 +02:00
parent 73e970b8ca
commit 43b436a89f
12 changed files with 1070 additions and 40 deletions

View file

@ -92,6 +92,7 @@ define_renderer_winit_compat_option(skia-vulkan)
define_renderer_winit_compat_option(software)
define_cargo_dependent_feature(interpreter "Enable support for the Slint interpreter to load .slint files at run-time" ON "NOT SLINT_FEATURE_FREESTANDING")
define_cargo_dependent_feature(live-reload "Enable support for the Slint live-reload to re-load changed .slint files at run-time" OFF "SLINT_FEATURE_INTERPRETER")
define_cargo_dependent_feature(backend-winit "Enable support for the winit crate to interaction with all windowing systems." ON "NOT SLINT_FEATURE_FREESTANDING")
define_cargo_dependent_feature(backend-winit-x11 "Enable support for the winit create to interact only with the X11 windowing system on Unix. Enable this option and turn off SLINT_FEATURE_BACKEND_WINIT for a smaller build with just X11 support on Unix." OFF "NOT SLINT_FEATURE_FREESTANDING")

View file

@ -26,6 +26,7 @@ name = "slint_cpp"
# the C++ crate's CMakeLists.txt as well as cbindgen.rs
[features]
interpreter = ["slint-interpreter", "std"]
live-reload = ["interpreter", "slint-interpreter/internal-live-reload"]
# Enable some function used by the integration tests
testing = ["dep:i-slint-backend-testing"]

View file

@ -896,6 +896,7 @@ fn gen_interpreter(
"Diagnostic",
"PropertyDescriptor",
"Box",
"LiveReloadingComponentInner",
])
.map(String::from)
.collect();
@ -942,6 +943,7 @@ fn gen_interpreter(
using slint::interpreter::ValueType;
using slint::interpreter::PropertyDescriptor;
using slint::interpreter::Diagnostic;
struct LiveReloadingComponentInner;
template <typename T> using Box = T*;
}",
)
@ -985,6 +987,7 @@ macro_rules! declare_features {
declare_features! {
interpreter
live_reload
testing
backend_qt
backend_winit

View file

@ -26,6 +26,10 @@ struct ErasedItemTreeBox : vtable::Dyn
ErasedItemTreeBox(ErasedItemTreeBox &) = delete;
};
}
namespace slint::private_api::live_reload {
class LiveReloadingComponent;
class LiveReloadModelWrapperBase;
}
/// The types in this namespace allow you to load a .slint file at runtime and show its UI.
///
@ -396,6 +400,8 @@ private:
slint::cbindgen_private::Value *inner;
friend struct Struct;
friend class ComponentInstance;
friend class slint::private_api::live_reload::LiveReloadingComponent;
friend class slint::private_api::live_reload::LiveReloadModelWrapperBase;
// Internal constructor that takes ownership of the value
explicit Value(slint::cbindgen_private::Value *&&inner) : inner(inner) { }
};

View file

@ -0,0 +1,347 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
#pragma once
#include "slint.h"
#ifndef SLINT_FEATURE_LIVE_RELOAD
# error SLINT_FEATURE_LIVE_RELOAD must be activated
#else
# include "slint-interpreter.h"
/// Internal API to support the live-reload generated code
namespace slint::private_api::live_reload {
template<typename T>
requires(std::convertible_to<T, slint::interpreter::Value>)
slint::interpreter::Value into_slint_value(const T &val)
{
return val;
}
template<typename T>
requires requires(T val) { val.into_slint_value(); }
slint::interpreter::Value into_slint_value(const T &val)
{
return val.into_slint_value();
}
inline slint::interpreter::Value into_slint_value(const slint::interpreter::Value &val)
{
return val;
}
template<typename T>
requires std::is_same_v<T, void>
inline void from_slint_value(const slint::interpreter::Value &, const T *)
{
}
inline bool from_slint_value(const slint::interpreter::Value &val, const bool *)
{
return val.to_bool().value();
}
inline slint::SharedString from_slint_value(const slint::interpreter::Value &val,
const slint::SharedString *)
{
return val.to_string().value();
}
inline int from_slint_value(const slint::interpreter::Value &val, const int *)
{
return val.to_number().value();
}
inline float from_slint_value(const slint::interpreter::Value &val, const float *)
{
return val.to_number().value();
}
inline slint::Color from_slint_value(const slint::interpreter::Value &val, const slint::Color *)
{
return val.to_brush().value().color();
}
inline interpreter::Value into_slint_value(const slint::Color &val)
{
return slint::Brush(val);
}
inline slint::Brush from_slint_value(const slint::interpreter::Value &val, const slint::Brush *)
{
return val.to_brush().value();
}
inline slint::Image from_slint_value(const slint::interpreter::Value &val, const slint::Image *)
{
return val.to_image().value();
}
/// duration
inline long int from_slint_value(const slint::interpreter::Value &val, const long int *)
{
return val.to_number().value();
}
inline interpreter::Value into_slint_value(const long int &val)
{
return double(val);
}
template<typename ModelData>
inline std::shared_ptr<slint::Model<ModelData>>
from_slint_value(const slint::interpreter::Value &,
const std::shared_ptr<slint::Model<ModelData>> *)
{
std::cout << "NOT IMPLEMENTED " << __PRETTY_FUNCTION__ << std::endl;
return {};
}
template<typename ModelData>
slint::interpreter::Value into_slint_value(const std::shared_ptr<slint::Model<ModelData>> &val);
inline slint::interpreter::Value into_slint_value(const slint::StandardListViewItem &val)
{
slint::interpreter::Struct s;
s.set_field("text", val.text);
return s;
}
inline slint::StandardListViewItem from_slint_value(const slint::interpreter::Value &val,
const slint::StandardListViewItem *)
{
auto s = val.to_struct().value();
return slint::StandardListViewItem { .text = s.get_field("text").value().to_string().value() };
}
inline slint::interpreter::Value into_slint_value(const slint::LogicalPosition &val)
{
slint::interpreter::Struct s;
s.set_field("x", val.x);
s.set_field("y", val.y);
return s;
}
inline slint::LogicalPosition from_slint_value(const slint::interpreter::Value &val,
const slint::LogicalPosition *)
{
auto s = val.to_struct().value();
return slint::LogicalPosition({ float(s.get_field("x").value().to_number().value()),
float(s.get_field("y").value().to_number().value()) });
}
template<typename T>
T from_slint_value(const slint::interpreter::Value &v)
{
return from_slint_value(v, static_cast<const T *>(nullptr));
}
class LiveReloadingComponent
{
const cbindgen_private::LiveReloadingComponentInner *inner;
public:
/// Libraries is an array of string that have in the form `lib=...`
LiveReloadingComponent(std::string_view file_name, std::string_view component_name,
const slint::SharedVector<slint::SharedString> &include_paths,
const slint::SharedVector<slint::SharedString> &libraries,
std::string_view style)
{
assert_main_thread();
inner = cbindgen_private::slint_live_reload_new(
string_to_slice(file_name), string_to_slice(component_name), &include_paths,
&libraries, string_to_slice(style));
}
LiveReloadingComponent(const LiveReloadingComponent &other) : inner(other.inner)
{
assert_main_thread();
cbindgen_private::slint_live_reload_clone(other.inner);
}
LiveReloadingComponent &operator=(const LiveReloadingComponent &other)
{
assert_main_thread();
if (this == &other)
return *this;
cbindgen_private::slint_live_reload_drop(inner);
inner = other.inner;
cbindgen_private::slint_live_reload_clone(inner);
return *this;
}
~LiveReloadingComponent()
{
assert_main_thread();
cbindgen_private::slint_live_reload_drop(inner);
}
void set_property(std::string_view name, const interpreter::Value &value) const
{
assert_main_thread();
return cbindgen_private::slint_live_reload_set_property(inner, string_to_slice(name),
value.inner);
}
interpreter::Value get_property(std::string_view name) const
{
assert_main_thread();
auto val = slint::interpreter::Value(
cbindgen_private::slint_live_reload_get_property(inner, string_to_slice(name)));
return val;
}
template<typename... Args>
interpreter::Value invoke(std::string_view name, Args &...args) const
{
assert_main_thread();
std::array<interpreter::Value, sizeof...(Args)> args_values { into_slint_value(args)... };
cbindgen_private::Slice<cbindgen_private::Value *> args_slice {
reinterpret_cast<cbindgen_private::Value **>(args_values.data()), args_values.size()
};
interpreter::Value val(cbindgen_private::slint_live_reload_invoke(
inner, string_to_slice(name), args_slice));
return val;
}
template<std::invocable<std::span<const interpreter::Value>> F>
requires(std::is_convertible_v<std::invoke_result_t<F, std::span<const interpreter::Value>>,
interpreter::Value>)
void set_callback(std::string_view name, F &&callback) const
{
assert_main_thread();
auto actual_cb =
[](void *data,
cbindgen_private::Slice<cbindgen_private::Box<cbindgen_private::Value>> arg) {
std::span<const interpreter::Value> args_view {
reinterpret_cast<const interpreter::Value *>(arg.ptr), arg.len
};
interpreter::Value r = (*reinterpret_cast<F *>(data))(args_view);
auto inner = r.inner;
r.inner = cbindgen_private::slint_interpreter_value_new();
return inner;
};
return cbindgen_private::slint_live_reload_set_callback(
inner, slint::private_api::string_to_slice(name), actual_cb,
new F(std::move(callback)), [](void *data) { delete reinterpret_cast<F *>(data); });
}
slint::Window &window() const
{
const cbindgen_private::WindowAdapterRcOpaque *win_ptr = nullptr;
cbindgen_private::slint_live_reload_window(inner, &win_ptr);
return const_cast<slint::Window &>(*reinterpret_cast<const slint::Window *>(win_ptr));
}
};
class LiveReloadModelWrapperBase : public private_api::ModelChangeListener
{
cbindgen_private::ModelNotifyOpaque notify;
// This means that the rust code has ownership of "this" until the drop function is called
std::shared_ptr<ModelChangeListener> self = nullptr;
void row_added(size_t index, size_t count) override
{
cbindgen_private::slint_interpreter_model_notify_row_added(&notify, index, count);
}
void row_changed(size_t index) override
{
cbindgen_private::slint_interpreter_model_notify_row_changed(&notify, index);
}
void row_removed(size_t index, size_t count) override
{
cbindgen_private::slint_interpreter_model_notify_row_removed(&notify, index, count);
}
void reset() override { cbindgen_private::slint_interpreter_model_notify_reset(&notify); }
static const ModelAdaptorVTable *vtable()
{
auto row_count = [](VRef<ModelAdaptorVTable> self) -> uintptr_t {
return reinterpret_cast<LiveReloadModelWrapperBase *>(self.instance)->row_count();
};
auto row_data = [](VRef<ModelAdaptorVTable> self,
uintptr_t row) -> slint::cbindgen_private::Value * {
std::optional<interpreter::Value> v =
reinterpret_cast<LiveReloadModelWrapperBase *>(self.instance)
->row_data(int(row));
if (v.has_value()) {
slint::cbindgen_private::Value *rval = v->inner;
v->inner = cbindgen_private::slint_interpreter_value_new();
return rval;
} else {
return nullptr;
}
};
auto set_row_data = [](VRef<ModelAdaptorVTable> self, uintptr_t row,
slint::cbindgen_private::Value *value) {
interpreter::Value v(std::move(value));
reinterpret_cast<LiveReloadModelWrapperBase *>(self.instance)->set_row_data(row, v);
};
auto get_notify =
[](VRef<ModelAdaptorVTable> self) -> const cbindgen_private::ModelNotifyOpaque * {
return &reinterpret_cast<LiveReloadModelWrapperBase *>(self.instance)->notify;
};
auto drop = [](vtable::VRefMut<ModelAdaptorVTable> self) {
reinterpret_cast<LiveReloadModelWrapperBase *>(self.instance)->self = nullptr;
};
static const ModelAdaptorVTable vt { row_count, row_data, set_row_data, get_notify, drop };
return &vt;
}
protected:
LiveReloadModelWrapperBase() { cbindgen_private::slint_interpreter_model_notify_new(&notify); }
virtual ~LiveReloadModelWrapperBase()
{
cbindgen_private::slint_interpreter_model_notify_destructor(&notify);
}
virtual int row_count() const = 0;
virtual std::optional<slint::interpreter::Value> row_data(int i) const = 0;
virtual void set_row_data(int i, const slint::interpreter::Value &value) = 0;
static interpreter::Value wrap(std::shared_ptr<LiveReloadModelWrapperBase> wrapper)
{
wrapper->self = wrapper;
return interpreter::Value(cbindgen_private::slint_interpreter_value_new_model(
reinterpret_cast<uint8_t *>(wrapper.get()), vtable()));
}
};
template<typename ModelData>
class LiveReloadModelWrapper : public LiveReloadModelWrapperBase
{
std::shared_ptr<slint::Model<ModelData>> model = nullptr;
int row_count() const override { return model->row_count(); }
std::optional<slint::interpreter::Value> row_data(int i) const override
{
if (auto v = model->row_data(i))
return into_slint_value(*v);
else
return {};
}
void set_row_data(int i, const slint::interpreter::Value &value) override
{
model->set_row_data(i, from_slint_value<ModelData>(value));
}
public:
LiveReloadModelWrapper(std::shared_ptr<slint::Model<ModelData>> model) : model(std::move(model))
{
}
static slint::interpreter::Value wrap(std::shared_ptr<slint::Model<ModelData>> model)
{
auto self = std::make_shared<LiveReloadModelWrapper<ModelData>>(model);
auto peer = std::weak_ptr<LiveReloadModelWrapperBase>(self);
model->attach_peer(peer);
return LiveReloadModelWrapperBase::wrap(self);
}
};
template<typename ModelData>
slint::interpreter::Value into_slint_value(const std::shared_ptr<slint::Model<ModelData>> &val)
{
if (!val) {
return {};
}
return LiveReloadModelWrapper<ModelData>::wrap(val);
}
} // namespace slint::private_api::live_reload
#endif // SLINT_FEATURE_LIVE_RELOAD