UI library wip: event handling, auto-layout, basic layouts & buttons

This commit is contained in:
Nikita Lisitsa 2024-07-28 19:28:38 +03:00
parent 546c0f2a7b
commit ae815ec538
25 changed files with 547 additions and 32 deletions

View file

@ -3,6 +3,6 @@ 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)
target_link_libraries(psemek-ui PUBLIC psemek-util psemek-react psemek-geom psemek-gfx psemek-app)
psemek_glob_tests(psemek-ui tests)

View file

@ -0,0 +1,18 @@
#pragma once
#include <psemek/ui/alignment.hpp>
#include <psemek/react/value.hpp>
#include <any>
namespace psemek::ui
{
struct aligned
{
react::value<std::any> child = {};
react::value<halignment> halign = halignment::center;
react::value<valignment> valign = valignment::center;
};
}

View file

@ -1,5 +1,8 @@
#pragma once
#include <psemek/geom/interval.hpp>
#include <psemek/util/enum.hpp>
namespace psemek::ui
{
@ -17,4 +20,43 @@ namespace psemek::ui
bottom,
};
template <typename T>
concept any_alignment = std::is_same_v<T, halignment> || std::is_same_v<T, valignment>;
inline float lerp_factor(halignment h)
{
switch (h)
{
case halignment::left: return 0;
case halignment::center: return 0.5f;
case halignment::right: return 1.f;
}
throw util::unknown_enum_value_exception{h};
}
inline float lerp_factor(valignment h)
{
switch (h)
{
case valignment::top: return 0.f;
case valignment::center: return 0.5f;
case valignment::bottom: return 1.f;
}
throw util::unknown_enum_value_exception{h};
}
template <any_alignment Alignment>
inline float align(geom::interval<float> const & range, Alignment a)
{
return geom::lerp(range, lerp_factor(a));
}
template <any_alignment Alignment>
inline geom::interval<float> align(geom::interval<float> const & range, float size, Alignment a)
{
return geom::interval{range.min, range.min + size} + (range.length() - size) * lerp_factor(a);
}
}

View file

@ -33,12 +33,14 @@ namespace psemek::ui
{
using element = box_layout::element;
react::value<std::vector<element>> children = {};
react::value<float> padding = 0.f;
};
struct vertical
{
using element = box_layout::element;
react::value<std::vector<element>> children = {};
react::value<float> padding = 0.f;
};
}

View file

@ -3,15 +3,13 @@
#include <psemek/react/value.hpp>
#include <psemek/react/source.hpp>
#include <any>
namespace psemek::ui
{
struct button
{
react::source<void> on_click = {};
react::value<std::any> child = {};
react::source<bool> mouseover = {};
react::source<bool> mousedown = {};
};
}

View file

@ -0,0 +1,29 @@
#pragma once
#include <psemek/react/value.hpp>
#include <psemek/geom/vector.hpp>
#include <any>
namespace psemek::ui
{
struct fixed_size
{
react::value<std::any> child = {};
react::value<geom::vector<float, 2>> size = {0.f, 0.f};
};
struct fixed_width
{
react::value<std::any> child = {};
react::value<float> width = 0.f;
};
struct fixed_height
{
react::value<std::any> child = {};
react::value<float> height = 0.f;
};
}

View file

@ -0,0 +1,29 @@
#pragma once
#include <psemek/ui/aligned.hpp>
#include <psemek/ui/impl/single_container.hpp>
#include <psemek/react/source.hpp>
namespace psemek::ui::impl
{
struct aligned_base
: single_container
{
aligned_base();
void reshape(geom::box<float, 2> const & new_shape) override;
react::value<struct size_constraints> size_constraints() const override;
void set_child(std::unique_ptr<component> child) override;
std::unique_ptr<component> release_child() override;
void update(aligned const & value);
private:
react::source<react::value<std::pair<halignment, valignment>>> align_;
react::source<react::value<struct size_constraints>> child_size_constraints_;
react::value<struct size_constraints> size_constraints_;
};
}

View file

