Serenity Operating System
at master 211 lines 5.8 kB view raw
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}