Serenity Operating System
1/*
2 * Copyright (c) 2021, Mustafa Quraish <mustafa@serenityos.org>
3 *
4 * SPDX-License-Identifier: BSD-2-Clause
5 */
6
7#include "CloneTool.h"
8#include "../ImageEditor.h"
9#include "../Layer.h"
10#include <LibGUI/BoxLayout.h>
11#include <LibGUI/Label.h>
12#include <LibGUI/Menu.h>
13#include <LibGUI/Painter.h>
14#include <LibGUI/ValueSlider.h>
15#include <LibGfx/Bitmap.h>
16
17namespace PixelPaint {
18
19void CloneTool::draw_point(Gfx::Bitmap& bitmap, Gfx::Color, Gfx::IntPoint point)
20{
21 if (!m_sample_location.has_value())
22 return;
23
24 auto source_point = point - m_cursor_offset.value();
25 for (int y = -size(); y < size(); y++) {
26 for (int x = -size(); x < size(); x++) {
27 auto target_x = point.x() + x;
28 auto target_y = point.y() + y;
29 auto distance = point.distance_from({ target_x, target_y });
30 if (target_x < 0 || target_x >= bitmap.width() || target_y < 0 || target_y >= bitmap.height())
31 continue;
32 if (distance >= size())
33 continue;
34
35 auto source_x = source_point.x() + x;
36 auto source_y = source_point.y() + y;
37 if (source_x < 0 || source_x >= bitmap.width() || source_y < 0 || source_y >= bitmap.height())
38 continue;
39
40 auto falloff = get_falloff(distance);
41 auto pixel_color = bitmap.get_pixel(source_x, source_y);
42 pixel_color.set_alpha(falloff * pixel_color.alpha());
43 bitmap.set_pixel(target_x, target_y, bitmap.get_pixel(target_x, target_y).blend(pixel_color));
44 }
45 }
46}
47
48void CloneTool::draw_line(Gfx::Bitmap& bitmap, Gfx::Color color, Gfx::IntPoint start, Gfx::IntPoint end)
49{
50 if (!m_sample_location.has_value())
51 return;
52 BrushTool::draw_line(bitmap, color, start, end);
53}
54
55Variant<Gfx::StandardCursor, NonnullRefPtr<Gfx::Bitmap const>> CloneTool::cursor()
56{
57 if (m_is_selecting_location)
58 return Gfx::StandardCursor::Eyedropper;
59 return Gfx::StandardCursor::Crosshair;
60}
61
62void CloneTool::on_mousemove(Layer* layer, MouseEvent& event)
63{
64 auto& image_event = event.image_event();
65 if (image_event.alt())
66 return;
67
68 if (m_cursor_offset.has_value()) {
69 m_sample_location = image_event.position() - m_cursor_offset.value();
70 // FIXME: This is a really inefficient way to update the marker's location
71 m_editor->update();
72 }
73
74 BrushTool::on_mousemove(layer, event);
75}
76
77void CloneTool::on_mousedown(Layer* layer, MouseEvent& event)
78{
79 auto& image_event = event.image_event();
80 if (image_event.alt()) {
81 m_sample_location = image_event.position();
82 m_cursor_offset = {};
83 // FIXME: This is a really dumb way to get the marker to show up
84 m_editor->update();
85 return;
86 }
87
88 if (!m_sample_location.has_value())
89 return;
90
91 if (!m_cursor_offset.has_value())
92 m_cursor_offset = event.image_event().position() - m_sample_location.value();
93
94 BrushTool::on_mousedown(layer, event);
95}
96
97void CloneTool::on_second_paint(Layer const*, GUI::PaintEvent& event)
98{
99 if (!m_sample_location.has_value())
100 return;
101
102 GUI::Painter painter(*m_editor);
103 painter.add_clip_rect(event.rect());
104
105 auto sample_pos = m_editor->content_to_frame_position(m_sample_location.value());
106 // We don't want the marker to be a single pixel and hide the color.
107 auto offset = AK::max(2, size() / 2);
108 Gfx::IntRect rect = {
109 (int)sample_pos.x() - offset,
110 (int)sample_pos.y() - offset,
111 offset * 2,
112 offset * 2
113 };
114 painter.draw_ellipse_intersecting(rect, m_marker_color, 1);
115}
116
117bool CloneTool::on_keydown(GUI::KeyEvent& event)
118{
119 if (event.key() == KeyCode::Key_Alt && !m_is_selecting_location) {
120 m_is_selecting_location = true;
121 m_editor->update_tool_cursor();
122 return true;
123 }
124 return Tool::on_keydown(event);
125}
126
127void CloneTool::on_keyup(GUI::KeyEvent& event)
128{
129 if (m_is_selecting_location && event.key() == KeyCode::Key_Alt) {
130 m_is_selecting_location = false;
131 m_editor->update_tool_cursor();
132 return;
133 }
134}
135
136ErrorOr<GUI::Widget*> CloneTool::get_properties_widget()
137{
138 if (!m_properties_widget) {
139 auto properties_widget = TRY(GUI::Widget::try_create());
140 (void)TRY(properties_widget->try_set_layout<GUI::VerticalBoxLayout>());
141
142 auto size_container = TRY(properties_widget->try_add<GUI::Widget>());
143 size_container->set_fixed_height(20);
144 (void)TRY(size_container->try_set_layout<GUI::HorizontalBoxLayout>());
145
146 auto size_label = TRY(size_container->try_add<GUI::Label>("Size:"));
147 size_label->set_text_alignment(Gfx::TextAlignment::CenterLeft);
148 size_label->set_fixed_size(80, 20);
149
150 auto size_slider = TRY(size_container->try_add<GUI::ValueSlider>(Orientation::Horizontal, "px"_short_string));
151 size_slider->set_range(1, 100);
152 size_slider->set_value(size());
153
154 size_slider->on_change = [this](int value) {
155 set_size(value);
156 };
157 set_primary_slider(size_slider);
158
159 auto hardness_container = TRY(properties_widget->try_add<GUI::Widget>());
160 hardness_container->set_fixed_height(20);
161 (void)TRY(hardness_container->try_set_layout<GUI::HorizontalBoxLayout>());
162
163 auto hardness_label = TRY(hardness_container->try_add<GUI::Label>("Hardness:"));
164 hardness_label->set_text_alignment(Gfx::TextAlignment::CenterLeft);
165 hardness_label->set_fixed_size(80, 20);
166
167 auto hardness_slider = TRY(hardness_container->try_add<GUI::ValueSlider>(Orientation::Horizontal, "%"_short_string));
168 hardness_slider->set_range(1, 100);
169 hardness_slider->on_change = [&](int value) {
170 set_hardness(value);
171 };
172 hardness_slider->set_value(100);
173 set_secondary_slider(hardness_slider);
174 m_properties_widget = properties_widget;
175 }
176
177 return m_properties_widget.ptr();
178}
179
180}