diff --git a/libs/ui/include/psemek/ui/rich_image_view.hpp b/libs/ui/include/psemek/ui/rich_image_view.hpp new file mode 100644 index 00000000..a324e037 --- /dev/null +++ b/libs/ui/include/psemek/ui/rich_image_view.hpp @@ -0,0 +1,58 @@ +#pragma once + +#include +#include +#include + +namespace psemek::ui +{ + + struct rich_image_view + : element + { + std::shared_ptr image() const { return image_; } + void set_image(std::shared_ptr image); + + // Zoom is defined as (size of painted pixels)/(size of texture pixels) + geom::interval zoom_range() const { return zoom_range_; } + void set_zoom_range(geom::interval range); + + geom::point center() const { return center_; } + float zoom() const { return zoom_; } + + void set_center(geom::point const & center); + void set_zoom(float zoom); + + geom::box region() const; + + bool allow_overflow() const { return allow_overflow_; } + void set_allow_overflow(bool value); + + bool on_event(mouse_move const & e) override; + bool on_event(mouse_click const & e) override; + bool on_event(mouse_wheel const & e) override; + bool on_event(key_press const & e) override; + + struct shape const & shape() const override { return shape_; } + void reshape(geom::box const & bbox) override; + + void draw(painter & p) const override; + + protected: + virtual void on_region_changed() {} + + private: + std::shared_ptr image_; + geom::interval zoom_range_ = {0.f, std::numeric_limits::infinity()}; + float zoom_ = 1.f; + geom::point center_{0.f, 0.f}; + bool allow_overflow_ = false; + gfx::color_rgba color_{0, 0, 0, 0}; + box_shape shape_; + + bool mouseover_ = false; + std::optional> mouse_; + std::optional> drag_; + }; + +} diff --git a/libs/ui/source/rich_image_view.cpp b/libs/ui/source/rich_image_view.cpp new file mode 100644 index 00000000..8cce9f1a --- /dev/null +++ b/libs/ui/source/rich_image_view.cpp @@ -0,0 +1,174 @@ +#include + +#include + +namespace psemek::ui +{ + + void rich_image_view::set_image(std::shared_ptr image) + { + image_ = std::move(image); + element::reshape(); + on_region_changed(); + } + + void rich_image_view::set_zoom_range(geom::interval range) + { + float z = zoom(); + zoom_range_ = range; + set_zoom(z); + } + + void rich_image_view::set_center(geom::point const & center) + { + center_ = center; + if (!allow_overflow_) + { + float const w = shape_.box[0].length() / zoom_ / 2.f; + float const h = shape_.box[1].length() / zoom_ / 2.f; + + geom::box b; + b[0] = {w, image_->width() - w}; + b[1] = {h, image_->height() - h}; + if (b[0].empty()) std::swap(b[0].min, b[0].max); + if (b[1].empty()) std::swap(b[1].min, b[1].max); + + center_ = geom::clamp(center_, b); + } + on_region_changed(); + } + + void rich_image_view::set_zoom(float zoom) + { + zoom = geom::clamp(zoom, zoom_range_); + + // Screen -> Texcoords + // mouse |-> (mouse - bbox.center) / zoom + center + // (mouse - bbox.center) / zoom0 + center0 = (mouse - bbox.center) / zoom1 + center1 + // center1 - center0 = (mouse - bbox.center) * (1 / zoom0 - 1 / zoom1) + + auto new_center = center_; + + if (mouse_) + new_center += (geom::cast(*mouse_) - shape_.box.center()) * (1.f / zoom_ - 1.f / zoom); + + zoom_ = zoom; + set_center(new_center); + } + + geom::box rich_image_view::region() const + { + float const w = shape_.box[0].length() / zoom_ / 2.f; + float const h = shape_.box[1].length() / zoom_ / 2.f; + + geom::box r; + r[0] = {center_[0] - w, center_[0] + w}; + r[1] = {center_[1] - h, center_[1] + h}; + return r; + } + + void rich_image_view::set_allow_overflow(bool value) + { + allow_overflow_ = value; + set_center(center_); + } + + bool rich_image_view::on_event(mouse_move const & e) + { + mouseover_ = geom::contains(shape_.box, geom::cast(e.position)); + mouse_ = e.position; + if (drag_) + { + set_center(center_ + geom::cast(*drag_ - e.position) / zoom_); + drag_ = e.position; + return true; + } + return false; + } + + bool rich_image_view::on_event(mouse_click const & e) + { + if (e.button == mouse_button::right) + { + if (e.down && mouse_ && mouseover_) + { + drag_ = *mouse_; + return true; + } + else if (!e.down && mouseover_) + { + drag_ = std::nullopt; + return true; + } + } + return false; + } + + bool rich_image_view::on_event(mouse_wheel const & e) + { + if (mouseover_) + { + set_zoom(zoom_ * std::pow(1.25f, e.delta)); + return true; + } + return false; + } + + bool rich_image_view::on_event(key_press const & e) + { + if (mouseover_ && e.down) + { + if (e.key == SDLK_KP_PLUS) + { + on_event(mouse_wheel{1}); + return true; + } + if (e.key == SDLK_KP_MINUS) + { + on_event(mouse_wheel{-1}); + return true; + } + } + return false; + } + + void rich_image_view::reshape(geom::box const & bbox) + { + shape_.box = bbox; + on_region_changed(); + } + + void rich_image_view::draw(painter & p) const + { + auto st = merged_style(); + + if (image_) + { + auto box = shape_.box; + auto reg = region(); + if (!allow_overflow_) + { + auto new_reg = reg; + new_reg[0].min = std::max(0.f, new_reg[0].min); + new_reg[1].min = std::max(0.f, new_reg[1].min); + new_reg[0].max = std::min(image_->width(), new_reg[0].max); + new_reg[1].max = std::min(image_->height(), new_reg[1].max); + + auto new_box = box; + new_box[0].min = geom::lerp(box[0], geom::unlerp(reg[0], new_reg[0].min)); + new_box[0].max = geom::lerp(box[0], geom::unlerp(reg[0], new_reg[0].max)); + new_box[1].min = geom::lerp(box[1], geom::unlerp(reg[1], new_reg[1].min)); + new_box[1].max = geom::lerp(box[1], geom::unlerp(reg[1], new_reg[1].max)); + + box = new_box; + reg = new_reg; + } + + if (*st->shadow_offset != geom::vector{0, 0}) + p.draw_rect(box + geom::cast(*st->shadow_offset), *st->shadow_color); + + p.draw_subimage(box, *image_, reg, color_); + } + } + +}