@ -34,7 +34,7 @@ namespace psemek::ui::impl
struct box_layout_base
: container
{
box_layout_base(react::value<float> margin);
box_layout_base();
void reshape(geom::box<float, 2> const & new_shape) override;
react::value<struct size_constraints> size_constraints() const override;
@ -45,7 +45,7 @@ namespace psemek::ui::impl
void update(typename detail::box_layout_type<Dimension>::type const & box_layout);
private:
react::value<float> margin_;
react::source<react::value<float>> padding_;
react::value<std::vector<react::value<box_layout::size_policy>>> size_policies_;
react::source<std::vector<react::value<struct size_constraints>>> children_size_constraints_;
react::value<struct size_constraints> size_constraints_;

View file

@ -0,0 +1,27 @@
#pragma once
#include <psemek/ui/button.hpp>
#include <psemek/ui/impl/component.hpp>
namespace psemek::ui::impl
{
struct button_base
: component
{
button_base();
bool on_event(mouse_move_event const & event) override;
bool on_event(mouse_button_event const & event) override;
void update(button const & value);
private:
bool is_mouseover_ = false;
bool is_mousedown_ = false;
react::source<bool> mouseover_;
react::source<bool> mousedown_;
};
}

View file

@ -1,6 +1,7 @@
#pragma once
#include <psemek/ui/impl/size_constraints.hpp>
#include <psemek/ui/impl/events.hpp>
#include <psemek/react/value.hpp>
#include <psemek/geom/box.hpp>
#include <psemek/geom/interval.hpp>
@ -13,8 +14,6 @@ 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;
@ -22,6 +21,12 @@ namespace psemek::ui::impl
virtual react::value<struct size_constraints> size_constraints() const;
virtual bool on_event(resize_event const &) { return false; }
virtual bool on_event(mouse_move_event const &) { return false; }
virtual bool on_event(mouse_wheel_event const &) { return false; }
virtual bool on_event(mouse_button_event const &) { return false; }
virtual bool on_event(key_event const &) { return false; }
virtual ~component() {}
private:

View file

@ -16,6 +16,12 @@ namespace psemek::ui::impl
void set_ui(react::value<std::any> ui);
bool on_event(resize_event const & event);
bool on_event(mouse_move_event const & event);
bool on_event(mouse_wheel_event const & event);
bool on_event(mouse_button_event const & event);
bool on_event(key_event const & event);
private:
psemek_declare_pimpl
};

View file

@ -0,0 +1,14 @@
#pragma once
#include <psemek/ui/impl/component_factory_base.hpp>
namespace psemek::ui::impl
{
struct default_component_factory
: component_factory_base
{
default_component_factory();
};
}

View file

@ -0,0 +1,14 @@
#pragma once
#include <psemek/app/events.hpp>
namespace psemek::ui::impl
{
using app::resize_event;
using app::mouse_move_event;
using app::mouse_button_event;
using app::mouse_wheel_event;
using app::key_event;
}

View file

@ -0,0 +1,28 @@
#pragma once
#include <psemek/ui/fixed_size.hpp>
#include <psemek/ui/impl/single_container.hpp>
#include <psemek/react/source.hpp>
namespace psemek::ui::impl
{
struct fixed_size_base
: single_container
{
fixed_size_base();
void reshape(geom::box<float, 2> const & new_shape) override;
react::value<struct size_constraints> size_constraints() const override;
void update(fixed_size const & value);
void update(fixed_width const & value);
void update(fixed_height const & value);
private:
react::source<react::value<geom::vector<std::optional<float>, 2>>> size_;
react::source<react::value<struct size_constraints>> child_size_constraints_;
react::value<struct size_constraints> size_constraints_;
};
}

View file

