New UI library automatic layout wip
This commit is contained in:
parent
2c13bcac15
commit
e8ea3e0fd4
13 changed files with 502 additions and 1 deletions
|
|
@ -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<weight, minimized>;
|
||||
using size_policy = std::variant<minimized, fixed, weight>;
|
||||
|
||||
struct element
|
||||
{
|
||||
|
|
|
|||
51
libs/ui/include/psemek/ui/impl/box_layout_base.hpp
Normal file
51
libs/ui/include/psemek/ui/impl/box_layout_base.hpp
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
#pragma once
|
||||
|
||||
#include <psemek/ui/impl/container.hpp>
|
||||
#include <psemek/ui/box_layout.hpp>
|
||||
#include <psemek/react/value.hpp>
|
||||
|
||||
namespace psemek::ui::impl
|
||||
{
|
||||
|
||||
namespace detail
|
||||
{
|
||||
|
||||
template <int Dimension>
|
||||
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 <int Dimension>
|
||||
struct box_layout_base
|
||||
: container
|
||||
{
|
||||
box_layout_base(react::value<float> margin);
|
||||
|
||||
void reshape(geom::box<float, 2> const & new_shape) override;
|
||||
geom::interval<float> size_constraints(int dimension, float other_dimension_size) const override;
|
||||
|
||||
void update(typename detail::box_layout_type<Dimension>::type const & box_layout);
|
||||
|
||||
private:
|
||||
react::value<float> margin_;
|
||||
react::value<std::vector<react::value<box_layout::size_policy>>> size_policies_;
|
||||
};
|
||||
|
||||
extern template struct box_layout_base<0>;
|
||||
extern template struct box_layout_base<1>;
|
||||
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
#pragma once
|
||||
|
||||
#include <psemek/util/span.hpp>
|
||||
#include <psemek/geom/box.hpp>
|
||||
#include <psemek/geom/interval.hpp>
|
||||
|
||||
#include <memory>
|
||||
|
||||
|
|
@ -9,9 +11,21 @@ namespace psemek::ui::impl
|
|||
|
||||
struct component
|
||||
{
|
||||
static constexpr float infinity = std::numeric_limits<float>::infinity();
|
||||
|
||||
virtual util::span<std::unique_ptr<component> const> children() const;
|
||||
|
||||
virtual geom::box<float, 2> const & shape() const;
|
||||
virtual void reshape(geom::box<float, 2> const & new_shape);
|
||||
|
||||
// size_constraints(0, height) is width_constraints(height)
|
||||
// size_constraints(1, width) is height_constraints(width)
|
||||
virtual geom::interval<float> size_constraints(int dimension, float other_dimension_size) const;
|
||||
|
||||
virtual ~component() {}
|
||||
|
||||
private:
|
||||
geom::box<float, 2> shape_;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
|||
21
libs/ui/include/psemek/ui/impl/rectangle_base.hpp
Normal file
21
libs/ui/include/psemek/ui/impl/rectangle_base.hpp
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
#pragma once
|
||||
|
||||
#include <psemek/ui/impl/component.hpp>
|
||||
#include <psemek/ui/rectangle.hpp>
|
||||
#include <psemek/react/value.hpp>
|
||||
|
||||
namespace psemek::ui::impl
|
||||
{
|
||||
|
||||
struct rectangle_base
|
||||
: component
|
||||
{
|
||||
geom::interval<float> size_constraints(int dimension, float other_dimension_size) const override;
|
||||
|
||||
void update(rectangle const & value);
|
||||
|
||||
private:
|
||||
react::value<bool> square_;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
@ -22,6 +22,11 @@ namespace psemek::ui::impl
|
|||
child_token_ = std::move(token);
|
||||
}
|
||||
|
||||
component * child() const
|
||||
{
|
||||
return child_.get();
|
||||
}
|
||||
|
||||
std::unique_ptr<component> release_child()
|
||||
{
|
||||
return std::move(child_);
|
||||
|
|
|
|||
21
libs/ui/include/psemek/ui/impl/single_container_base.hpp
Normal file
21
libs/ui/include/psemek/ui/impl/single_container_base.hpp
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
#pragma once
|
||||
|
||||
#include <psemek/ui/impl/single_container.hpp>
|
||||
#include <psemek/react/value.hpp>
|
||||
|
||||
namespace psemek::ui::impl
|
||||
{
|
||||
|
||||
struct single_container_base
|
||||
: single_container
|
||||
{
|
||||
single_container_base(react::value<geom::vector<float, 2>> margin);
|
||||
|
||||
void reshape(geom::box<float, 2> const & new_shape) override;
|
||||
geom::interval<float> size_constraints(int dimension, float other_dimension_size) const override;
|
||||
|
||||
private:
|
||||
react::value<geom::vector<float, 2>> margin_;
|
||||
};
|
||||
|
||||
}
|
||||
20
libs/ui/include/psemek/ui/impl/stack_layout_base.hpp
Normal file
20
libs/ui/include/psemek/ui/impl/stack_layout_base.hpp
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
#pragma once
|
||||
|
||||
#include <psemek/ui/impl/container.hpp>
|
||||
#include <psemek/ui/stack_layout.hpp>
|
||||
#include <psemek/react/value.hpp>
|
||||
|
||||
namespace psemek::ui::impl
|
||||
{
|
||||
|
||||
struct stack_layout_base
|
||||
: container
|
||||
{
|
||||
void reshape(geom::box<float, 2> const & new_shape) override;
|
||||
geom::interval<float> size_constraints(int dimension, float other_dimension_size) const override;
|
||||
|
||||
void update(stack_layout const &)
|
||||
{}
|
||||
};
|
||||
|
||||
}
|
||||
186
libs/ui/source/impl/box_layout_base.cpp
Normal file
186
libs/ui/source/impl/box_layout_base.cpp
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
#include <psemek/ui/impl/box_layout_base.hpp>
|
||||
#include <psemek/react/map.hpp>
|
||||
#include <psemek/react/join.hpp>
|
||||
|
||||
namespace psemek::ui::impl
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
static constexpr box_layout::size_policy default_policy = box_layout::weight{};
|
||||
|
||||
template <int Dimension>
|
||||
std::vector<float> allocate(float total_size, float other_dimension_size,
|
||||
std::vector<react::value<box_layout::size_policy>> const & policies, util::span<std::unique_ptr<component> const> children)
|
||||
{
|
||||
std::vector<float> 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<box_layout::minimized>(&policy))
|
||||
{
|
||||
result[i] = children[i]->size_constraints(Dimension, other_dimension_size).min;
|
||||
total_size -= result[i];
|
||||
}
|
||||
else if (auto fixed = std::get_if<box_layout::fixed>(&policy))
|
||||
{
|
||||
result[i] = fixed->value;
|
||||
total_size -= result[i];
|
||||
}
|
||||
else if (auto weight = std::get_if<box_layout::weight>(&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<box_layout::weight>(&policy))
|
||||
{
|
||||
result[i] = total_size * weight->value / total_weight;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
template <int Dimension>
|
||||
box_layout_base<Dimension>::box_layout_base(react::value<float> margin)
|
||||
: margin_(margin)
|
||||
{}
|
||||
|
||||
template <int Dimension>
|
||||
void box_layout_base<Dimension>::reshape(geom::box<float, 2> const & new_shape)
|
||||
{
|
||||
container::reshape(new_shape);
|
||||
|
||||
if (children().empty())
|
||||
return;
|
||||
|
||||
float const margin = *margin_;
|
||||
|
||||
auto sizes = allocate<Dimension>(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<float, 2> 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 <int Dimension>
|
||||
geom::interval<float> box_layout_base<Dimension>::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<box_layout::fixed>(&policy))
|
||||
result += fixed->value;
|
||||
else if (std::get_if<box_layout::minimized>(&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<box_layout::weight>(&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<Dimension>(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 <int Dimension>
|
||||
void box_layout_base<Dimension>::update(typename detail::box_layout_type<Dimension>::type const & box_layout)
|
||||
{
|
||||
size_policies_ = react::map([](auto const & children){
|
||||
std::vector<react::value<box_layout::size_policy>> 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>;
|
||||
|
||||
}
|
||||
|
|
@ -8,4 +8,19 @@ namespace psemek::ui::impl
|
|||
return {};
|
||||
}
|
||||
|
||||
geom::box<float, 2> const & component::shape() const
|
||||
{
|
||||
return shape_;
|
||||
}
|
||||
|
||||
void component::reshape(geom::box<float, 2> const & new_shape)
|
||||
{
|
||||
shape_ = new_shape;
|
||||
}
|
||||
|
||||
geom::interval<float> component::size_constraints(int, float) const
|
||||
{
|
||||
return {0.f, infinity};
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
19
libs/ui/source/impl/rectangle_base.cpp
Normal file
19
libs/ui/source/impl/rectangle_base.cpp
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
#include <psemek/ui/impl/rectangle_base.hpp>
|
||||
|
||||
namespace psemek::ui::impl
|
||||
{
|
||||
|
||||
geom::interval<float> 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;
|
||||
}
|
||||
|
||||
}
|
||||
28
libs/ui/source/impl/single_container_base.cpp
Normal file
28
libs/ui/source/impl/single_container_base.cpp
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
#include <psemek/ui/impl/single_container_base.hpp>
|
||||
|
||||
namespace psemek::ui::impl
|
||||
{
|
||||
|
||||
single_container_base::single_container_base(react::value<geom::vector<float, 2>> margin)
|
||||
: margin_(std::move(margin))
|
||||
{}
|
||||
|
||||
void single_container_base::reshape(geom::box<float, 2> const & new_shape)
|
||||
{
|
||||
single_container::reshape(new_shape);
|
||||
|
||||
if (child())
|
||||
child()->reshape(geom::shrink(new_shape, *margin_));
|
||||
}
|
||||
|
||||
geom::interval<float> single_container_base::size_constraints(int dimension, float other_dimension_size) const
|
||||
{
|
||||
geom::interval<float> 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];
|
||||
}
|
||||
|
||||
}
|
||||
24
libs/ui/source/impl/stack_layout_base.cpp
Normal file
24
libs/ui/source/impl/stack_layout_base.cpp
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
#include <psemek/ui/impl/stack_layout_base.hpp>
|
||||
|
||||
namespace psemek::ui::impl
|
||||
{
|
||||
|
||||
void stack_layout_base::reshape(geom::box<float, 2> const & new_shape)
|
||||
{
|
||||
for (auto const & child : children())
|
||||
if (child)
|
||||
child->reshape(new_shape);
|
||||
}
|
||||
|
||||
geom::interval<float> 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;
|
||||
}
|
||||
|
||||
}
|
||||
92
libs/ui/tests/layout.cpp
Normal file
92
libs/ui/tests/layout.cpp
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
#include <psemek/test/test.hpp>
|
||||
|
||||
#include <psemek/ui/impl/component_factory_base.hpp>
|
||||
#include <psemek/ui/impl/rectangle_base.hpp>
|
||||
#include <psemek/ui/impl/single_container_base.hpp>
|
||||
#include <psemek/ui/impl/stack_layout_base.hpp>
|
||||
#include <psemek/ui/impl/box_layout_base.hpp>
|
||||
|
||||
#include <psemek/react/source.hpp>
|
||||
|
||||
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<rectangle, impl::rectangle_base>();
|
||||
register_type<stack_layout, impl::stack_layout_base>();
|
||||
register_type<box_layout::horizontal, impl::box_layout_base<0>>([]{ return std::make_unique<impl::box_layout_base<0>>(layout_margin); });
|
||||
register_type<box_layout::vertical, impl::box_layout_base<1>>([]{ return std::make_unique<impl::box_layout_base<1>>(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<float>(0.f, infinity));
|
||||
expect_equal(ui_root->size_constraints(1, 100.f), geom::interval<float>(0.f, infinity));
|
||||
|
||||
square.set(true);
|
||||
|
||||
expect_equal(ui_root->size_constraints(0, 100.f), geom::interval<float>(100.f, 100.f));
|
||||
expect_equal(ui_root->size_constraints(1, 100.f), geom::interval<float>(100.f, 100.f));
|
||||
|
||||
ui_root->reshape({{{0.f, 10.f}, {0.f, 10.f}}});
|
||||
|
||||
expect_equal(ui_root->shape(), (geom::box<float, 2>{{{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::size_policy>(box_layout::minimized{});
|
||||
auto policy1 = react::source<box_layout::size_policy>(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<float>(layout_margin, infinity));
|
||||
expect_equal(ui_root->size_constraints(1, 100.f), geom::interval<float>(0.f, infinity));
|
||||
|
||||
square0.set(true);
|
||||
|
||||
expect_equal(ui_root->size_constraints(0, 100.f), geom::interval<float>(100.f + layout_margin, infinity));
|
||||
expect_equal(ui_root->size_constraints(1, 100.f), geom::interval<float>(100.f, 100.f));
|
||||
|
||||
ui_root->reshape({{{0.f, 10.f}, {0.f, 10.f}}});
|
||||
|
||||
expect_equal(ui_root->shape(), (geom::box<float, 2>{{{0.f, 10.f}, {0.f, 10.f}}}));
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue