psemek/libs/ui/source/selector.cpp

325 lines
6.9 KiB
C++

#include <psemek/ui/selector.hpp>
#include <psemek/ui/screen.hpp>
#include <psemek/ui/positioner.hpp>
#include <psemek/ui/event_interceptor.hpp>
#include <psemek/util/recursive.hpp>
#include <psemek/geom/contains.hpp>
#include <stdexcept>
namespace psemek::ui
{
selector::selector()
: children_range_(1, nullptr)
{}
selector::~selector()
{
release_children();
}
void selector::set_parent(element * parent)
{
if (!parent && mouseover_callback_)
post([cb = mouseover_callback_]{ cb(std::nullopt); });
element::set_parent(parent);
}
void selector::reshape(geom::box<float, 2> const & box)
{
shape_.box = box;
auto st = merged_own_style();
geom::interval<float> x_range = geom::shrink<float>(box[0], (*st->inner_margin)[0]);
float y = box[1].min;
for (std::size_t i = 0; i < children_.size(); ++i)
{
auto c = children_[i].get();
float y_start = y;
y += (*st->inner_margin)[1];
if (c)
{
auto sc = c->size_constraints();
c->reshape({{x_range, {y, y + sc[1].min}}});
y += sc[1].min;
}
y += (*st->inner_margin)[1];
child_boxes_[i] = {{box[0], {y_start, y}}};
y += y_extra() * *st->scale;
}
if (active_submenu_)
{
auto sc = active_submenu_->size_constraints();
geom::box<float, 2> box{{{0.f, sc[0].min}, {0.f, sc[1].min}}};
box[0] += shape_.box[0].max;
box[1] += active_submenu_y_;
active_submenu_->reshape(box);
}
}
geom::box<float, 2> selector::size_constraints() const
{
auto st = merged_own_style();
auto x_range = geom::interval<float>::full();
float y_sum = 0.f;
for (std::size_t i = 0; i < size(); ++i)
{
auto c = get(i);
if (!c) continue;
auto sc = c->size_constraints();
if (submenu_[i])
sc[0] += *st->scale * submenu_extra();
x_range &= sc[0];
y_sum += sc[1].min;
}
x_range += (*st->inner_margin)[0] * 2.f;
y_sum += (*st->inner_margin)[1] * 2.f * size();
if (!children_.empty())
y_sum += *st->scale * y_extra() * static_cast<int>(size() - 1);
return {{x_range, {y_sum, y_sum}}};
}
bool selector::on_event(mouse_move const & e)
{
auto const p = geom::cast<float>(e.position);
std::optional<std::size_t> new_selected;
for (std::size_t i = 0; i < child_boxes_.size(); ++i)
{
if (geom::contains(child_boxes_[i], p))
{
new_selected = i;
break;
}
}
if (new_selected && new_selected != selected_)
on_submenu(*new_selected);
if (new_selected != selected_ && mouseover_callback_)
post([cb = mouseover_callback_, i = new_selected]{ cb(i); });
selected_ = new_selected;
return false;
}
bool selector::on_event(mouse_click const & e)
{
if (e.down && e.button == mouse_button::left && selected_)
{
if (!submenu_[*selected_] && callback_)
post([cb = callback_, i = *selected_]{ cb(i); });
return true;
}
return false;
}
std::size_t selector::size() const
{
return children_.size();
}
void selector::resize(std::size_t size)
{
children_.resize(size);
submenu_.resize(size);
child_boxes_.resize(size);
children_range_.resize(size + 1);
post_reshape();
}
std::size_t selector::add(std::shared_ptr<element> child)
{
children_.push_back(child);
submenu_.push_back(nullptr);
child_boxes_.emplace_back();
children_range_.push_back(child.get());
child->set_parent(this);
post_reshape();
return children_.size() - 1;
}
std::shared_ptr<element> selector::get(std::size_t index) const
{
return children_[index];
}
void selector::set_submenu(std::size_t index, std::shared_ptr<selector> submenu)
{
submenu_[index] = std::move(submenu);
}
std::shared_ptr<selector> selector::submenu(std::size_t index) const
{
return submenu_[index];
}
void selector::clear()
{
release_children();
children_.clear();
submenu_.clear();
child_boxes_.clear();
children_range_.assign(1, nullptr);
post_reshape();
}
geom::interval<float> selector::y_range(std::size_t index) const
{
return child_boxes_[index][1];
}
void selector::on_selected(callback_type callback)
{
callback_ = std::move(callback);
}
void selector::on_mouseover(std::function<void(std::optional<std::size_t>)> callback)
{
mouseover_callback_ = std::move(callback);
}
selector::callback_type selector::on_selected() const
{
return callback_;
}
void selector::on_submenu(std::size_t index)
{
release_submenu();
auto new_submenu = submenu_[index].get();
if (new_submenu)
{
children_range_[0] = new_submenu;
active_submenu_ = new_submenu;
new_submenu->set_parent(this);
active_submenu_y_ = child_boxes_[index][1].min;
post_reshape();
}
}
void selector::release_submenu()
{
if (active_submenu_)
{
active_submenu_->release_submenu();
active_submenu_->set_parent(nullptr);
active_submenu_ = nullptr;
children_range_[0] = nullptr;
}
}
namespace
{
struct opaque_event_interceptor
: event_interceptor
{
bool transparent() const override { return false; }
};
}
bool spawn(element * root, std::shared_ptr<selector> selector, geom::point<float, 2> const & position, std::function<void()> on_canceled)
{
ui::screen * screen = find_last_parent_of_type<struct screen>(root);
if (!screen)
return false;
auto event_interceptor= std::make_shared<opaque_event_interceptor>();
auto positioner = std::make_shared<struct positioner>();
positioner->set_child(selector);
event_interceptor->set_child(positioner);
auto close = [selector_root = event_interceptor.get(), selector = selector.get(), on_canceled]{
selector->on_selected([](std::size_t){});
if (on_canceled)
on_canceled();
auto p = dynamic_cast<struct screen *>(selector_root->parent());
if (p)
p->remove_child(selector_root);
};
auto patch_callback = [&](struct selector * target){
target->on_selected([close, cb = target->on_selected()](std::size_t index){
close();
if (cb)
cb(index);
});
};
auto patch_callback_recursive = util::recursive([&](auto && self, struct selector * target) -> void {
patch_callback(target);
for (std::size_t i = 0; i < target->size(); ++i)
if (auto s = target->submenu(i))
self(s.get());
});
patch_callback_recursive(selector.get());
selector->on_selected([close, cb = selector->on_selected()](std::size_t index){
close();
if (cb)
cb(index);
});
event_interceptor->on_mouse_click([close](ui::mouse_click const & e) -> bool {
if (e.down && (e.button == ui::mouse_button::right || e.button == ui::mouse_button::left))
{
close();
return true;
}
return false;
});
event_interceptor->on_key_press([close](key_press const & e) -> bool {
if (e.down && e.key == SDLK_ESCAPE)
{
close();
return true;
}
return false;
});
event_interceptor->on_mouse_move([](mouse_move const &){ return true; });
send_fake_mouse_move_event(event_interceptor.get(), true);
positioner->set_position(position, positioner::x_align::left, positioner::y_align::top);
screen->add_child(event_interceptor, screen::x_policy::fill, screen::y_policy::fill);
return true;
}
}