@ -2,6 +2,8 @@
#include <psemek/geom/box.hpp>
#include <limits>
namespace psemek::ui::impl
{
@ -9,6 +11,8 @@ namespace psemek::ui::impl
{
geom::box<float, 2> box;
static constexpr float infinity = std::numeric_limits<float>::infinity();
static size_constraints max();
};

View file

@ -0,0 +1,71 @@
#include <psemek/ui/impl/aligned_base.hpp>
#include <psemek/react/join.hpp>
#include <psemek/react/map.hpp>
namespace psemek::ui::impl
{
namespace
{
size_constraints compute_size_constraints(size_constraints const & child_size_constraints)
{
return {
.box = {{
{child_size_constraints.box[0].min, size_constraints::infinity},
{child_size_constraints.box[1].min, size_constraints::infinity},
}}
};
}
}
aligned_base::aligned_base()
: align_({halignment::center, valignment::center})
, child_size_constraints_(size_constraints::max())
, size_constraints_(react::map(compute_size_constraints, react::join(child_size_constraints_)))
{}
void aligned_base::reshape(geom::box<float, 2> const & new_shape)
{
single_container::reshape(new_shape);
auto [ halign, valign ] = **align_;
auto child_size_constraints = **child_size_constraints_;
if (auto child = this->child())
child->reshape({
align(new_shape[0], child_size_constraints.box[0].min, halign),
align(new_shape[1], child_size_constraints.box[1].min, valign),
});
}
react::value<struct size_constraints> aligned_base::size_constraints() const
{
return size_constraints_;
}
void aligned_base::set_child(std::unique_ptr<component> child)
{
if (child)
child_size_constraints_.set(child->size_constraints());
else
child_size_constraints_.set(size_constraints::max());
single_container::set_child(std::move(child));
}
std::unique_ptr<component> aligned_base::release_child()
{
child_size_constraints_.set(size_constraints::max());
return single_container::release_child();
}
void aligned_base::update(aligned const & value)
{
align_.set(react::map([](auto const & halign, auto const & valign){ return std::pair{halign, valign}; }, value.halign, value.valign));
}
}

View file

@ -12,7 +12,7 @@ namespace psemek::ui::impl
template <int Dimension>
size_constraints compute_size_constraints(std::vector<box_layout::size_policy> const & size_policies,
std::vector<size_constraints> const & children_size_constraints, float margin)
std::vector<size_constraints> const & children_size_constraints, float padding)
{
size_constraints minimized = size_constraints::max();
size_constraints weight_unit = minimized;
@ -39,7 +39,7 @@ namespace psemek::ui::impl
if (size_policies.size() > 0)
{
geom::vector shift_delta{0.f, 0.f};
shift_delta[Dimension] = margin * (size_policies.size() - 1);
shift_delta[Dimension] = padding * (size_policies.size() - 1);
result = shift(std::move(result), shift_delta);
}
@ -47,18 +47,21 @@ namespace psemek::ui::impl
}
template <int Dimension>
std::vector<float> allocate(float total_size, std::vector<react::value<box_layout::size_policy>> const & policies,
util::span<std::unique_ptr<component> const> children)
std::vector<float> allocate(float total_size, std::vector<react::value<box_layout::size_policy>> const & size_policies,
util::span<std::unique_ptr<component> const> children, float padding)
{
std::vector<float> result(policies.size(), 0.f);
std::vector<float> result(size_policies.size(), 0.f);
if (!size_policies.empty())
total_size -= padding * (size_policies.size() - 1);
float total_weight = 0.f;
// First, allocate minimized elements
// Also compute the total weight for weighted elements
for (std::size_t i = 0; i < policies.size(); ++i)
for (std::size_t i = 0; i < size_policies.size(); ++i)
{
auto const policy = policies[i] ? *policies[i] : default_policy;
auto const policy = size_policies[i] ? *size_policies[i] : default_policy;
if (std::get_if<box_layout::minimized>(&policy))
{
@ -72,9 +75,9 @@ namespace psemek::ui::impl
}
// Next, allocate weighted elements
for (std::size_t i = 0; i < policies.size(); ++i)
for (std::size_t i = 0; i < size_policies.size(); ++i)
{
auto const policy = policies[i] ? *policies[i] : default_policy;
auto const policy = size_policies[i] ? *size_policies[i] : default_policy;
if (auto weight = std::get_if<box_layout::weight>(&policy))
{
@ -88,18 +91,18 @@ namespace psemek::ui::impl
}
template <int Dimension>
box_layout_base<Dimension>::box_layout_base(react::value<float> margin)
: margin_(margin)
box_layout_base<Dimension>::box_layout_base()
: padding_(0.f)
, size_policies_({})
, children_size_constraints_()
, size_constraints_(
react::map(
[](auto const & size_policies, auto const & children_size_constraints, auto const & margin){
return compute_size_constraints<Dimension>(size_policies, children_size_constraints, margin);
[](auto const & size_policies, auto const & children_size_constraints, auto const & padding){
return compute_size_constraints<Dimension>(size_policies, children_size_constraints, padding);
},
react::join(react::map(react::unpack_with_default(default_policy), size_policies_)),
react::join(react::map(react::unpack, children_size_constraints_)),
margin_
react::join(padding_)
)
)
{}
@ -112,9 +115,9 @@ namespace psemek::ui::impl
if (children().empty())
return;
float const margin = *margin_;
auto padding = **padding_;
auto sizes = allocate<Dimension>(new_shape[Dimension].length() - (children().size() - 1) * margin, *size_policies_, children());
auto sizes = allocate<Dimension>(new_shape[Dimension].length(), *size_policies_, children(), padding);
float pen = new_shape[Dimension].min;
for (std::size_t i = 0; i < children().size(); ++i)
@ -127,7 +130,7 @@ namespace psemek::ui::impl
children()[i]->reshape(child_shape);
pen += sizes[i];
pen += margin;
pen += padding;
}
}
@ -171,6 +174,8 @@ namespace psemek::ui::impl
size_policies.push_back(child.policy);
return size_policies;
}, box_layout.children);
padding_.set(box_layout.padding);
}
template struct box_layout_base<0>;

