diff --git a/src/kdbindings/binding.h b/src/kdbindings/binding.h index 4bcd1cc..400889c 100644 --- a/src/kdbindings/binding.h +++ b/src/kdbindings/binding.h @@ -125,6 +125,24 @@ inline std::unique_ptr> makeBinding(EvaluatorT &evaluator return std::make_unique>(Private::makeNode(property), evaluator); } +/** + * @brief Creates a binding from a const property using a specified evaluator. + * + * @tparam T The type of the value that the Binding expression evaluates to. + * @tparam EvaluatorT The type of the evaluator that is used to evaluate the Binding. + * @param evaluator The evaluator that is used to evaluate the Binding. + * @param property The const Property to create a Binding from. + * @return std::unique_ptr> A new Binding that is powered by the evaluator. + * + * *Note: Using a const Property ensures that the source cannot be modified through this binding, + * maintaining data integrity and supporting scenarios where data should only be observed, not altered.* + */ +template +inline std::unique_ptr> makeBinding(EvaluatorT &evaluator, const Property &property) +{ + return std::make_unique>(Private::makeNode(property), evaluator); +} + /** * @brief Helper function to create a Binding from a root Node. * @@ -227,6 +245,24 @@ inline std::unique_ptr> makeBinding(Proper return std::make_unique>(Private::makeNode(property)); } +/** + * @brief Creates an immediate mode binding from a const property. + * + * @tparam T The type of the value that the Binding expression evaluates to. + * @param property The const Property to create a Binding from. + * @return std::unique_ptr> A new Binding that is powered by an + * immediate mode evaluator, ensuring quick updates to changes. + * + * *Note: This binding type is especially suited for scenarios where you need to reflect + * changes in a property to some dependent components without the need to alter the + * source property itself.* + */ +template +inline std::unique_ptr> makeBinding(const Property &property) +{ + return std::make_unique>(Private::makeNode(property)); +} + /** * @brief Helper function to create an immediate mode Binding from a root Node. * diff --git a/src/kdbindings/make_node.h b/src/kdbindings/make_node.h index 22bd374..0f93d99 100644 --- a/src/kdbindings/make_node.h +++ b/src/kdbindings/make_node.h @@ -28,6 +28,11 @@ struct bindable_value_type_> { using type = T; }; +template +struct bindable_value_type_> { + using type = T; +}; + template struct bindable_value_type_> { using type = T; @@ -69,6 +74,12 @@ inline Node makeNode(Property &property) return Node(std::make_unique>(property)); } +template +inline Node makeNode(const Property &property) +{ + return Node(std::make_unique>(property)); +} + template inline Node makeNode(Node &&node) { diff --git a/src/kdbindings/node.h b/src/kdbindings/node.h index aafe039..c4eb361 100644 --- a/src/kdbindings/node.h +++ b/src/kdbindings/node.h @@ -152,7 +152,7 @@ template class PropertyNode : public NodeInterface { public: - explicit PropertyNode(Property &property) + explicit PropertyNode(const Property &property) : m_parent(nullptr), m_dirty(false) { setProperty(property); @@ -184,8 +184,7 @@ class PropertyNode : public NodeInterface return m_property->get(); } - // This must currently take a const reference, as the "moved" signal emits a const& - void propertyMoved(Property &property) + void propertyMoved(const Property &property) { if (&property != m_property) { m_property = &property; @@ -206,15 +205,17 @@ class PropertyNode : public NodeInterface const bool *dirtyVariable() const override { return &m_dirty; } private: - void setProperty(Property &property) + void setProperty(const Property &property) { m_property = &property; - m_valueChangedHandle = m_property->valueChanged().connect(&PropertyNode::markDirty, this); - m_movedHandle = m_property->m_moved.connect(&PropertyNode::propertyMoved, this); - m_destroyedHandle = m_property->destroyed().connect(&PropertyNode::propertyDestroyed, this); + + // Connect to all signals, even for const properties + m_valueChangedHandle = m_property->valueChanged().connect([this]() { this->markDirty(); }); + m_movedHandle = m_property->m_moved.connect([this](const Property &newProp) { this->propertyMoved(newProp); }); + m_destroyedHandle = m_property->destroyed().connect([this]() { this->propertyDestroyed(); }); } - Property *m_property; + const Property *m_property; ConnectionHandle m_movedHandle; ConnectionHandle m_valueChangedHandle; ConnectionHandle m_destroyedHandle; diff --git a/src/kdbindings/property.h b/src/kdbindings/property.h index c88c082..122eaed 100644 --- a/src/kdbindings/property.h +++ b/src/kdbindings/property.h @@ -386,7 +386,7 @@ class Property // keep track of moved Properties. template friend class Private::PropertyNode; - Signal &> m_moved; + mutable Signal &> m_moved; mutable Signal<> m_destroyed; std::unique_ptr> m_updater; diff --git a/tests/binding/tst_binding.cpp b/tests/binding/tst_binding.cpp index d502a8a..9130941 100644 --- a/tests/binding/tst_binding.cpp +++ b/tests/binding/tst_binding.cpp @@ -313,6 +313,41 @@ TEST_CASE("Create property bindings using helper function") } } +TEST_CASE("Binding with const Property") +{ + SUBCASE("A binding created from a const property reflects its value") + { + const Property constSource(5); + auto bound = makeBoundProperty(constSource); + + REQUIRE(bound.get() == 5); // Ensure the bound property reflects the const source + } + + SUBCASE("A binding to a const property does not allow modification through set") + { + const Property constSource(5); + auto bound = makeBoundProperty(constSource); + + REQUIRE_THROWS_AS(bound.set(10), ReadOnlyProperty); + } + + SUBCASE("Updates to const property are reflected in the binding") + { + Property source(5); + const Property &constRefSource = source; // Create a const reference to a non-const Property + auto bound = makeBoundProperty(constRefSource); + + REQUIRE(bound.get() == 5); + + bool called = false; + (void)bound.valueChanged().connect([&called]() { called = true; }); + + source = 10; // Change the original non-const property + REQUIRE(called); + REQUIRE(bound.get() == 10); + } +} + TEST_CASE("Binding reassignment") { SUBCASE("A binding can not be override by a constant value")