
We often want to zoom and fit the content of the widget into the visible frame (or some rect within the frame), and it makes sense to move this functionality into the AbstractZoomPanWidget to minimize the amount of coordinate math derived classes need to do. This commit moves the code that implements this functionality from `PixelPaint::ImageEditor` into `AbstractZoomPanWidget` so that we can also use it for other applications (such as ImageViewer!)
208 lines
6 KiB
C++
208 lines
6 KiB
C++
/*
|
|
* Copyright (c) 2022, Mustafa Quraish <mustafa@serenityos.org>
|
|
*
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*/
|
|
|
|
#include "AbstractZoomPanWidget.h"
|
|
|
|
namespace GUI {
|
|
|
|
constexpr float wheel_zoom_factor = 8.0f;
|
|
|
|
void AbstractZoomPanWidget::set_scale(float new_scale)
|
|
{
|
|
if (m_original_rect.is_null())
|
|
return;
|
|
|
|
m_scale = clamp(new_scale, m_min_scale, m_max_scale);
|
|
Gfx::IntSize new_size;
|
|
new_size.set_width(m_original_rect.width() * m_scale);
|
|
new_size.set_height(m_original_rect.height() * m_scale);
|
|
m_content_rect.set_size(new_size);
|
|
|
|
if (on_scale_change)
|
|
on_scale_change(m_scale);
|
|
|
|
relayout();
|
|
}
|
|
|
|
void AbstractZoomPanWidget::scale_by(float delta)
|
|
{
|
|
float new_scale = m_scale * AK::exp2(delta);
|
|
set_scale(new_scale);
|
|
}
|
|
|
|
void AbstractZoomPanWidget::scale_centered(float new_scale, Gfx::IntPoint const& center)
|
|
{
|
|
if (m_original_rect.is_null())
|
|
return;
|
|
|
|
new_scale = clamp(new_scale, m_min_scale, m_max_scale);
|
|
if (new_scale == m_scale)
|
|
return;
|
|
|
|
Gfx::FloatPoint focus_point {
|
|
center.x() - width() / 2.0f,
|
|
center.y() - height() / 2.0f
|
|
};
|
|
m_origin = (m_origin + focus_point) * (new_scale / m_scale) - focus_point;
|
|
set_scale(new_scale);
|
|
}
|
|
|
|
void AbstractZoomPanWidget::start_panning(Gfx::IntPoint const& position)
|
|
{
|
|
m_saved_cursor = override_cursor();
|
|
set_override_cursor(Gfx::StandardCursor::Drag);
|
|
m_pan_start = m_origin;
|
|
m_pan_mouse_pos = position;
|
|
m_is_panning = true;
|
|
}
|
|
|
|
void AbstractZoomPanWidget::stop_panning()
|
|
{
|
|
m_is_panning = false;
|
|
set_override_cursor(m_saved_cursor);
|
|
}
|
|
|
|
void AbstractZoomPanWidget::pan_to(Gfx::IntPoint const& position)
|
|
{
|
|
// NOTE: `position` here (and `m_pan_mouse_pos`) are both in frame coordinates, not
|
|
// content coordinates, by design. The derived class should not have to keep track of
|
|
// the (zoomed) content coordinates itself, but just pass along the mouse position.
|
|
auto delta = position - m_pan_mouse_pos;
|
|
m_origin = m_pan_start.translated(-delta.x(), -delta.y());
|
|
relayout();
|
|
}
|
|
|
|
Gfx::FloatPoint AbstractZoomPanWidget::frame_to_content_position(Gfx::IntPoint const& frame_position) const
|
|
{
|
|
Gfx::FloatPoint content_position;
|
|
content_position.set_x(((float)frame_position.x() - (float)m_content_rect.x()) / m_scale);
|
|
content_position.set_y(((float)frame_position.y() - (float)m_content_rect.y()) / m_scale);
|
|
return content_position;
|
|
}
|
|
|
|
Gfx::FloatRect AbstractZoomPanWidget::frame_to_content_rect(Gfx::IntRect const& frame_rect) const
|
|
{
|
|
Gfx::FloatRect content_rect;
|
|
content_rect.set_location(frame_to_content_position(frame_rect.location()));
|
|
content_rect.set_width((float)frame_rect.width() / m_scale);
|
|
content_rect.set_height((float)frame_rect.height() / m_scale);
|
|
return content_rect;
|
|
}
|
|
|
|
Gfx::FloatPoint AbstractZoomPanWidget::content_to_frame_position(Gfx::IntPoint const& content_position) const
|
|
{
|
|
Gfx::FloatPoint frame_position;
|
|
frame_position.set_x(m_content_rect.x() + ((float)content_position.x() * m_scale));
|
|
frame_position.set_y(m_content_rect.y() + ((float)content_position.y() * m_scale));
|
|
return frame_position;
|
|
}
|
|
|
|
Gfx::FloatRect AbstractZoomPanWidget::content_to_frame_rect(Gfx::IntRect const& content_rect) const
|
|
{
|
|
Gfx::FloatRect frame_rect;
|
|
frame_rect.set_location(content_to_frame_position(content_rect.location()));
|
|
frame_rect.set_width((float)content_rect.width() * m_scale);
|
|
frame_rect.set_height((float)content_rect.height() * m_scale);
|
|
return frame_rect;
|
|
}
|
|
|
|
void AbstractZoomPanWidget::mousewheel_event(GUI::MouseEvent& event)
|
|
{
|
|
float new_scale = scale() / AK::exp2(event.wheel_delta_y() / wheel_zoom_factor);
|
|
scale_centered(new_scale, event.position());
|
|
}
|
|
|
|
void AbstractZoomPanWidget::mousedown_event(GUI::MouseEvent& event)
|
|
{
|
|
if (!m_is_panning && event.button() == GUI::MouseButton::Middle) {
|
|
start_panning(event.position());
|
|
event.accept();
|
|
return;
|
|
}
|
|
}
|
|
|
|
void AbstractZoomPanWidget::resize_event(GUI::ResizeEvent& event)
|
|
{
|
|
relayout();
|
|
GUI::Widget::resize_event(event);
|
|
}
|
|
|
|
void AbstractZoomPanWidget::mousemove_event(GUI::MouseEvent& event)
|
|
{
|
|
if (!m_is_panning)
|
|
return;
|
|
pan_to(event.position());
|
|
event.accept();
|
|
}
|
|
|
|
void AbstractZoomPanWidget::mouseup_event(GUI::MouseEvent& event)
|
|
{
|
|
if (m_is_panning && event.button() == GUI::MouseButton::Middle) {
|
|
stop_panning();
|
|
event.accept();
|
|
return;
|
|
}
|
|
}
|
|
|
|
void AbstractZoomPanWidget::relayout()
|
|
{
|
|
if (m_original_rect.is_null())
|
|
return;
|
|
|
|
Gfx::IntSize new_size = m_content_rect.size();
|
|
|
|
Gfx::IntPoint new_location;
|
|
new_location.set_x((width() / 2) - (new_size.width() / 2) - m_origin.x());
|
|
new_location.set_y((height() / 2) - (new_size.height() / 2) - m_origin.y());
|
|
m_content_rect.set_location(new_location);
|
|
|
|
handle_relayout(m_content_rect);
|
|
}
|
|
|
|
void AbstractZoomPanWidget::reset_view()
|
|
{
|
|
m_origin = { 0, 0 };
|
|
set_scale(1.0f);
|
|
}
|
|
|
|
void AbstractZoomPanWidget::set_content_rect(Gfx::IntRect const& content_rect)
|
|
{
|
|
m_content_rect = enclosing_int_rect(content_to_frame_rect(content_rect));
|
|
update();
|
|
}
|
|
|
|
void AbstractZoomPanWidget::set_scale_bounds(float min_scale, float max_scale)
|
|
{
|
|
m_min_scale = min_scale;
|
|
m_max_scale = max_scale;
|
|
}
|
|
|
|
void AbstractZoomPanWidget::fit_content_to_rect(Gfx::IntRect const& viewport_rect, FitType type)
|
|
{
|
|
const float border_ratio = 0.95f;
|
|
auto image_size = m_original_rect.size();
|
|
auto height_ratio = floorf(border_ratio * viewport_rect.height()) / (float)image_size.height();
|
|
auto width_ratio = floorf(border_ratio * viewport_rect.width()) / (float)image_size.width();
|
|
|
|
float new_scale = 1.0f;
|
|
switch (type) {
|
|
case FitType::Width:
|
|
new_scale = width_ratio;
|
|
break;
|
|
case FitType::Height:
|
|
new_scale = height_ratio;
|
|
break;
|
|
case FitType::Both:
|
|
new_scale = min(height_ratio, width_ratio);
|
|
break;
|
|
}
|
|
|
|
auto const& offset = rect().center() - viewport_rect.center();
|
|
set_origin(Gfx::FloatPoint(offset.x(), offset.y()));
|
|
set_scale(new_scale);
|
|
}
|
|
|
|
}
|