diff --git a/libs/ui/CMakeLists.txt b/libs/ui/CMakeLists.txt index 3b9538e9..f603e7af 100644 --- a/libs/ui/CMakeLists.txt +++ b/libs/ui/CMakeLists.txt @@ -4,3 +4,5 @@ file(GLOB_RECURSE PSEMEK_UI_SOURCES RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}" "sour psemek_add_library(psemek-ui ${PSEMEK_UI_HEADERS} ${PSEMEK_UI_SOURCES}) target_include_directories(psemek-ui PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include") target_link_libraries(psemek-ui PUBLIC psemek-util psemek-react psemek-geom psemek-gfx) + +psemek_glob_tests(psemek-ui tests) diff --git a/libs/ui/include/psemek/ui/impl/component.hpp b/libs/ui/include/psemek/ui/impl/component.hpp index a7741b5c..34021a4a 100644 --- a/libs/ui/include/psemek/ui/impl/component.hpp +++ b/libs/ui/include/psemek/ui/impl/component.hpp @@ -1,10 +1,16 @@ #pragma once +#include + +#include + namespace psemek::ui::impl { struct component { + virtual util::span const> children() const; + virtual ~component() {} }; diff --git a/libs/ui/include/psemek/ui/impl/component_factory_base.hpp b/libs/ui/include/psemek/ui/impl/component_factory_base.hpp index 01e8e568..cbc4f51e 100644 --- a/libs/ui/include/psemek/ui/impl/component_factory_base.hpp +++ b/libs/ui/include/psemek/ui/impl/component_factory_base.hpp @@ -44,19 +44,10 @@ namespace psemek::ui::impl throw component_not_supported_exception(type); } - template - void register_factory(Factory factory) + template + void register_type(Factory && factory) { - type_factories_[typeid(Type)] = [factory = std::move(factory)](std::unique_ptr root, std::any const & value) - { - return factory(std::move(root), *std::any_cast(&value)); - }; - } - - template - void register_type() - { - type_factories_[typeid(Type)] = [this](std::unique_ptr root, std::any const & value) + type_factories_[typeid(Type)] = [this, factory = std::move(factory)](std::unique_ptr root, std::any const & value) { std::unique_ptr impl; if (root) @@ -71,7 +62,7 @@ namespace psemek::ui::impl root.reset(); if (!impl) - impl = std::make_unique(); + impl = factory(); auto const & typed_value = *std::any_cast(&value); @@ -80,60 +71,90 @@ namespace psemek::ui::impl if constexpr (std::is_base_of_v) { impl->release_child_token(); - impl->set_child_token(typed_value.child.subscribe([this, impl = impl.get()](std::any const & child){ - impl->set_child(reconciliate(impl->release_child(), child)); - }, true)); + if (typed_value.child) + { + impl->set_child_token(typed_value.child.subscribe([this, impl = impl.get()](std::any const & child){ + impl->set_child(reconciliate(impl->release_child(), child)); + }, true)); + } + else + { + impl->release_child(); + } } else if constexpr (std::is_base_of_v) { - impl->set_children_token(typed_value.children.subscribe([this, impl = impl.get()](std::vector const & children_values){ - impl->release_child_tokens(); + if (typed_value.children) + { + impl->set_children_token(typed_value.children.subscribe([this, impl = impl.get()](std::vector const & children_values){ + impl->release_child_tokens(); - std::unordered_map> child_by_key; - { - auto children = impl->release_children(); - auto keys = impl->release_child_keys(); + std::unordered_map> child_by_key; - for (std::size_t i = 0; i < children.size(); ++i) - if (keys[i]) - child_by_key[std::move(*keys[i])] = std::move(children[i]); - } - - std::vector> children(children_values.size()); - std::vector> child_keys(children_values.size()); - - for (std::size_t i = 0; i < children_values.size(); ++i) - { - if (auto const & key = children_values[i].key) + auto old_children = impl->release_children(); { - if (auto it = child_by_key.find(*key); it != child_by_key.end()) - children[i] = std::move(it->second); - child_keys[i] = *key; + auto keys = impl->release_child_keys(); + + for (std::size_t i = 0; i < old_children.size(); ++i) + if (keys[i]) + child_by_key[std::move(*keys[i])] = std::move(old_children[i]); } - } - impl->set_children(std::move(children)); - impl->set_child_keys(std::move(child_keys)); + std::vector> children(children_values.size()); + std::vector> child_keys(children_values.size()); - std::vector::subscription_token> child_tokens(children_values.size()); + for (std::size_t i = 0; i < children_values.size(); ++i) + { + if (auto const & key = children_values[i].key) + { + if (auto it = child_by_key.find(*key); it != child_by_key.end()) + children[i] = std::move(it->second); - for (std::size_t i = 0; i < children_values.size(); ++i) - { - child_tokens[i] = children_values[i].element.subscribe([this, impl, i](std::any const & value){ - auto children = impl->release_children(); - children[i] = reconciliate(std::move(children[i]), value); - impl->set_children(std::move(children)); - }, true); - } + child_keys[i] = *key; + } - impl->set_child_tokens(std::move(child_tokens)); - }, true)); + if (!children[i] && i < old_children.size() && old_children[i]) + children[i] = std::move(old_children[i]); + } + + impl->set_children(std::move(children)); + impl->set_child_keys(std::move(child_keys)); + + std::vector::subscription_token> child_tokens(children_values.size()); + + for (std::size_t i = 0; i < children_values.size(); ++i) + { + if (children_values[i].element) + { + child_tokens[i] = children_values[i].element.subscribe([this, impl, i](std::any const & value){ + auto children = impl->release_children(); + children[i] = reconciliate(std::move(children[i]), value); + impl->set_children(std::move(children)); + }, true); + } + } + + impl->set_child_tokens(std::move(child_tokens)); + }, true)); + } + else + { + impl->release_child_tokens(); + impl->release_child_keys(); + impl->release_children(); + } } return impl; }; } + template + void register_type() + { + register_type([]{ return std::make_unique(); }); + } + private: std::unordered_map(std::unique_ptr, std::any const &)>> type_factories_; }; diff --git a/libs/ui/include/psemek/ui/impl/container.hpp b/libs/ui/include/psemek/ui/impl/container.hpp index 2a63a339..fda3ab5e 100644 --- a/libs/ui/include/psemek/ui/impl/container.hpp +++ b/libs/ui/include/psemek/ui/impl/container.hpp @@ -53,6 +53,11 @@ namespace psemek::ui::impl return std::move(child_keys_); } + util::span const> children() const override + { + return children_; + } + private: util::signal::subscription_token children_token_; std::vector> children_; diff --git a/libs/ui/include/psemek/ui/impl/single_container.hpp b/libs/ui/include/psemek/ui/impl/single_container.hpp index 1eaf2295..cd7f60b5 100644 --- a/libs/ui/include/psemek/ui/impl/single_container.hpp +++ b/libs/ui/include/psemek/ui/impl/single_container.hpp @@ -32,6 +32,11 @@ namespace psemek::ui::impl return std::move(child_token_); } + util::span const> children() const override + { + return {&child_, 1}; + } + private: util::signal::subscription_token child_token_; std::unique_ptr child_; diff --git a/libs/ui/include/psemek/ui/rectangle.hpp b/libs/ui/include/psemek/ui/rectangle.hpp index 28698053..0ad49f85 100644 --- a/libs/ui/include/psemek/ui/rectangle.hpp +++ b/libs/ui/include/psemek/ui/rectangle.hpp @@ -6,7 +6,7 @@ namespace psemek::ui { - struct rectagle + struct rectangle { react::value color = {}; react::value square = {}; diff --git a/libs/ui/source/impl/component.cpp b/libs/ui/source/impl/component.cpp new file mode 100644 index 00000000..381defcd --- /dev/null +++ b/libs/ui/source/impl/component.cpp @@ -0,0 +1,11 @@ +#include + +namespace psemek::ui::impl +{ + + util::span const> component::children() const + { + return {}; + } + +} diff --git a/libs/ui/tests/reconciliation.cpp b/libs/ui/tests/reconciliation.cpp new file mode 100644 index 00000000..c93612b9 --- /dev/null +++ b/libs/ui/tests/reconciliation.cpp @@ -0,0 +1,364 @@ +#include + +#include +#include +#include +#include +#include + +#include + +using namespace psemek; +using namespace psemek::ui; + +namespace +{ + + struct rectangle_impl + : impl::component + { + void update(rectangle const &) + {} + }; + + struct label_impl + : impl::component + { + void update(label const &) + {} + }; + + struct button_impl + : impl::single_container + { + void update(button const &) + {} + }; + + struct stack_layout_impl + : impl::container + { + void update(stack_layout const &) + {} + }; + + struct test_component_factory + : impl::component_factory_base + { + int rectangle_count = 0; + int label_count = 0; + int button_count = 0; + int stack_layout_count = 0; + + test_component_factory() + { + register_type([this]{ ++rectangle_count; return std::make_unique(); }); + register_type([this]{ ++label_count; return std::make_unique(); }); + register_type([this]{ ++button_count; return std::make_unique(); }); + register_type([this]{ ++stack_layout_count; return std::make_unique(); }); + } + }; + + struct check_children_helper + { + impl::component & component; + std::size_t i = 0; + + template + void check() + { + if constexpr (std::is_same_v) + { + expect_null(component.children()[i]); + } + else + { + expect_non_null(component.children()[i]); + expect_dynamic_type(*component.children()[i], Type); + } + ++i; + } + }; + + template + void check_children(impl::component & component) + { + expect_equal(component.children().size(), sizeof...(Types)); + + check_children_helper helper{component}; + + (void)helper; + (helper.check(), ...); + } + +} + +test_case(ui_impl_factory_element) +{ + test_component_factory factory; + + auto test_ui = label{}; + auto ui_root = factory.reconciliate(nullptr, test_ui); + + expect_non_null(ui_root); + expect_dynamic_type(*ui_root, label_impl); + + expect_equal(factory.label_count, 1); +} + +test_case(ui_impl_factory_single__container) +{ + test_component_factory factory; + + auto test_ui = button{ + .child = label{} + }; + auto ui_root = factory.reconciliate(nullptr, test_ui); + + expect_non_null(ui_root); + expect_dynamic_type(*ui_root, button_impl); + check_children(*ui_root); + + expect_equal(factory.label_count, 1); + expect_equal(factory.button_count, 1); +} + +test_case(ui_impl_factory_single__container__null) +{ + test_component_factory factory; + + auto test_ui = button{}; + auto ui_root = factory.reconciliate(nullptr, test_ui); + + expect_non_null(ui_root); + expect_dynamic_type(*ui_root, button_impl); + check_children(*ui_root); + + expect_equal(factory.button_count, 1); +} + +test_case(ui_impl_factory_container) +{ + test_component_factory factory; + + auto test_ui = stack_layout{{ + {label{}}, + {button{.child = label{}}}, + {label{}}, + }}; + auto ui_root = factory.reconciliate(nullptr, test_ui); + + expect_non_null(ui_root); + expect_dynamic_type(*ui_root, stack_layout_impl); + check_children(*ui_root); + check_children(*ui_root->children()[1]); + + expect_equal(factory.label_count, 3); + expect_equal(factory.button_count, 1); + expect_equal(factory.stack_layout_count, 1); +} + +test_case(ui_impl_factory_container__null) +{ + test_component_factory factory; + + auto test_ui = stack_layout{}; + auto ui_root = factory.reconciliate(nullptr, test_ui); + + expect_non_null(ui_root); + expect_dynamic_type(*ui_root, stack_layout_impl); + check_children<>(*ui_root); + + expect_equal(factory.stack_layout_count, 1); +} + +test_case(ui_impl_factory_container__null__child) +{ + test_component_factory factory; + + auto test_ui = stack_layout{{ + {}, + {}, + }}; + auto ui_root = factory.reconciliate(nullptr, test_ui); + + expect_non_null(ui_root); + expect_dynamic_type(*ui_root, stack_layout_impl); + check_children(*ui_root); + + expect_equal(factory.stack_layout_count, 1); +} + +test_case(ui_impl_reconciliate_single__container) +{ + test_component_factory factory; + + auto child = react::source(label{}); + + expect_equal((*child).type(), typeid(label{})); + + auto test_ui = button{ + .child = child + }; + + expect_equal((*test_ui.child).type(), typeid(label{})); + + auto ui_root = factory.reconciliate(nullptr, test_ui); + + expect_non_null(ui_root); + expect_dynamic_type(*ui_root, button_impl); + check_children(*ui_root); + + expect_equal(factory.rectangle_count, 0); + expect_equal(factory.label_count, 1); + expect_equal(factory.button_count, 1); + + child.set(label{}); + + check_children(*ui_root); + + expect_equal(factory.rectangle_count, 0); + expect_equal(factory.label_count, 1); + expect_equal(factory.button_count, 1); + + child.set(rectangle{}); + + check_children(*ui_root); + + expect_equal(factory.rectangle_count, 1); + expect_equal(factory.label_count, 1); + expect_equal(factory.button_count, 1); + + child.set(label{}); + + check_children(*ui_root); + + expect_equal(factory.rectangle_count, 1); + expect_equal(factory.label_count, 2); + expect_equal(factory.button_count, 1); +} + +test_case(ui_impl_reconciliate_container__no__keys) +{ + test_component_factory factory; + + react::source> children; + + auto test_ui = stack_layout{children}; + + auto ui_root = factory.reconciliate(nullptr, test_ui); + + expect_equal(factory.stack_layout_count, 1); + expect_equal(factory.label_count, 0); + expect_equal(factory.button_count, 0); + + expect_non_null(ui_root); + expect_dynamic_type(*ui_root, stack_layout_impl); + check_children<>(*ui_root); + + children.set({{label{}}}); + + check_children(*ui_root); + + expect_equal(factory.stack_layout_count, 1); + expect_equal(factory.label_count, 1); + expect_equal(factory.button_count, 0); + + children.set({{label{}}, {label{}}}); + + check_children(*ui_root); + + expect_equal(factory.stack_layout_count, 1); + expect_equal(factory.label_count, 2); + expect_equal(factory.button_count, 0); + + children.set({{label{}}, {label{}}, {label{}}}); + + expect_equal(factory.stack_layout_count, 1); + expect_equal(factory.label_count, 3); + expect_equal(factory.button_count, 0); + + children.set({{label{}}, {button{}}, {label{}}}); + + expect_equal(factory.stack_layout_count, 1); + expect_equal(factory.label_count, 3); + expect_equal(factory.button_count, 1); + + children.set({{button{}}, {button{}}, {label{}}}); + + expect_equal(factory.stack_layout_count, 1); + expect_equal(factory.label_count, 3); + expect_equal(factory.button_count, 2); + + children.set({{button{}}, {button{}}}); + + expect_equal(factory.stack_layout_count, 1); + expect_equal(factory.label_count, 3); + expect_equal(factory.button_count, 2); + + children.set({{button{}}}); + + expect_equal(factory.stack_layout_count, 1); + expect_equal(factory.label_count, 3); + expect_equal(factory.button_count, 2); + + children.set({}); + + expect_equal(factory.stack_layout_count, 1); + expect_equal(factory.label_count, 3); + expect_equal(factory.button_count, 2); +} + +test_case(ui_impl_reconciliate_container__keys) +{ + test_component_factory factory; + + react::source> children; + + auto test_ui = stack_layout{children}; + + auto ui_root = factory.reconciliate(nullptr, test_ui); + + expect_equal(factory.stack_layout_count, 1); + expect_equal(factory.label_count, 0); + + expect_non_null(ui_root); + expect_dynamic_type(*ui_root, stack_layout_impl); + check_children<>(*ui_root); + + children.set({{label{}, "Label 0"}}); + + check_children(*ui_root); + + auto label0 = ui_root->children()[0].get(); + + expect_equal(factory.stack_layout_count, 1); + expect_equal(factory.label_count, 1); + + children.set({{label{}, "Label 0"}, {label{}, "Label 1"}}); + + check_children(*ui_root); + expect_equal(ui_root->children()[0].get(), label0); + auto label1 = ui_root->children()[1].get(); + + expect_equal(factory.stack_layout_count, 1); + expect_equal(factory.label_count, 2); + + children.set({{label{}, "Label 1"}, {label{}, "Label 0"}}); + + check_children(*ui_root); + expect_equal(ui_root->children()[0].get(), label1); + expect_equal(ui_root->children()[1].get(), label0); + + expect_equal(factory.stack_layout_count, 1); + expect_equal(factory.label_count, 2); + + children.set({{label{}, "Label 0"}, {label{}, "Label 2"}}); + + check_children(*ui_root); + expect_equal(ui_root->children()[0].get(), label0); + + expect_equal(factory.stack_layout_count, 1); + expect_equal(factory.label_count, 3); +} +