New UI library wip: reconciliation fixes & tests
This commit is contained in:
parent
546db9fb98
commit
cddc8a3235
8 changed files with 465 additions and 51 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,16 @@
|
|||
#pragma once
|
||||
|
||||
#include <psemek/util/span.hpp>
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace psemek::ui::impl
|
||||
{
|
||||
|
||||
struct component
|
||||
{
|
||||
virtual util::span<std::unique_ptr<component> const> children() const;
|
||||
|
||||
virtual ~component() {}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -44,19 +44,10 @@ namespace psemek::ui::impl
|
|||
throw component_not_supported_exception(type);
|
||||
}
|
||||
|
||||
template <typename Type, typename Factory>
|
||||
void register_factory(Factory factory)
|
||||
template <typename Type, typename ImplType, typename Factory>
|
||||
void register_type(Factory && factory)
|
||||
{
|
||||
type_factories_[typeid(Type)] = [factory = std::move(factory)](std::unique_ptr<component> root, std::any const & value)
|
||||
{
|
||||
return factory(std::move(root), *std::any_cast<Type>(&value));
|
||||
};
|
||||
}
|
||||
|
||||
template <typename Type, typename ImplType>
|
||||
void register_type()
|
||||
{
|
||||
type_factories_[typeid(Type)] = [this](std::unique_ptr<component> root, std::any const & value)
|
||||
type_factories_[typeid(Type)] = [this, factory = std::move(factory)](std::unique_ptr<component> root, std::any const & value)
|
||||
{
|
||||
std::unique_ptr<ImplType> impl;
|
||||
if (root)
|
||||
|
|
@ -71,7 +62,7 @@ namespace psemek::ui::impl
|
|||
root.reset();
|
||||
|
||||
if (!impl)
|
||||
impl = std::make_unique<ImplType>();
|
||||
impl = factory();
|
||||
|
||||
auto const & typed_value = *std::any_cast<Type>(&value);
|
||||
|
||||
|
|
@ -80,60 +71,90 @@ namespace psemek::ui::impl
|
|||
if constexpr (std::is_base_of_v<single_container, ImplType>)
|
||||
{
|
||||
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<container, ImplType>)
|
||||
{
|
||||
impl->set_children_token(typed_value.children.subscribe([this, impl = impl.get()](std::vector<typename Type::element> 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<typename Type::element> const & children_values){
|
||||
impl->release_child_tokens();
|
||||
|
||||
std::unordered_map<key, std::unique_ptr<component>> child_by_key;
|
||||
{
|
||||
auto children = impl->release_children();
|
||||
auto keys = impl->release_child_keys();
|
||||
std::unordered_map<key, std::unique_ptr<component>> 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<std::unique_ptr<component>> children(children_values.size());
|
||||
std::vector<std::optional<key>> 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<std::unique_ptr<component>> children(children_values.size());
|
||||
std::vector<std::optional<key>> child_keys(children_values.size());
|
||||
|
||||
std::vector<util::signal<std::any>::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<util::signal<std::any>::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 <typename Type, typename ImplType>
|
||||
void register_type()
|
||||
{
|
||||
register_type<Type, ImplType>([]{ return std::make_unique<ImplType>(); });
|
||||
}
|
||||
|
||||
private:
|
||||
std::unordered_map<std::type_index, util::function<std::unique_ptr<component>(std::unique_ptr<component>, std::any const &)>> type_factories_;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -53,6 +53,11 @@ namespace psemek::ui::impl
|
|||
return std::move(child_keys_);
|
||||
}
|
||||
|
||||
util::span<std::unique_ptr<component> const> children() const override
|
||||
{
|
||||
return children_;
|
||||
}
|
||||
|
||||
private:
|
||||
util::signal<void>::subscription_token children_token_;
|
||||
std::vector<std::unique_ptr<component>> children_;
|
||||
|
|
|
|||
|
|
@ -32,6 +32,11 @@ namespace psemek::ui::impl
|
|||
return std::move(child_token_);
|
||||
}
|
||||
|
||||
util::span<std::unique_ptr<component> const> children() const override
|
||||
{
|
||||
return {&child_, 1};
|
||||
}
|
||||
|
||||
private:
|
||||
util::signal<void>::subscription_token child_token_;
|
||||
std::unique_ptr<component> child_;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
namespace psemek::ui
|
||||
{
|
||||
|
||||
struct rectagle
|
||||
struct rectangle
|
||||
{
|
||||
react::value<gfx::color_rgba> color = {};
|
||||
react::value<bool> square = {};
|
||||
|
|
|
|||
11
libs/ui/source/impl/component.cpp
Normal file
11
libs/ui/source/impl/component.cpp
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
#include <psemek/ui/impl/component.hpp>
|
||||
|
||||
namespace psemek::ui::impl
|
||||
{
|
||||
|
||||
util::span<std::unique_ptr<component> const> component::children() const
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
}
|
||||
364
libs/ui/tests/reconciliation.cpp
Normal file
364
libs/ui/tests/reconciliation.cpp
Normal file
|
|
@ -0,0 +1,364 @@
|
|||
#include <psemek/test/test.hpp>
|
||||
|
||||
#include <psemek/ui/impl/component_factory_base.hpp>
|
||||
#include <psemek/ui/label.hpp>
|
||||
#include <psemek/ui/rectangle.hpp>
|
||||
#include <psemek/ui/button.hpp>
|
||||
#include <psemek/ui/stack_layout.hpp>
|
||||
|
||||
#include <type_traits>
|
||||
|
||||
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<rectangle, rectangle_impl>([this]{ ++rectangle_count; return std::make_unique<rectangle_impl>(); });
|
||||
register_type<label, label_impl>([this]{ ++label_count; return std::make_unique<label_impl>(); });
|
||||
register_type<button, button_impl>([this]{ ++button_count; return std::make_unique<button_impl>(); });
|
||||
register_type<stack_layout, stack_layout_impl>([this]{ ++stack_layout_count; return std::make_unique<stack_layout_impl>(); });
|
||||
}
|
||||
};
|
||||
|
||||
struct check_children_helper
|
||||
{
|
||||
impl::component & component;
|
||||
std::size_t i = 0;
|
||||
|
||||
template <typename Type>
|
||||
void check()
|
||||
{
|
||||
if constexpr (std::is_same_v<Type, std::nullptr_t>)
|
||||
{
|
||||
expect_null(component.children()[i]);
|
||||
}
|
||||
else
|
||||
{
|
||||
expect_non_null(component.children()[i]);
|
||||
expect_dynamic_type(*component.children()[i], Type);
|
||||
}
|
||||
++i;
|
||||
}
|
||||
};
|
||||
|
||||
template <typename ... Types>
|
||||
void check_children(impl::component & component)
|
||||
{
|
||||
expect_equal(component.children().size(), sizeof...(Types));
|
||||
|
||||
check_children_helper helper{component};
|
||||
|
||||
(void)helper;
|
||||
(helper.check<Types>(), ...);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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<label_impl>(*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<std::nullptr_t>(*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<label_impl, button_impl, label_impl>(*ui_root);
|
||||
check_children<label_impl>(*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<std::nullptr_t, std::nullptr_t>(*ui_root);
|
||||
|
||||
expect_equal(factory.stack_layout_count, 1);
|
||||
}
|
||||
|
||||
test_case(ui_impl_reconciliate_single__container)
|
||||
{
|
||||
test_component_factory factory;
|
||||
|
||||
auto child = react::source<std::any>(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<label_impl>(*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<label_impl>(*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<rectangle_impl>(*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<label_impl>(*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<std::vector<stack_layout::element>> 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<label_impl>(*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<label_impl, label_impl>(*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<std::vector<stack_layout::element>> 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<label_impl>(*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<label_impl, label_impl>(*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<label_impl, label_impl>(*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<label_impl, label_impl>(*ui_root);
|
||||
expect_equal(ui_root->children()[0].get(), label0);
|
||||
|
||||
expect_equal(factory.stack_layout_count, 1);
|
||||
expect_equal(factory.label_count, 3);
|
||||
}
|
||||
|
||||
Loading…
Add table
Reference in a new issue