Serenity Operating System
1/*
2 * Copyright (c) 2022, Mustafa Quraish <mustafa@serenityos.org>
3 * Copyright (c) 2022, Jelle Raaijmakers <jelle@gmta.nl>
4 *
5 * SPDX-License-Identifier: BSD-2-Clause
6 */
7
8#include "AbstractZoomPanWidget.h"
9
10namespace GUI {
11
12constexpr float wheel_zoom_factor = 8.0f;
13
14void AbstractZoomPanWidget::set_scale(float new_scale)
15{
16 if (m_original_rect.is_empty())
17 return;
18
19 m_scale = clamp(new_scale, m_min_scale, m_max_scale);
20 m_content_rect.set_size({
21 m_original_rect.width() * m_scale,
22 m_original_rect.height() * m_scale,
23 });
24
25 if (on_scale_change)
26 on_scale_change(m_scale);
27
28 relayout();
29}
30
31void AbstractZoomPanWidget::scale_by(float delta)
32{
33 float new_scale = m_scale * AK::exp2(delta);
34 set_scale(new_scale);
35}
36
37void AbstractZoomPanWidget::scale_centered(float new_scale, Gfx::IntPoint center)
38{
39 if (m_original_rect.is_empty())
40 return;
41
42 new_scale = clamp(new_scale, m_min_scale, m_max_scale);
43 if (new_scale == m_scale)
44 return;
45
46 Gfx::FloatPoint focus_point {
47 center.x() - width() / 2.0f,
48 center.y() - height() / 2.0f,
49 };
50 m_origin = (m_origin + focus_point) * (new_scale / m_scale) - focus_point;
51 set_scale(new_scale);
52}
53
54void AbstractZoomPanWidget::start_panning(Gfx::IntPoint position)
55{
56 m_saved_cursor = override_cursor();
57 set_override_cursor(Gfx::StandardCursor::Drag);
58 m_pan_start = m_origin;
59 m_pan_mouse_pos = position;
60 m_is_panning = true;
61}
62
63void AbstractZoomPanWidget::stop_panning()
64{
65 m_is_panning = false;
66 set_override_cursor(m_saved_cursor);
67}
68
69void AbstractZoomPanWidget::pan_to(Gfx::IntPoint position)
70{
71 // NOTE: `position` here (and `m_pan_mouse_pos`) are both in frame coordinates, not
72 // content coordinates, by design. The derived class should not have to keep track of
73 // the (zoomed) content coordinates itself, but just pass along the mouse position.
74 auto delta = position - m_pan_mouse_pos;
75 m_origin = m_pan_start.translated(-delta.x(), -delta.y());
76 relayout();
77}
78
79Gfx::FloatPoint AbstractZoomPanWidget::frame_to_content_position(Gfx::IntPoint frame_position) const
80{
81 return {
82 (static_cast<float>(frame_position.x()) - m_content_rect.x()) / m_scale,
83 (static_cast<float>(frame_position.y()) - m_content_rect.y()) / m_scale,
84 };
85}
86
87Gfx::FloatRect AbstractZoomPanWidget::frame_to_content_rect(Gfx::IntRect const& frame_rect) const
88{
89 Gfx::FloatRect content_rect;
90 content_rect.set_location(frame_to_content_position(frame_rect.location()));
91 content_rect.set_size({
92 frame_rect.width() / m_scale,
93 frame_rect.height() / m_scale,
94 });
95 return content_rect;
96}
97
98Gfx::FloatPoint AbstractZoomPanWidget::content_to_frame_position(Gfx::IntPoint content_position) const
99{
100 return {
101 m_content_rect.x() + content_position.x() * m_scale,
102 m_content_rect.y() + content_position.y() * m_scale,
103 };
104}
105
106Gfx::FloatRect AbstractZoomPanWidget::content_to_frame_rect(Gfx::IntRect const& content_rect) const
107{
108 Gfx::FloatRect frame_rect;
109 frame_rect.set_location(content_to_frame_position(content_rect.location()));
110 frame_rect.set_size({
111 content_rect.width() * m_scale,
112 content_rect.height() * m_scale,
113 });
114 return frame_rect;
115}
116
117void AbstractZoomPanWidget::mousewheel_event(GUI::MouseEvent& event)
118{
119 float new_scale = scale() / AK::exp2(event.wheel_delta_y() / wheel_zoom_factor);
120 scale_centered(new_scale, event.position());
121}
122
123void AbstractZoomPanWidget::mousedown_event(GUI::MouseEvent& event)
124{
125 if (!m_is_panning && event.button() == GUI::MouseButton::Middle) {
126 start_panning(event.position());
127 event.accept();
128 return;
129 }
130}
131
132void AbstractZoomPanWidget::resize_event(GUI::ResizeEvent& event)
133{
134 relayout();
135 GUI::Widget::resize_event(event);
136}
137
138void AbstractZoomPanWidget::mousemove_event(GUI::MouseEvent& event)
139{
140 if (!m_is_panning)
141 return;
142 pan_to(event.position());
143 event.accept();
144}
145
146void AbstractZoomPanWidget::mouseup_event(GUI::MouseEvent& event)
147{
148 if (m_is_panning && event.button() == GUI::MouseButton::Middle) {
149 stop_panning();
150 event.accept();
151 return;
152 }
153}
154
155void AbstractZoomPanWidget::relayout()
156{
157 if (m_original_rect.is_empty())
158 return;
159
160 m_content_rect.set_location({
161 (width() / 2) - (m_content_rect.width() / 2) - m_origin.x(),
162 (height() / 2) - (m_content_rect.height() / 2) - m_origin.y(),
163 });
164
165 handle_relayout(m_content_rect);
166}
167
168void AbstractZoomPanWidget::reset_view()
169{
170 m_origin = { 0, 0 };
171 set_scale(1.0f);
172}
173
174void AbstractZoomPanWidget::set_content_rect(Gfx::IntRect const& content_rect)
175{
176 m_content_rect = enclosing_int_rect(content_to_frame_rect(content_rect));
177 update();
178}
179
180void AbstractZoomPanWidget::set_scale_bounds(float min_scale, float max_scale)
181{
182 m_min_scale = min_scale;
183 m_max_scale = max_scale;
184}
185
186void AbstractZoomPanWidget::fit_content_to_rect(Gfx::IntRect const& viewport_rect, FitType type)
187{
188 float const border_ratio = 0.95f;
189 auto image_size = m_original_rect.size();
190 auto height_ratio = floorf(border_ratio * viewport_rect.height()) / image_size.height();
191 auto width_ratio = floorf(border_ratio * viewport_rect.width()) / image_size.width();
192
193 float new_scale = 1.0f;
194 switch (type) {
195 case FitType::Width:
196 new_scale = width_ratio;
197 break;
198 case FitType::Height:
199 new_scale = height_ratio;
200 break;
201 case FitType::Both:
202 new_scale = min(height_ratio, width_ratio);
203 break;
204 }
205
206 auto const& offset = rect().center() - viewport_rect.center();
207 set_origin({ offset.x(), offset.y() });
208 set_scale(new_scale);
209}
210
211}