View file

@ -0,0 +1,47 @@
#include <psemek/ui/impl/button_base.hpp>
#include <psemek/geom/contains.hpp>
namespace psemek::ui::impl
{
button_base::button_base()
{}
bool button_base::on_event(mouse_move_event const & event)
{
bool const mouseover = geom::contains(shape(), geom::cast<float>(event.position));
if (is_mouseover_ != mouseover)
{
is_mouseover_ = mouseover;
mouseover_.set(is_mouseover_);
}
return is_mouseover_;
}
bool button_base::on_event(mouse_button_event const & event)
{
if (is_mouseover_)
{
bool const mousedown = event.button == app::mouse_button::left && event.down;
if (is_mousedown_ != mousedown)
{
is_mousedown_ = mousedown;
mousedown_.set(is_mouseover_);
}
return true;
}
return false;
}
void button_base::update(button const & value)
{
mouseover_ = value.mouseover;
mousedown_ = value.mousedown;
}
}

View file

@ -1,4 +1,5 @@
#include <psemek/ui/impl/container.hpp>
#include <psemek/util/range.hpp>
namespace psemek::ui::impl
{

View file

@ -7,9 +7,69 @@ namespace psemek::ui::impl
{
component_factory & factory;
geom::vector<int, 2> screen_size = {0, 0};
util::signal<>::subscription_token root_token;
util::signal<>::subscription_token root_reshape_token;
std::unique_ptr<ui::impl::component> root;
impl(component_factory & factory)
: factory(factory)
{}
void set_ui(react::value<std::any> ui)
{
root_token = ui.subscribe([this](std::any const & value)
{
root = factory.reconciliate(std::move(root), value);
subscribe_reshape();
}, true);
subscribe_reshape();
}
void subscribe_reshape()
{
root_reshape_token = nullptr;
if (root)
root_reshape_token = root->size_constraints().subscribe([this](psemek::ui::impl::size_constraints const &)
{
do_reshape();
});
}
void do_reshape()
{
if (root)
root->reshape({{{0.f, screen_size[0]}, {0.f, screen_size[1]}}});
}
template <typename Event>
bool on_event(Event const & event)
{
return on_event_impl(event, root.get());
}
bool on_event(resize_event const & event)
{
screen_size = event.size;
bool result = on_event_impl(event, root.get());
do_reshape();
return result;
}
template <typename Event>
bool on_event_impl(Event const & event, component * element)
{
if (!element)
return false;
for (auto const & child : element->children())
if (on_event_impl(event, child.get()))
return true;
return element->on_event(event);
}
};
controller::controller(component_factory & factory)
@ -20,7 +80,32 @@ namespace psemek::ui::impl
void controller::set_ui(react::value<std::any> ui)
{
(void)ui;
impl().set_ui(std::move(ui));
}
bool controller::on_event(resize_event const & event)
{
return impl().on_event(event);
}
bool controller::on_event(mouse_move_event const & event)
{
return impl().on_event(event);
}
bool controller::on_event(mouse_wheel_event const & event)
{
return impl().on_event(event);
}
bool controller::on_event(mouse_button_event const & event)
{
return impl().on_event(event);
}
bool controller::on_event(key_event const & event)
{
return impl().on_event(event);
}
}

