Add scrollable & zoomable image view element
This commit is contained in:
parent
486a8b24aa
commit
fccf2e382e
2 changed files with 232 additions and 0 deletions
58
libs/ui/include/psemek/ui/rich_image_view.hpp
Normal file
58
libs/ui/include/psemek/ui/rich_image_view.hpp
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
#pragma once
|
||||
|
||||
#include <psemek/ui/element.hpp>
|
||||
#include <psemek/ui/box_shape.hpp>
|
||||
#include <psemek/gfx/texture.hpp>
|
||||
|
||||
namespace psemek::ui
|
||||
{
|
||||
|
||||
struct rich_image_view
|
||||
: element
|
||||
{
|
||||
std::shared_ptr<gfx::texture_2d> image() const { return image_; }
|
||||
void set_image(std::shared_ptr<gfx::texture_2d> image);
|
||||
|
||||
// Zoom is defined as (size of painted pixels)/(size of texture pixels)
|
||||
geom::interval<float> zoom_range() const { return zoom_range_; }
|
||||
void set_zoom_range(geom::interval<float> range);
|
||||
|
||||
geom::point<float, 2> center() const { return center_; }
|
||||
float zoom() const { return zoom_; }
|
||||
|
||||
void set_center(geom::point<float, 2> const & center);
|
||||
void set_zoom(float zoom);
|
||||
|
||||
geom::box<float, 2> 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<float, 2> const & bbox) override;
|
||||
|
||||
void draw(painter & p) const override;
|
||||
|
||||
protected:
|
||||
virtual void on_region_changed() {}
|
||||
|
||||
private:
|
||||
std::shared_ptr<gfx::texture_2d> image_;
|
||||
geom::interval<float> zoom_range_ = {0.f, std::numeric_limits<float>::infinity()};
|
||||
float zoom_ = 1.f;
|
||||
geom::point<float, 2> 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<geom::point<int, 2>> mouse_;
|
||||
std::optional<geom::point<int, 2>> drag_;
|
||||
};
|
||||
|
||||
}
|
||||
174
libs/ui/source/rich_image_view.cpp
Normal file
174
libs/ui/source/rich_image_view.cpp
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
#include <psemek/ui/rich_image_view.hpp>
|
||||
|
||||
#include <psemek/geom/contains.hpp>
|
||||
|
||||
namespace psemek::ui
|
||||
{
|
||||
|
||||
void rich_image_view::set_image(std::shared_ptr<gfx::texture_2d> image)
|
||||
{
|
||||
image_ = std::move(image);
|
||||
element::reshape();
|
||||
on_region_changed();
|
||||
}
|
||||
|
||||
void rich_image_view::set_zoom_range(geom::interval<float> range)
|
||||
{
|
||||
float z = zoom();
|
||||
zoom_range_ = range;
|
||||
set_zoom(z);
|
||||
}
|
||||
|
||||
void rich_image_view::set_center(geom::point<float, 2> 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<float, 2> 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<float>(*mouse_) - shape_.box.center()) * (1.f / zoom_ - 1.f / zoom);
|
||||
|
||||
zoom_ = zoom;
|
||||
set_center(new_center);
|
||||
}
|
||||
|
||||
geom::box<float, 2> 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<float, 2> 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<float>(e.position));
|
||||
mouse_ = e.position;
|
||||
if (drag_)
|
||||
{
|
||||
set_center(center_ + geom::cast<float>(*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<float, 2> 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<float>(image_->width(), new_reg[0].max);
|
||||
new_reg[1].max = std::min<float>(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<float>(*st->shadow_offset), *st->shadow_color);
|
||||
|
||||
p.draw_subimage(box, *image_, reg, color_);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue