From e8ea3e0fd404bfa72687497579689bc85d151e93 Mon Sep 17 00:00:00 2001 From: lisyarus Date: Fri, 21 Apr 2023 17:26:44 +0300 Subject: [PATCH] New UI library automatic layout wip --- libs/ui/include/psemek/ui/box_layout.hpp | 7 +- .../psemek/ui/impl/box_layout_base.hpp | 51 +++++ libs/ui/include/psemek/ui/impl/component.hpp | 14 ++ .../include/psemek/ui/impl/rectangle_base.hpp | 21 ++ .../psemek/ui/impl/single_container.hpp | 5 + .../psemek/ui/impl/single_container_base.hpp | 21 ++ .../psemek/ui/impl/stack_layout_base.hpp | 20 ++ libs/ui/source/impl/box_layout_base.cpp | 186 ++++++++++++++++++ libs/ui/source/impl/component.cpp | 15 ++ libs/ui/source/impl/rectangle_base.cpp | 19 ++ libs/ui/source/impl/single_container_base.cpp | 28 +++ libs/ui/source/impl/stack_layout_base.cpp | 24 +++ libs/ui/tests/layout.cpp | 92 +++++++++ 13 files changed, 502 insertions(+), 1 deletion(-) create mode 100644 libs/ui/include/psemek/ui/impl/box_layout_base.hpp create mode 100644 libs/ui/include/psemek/ui/impl/rectangle_base.hpp create mode 100644 libs/ui/include/psemek/ui/impl/single_container_base.hpp create mode 100644 libs/ui/include/psemek/ui/impl/stack_layout_base.hpp create mode 100644 libs/ui/source/impl/box_layout_base.cpp create mode 100644 libs/ui/source/impl/rectangle_base.cpp create mode 100644 libs/ui/source/impl/single_container_base.cpp create mode 100644 libs/ui/source/impl/stack_layout_base.cpp create mode 100644 libs/ui/tests/layout.cpp diff --git a/libs/ui/include/psemek/ui/box_layout.hpp b/libs/ui/include/psemek/ui/box_layout.hpp index b4dae01c..9ca357d6 100644 --- a/libs/ui/include/psemek/ui/box_layout.hpp +++ b/libs/ui/include/psemek/ui/box_layout.hpp @@ -15,12 +15,17 @@ namespace psemek::ui struct minimized {}; + struct fixed + { + float value = 0.f; + }; + struct weight { float value = 1.f; }; - using size_policy = std::variant; + using size_policy = std::variant; struct element { diff --git a/libs/ui/include/psemek/ui/impl/box_layout_base.hpp b/libs/ui/include/psemek/ui/impl/box_layout_base.hpp new file mode 100644 index 00000000..913cce96 --- /dev/null +++ b/libs/ui/include/psemek/ui/impl/box_layout_base.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include +#include +#include + +namespace psemek::ui::impl +{ + + namespace detail + { + + template + struct box_layout_type; + + template <> + struct box_layout_type<0> + { + using type = box_layout::horizontal; + }; + + template <> + struct box_layout_type<1> + { + using type = box_layout::vertical; + }; + + } + + // Dimension == 0 is horizontal layout + // Dimension == 1 is vertical layout + template + struct box_layout_base + : container + { + box_layout_base(react::value margin); + + void reshape(geom::box const & new_shape) override; + geom::interval size_constraints(int dimension, float other_dimension_size) const override; + + void update(typename detail::box_layout_type::type const & box_layout); + + private: + react::value margin_; + react::value>> size_policies_; + }; + + extern template struct box_layout_base<0>; + extern template struct box_layout_base<1>; + +} diff --git a/libs/ui/include/psemek/ui/impl/component.hpp b/libs/ui/include/psemek/ui/impl/component.hpp index 34021a4a..66a9900c 100644 --- a/libs/ui/include/psemek/ui/impl/component.hpp +++ b/libs/ui/include/psemek/ui/impl/component.hpp @@ -1,6 +1,8 @@ #pragma once #include +#include +#include #include @@ -9,9 +11,21 @@ namespace psemek::ui::impl struct component { + static constexpr float infinity = std::numeric_limits::infinity(); + virtual util::span const> children() const; + virtual geom::box const & shape() const; + virtual void reshape(geom::box const & new_shape); + + // size_constraints(0, height) is width_constraints(height) + // size_constraints(1, width) is height_constraints(width) + virtual geom::interval size_constraints(int dimension, float other_dimension_size) const; + virtual ~component() {} + + private: + geom::box shape_; }; } diff --git a/libs/ui/include/psemek/ui/impl/rectangle_base.hpp b/libs/ui/include/psemek/ui/impl/rectangle_base.hpp new file mode 100644 index 00000000..ce177d09 --- /dev/null +++ b/libs/ui/include/psemek/ui/impl/rectangle_base.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include +#include +#include + +namespace psemek::ui::impl +{ + + struct rectangle_base + : component + { + geom::interval size_constraints(int dimension, float other_dimension_size) const override; + + void update(rectangle const & value); + + private: + react::value square_; + }; + +} diff --git a/libs/ui/include/psemek/ui/impl/single_container.hpp b/libs/ui/include/psemek/ui/impl/single_container.hpp index cd7f60b5..2efae3cb 100644 --- a/libs/ui/include/psemek/ui/impl/single_container.hpp +++ b/libs/ui/include/psemek/ui/impl/single_container.hpp @@ -22,6 +22,11 @@ namespace psemek::ui::impl child_token_ = std::move(token); } + component * child() const + { + return child_.get(); + } + std::unique_ptr release_child() { return std::move(child_); diff --git a/libs/ui/include/psemek/ui/impl/single_container_base.hpp b/libs/ui/include/psemek/ui/impl/single_container_base.hpp new file mode 100644 index 00000000..da154183 --- /dev/null +++ b/libs/ui/include/psemek/ui/impl/single_container_base.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include +#include + +namespace psemek::ui::impl +{ + + struct single_container_base + : single_container + { + single_container_base(react::value> margin); + + void reshape(geom::box const & new_shape) override; + geom::interval size_constraints(int dimension, float other_dimension_size) const override; + + private: + react::value> margin_; + }; + +} diff --git a/libs/ui/include/psemek/ui/impl/stack_layout_base.hpp b/libs/ui/include/psemek/ui/impl/stack_layout_base.hpp new file mode 100644 index 00000000..0b33aeeb --- /dev/null +++ b/libs/ui/include/psemek/ui/impl/stack_layout_base.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include +#include +#include + +namespace psemek::ui::impl +{ + + struct stack_layout_base + : container + { + void reshape(geom::box const & new_shape) override; + geom::interval size_constraints(int dimension, float other_dimension_size) const override; + + void update(stack_layout const &) + {} + }; + +} diff --git a/libs/ui/source/impl/box_layout_base.cpp b/libs/ui/source/impl/box_layout_base.cpp new file mode 100644 index 00000000..e778fb5a --- /dev/null +++ b/libs/ui/source/impl/box_layout_base.cpp @@ -0,0 +1,186 @@ +#include +#include +#include + +namespace psemek::ui::impl +{ + + namespace + { + + static constexpr box_layout::size_policy default_policy = box_layout::weight{}; + + template + std::vector allocate(float total_size, float other_dimension_size, + std::vector> const & policies, util::span const> children) + { + std::vector result(policies.size(), 0.f); + + float total_weight = 0.f; + + // First, allocate minimized & fixed elements + // Also compute the total weight for weighted elements + for (std::size_t i = 0; i < policies.size(); ++i) + { + auto const policy = policies[i] ? *policies[i] : default_policy; + + if (std::get_if(&policy)) + { + result[i] = children[i]->size_constraints(Dimension, other_dimension_size).min; + total_size -= result[i]; + } + else if (auto fixed = std::get_if(&policy)) + { + result[i] = fixed->value; + total_size -= result[i]; + } + else if (auto weight = std::get_if(&policy)) + { + total_weight += weight->value; + } + } + + // Next, allocate weighted elements + for (std::size_t i = 0; i < policies.size(); ++i) + { + auto const policy = policies[i] ? *policies[i] : default_policy; + + if (auto weight = std::get_if(&policy)) + { + result[i] = total_size * weight->value / total_weight; + } + } + + return result; + } + + } + + template + box_layout_base::box_layout_base(react::value margin) + : margin_(margin) + {} + + template + void box_layout_base::reshape(geom::box const & new_shape) + { + container::reshape(new_shape); + + if (children().empty()) + return; + + float const margin = *margin_; + + auto sizes = allocate(new_shape[Dimension].length() - (children().size() - 1) * margin, new_shape[Dimension ^ 1].length(), *size_policies_, children()); + + float pen = new_shape[Dimension].min; + for (std::size_t i = 0; i < children().size(); ++i) + { + if (!children()[i]) continue; + + geom::box child_shape; + child_shape[Dimension] = {pen, pen + sizes[i]}; + child_shape[Dimension ^ 1] = new_shape[Dimension ^ 1]; + children()[i]->reshape(child_shape); + + pen += sizes[i]; + pen += margin; + } + } + + template + geom::interval box_layout_base::size_constraints(int dimension, float other_dimension_size) const + { + if (children().empty()) + return container::size_constraints(dimension, other_dimension_size); + + float const margin = *margin_; + + if (dimension == Dimension) + { + auto result = geom::interval{0.f, 0.f}; + + auto const & policies = *size_policies_; + + auto weighted_unit_constraints = geom::interval{0.f, infinity}; + float weight_sum = 0.f; + + for (std::size_t i = 0; i < children().size(); ++i) + { + auto const & child = children()[i]; + auto const policy = policies[i] ? *policies[i] : default_policy; + + if (auto fixed = std::get_if(&policy)) + result += fixed->value; + else if (std::get_if(&policy)) + { + if (child) + { + auto child_constraints = child->size_constraints(dimension, other_dimension_size); + result.min += child_constraints.min; + result.max += child_constraints.max; + } + else + { + result.max = infinity; + } + } + else if (auto weight = std::get_if(&policy)) + { + if (child) + { + auto child_constraints = child->size_constraints(dimension, other_dimension_size); + child_constraints.min /= weight->value; + child_constraints.max /= weight->value; + weighted_unit_constraints &= child_constraints; + } + + weight_sum += weight->value; + } + } + + // Prevent NaN in result.max (inf * 0) + if (weight_sum > 0.f) + { + result.min += weighted_unit_constraints.min * weight_sum; + result.max += weighted_unit_constraints.max * weight_sum; + } + + result += (children().size() - 1) * margin; + + return result; + } + else + { + float const margin = *margin_; + + auto sizes = allocate(other_dimension_size - (children().size() - 1) * margin, shape()[1].length(), *size_policies_, children()); + + auto result = container::size_constraints(dimension, other_dimension_size); + + for (std::size_t i = 0; i < children().size(); ++i) + { + if (!children()[i]) continue; + result &= children()[i]->size_constraints(dimension, sizes[i]); + } + + return result; + } + } + + template + void box_layout_base::update(typename detail::box_layout_type::type const & box_layout) + { + size_policies_ = react::map([](auto const & children){ + std::vector> size_policies; + size_policies.reserve(children.size()); + for (auto const & child : children) + size_policies.push_back(child.policy); + return size_policies; + }, box_layout.children); + } + + template struct box_layout_base<0>; + template struct box_layout_base<1>; + +} diff --git a/libs/ui/source/impl/component.cpp b/libs/ui/source/impl/component.cpp index 381defcd..c425f382 100644 --- a/libs/ui/source/impl/component.cpp +++ b/libs/ui/source/impl/component.cpp @@ -8,4 +8,19 @@ namespace psemek::ui::impl return {}; } + geom::box const & component::shape() const + { + return shape_; + } + + void component::reshape(geom::box const & new_shape) + { + shape_ = new_shape; + } + + geom::interval component::size_constraints(int, float) const + { + return {0.f, infinity}; + } + } diff --git a/libs/ui/source/impl/rectangle_base.cpp b/libs/ui/source/impl/rectangle_base.cpp new file mode 100644 index 00000000..a70040d0 --- /dev/null +++ b/libs/ui/source/impl/rectangle_base.cpp @@ -0,0 +1,19 @@ +#include + +namespace psemek::ui::impl +{ + + geom::interval rectangle_base::size_constraints(int dimension, float other_dimension_size) const + { + if (square_ && *square_) + return {other_dimension_size, other_dimension_size}; + else + return component::size_constraints(dimension, other_dimension_size); + } + + void rectangle_base::update(rectangle const & value) + { + square_ = value.square; + } + +} diff --git a/libs/ui/source/impl/single_container_base.cpp b/libs/ui/source/impl/single_container_base.cpp new file mode 100644 index 00000000..81cc1b98 --- /dev/null +++ b/libs/ui/source/impl/single_container_base.cpp @@ -0,0 +1,28 @@ +#include + +namespace psemek::ui::impl +{ + + single_container_base::single_container_base(react::value> margin) + : margin_(std::move(margin)) + {} + + void single_container_base::reshape(geom::box const & new_shape) + { + single_container::reshape(new_shape); + + if (child()) + child()->reshape(geom::shrink(new_shape, *margin_)); + } + + geom::interval single_container_base::size_constraints(int dimension, float other_dimension_size) const + { + geom::interval result = single_container::size_constraints(dimension, other_dimension_size); + + if (child()) + result = child()->size_constraints(dimension, other_dimension_size - 2.f * (*margin_)[dimension ^ 1]); + + return result + 2.f * (*margin_)[dimension]; + } + +} diff --git a/libs/ui/source/impl/stack_layout_base.cpp b/libs/ui/source/impl/stack_layout_base.cpp new file mode 100644 index 00000000..676a0387 --- /dev/null +++ b/libs/ui/source/impl/stack_layout_base.cpp @@ -0,0 +1,24 @@ +#include + +namespace psemek::ui::impl +{ + + void stack_layout_base::reshape(geom::box const & new_shape) + { + for (auto const & child : children()) + if (child) + child->reshape(new_shape); + } + + geom::interval stack_layout_base::size_constraints(int dimension, float other_dimension_size) const + { + auto result = container::size_constraints(dimension, other_dimension_size); + + for (auto const & child : children()) + if (child) + result &= child->size_constraints(dimension, other_dimension_size); + + return result; + } + +} diff --git a/libs/ui/tests/layout.cpp b/libs/ui/tests/layout.cpp new file mode 100644 index 00000000..65a53b9a --- /dev/null +++ b/libs/ui/tests/layout.cpp @@ -0,0 +1,92 @@ +#include + +#include +#include +#include +#include +#include + +#include + +using namespace psemek; +using namespace psemek::ui; + +namespace +{ + + constexpr float layout_margin = 5.f; + constexpr float infinity = impl::component::infinity; + + struct test_component_factory + : impl::component_factory_base + { + test_component_factory() + { + register_type(); + register_type(); + register_type>([]{ return std::make_unique>(layout_margin); }); + register_type>([]{ return std::make_unique>(layout_margin); }); + } + }; + +} + +test_case(ui_layout_rectangle) +{ + test_component_factory factory; + + auto square = react::source(false); + + auto test_ui = rectangle{.square = square}; + + auto ui_root = factory.reconciliate(nullptr, test_ui); + + expect_equal(ui_root->size_constraints(0, 100.f), geom::interval(0.f, infinity)); + expect_equal(ui_root->size_constraints(1, 100.f), geom::interval(0.f, infinity)); + + square.set(true); + + expect_equal(ui_root->size_constraints(0, 100.f), geom::interval(100.f, 100.f)); + expect_equal(ui_root->size_constraints(1, 100.f), geom::interval(100.f, 100.f)); + + ui_root->reshape({{{0.f, 10.f}, {0.f, 10.f}}}); + + expect_equal(ui_root->shape(), (geom::box{{{0.f, 10.f}, {0.f, 10.f}}})); +} + + +test_case(ui_layout_hbox__2__rectangles) +{ + test_component_factory factory; + + auto square0 = react::source(false); + auto square1 = react::source(false); + + auto policy0 = react::source(box_layout::minimized{}); + auto policy1 = react::source(box_layout::minimized{}); + + auto test_ui = box_layout::horizontal{{ + { + .element = rectangle{.square = square0}, + .policy = policy0, + }, + { + .element = rectangle{.square = square1}, + .policy = policy1, + } + }}; + + auto ui_root = factory.reconciliate(nullptr, test_ui); + + expect_equal(ui_root->size_constraints(0, 100.f), geom::interval(layout_margin, infinity)); + expect_equal(ui_root->size_constraints(1, 100.f), geom::interval(0.f, infinity)); + + square0.set(true); + + expect_equal(ui_root->size_constraints(0, 100.f), geom::interval(100.f + layout_margin, infinity)); + expect_equal(ui_root->size_constraints(1, 100.f), geom::interval(100.f, 100.f)); + + ui_root->reshape({{{0.f, 10.f}, {0.f, 10.f}}}); + + expect_equal(ui_root->shape(), (geom::box{{{0.f, 10.f}, {0.f, 10.f}}})); +}