diff --git a/Userland/Libraries/LibGUI/AbstractZoomPanWidget.cpp b/Userland/Libraries/LibGUI/AbstractZoomPanWidget.cpp new file mode 100644 index 0000000000..91e0b488a0 --- /dev/null +++ b/Userland/Libraries/LibGUI/AbstractZoomPanWidget.cpp @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2022, Mustafa Quraish + * + * 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() / 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; +} + +} diff --git a/Userland/Libraries/LibGUI/AbstractZoomPanWidget.h b/Userland/Libraries/LibGUI/AbstractZoomPanWidget.h new file mode 100644 index 0000000000..cb0485203b --- /dev/null +++ b/Userland/Libraries/LibGUI/AbstractZoomPanWidget.h @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2022, Mustafa Quraish + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include + +namespace GUI { + +class AbstractZoomPanWidget : public GUI::Frame { + C_OBJECT(AbstractZoomPanWidget); + +public: + void set_scale(float scale); + float scale() const { return m_scale; } + void set_scale_bounds(float min_scale, float max_scale); + + void scale_by(float amount); + void scale_centered(float new_scale, Gfx::IntPoint const& center); + + bool is_panning() const { return m_is_panning; } + void start_panning(Gfx::IntPoint const& position); + void stop_panning(); + + void pan_to(Gfx::IntPoint const& position); + + // Should be overridden by derived classes if they want updates. + virtual void handle_relayout(Gfx::IntRect const&) { update(); } + void relayout(); + + Gfx::FloatPoint frame_to_content_position(Gfx::IntPoint const& frame_position) const; + Gfx::FloatRect frame_to_content_rect(Gfx::IntRect const& frame_rect) const; + Gfx::FloatPoint content_to_frame_position(Gfx::IntPoint const& content_position) const; + Gfx::FloatRect content_to_frame_rect(Gfx::IntRect const& content_rect) const; + + virtual void mousewheel_event(GUI::MouseEvent& event) override; + virtual void mousedown_event(GUI::MouseEvent& event) override; + virtual void resize_event(GUI::ResizeEvent& event) override; + virtual void mousemove_event(GUI::MouseEvent& event) override; + virtual void mouseup_event(GUI::MouseEvent& event) override; + + void set_original_rect(Gfx::IntRect const& rect) { m_original_rect = rect; } + void set_content_rect(Gfx::IntRect const& content_rect); + void set_origin(Gfx::FloatPoint const& origin) { m_origin = origin; } + + void reset_view(); + + Gfx::IntRect content_rect() const { return m_content_rect; } + + Function on_scale_change; + +private: + Gfx::IntRect m_original_rect; + Gfx::IntRect m_content_rect; + + Gfx::IntPoint m_pan_mouse_pos; + Gfx::FloatPoint m_origin; + Gfx::FloatPoint m_pan_start; + bool m_is_panning { false }; + + float m_min_scale { 0.1f }; + float m_max_scale { 10.0f }; + float m_scale { 1.0f }; + + AK::Variant> m_saved_cursor { Gfx::StandardCursor::None }; +}; + +} diff --git a/Userland/Libraries/LibGUI/CMakeLists.txt b/Userland/Libraries/LibGUI/CMakeLists.txt index 026acc0966..a0d06444e6 100644 --- a/Userland/Libraries/LibGUI/CMakeLists.txt +++ b/Userland/Libraries/LibGUI/CMakeLists.txt @@ -9,6 +9,7 @@ set(SOURCES AbstractSlider.cpp AbstractTableView.cpp AbstractView.cpp + AbstractZoomPanWidget.cpp Action.cpp ActionGroup.cpp Application.cpp