New UI library wip: reconciliation fixes & tests

This commit is contained in:
Nikita Lisitsa 2023-04-19 19:01:24 +03:00
parent 546db9fb98
commit cddc8a3235
8 changed files with 465 additions and 51 deletions

View file

@ -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)

View file

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

View file

@ -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_;
};

View file

@ -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_;

View file

@ -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_;

View file

@ -6,7 +6,7 @@
namespace psemek::ui
{
struct rectagle
struct rectangle
{
react::value<gfx::color_rgba> color = {};
react::value<bool> square = {};

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

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