#include #include #include #include #include #include #include #include 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 const & box) { shape_.box = box; auto st = merged_own_style(); geom::interval x_range = geom::shrink(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 submenu = submenu_[*active_submenu_]; auto sc = submenu->size_constraints(); geom::box box{{{0.f, sc[0].min}, {0.f, sc[1].min}}}; box[0] += shape_.box[0].max; box[1] += active_submenu_y_; submenu->reshape(box); } } geom::box selector::size_constraints() const { auto st = merged_own_style(); auto x_range = geom::interval::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(size() - 1); return {{x_range, {y_sum, y_sum}}}; } bool selector::on_event(mouse_move const & e) { auto const p = geom::cast(e.position); std::optional 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 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 selector::get(std::size_t index) const { return children_[index]; } void selector::set_submenu(std::size_t index, std::shared_ptr submenu) { submenu_[index] = std::move(submenu); } std::shared_ptr 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 selector::y_range(std::size_t index) const { return child_boxes_[index][1]; } std::optional selector::selected() const { if (active_submenu_) return active_submenu_; if (selected_) return selected_; return std::nullopt; } void selector::on_selected(callback_type callback) { callback_ = std::move(callback); } void selector::on_mouseover(std::function)> 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 submenu = submenu_[index].get(); if (submenu) { children_range_[0] = submenu; active_submenu_ = index; submenu->set_parent(this); active_submenu_y_ = child_boxes_[index][1].min; post_reshape(); } } void selector::release_submenu() { if (active_submenu_) { auto submenu = submenu_[*active_submenu_].get(); submenu->release_submenu(); submenu->set_parent(nullptr); active_submenu_ = std::nullopt; children_range_[0] = nullptr; } } namespace { struct opaque_event_interceptor : event_interceptor { bool transparent() const override { return false; } }; } bool spawn_selector(element * root, std::shared_ptr selector, geom::point const & position, std::function on_canceled) { ui::screen * screen = find_last_parent_of_type(root); if (!screen) return false; auto event_interceptor = std::make_shared(); auto positioner = std::make_shared(); positioner->set_child(selector); event_interceptor->set_child(positioner); auto loop = screen->loop(); auto close = [selector_root = event_interceptor.get(), selector = selector.get(), on_canceled]{ if (on_canceled) on_canceled(); auto p = dynamic_cast(selector_root->parent()); if (p) p->remove_child(selector_root); }; std::vector> old_callbacks; util::recursive([&](auto && self, struct selector * target) -> void{ old_callbacks.push_back({target, target->on_selected()}); for (std::size_t i = 0; i < target->size(); ++i) if (auto s = target->submenu(i)) self(s.get()); })(selector.get()); auto revert_callbacks = [old_callbacks = util::to_shared(std::move(old_callbacks))]{ for (auto & p : *old_callbacks) p.first->on_selected(std::move(p.second)); }; auto cancel = [close, revert_callbacks, selector]{ close(); revert_callbacks(); }; auto patch_callback_recursive = util::recursive([&](auto && self, struct selector * target) -> void { target->on_selected([target, close, cb = target->on_selected()](std::size_t index){ [[maybe_unused]] auto shared_target = target->shared_from_this(); close(); if (cb) cb(index); target->on_selected(cb); }); for (std::size_t i = 0; i < target->size(); ++i) if (auto s = target->submenu(i)) self(s.get()); }); patch_callback_recursive(selector.get()); event_interceptor->on_mouse_click([loop, cancel](ui::mouse_click const & e) -> bool { if (e.down && (e.button == ui::mouse_button::right || e.button == ui::mouse_button::left)) { loop->post(cancel); return true; } return false; }); event_interceptor->on_key_press([loop, cancel](key_press const & e) -> bool { if (e.down && e.key == SDLK_ESCAPE) { loop->post(cancel); 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; } }