View file

@ -0,0 +1,28 @@
#include <psemek/ui/impl/default_component_factory.hpp>
#include <psemek/ui/stack_layout.hpp>
#include <psemek/ui/box_layout.hpp>
#include <psemek/ui/aligned.hpp>
#include <psemek/ui/fixed_size.hpp>
#include <psemek/ui/button.hpp>
#include <psemek/ui/impl/stack_layout_base.hpp>
#include <psemek/ui/impl/box_layout_base.hpp>
#include <psemek/ui/impl/aligned_base.hpp>
#include <psemek/ui/impl/fixed_size_base.hpp>
#include <psemek/ui/impl/button_base.hpp>
namespace psemek::ui::impl
{
default_component_factory::default_component_factory()
{
register_type<stack_layout, impl::stack_layout_base>();
register_type<box_layout::horizontal, impl::box_layout_base<0>>();
register_type<box_layout::vertical, impl::box_layout_base<1>>();
register_type<aligned, impl::aligned_base>();
register_type<fixed_size, impl::fixed_size_base>();
register_type<fixed_width, impl::fixed_size_base>();
register_type<fixed_height, impl::fixed_size_base>();
register_type<button, impl::button_base>();
}
}

View file

@ -0,0 +1,65 @@
#include <psemek/ui/impl/fixed_size_base.hpp>
#include <psemek/ui/alignment.hpp>
#include <psemek/react/join.hpp>
#include <psemek/react/map.hpp>
namespace psemek::ui::impl
{
namespace
{
size_constraints compute_size_constraints(geom::vector<std::optional<float>, 2> const & size)
{
return {
.box = {{
size[0] ? geom::interval<float>::singleton(*(size[0])) : geom::interval<float>(0.f, size_constraints::infinity),
size[1] ? geom::interval<float>::singleton(*(size[1])) : geom::interval<float>(0.f, size_constraints::infinity),
}},
};
}
geom::box<float, 2> compute_shape(geom::box<float, 2> const & shape, geom::vector<std::optional<float>, 2> const & size)
{
return {{
size[0] ? align(shape[0], *(size[0]), halignment::center) : shape[0],
size[1] ? align(shape[1], *(size[1]), valignment::center) : shape[1],
}};
}
}
fixed_size_base::fixed_size_base()
: size_({0.f, 0.f})
, size_constraints_(react::map(compute_size_constraints, react::join(size_)))
{}
void fixed_size_base::reshape(geom::box<float, 2> const & new_shape)
{
single_container::reshape(new_shape);
if (auto child = this->child())
child->reshape(compute_shape(new_shape, **size_));
}
react::value<struct size_constraints> fixed_size_base::size_constraints() const
{
return size_constraints_;
}
void fixed_size_base::update(fixed_size const & value)
{
size_.set(react::map([](geom::vector<float, 2> const & size){ return geom::vector<std::optional<float>, 2>{size[0], size[1]}; }, value.size));
}
void fixed_size_base::update(fixed_width const & value)
{
size_.set(react::map([](float width){ return geom::vector<std::optional<float>, 2>{width, std::nullopt}; }, value.width));
}
void fixed_size_base::update(fixed_height const & value)
{
size_.set(react::map([](float height){ return geom::vector<std::optional<float>, 2>{std::nullopt, height}; }, value.height));
}
}

View file

@ -39,5 +39,4 @@ namespace psemek::ui::impl
return child;
}
}

View file

@ -1,14 +1,10 @@
#include <psemek/ui/impl/size_constraints.hpp>
#include <limits>
namespace psemek::ui::impl
{
size_constraints size_constraints::max()
{
static constexpr float infinity = std::numeric_limits<float>::infinity();
return
{{{
{0.f, infinity},

View file

@ -26,6 +26,8 @@ namespace psemek::ui::impl
void stack_layout_base::reshape(geom::box<float, 2> const & new_shape)
{
container::reshape(new_shape);
for (auto const & child : children())
if (child)
child->reshape(new_shape);