Serenity Operating System
1/*
2 * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
3 * Copyright (c) 2022-2023, the SerenityOS developers.
4 *
5 * SPDX-License-Identifier: BSD-2-Clause
6 */
7
8#include "MoveTool.h"
9#include "../Image.h"
10#include "../ImageEditor.h"
11#include "../Layer.h"
12#include <AK/String.h>
13#include <LibGUI/Action.h>
14#include <LibGUI/BoxLayout.h>
15#include <LibGUI/Label.h>
16#include <LibGUI/Menu.h>
17#include <LibGUI/MessageBox.h>
18#include <LibGUI/Painter.h>
19#include <LibGUI/Window.h>
20#include <LibGfx/Filters/ContrastFilter.h>
21
22namespace PixelPaint {
23
24constexpr int resize_anchor_min_size = 5;
25constexpr int resize_anchor_max_size = 20;
26
27void MoveTool::on_mousedown(Layer* layer, MouseEvent& event)
28{
29 if (event.image_event().button() == GUI::MouseButton::Secondary) {
30 m_editor->start_panning(event.raw_event().position());
31 return;
32 }
33
34 if (!layer)
35 return;
36
37 auto& layer_event = event.layer_event();
38 auto& image_event = event.image_event();
39 if (layer_event.button() != GUI::MouseButton::Primary)
40 return;
41 if (!layer->rect().contains(layer_event.position()) && !m_resize_anchor_location.has_value())
42 return;
43 m_scaling = m_resize_anchor_location.has_value();
44 m_layer_being_moved = *layer;
45 m_event_origin = image_event.position();
46 m_layer_origin = layer->location();
47 m_new_layer_rect = m_editor->active_layer()->relative_rect();
48}
49
50void MoveTool::on_mousemove(Layer* layer, MouseEvent& event)
51{
52 if (m_editor->is_panning()) {
53 m_editor->pan_to(event.raw_event().position());
54 return;
55 }
56
57 if (!layer)
58 return;
59
60 if (!m_scaling) {
61 auto current_resize_anchor_location = resize_anchor_location_from_cursor_position(layer, event);
62 if (m_resize_anchor_location != current_resize_anchor_location) {
63 m_resize_anchor_location = current_resize_anchor_location;
64 m_editor->update_tool_cursor();
65 }
66 }
67
68 if (!m_layer_being_moved)
69 return;
70
71 auto cursor_position = event.image_event().position();
72 auto delta = cursor_position - m_event_origin;
73 auto rect_being_moved = m_layer_being_moved->relative_rect();
74 Gfx::IntPoint scaling_origin;
75 Gfx::IntPoint opposite_corner;
76 if (m_scaling) {
77 VERIFY(m_resize_anchor_location.has_value());
78 switch (m_resize_anchor_location.value()) {
79 case ResizeAnchorLocation::TopLeft:
80 scaling_origin = rect_being_moved.top_left();
81 opposite_corner = rect_being_moved.bottom_right().translated(1, 1);
82 break;
83 case ResizeAnchorLocation::BottomRight:
84 scaling_origin = rect_being_moved.bottom_right().translated(1, 1);
85 opposite_corner = rect_being_moved.top_left();
86 break;
87 case ResizeAnchorLocation::BottomLeft:
88 scaling_origin = rect_being_moved.bottom_left().translated(0, 1);
89 opposite_corner = rect_being_moved.top_right().translated(1, 0);
90 break;
91 case ResizeAnchorLocation::TopRight:
92 scaling_origin = rect_being_moved.top_right().translated(1, 0);
93 opposite_corner = rect_being_moved.bottom_left().translated(0, 1);
94 break;
95 }
96 scaling_origin.translate_by(delta);
97 if (m_keep_aspect_ratio) {
98 auto aspect_ratio = m_layer_being_moved->size().aspect_ratio();
99 scaling_origin = opposite_corner.end_point_for_aspect_ratio(scaling_origin, aspect_ratio);
100 }
101
102 auto scaled_rect = Gfx::IntRect::from_two_points(scaling_origin, opposite_corner);
103 if (!scaled_rect.is_empty())
104 m_new_layer_rect = scaled_rect;
105 } else {
106 m_layer_being_moved->set_location(m_layer_origin.translated(delta));
107 }
108 m_editor->update();
109}
110
111void MoveTool::on_mouseup(Layer* layer, MouseEvent& event)
112{
113 if (event.image_event().button() == GUI::MouseButton::Secondary) {
114 m_editor->stop_panning();
115 m_editor->set_override_cursor(cursor());
116 return;
117 }
118
119 if (!layer)
120 return;
121
122 auto& layer_event = event.layer_event();
123 if (layer_event.button() != GUI::MouseButton::Primary)
124 return;
125
126 if (m_scaling) {
127 auto resized_or_error = m_editor->active_layer()->resize(m_new_layer_rect, Gfx::Painter::ScalingMode::BilinearBlend);
128 if (resized_or_error.is_error())
129 GUI::MessageBox::show_error(m_editor->window(), MUST(String::formatted("Failed to resize layer: {}", resized_or_error.error().string_literal())));
130 else
131 m_editor->layers_did_change();
132 }
133
134 m_scaling = false;
135 m_layer_being_moved = nullptr;
136 m_cached_preview_bitmap = nullptr;
137 m_editor->update_tool_cursor();
138 m_editor->did_complete_action(tool_name());
139}
140
141bool MoveTool::on_keydown(GUI::KeyEvent& event)
142{
143 if (event.key() == Key_Shift)
144 m_keep_aspect_ratio = true;
145
146 if (event.key() == Key_Alt)
147 toggle_selection_mode();
148
149 if (m_scaling)
150 return true;
151
152 if (!(event.modifiers() == Mod_None || event.modifiers() == Mod_Shift))
153 return false;
154
155 auto* layer = m_editor->active_layer();
156 if (!layer)
157 return false;
158
159 auto new_location = layer->location();
160 auto speed = event.shift() ? 10 : 1;
161 switch (event.key()) {
162 case Key_Up:
163 new_location.translate_by(0, -speed);
164 break;
165 case Key_Down:
166 new_location.translate_by(0, speed);
167 break;
168 case Key_Left:
169 new_location.translate_by(-speed, 0);
170 break;
171 case Key_Right:
172 new_location.translate_by(speed, 0);
173 break;
174 default:
175 return false;
176 }
177
178 layer->set_location(new_location);
179 m_editor->layers_did_change();
180 return true;
181}
182
183void MoveTool::on_keyup(GUI::KeyEvent& event)
184{
185 if (event.key() == Key_Shift)
186 m_keep_aspect_ratio = false;
187
188 if (event.key() == Key_Alt)
189 toggle_selection_mode();
190}
191
192void MoveTool::on_second_paint(Layer const* layer, GUI::PaintEvent& event)
193{
194 VERIFY(m_editor);
195 GUI::Painter painter(*m_editor);
196 painter.add_clip_rect(event.rect());
197 auto content_rect = m_scaling ? m_new_layer_rect : m_editor->active_layer()->relative_rect();
198 auto rect_in_editor = m_editor->content_to_frame_rect(content_rect).to_type<int>();
199 if (m_scaling && (!m_cached_preview_bitmap.is_null() || !update_cached_preview_bitmap(layer).is_error())) {
200 Gfx::PainterStateSaver saver(painter);
201 painter.add_clip_rect(m_editor->content_rect());
202 painter.draw_scaled_bitmap(rect_in_editor, *m_cached_preview_bitmap, m_cached_preview_bitmap->rect(), 1.0f, Gfx::Painter::ScalingMode::BilinearBlend);
203 }
204 painter.draw_rect_with_thickness(rect_in_editor, Color::Black, 3);
205 painter.draw_rect_with_thickness(rect_in_editor, Color::White, 1);
206 auto size = resize_anchor_size(rect_in_editor);
207 if (size < resize_anchor_min_size)
208 return;
209
210 auto resize_anchors = resize_anchor_rects(rect_in_editor, size);
211 for (auto const& resize_anchor_rect : resize_anchors) {
212 painter.draw_rect_with_thickness(resize_anchor_rect, Color::Black, 3);
213 painter.draw_rect_with_thickness(resize_anchor_rect, Color::White, 1);
214 }
215}
216
217Gfx::IntRect MoveTool::resize_anchor_rect_from_position(Gfx::IntPoint position, int size)
218{
219 auto resize_anchor_rect_top_left = position.translated(-size / 2);
220 return Gfx::IntRect(resize_anchor_rect_top_left, Gfx::IntSize(size, size));
221}
222
223int MoveTool::resize_anchor_size(Gfx::IntRect layer_rect_in_frame_coordinates)
224{
225 auto shortest_side = min(layer_rect_in_frame_coordinates.width(), layer_rect_in_frame_coordinates.height());
226 if (shortest_side <= 1)
227 return 1;
228 int x = ceilf(shortest_side / 3.0f);
229 return min(resize_anchor_max_size, x);
230}
231
232Array<Gfx::IntRect, 4> MoveTool::resize_anchor_rects(Gfx::IntRect layer_rect_in_frame_coordinates, int resize_anchor_size)
233{
234 return Array {
235 resize_anchor_rect_from_position(layer_rect_in_frame_coordinates.top_left(), resize_anchor_size),
236 resize_anchor_rect_from_position(layer_rect_in_frame_coordinates.top_right().translated(1, 0), resize_anchor_size),
237 resize_anchor_rect_from_position(layer_rect_in_frame_coordinates.bottom_left().translated(0, 1), resize_anchor_size),
238 resize_anchor_rect_from_position(layer_rect_in_frame_coordinates.bottom_right().translated(1), resize_anchor_size)
239 };
240}
241
242ErrorOr<void> MoveTool::update_cached_preview_bitmap(Layer const* layer)
243{
244 auto editor_rect_size = m_editor->frame_inner_rect().size();
245 auto const& source_bitmap = layer->content_bitmap();
246 auto preview_bitmap_size = editor_rect_size.contains(source_bitmap.size()) ? source_bitmap.size() : editor_rect_size;
247
248 m_cached_preview_bitmap = TRY(Gfx::Bitmap::create(source_bitmap.format(), preview_bitmap_size));
249 GUI::Painter preview_painter(*m_cached_preview_bitmap);
250 preview_painter.draw_scaled_bitmap(m_cached_preview_bitmap->rect(), source_bitmap, source_bitmap.rect(), 0.8f, Gfx::Painter::ScalingMode::BilinearBlend);
251 Gfx::ContrastFilter preview_filter(0.5f);
252 preview_filter.apply(*m_cached_preview_bitmap, m_cached_preview_bitmap->rect(), *m_cached_preview_bitmap, m_cached_preview_bitmap->rect());
253 return {};
254}
255
256Optional<ResizeAnchorLocation const> MoveTool::resize_anchor_location_from_cursor_position(Layer const* layer, MouseEvent& event)
257{
258 auto layer_rect = m_editor->content_to_frame_rect(layer->relative_rect()).to_type<int>();
259 auto size = max(resize_anchor_min_size, resize_anchor_size(layer_rect));
260
261 auto cursor_within_resize_anchor_rect = [&event, size](Gfx::IntPoint layer_position_in_frame_coordinates) {
262 auto resize_anchor_rect = resize_anchor_rect_from_position(layer_position_in_frame_coordinates, size);
263 return resize_anchor_rect.contains(event.raw_event().position());
264 };
265
266 if (cursor_within_resize_anchor_rect(layer_rect.top_left()))
267 return ResizeAnchorLocation::TopLeft;
268 if (cursor_within_resize_anchor_rect(layer_rect.top_right().translated(1, 0)))
269 return ResizeAnchorLocation::TopRight;
270 if (cursor_within_resize_anchor_rect(layer_rect.bottom_left().translated(0, 1)))
271 return ResizeAnchorLocation::BottomLeft;
272 if (cursor_within_resize_anchor_rect(layer_rect.bottom_right().translated(1)))
273 return ResizeAnchorLocation::BottomRight;
274 return {};
275}
276
277Variant<Gfx::StandardCursor, NonnullRefPtr<Gfx::Bitmap const>> MoveTool::cursor()
278{
279 if (m_resize_anchor_location.has_value()) {
280 switch (m_resize_anchor_location.value()) {
281 case ResizeAnchorLocation::TopLeft:
282 case ResizeAnchorLocation::BottomRight:
283 return Gfx::StandardCursor::ResizeDiagonalTLBR;
284 case ResizeAnchorLocation::BottomLeft:
285 case ResizeAnchorLocation::TopRight:
286 return Gfx::StandardCursor::ResizeDiagonalBLTR;
287 }
288 }
289 return Gfx::StandardCursor::Move;
290}
291
292ErrorOr<GUI::Widget*> MoveTool::get_properties_widget()
293{
294 if (!m_properties_widget) {
295 auto properties_widget = TRY(GUI::Widget::try_create());
296 (void)TRY(properties_widget->try_set_layout<GUI::VerticalBoxLayout>());
297
298 auto selection_mode_container = TRY(properties_widget->try_add<GUI::Widget>());
299 (void)TRY(selection_mode_container->try_set_layout<GUI::HorizontalBoxLayout>());
300 selection_mode_container->set_fixed_height(46);
301 auto selection_mode_label = TRY(selection_mode_container->try_add<GUI::Label>("Selection Mode:"));
302 selection_mode_label->set_text_alignment(Gfx::TextAlignment::CenterLeft);
303 selection_mode_label->set_fixed_size(80, 40);
304
305 auto mode_radio_container = TRY(selection_mode_container->try_add<GUI::Widget>());
306 (void)TRY(mode_radio_container->try_set_layout<GUI::VerticalBoxLayout>());
307 m_selection_mode_foreground = TRY(mode_radio_container->try_add<GUI::RadioButton>(TRY("Foreground"_string)));
308
309 m_selection_mode_active = TRY(mode_radio_container->try_add<GUI::RadioButton>(TRY("Active Layer"_string)));
310
311 m_selection_mode_foreground->on_checked = [this](bool) {
312 m_layer_selection_mode = LayerSelectionMode::ForegroundLayer;
313 };
314 m_selection_mode_active->on_checked = [this](bool) {
315 m_layer_selection_mode = LayerSelectionMode::ActiveLayer;
316 };
317
318 m_selection_mode_foreground->set_checked(true);
319 m_properties_widget = properties_widget;
320 }
321
322 return m_properties_widget.ptr();
323}
324
325void MoveTool::toggle_selection_mode()
326{
327 if (m_selection_mode_foreground->is_checked())
328 m_selection_mode_active->set_checked(true);
329 else
330 m_selection_mode_foreground->set_checked(true);
331}
332
333}