summaryrefslogtreecommitdiff
path: root/Userland/Applications/PixelPaint
diff options
context:
space:
mode:
authorAndreas Kling <kling@serenityos.org>2021-01-12 12:05:23 +0100
committerAndreas Kling <kling@serenityos.org>2021-01-12 12:05:23 +0100
commitdc28c07fa526841e05e16161c74a6c23984f1dd5 (patch)
treed68796bc7708eba33fbf7247e1a92188ac5acf6f /Userland/Applications/PixelPaint
parentaa939c4b4b8a7eb1d22b166ebb5fb737d6e66714 (diff)
downloadserenity-dc28c07fa526841e05e16161c74a6c23984f1dd5.zip
Applications: Move to Userland/Applications/
Diffstat (limited to 'Userland/Applications/PixelPaint')
-rw-r--r--Userland/Applications/PixelPaint/BrushTool.cpp169
-rw-r--r--Userland/Applications/PixelPaint/BrushTool.h54
-rw-r--r--Userland/Applications/PixelPaint/BucketTool.cpp133
-rw-r--r--Userland/Applications/PixelPaint/BucketTool.h46
-rw-r--r--Userland/Applications/PixelPaint/CMakeLists.txt27
-rw-r--r--Userland/Applications/PixelPaint/CreateNewImageDialog.cpp91
-rw-r--r--Userland/Applications/PixelPaint/CreateNewImageDialog.h49
-rw-r--r--Userland/Applications/PixelPaint/CreateNewLayerDialog.cpp95
-rw-r--r--Userland/Applications/PixelPaint/CreateNewLayerDialog.h49
-rw-r--r--Userland/Applications/PixelPaint/EllipseTool.cpp137
-rw-r--r--Userland/Applications/PixelPaint/EllipseTool.h64
-rw-r--r--Userland/Applications/PixelPaint/EraseTool.cpp120
-rw-r--r--Userland/Applications/PixelPaint/EraseTool.h56
-rw-r--r--Userland/Applications/PixelPaint/FilterParams.h191
-rw-r--r--Userland/Applications/PixelPaint/Image.cpp331
-rw-r--r--Userland/Applications/PixelPaint/Image.h114
-rw-r--r--Userland/Applications/PixelPaint/ImageEditor.cpp418
-rw-r--r--Userland/Applications/PixelPaint/ImageEditor.h125
-rw-r--r--Userland/Applications/PixelPaint/Layer.cpp108
-rw-r--r--Userland/Applications/PixelPaint/Layer.h95
-rw-r--r--Userland/Applications/PixelPaint/LayerListWidget.cpp285
-rw-r--r--Userland/Applications/PixelPaint/LayerListWidget.h89
-rw-r--r--Userland/Applications/PixelPaint/LayerPropertiesWidget.cpp107
-rw-r--r--Userland/Applications/PixelPaint/LayerPropertiesWidget.h53
-rw-r--r--Userland/Applications/PixelPaint/LineTool.cpp156
-rw-r--r--Userland/Applications/PixelPaint/LineTool.h59
-rw-r--r--Userland/Applications/PixelPaint/MoveTool.cpp139
-rw-r--r--Userland/Applications/PixelPaint/MoveTool.h52
-rw-r--r--Userland/Applications/PixelPaint/PaletteWidget.cpp178
-rw-r--r--Userland/Applications/PixelPaint/PaletteWidget.h52
-rw-r--r--Userland/Applications/PixelPaint/PenTool.cpp128
-rw-r--r--Userland/Applications/PixelPaint/PenTool.h54
-rw-r--r--Userland/Applications/PixelPaint/PickerTool.cpp53
-rw-r--r--Userland/Applications/PixelPaint/PickerTool.h41
-rw-r--r--Userland/Applications/PixelPaint/RectangleTool.cpp137
-rw-r--r--Userland/Applications/PixelPaint/RectangleTool.h63
-rw-r--r--Userland/Applications/PixelPaint/SprayTool.cpp176
-rw-r--r--Userland/Applications/PixelPaint/SprayTool.h60
-rw-r--r--Userland/Applications/PixelPaint/Tool.cpp51
-rw-r--r--Userland/Applications/PixelPaint/Tool.h63
-rw-r--r--Userland/Applications/PixelPaint/ToolPropertiesWidget.cpp61
-rw-r--r--Userland/Applications/PixelPaint/ToolPropertiesWidget.h54
-rw-r--r--Userland/Applications/PixelPaint/ToolboxWidget.cpp140
-rw-r--r--Userland/Applications/PixelPaint/ToolboxWidget.h60
-rw-r--r--Userland/Applications/PixelPaint/main.cpp387
45 files changed, 5170 insertions, 0 deletions
diff --git a/Userland/Applications/PixelPaint/BrushTool.cpp b/Userland/Applications/PixelPaint/BrushTool.cpp
new file mode 100644
index 0000000000..200e270ea7
--- /dev/null
+++ b/Userland/Applications/PixelPaint/BrushTool.cpp
@@ -0,0 +1,169 @@
+/*
+ * Copyright (c) 2020, Ben Jilks <benjyjilks@gmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "BrushTool.h"
+#include "ImageEditor.h"
+#include "Layer.h"
+#include <LibGUI/Action.h>
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/Label.h>
+#include <LibGUI/Painter.h>
+#include <LibGUI/Slider.h>
+#include <LibGfx/Color.h>
+#include <LibGfx/Rect.h>
+#include <utility>
+
+namespace PixelPaint {
+
+BrushTool::BrushTool()
+{
+}
+
+BrushTool::~BrushTool()
+{
+}
+
+void BrushTool::on_mousedown(Layer&, GUI::MouseEvent& event, GUI::MouseEvent&)
+{
+ if (event.button() != GUI::MouseButton::Left && event.button() != GUI::MouseButton::Right)
+ return;
+
+ m_last_position = event.position();
+}
+
+void BrushTool::on_mousemove(Layer& layer, GUI::MouseEvent& event, GUI::MouseEvent&)
+{
+ if (!(event.buttons() & GUI::MouseButton::Left || event.buttons() & GUI::MouseButton::Right))
+ return;
+
+ draw_line(layer.bitmap(), m_editor->color_for(event), m_last_position, event.position());
+ layer.did_modify_bitmap(*m_editor->image());
+ m_last_position = event.position();
+ m_was_drawing = true;
+}
+
+void BrushTool::on_mouseup(Layer&, GUI::MouseEvent&, GUI::MouseEvent&)
+{
+ if (m_was_drawing) {
+ m_editor->did_complete_action();
+ m_was_drawing = false;
+ }
+}
+
+void BrushTool::draw_point(Gfx::Bitmap& bitmap, const Gfx::Color& color, const Gfx::IntPoint& point)
+{
+ for (int y = point.y() - m_size; y < point.y() + m_size; y++) {
+ for (int x = point.x() - m_size; x < point.x() + m_size; x++) {
+ auto distance = point.distance_from({ x, y });
+ if (x < 0 || x >= bitmap.width() || y < 0 || y >= bitmap.height())
+ continue;
+ if (distance >= m_size)
+ continue;
+
+ auto falloff = (1.0 - (distance / (float)m_size)) * (1.0f / (100 - m_hardness));
+ auto pixel_color = color;
+ pixel_color.set_alpha(falloff * 255);
+ bitmap.set_pixel(x, y, bitmap.get_pixel(x, y).blend(pixel_color));
+ }
+ }
+}
+
+void BrushTool::draw_line(Gfx::Bitmap& bitmap, const Gfx::Color& color, const Gfx::IntPoint& start, const Gfx::IntPoint& end)
+{
+ int length_x = end.x() - start.x();
+ int length_y = end.y() - start.y();
+ float y_step = length_y == 0 ? 0 : (float)(length_y) / (float)(length_x);
+ if (y_step > abs(length_y))
+ y_step = abs(length_y);
+ if (y_step < -abs(length_y))
+ y_step = -abs(length_y);
+ if (y_step == 0 && start.x() == end.x())
+ return;
+
+ int start_x = start.x();
+ int end_x = end.x();
+ int start_y = start.y();
+ int end_y = end.y();
+ if (start_x > end_x) {
+ swap(start_x, end_x);
+ swap(start_y, end_y);
+ }
+
+ float y = start_y;
+ for (int x = start_x; x <= end_x; x++) {
+ int start_step_y = y;
+ int end_step_y = y + y_step;
+ if (start_step_y > end_step_y)
+ swap(start_step_y, end_step_y);
+ for (int i = start_step_y; i <= end_step_y; i++)
+ draw_point(bitmap, color, { x, i });
+ y += y_step;
+ }
+}
+
+GUI::Widget* BrushTool::get_properties_widget()
+{
+ if (!m_properties_widget) {
+ m_properties_widget = GUI::Widget::construct();
+ m_properties_widget->set_layout<GUI::VerticalBoxLayout>();
+
+ auto& size_container = m_properties_widget->add<GUI::Widget>();
+ size_container.set_fixed_height(20);
+ size_container.set_layout<GUI::HorizontalBoxLayout>();
+
+ auto& size_label = size_container.add<GUI::Label>("Size:");
+ size_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
+ size_label.set_fixed_size(80, 20);
+
+ auto& size_slider = size_container.add<GUI::HorizontalSlider>();
+ size_slider.set_fixed_height(20);
+ size_slider.set_range(1, 100);
+ size_slider.set_value(m_size);
+ size_slider.on_change = [this](int value) {
+ m_size = value;
+ };
+
+ auto& hardness_container = m_properties_widget->add<GUI::Widget>();
+ hardness_container.set_fixed_height(20);
+ hardness_container.set_layout<GUI::HorizontalBoxLayout>();
+
+ auto& hardness_label = hardness_container.add<GUI::Label>("Hardness:");
+ hardness_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
+ hardness_label.set_fixed_size(80, 20);
+
+ auto& hardness_slider = hardness_container.add<GUI::HorizontalSlider>();
+ hardness_slider.set_fixed_height(20);
+ hardness_slider.set_range(1, 99);
+ hardness_slider.set_value(m_hardness);
+ hardness_slider.on_change = [this](int value) {
+ m_hardness = value;
+ };
+ }
+
+ return m_properties_widget.ptr();
+}
+
+}
diff --git a/Userland/Applications/PixelPaint/BrushTool.h b/Userland/Applications/PixelPaint/BrushTool.h
new file mode 100644
index 0000000000..939ccdeef4
--- /dev/null
+++ b/Userland/Applications/PixelPaint/BrushTool.h
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2020, Ben Jilks <benjyjilks@gmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "Tool.h"
+
+namespace PixelPaint {
+
+class BrushTool final : public Tool {
+public:
+ BrushTool();
+ virtual ~BrushTool() override;
+
+ virtual void on_mousedown(Layer&, GUI::MouseEvent& layer_event, GUI::MouseEvent& image_event) override;
+ virtual void on_mousemove(Layer&, GUI::MouseEvent& layer_event, GUI::MouseEvent& image_event) override;
+ virtual void on_mouseup(Layer&, GUI::MouseEvent& layer_event, GUI::MouseEvent& image_event) override;
+ virtual GUI::Widget* get_properties_widget() override;
+
+private:
+ RefPtr<GUI::Widget> m_properties_widget;
+ int m_size { 20 };
+ int m_hardness { 80 };
+ bool m_was_drawing { false };
+ Gfx::IntPoint m_last_position;
+
+ void draw_line(Gfx::Bitmap& bitmap, const Gfx::Color& color, const Gfx::IntPoint& start, const Gfx::IntPoint& end);
+ void draw_point(Gfx::Bitmap& bitmap, const Gfx::Color& color, const Gfx::IntPoint& point);
+};
+
+}
diff --git a/Userland/Applications/PixelPaint/BucketTool.cpp b/Userland/Applications/PixelPaint/BucketTool.cpp
new file mode 100644
index 0000000000..6cffddac20
--- /dev/null
+++ b/Userland/Applications/PixelPaint/BucketTool.cpp
@@ -0,0 +1,133 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "BucketTool.h"
+#include "ImageEditor.h"
+#include "Layer.h"
+#include <AK/Queue.h>
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/Label.h>
+#include <LibGUI/Painter.h>
+#include <LibGUI/Slider.h>
+#include <LibGfx/Bitmap.h>
+#include <LibGfx/Rect.h>
+
+namespace PixelPaint {
+
+BucketTool::BucketTool()
+{
+}
+
+BucketTool::~BucketTool()
+{
+}
+
+static float color_distance_squared(const Gfx::Color& lhs, const Gfx::Color& rhs)
+{
+ int a = rhs.red() - lhs.red();
+ int b = rhs.green() - lhs.green();
+ int c = rhs.blue() - lhs.blue();
+ return (a * a + b * b + c * c) / (255.0f * 255.0f);
+}
+
+static void flood_fill(Gfx::Bitmap& bitmap, const Gfx::IntPoint& start_position, Color target_color, Color fill_color, int threshold)
+{
+ ASSERT(bitmap.bpp() == 32);
+
+ if (target_color == fill_color)
+ return;
+
+ if (!bitmap.rect().contains(start_position))
+ return;
+
+ float threshold_normalized_squared = (threshold / 100.0f) * (threshold / 100.0f);
+
+ Queue<Gfx::IntPoint> queue;
+ queue.enqueue(start_position);
+ while (!queue.is_empty()) {
+ auto position = queue.dequeue();
+
+ auto pixel_color = bitmap.get_pixel<Gfx::StorageFormat::RGBA32>(position.x(), position.y());
+ if (color_distance_squared(pixel_color, target_color) > threshold_normalized_squared)
+ continue;
+
+ bitmap.set_pixel<Gfx::StorageFormat::RGBA32>(position.x(), position.y(), fill_color);
+
+ if (position.x() != 0)
+ queue.enqueue(position.translated(-1, 0));
+
+ if (position.x() != bitmap.width() - 1)
+ queue.enqueue(position.translated(1, 0));
+
+ if (position.y() != 0)
+ queue.enqueue(position.translated(0, -1));
+
+ if (position.y() != bitmap.height() - 1)
+ queue.enqueue(position.translated(0, 1));
+ }
+}
+
+void BucketTool::on_mousedown(Layer& layer, GUI::MouseEvent& event, GUI::MouseEvent&)
+{
+ if (!layer.rect().contains(event.position()))
+ return;
+
+ GUI::Painter painter(layer.bitmap());
+ auto target_color = layer.bitmap().get_pixel(event.x(), event.y());
+
+ flood_fill(layer.bitmap(), event.position(), target_color, m_editor->color_for(event), m_threshold);
+
+ layer.did_modify_bitmap(*m_editor->image());
+ m_editor->did_complete_action();
+}
+
+GUI::Widget* BucketTool::get_properties_widget()
+{
+ if (!m_properties_widget) {
+ m_properties_widget = GUI::Widget::construct();
+ m_properties_widget->set_layout<GUI::VerticalBoxLayout>();
+
+ auto& threshold_container = m_properties_widget->add<GUI::Widget>();
+ threshold_container.set_fixed_height(20);
+ threshold_container.set_layout<GUI::HorizontalBoxLayout>();
+
+ auto& threshold_label = threshold_container.add<GUI::Label>("Threshold:");
+ threshold_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
+ threshold_label.set_fixed_size(80, 20);
+
+ auto& threshold_slider = threshold_container.add<GUI::HorizontalSlider>();
+ threshold_slider.set_fixed_height(20);
+ threshold_slider.set_range(0, 100);
+ threshold_slider.set_value(m_threshold);
+ threshold_slider.on_change = [this](int value) {
+ m_threshold = value;
+ };
+ }
+
+ return m_properties_widget.ptr();
+}
+
+}
diff --git a/Userland/Applications/PixelPaint/BucketTool.h b/Userland/Applications/PixelPaint/BucketTool.h
new file mode 100644
index 0000000000..b4bf700815
--- /dev/null
+++ b/Userland/Applications/PixelPaint/BucketTool.h
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "Tool.h"
+
+namespace PixelPaint {
+
+class BucketTool final : public Tool {
+public:
+ BucketTool();
+ virtual ~BucketTool() override;
+
+ virtual void on_mousedown(Layer&, GUI::MouseEvent& layer_event, GUI::MouseEvent& image_event) override;
+ virtual GUI::Widget* get_properties_widget() override;
+
+private:
+ RefPtr<GUI::Widget> m_properties_widget;
+ int m_threshold { 0 };
+};
+
+}
diff --git a/Userland/Applications/PixelPaint/CMakeLists.txt b/Userland/Applications/PixelPaint/CMakeLists.txt
new file mode 100644
index 0000000000..713845c80e
--- /dev/null
+++ b/Userland/Applications/PixelPaint/CMakeLists.txt
@@ -0,0 +1,27 @@
+set(SOURCES
+ BrushTool.cpp
+ BucketTool.cpp
+ CreateNewImageDialog.cpp
+ CreateNewLayerDialog.cpp
+ EllipseTool.cpp
+ EraseTool.cpp
+ Image.cpp
+ ImageEditor.cpp
+ Layer.cpp
+ LayerListWidget.cpp
+ LayerPropertiesWidget.cpp
+ LineTool.cpp
+ main.cpp
+ MoveTool.cpp
+ PaletteWidget.cpp
+ PenTool.cpp
+ PickerTool.cpp
+ RectangleTool.cpp
+ SprayTool.cpp
+ ToolboxWidget.cpp
+ ToolPropertiesWidget.cpp
+ Tool.cpp
+)
+
+serenity_app(PixelPaint ICON app-pixel-paint)
+target_link_libraries(PixelPaint LibGUI LibGfx)
diff --git a/Userland/Applications/PixelPaint/CreateNewImageDialog.cpp b/Userland/Applications/PixelPaint/CreateNewImageDialog.cpp
new file mode 100644
index 0000000000..0696b69ba8
--- /dev/null
+++ b/Userland/Applications/PixelPaint/CreateNewImageDialog.cpp
@@ -0,0 +1,91 @@
+/*
+ * Copyright (c) 2020, Ben Jilks <benjyjilks@gmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "CreateNewImageDialog.h"
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/Button.h>
+#include <LibGUI/Label.h>
+#include <LibGUI/SpinBox.h>
+#include <LibGUI/TextBox.h>
+
+namespace PixelPaint {
+
+CreateNewImageDialog::CreateNewImageDialog(GUI::Window* parent_window)
+ : Dialog(parent_window)
+{
+ set_title("Create new image");
+ resize(200, 200);
+
+ auto& main_widget = set_main_widget<GUI::Widget>();
+ main_widget.set_fill_with_background_color(true);
+
+ auto& layout = main_widget.set_layout<GUI::VerticalBoxLayout>();
+ layout.set_margins({ 4, 4, 4, 4 });
+
+ auto& name_label = main_widget.add<GUI::Label>("Name:");
+ name_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
+
+ m_name_textbox = main_widget.add<GUI::TextBox>();
+ m_name_textbox->on_change = [this] {
+ m_image_name = m_name_textbox->text();
+ };
+
+ auto& width_label = main_widget.add<GUI::Label>("Width:");
+ width_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
+
+ auto& width_spinbox = main_widget.add<GUI::SpinBox>();
+
+ auto& height_label = main_widget.add<GUI::Label>("Height:");
+ height_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
+
+ auto& height_spinbox = main_widget.add<GUI::SpinBox>();
+
+ auto& button_container = main_widget.add<GUI::Widget>();
+ button_container.set_layout<GUI::HorizontalBoxLayout>();
+
+ auto& ok_button = button_container.add<GUI::Button>("OK");
+ ok_button.on_click = [this](auto) {
+ done(ExecOK);
+ };
+
+ auto& cancel_button = button_container.add<GUI::Button>("Cancel");
+ cancel_button.on_click = [this](auto) {
+ done(ExecCancel);
+ };
+
+ width_spinbox.on_change = [this](int value) {
+ m_image_size.set_width(value);
+ };
+
+ height_spinbox.on_change = [this](int value) {
+ m_image_size.set_height(value);
+ };
+
+ width_spinbox.set_range(0, 16384);
+ height_spinbox.set_range(0, 16384);
+}
+
+}
diff --git a/Userland/Applications/PixelPaint/CreateNewImageDialog.h b/Userland/Applications/PixelPaint/CreateNewImageDialog.h
new file mode 100644
index 0000000000..b46b65adab
--- /dev/null
+++ b/Userland/Applications/PixelPaint/CreateNewImageDialog.h
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2020, Ben Jilks <benjyjilks@gmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <LibGUI/Dialog.h>
+
+namespace PixelPaint {
+
+class CreateNewImageDialog final : public GUI::Dialog {
+ C_OBJECT(CreateNewImageDialog)
+
+public:
+ const Gfx::IntSize& image_size() const { return m_image_size; }
+ const String& image_name() const { return m_image_name; }
+
+private:
+ CreateNewImageDialog(GUI::Window* parent_window);
+
+ String m_image_name;
+ Gfx::IntSize m_image_size;
+
+ RefPtr<GUI::TextBox> m_name_textbox;
+};
+
+}
diff --git a/Userland/Applications/PixelPaint/CreateNewLayerDialog.cpp b/Userland/Applications/PixelPaint/CreateNewLayerDialog.cpp
new file mode 100644
index 0000000000..02f65b51ad
--- /dev/null
+++ b/Userland/Applications/PixelPaint/CreateNewLayerDialog.cpp
@@ -0,0 +1,95 @@
+/*
+ * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "CreateNewLayerDialog.h"
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/Button.h>
+#include <LibGUI/Label.h>
+#include <LibGUI/SpinBox.h>
+#include <LibGUI/TextBox.h>
+
+namespace PixelPaint {
+
+CreateNewLayerDialog::CreateNewLayerDialog(const Gfx::IntSize& suggested_size, GUI::Window* parent_window)
+ : Dialog(parent_window)
+{
+ set_title("Create new layer");
+ set_icon(parent_window->icon());
+ resize(200, 200);
+
+ auto& main_widget = set_main_widget<GUI::Widget>();
+ main_widget.set_fill_with_background_color(true);
+
+ auto& layout = main_widget.set_layout<GUI::VerticalBoxLayout>();
+ layout.set_margins({ 4, 4, 4, 4 });
+
+ auto& name_label = main_widget.add<GUI::Label>("Name:");
+ name_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
+
+ m_name_textbox = main_widget.add<GUI::TextBox>();
+ m_name_textbox->on_change = [this] {
+ m_layer_name = m_name_textbox->text();
+ };
+
+ auto& width_label = main_widget.add<GUI::Label>("Width:");
+ width_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
+
+ auto& width_spinbox = main_widget.add<GUI::SpinBox>();
+
+ auto& height_label = main_widget.add<GUI::Label>("Height:");
+ height_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
+
+ auto& height_spinbox = main_widget.add<GUI::SpinBox>();
+
+ auto& button_container = main_widget.add<GUI::Widget>();
+ button_container.set_layout<GUI::HorizontalBoxLayout>();
+
+ auto& ok_button = button_container.add<GUI::Button>("OK");
+ ok_button.on_click = [this](auto) {
+ done(ExecOK);
+ };
+
+ auto& cancel_button = button_container.add<GUI::Button>("Cancel");
+ cancel_button.on_click = [this](auto) {
+ done(ExecCancel);
+ };
+
+ width_spinbox.on_change = [this](int value) {
+ m_layer_size.set_width(value);
+ };
+
+ height_spinbox.on_change = [this](int value) {
+ m_layer_size.set_height(value);
+ };
+
+ width_spinbox.set_range(0, 16384);
+ height_spinbox.set_range(0, 16384);
+
+ width_spinbox.set_value(suggested_size.width());
+ height_spinbox.set_value(suggested_size.height());
+}
+
+}
diff --git a/Userland/Applications/PixelPaint/CreateNewLayerDialog.h b/Userland/Applications/PixelPaint/CreateNewLayerDialog.h
new file mode 100644
index 0000000000..bb07ff9943
--- /dev/null
+++ b/Userland/Applications/PixelPaint/CreateNewLayerDialog.h
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <LibGUI/Dialog.h>
+
+namespace PixelPaint {
+
+class CreateNewLayerDialog final : public GUI::Dialog {
+ C_OBJECT(CreateNewLayerDialog);
+
+public:
+ const Gfx::IntSize& layer_size() const { return m_layer_size; }
+ const String& layer_name() const { return m_layer_name; }
+
+private:
+ CreateNewLayerDialog(const Gfx::IntSize& suggested_size, GUI::Window* parent_window);
+
+ Gfx::IntSize m_layer_size;
+ String m_layer_name;
+
+ RefPtr<GUI::TextBox> m_name_textbox;
+};
+
+}
diff --git a/Userland/Applications/PixelPaint/EllipseTool.cpp b/Userland/Applications/PixelPaint/EllipseTool.cpp
new file mode 100644
index 0000000000..0501e2fb96
--- /dev/null
+++ b/Userland/Applications/PixelPaint/EllipseTool.cpp
@@ -0,0 +1,137 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "EllipseTool.h"
+#include "ImageEditor.h"
+#include "Layer.h"
+#include <LibGUI/Action.h>
+#include <LibGUI/Menu.h>
+#include <LibGUI/Painter.h>
+#include <LibGfx/Rect.h>
+#include <math.h>
+
+namespace PixelPaint {
+
+EllipseTool::EllipseTool()
+{
+}
+
+EllipseTool::~EllipseTool()
+{
+}
+
+void EllipseTool::draw_using(GUI::Painter& painter, const Gfx::IntRect& ellipse_intersecting_rect)
+{
+ switch (m_mode) {
+ case Mode::Outline:
+ painter.draw_ellipse_intersecting(ellipse_intersecting_rect, m_editor->color_for(m_drawing_button), m_thickness);
+ break;
+ default:
+ ASSERT_NOT_REACHED();
+ }
+}
+
+void EllipseTool::on_mousedown(Layer&, GUI::MouseEvent& event, GUI::MouseEvent&)
+{
+ if (event.button() != GUI::MouseButton::Left && event.button() != GUI::MouseButton::Right)
+ return;
+
+ if (m_drawing_button != GUI::MouseButton::None)
+ return;
+
+ m_drawing_button = event.button();
+ m_ellipse_start_position = event.position();
+ m_ellipse_end_position = event.position();
+ m_editor->update();
+}
+
+void EllipseTool::on_mouseup(Layer& layer, GUI::MouseEvent& event, GUI::MouseEvent&)
+{
+ if (event.button() == m_drawing_button) {
+ GUI::Painter painter(layer.bitmap());
+ draw_using(painter, Gfx::IntRect::from_two_points(m_ellipse_start_position, m_ellipse_end_position));
+ m_drawing_button = GUI::MouseButton::None;
+ m_editor->update();
+ m_editor->did_complete_action();
+ }
+}
+
+void EllipseTool::on_mousemove(Layer&, GUI::MouseEvent& event, GUI::MouseEvent&)
+{
+ if (m_drawing_button == GUI::MouseButton::None)
+ return;
+
+ m_ellipse_end_position = event.position();
+ m_editor->update();
+}
+
+void EllipseTool::on_second_paint(const Layer& layer, GUI::PaintEvent& event)
+{
+ if (m_drawing_button == GUI::MouseButton::None)
+ return;
+
+ GUI::Painter painter(*m_editor);
+ painter.add_clip_rect(event.rect());
+ auto preview_start = m_editor->layer_position_to_editor_position(layer, m_ellipse_start_position).to_type<int>();
+ auto preview_end = m_editor->layer_position_to_editor_position(layer, m_ellipse_end_position).to_type<int>();
+ draw_using(painter, Gfx::IntRect::from_two_points(preview_start, preview_end));
+}
+
+void EllipseTool::on_keydown(GUI::KeyEvent& event)
+{
+ if (event.key() == Key_Escape && m_drawing_button != GUI::MouseButton::None) {
+ m_drawing_button = GUI::MouseButton::None;
+ m_editor->update();
+ event.accept();
+ }
+}
+
+void EllipseTool::on_tool_button_contextmenu(GUI::ContextMenuEvent& event)
+{
+ if (!m_context_menu) {
+ m_context_menu = GUI::Menu::construct();
+ m_context_menu->add_action(GUI::Action::create("Outline", [this](auto&) {
+ m_mode = Mode::Outline;
+ }));
+ m_context_menu->add_separator();
+ m_thickness_actions.set_exclusive(true);
+ auto insert_action = [&](int size, bool checked = false) {
+ auto action = GUI::Action::create_checkable(String::number(size), [this, size](auto&) {
+ m_thickness = size;
+ });
+ action->set_checked(checked);
+ m_thickness_actions.add_action(*action);
+ m_context_menu->add_action(move(action));
+ };
+ insert_action(1, true);
+ insert_action(2);
+ insert_action(3);
+ insert_action(4);
+ }
+ m_context_menu->popup(event.screen_position());
+}
+
+}
diff --git a/Userland/Applications/PixelPaint/EllipseTool.h b/Userland/Applications/PixelPaint/EllipseTool.h
new file mode 100644
index 0000000000..0b1c10c89a
--- /dev/null
+++ b/Userland/Applications/PixelPaint/EllipseTool.h
@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "Tool.h"
+#include <LibGUI/ActionGroup.h>
+#include <LibGfx/Point.h>
+
+namespace PixelPaint {
+
+class EllipseTool final : public Tool {
+public:
+ EllipseTool();
+ virtual ~EllipseTool() override;
+
+ virtual void on_mousedown(Layer&, GUI::MouseEvent& layer_event, GUI::MouseEvent& image_event) override;
+ virtual void on_mousemove(Layer&, GUI::MouseEvent& layer_event, GUI::MouseEvent& image_event) override;
+ virtual void on_mouseup(Layer&, GUI::MouseEvent& layer_event, GUI::MouseEvent& image_event) override;
+ virtual void on_tool_button_contextmenu(GUI::ContextMenuEvent&) override;
+ virtual void on_second_paint(const Layer&, GUI::PaintEvent&) override;
+ virtual void on_keydown(GUI::KeyEvent&) override;
+
+private:
+ enum class Mode {
+ Outline,
+ // FIXME: Add Mode::Fill
+ };
+
+ void draw_using(GUI::Painter&, const Gfx::IntRect&);
+
+ GUI::MouseButton m_drawing_button { GUI::MouseButton::None };
+ Gfx::IntPoint m_ellipse_start_position;
+ Gfx::IntPoint m_ellipse_end_position;
+ RefPtr<GUI::Menu> m_context_menu;
+ int m_thickness { 1 };
+ GUI::ActionGroup m_thickness_actions;
+ Mode m_mode { Mode::Outline };
+};
+
+}
diff --git a/Userland/Applications/PixelPaint/EraseTool.cpp b/Userland/Applications/PixelPaint/EraseTool.cpp
new file mode 100644
index 0000000000..19d1482ba8
--- /dev/null
+++ b/Userland/Applications/PixelPaint/EraseTool.cpp
@@ -0,0 +1,120 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "EraseTool.h"
+#include "ImageEditor.h"
+#include "Layer.h"
+#include <LibGUI/Action.h>
+#include <LibGUI/Menu.h>
+#include <LibGUI/Painter.h>
+#include <LibGfx/Bitmap.h>
+
+namespace PixelPaint {
+
+EraseTool::EraseTool()
+{
+}
+
+EraseTool::~EraseTool()
+{
+}
+
+Gfx::IntRect EraseTool::build_rect(const Gfx::IntPoint& pos, const Gfx::IntRect& widget_rect)
+{
+ const int base_eraser_size = 10;
+ const int eraser_size = (base_eraser_size * m_thickness);
+ const int eraser_radius = eraser_size / 2;
+ const auto ex = pos.x();
+ const auto ey = pos.y();
+ return Gfx::IntRect(ex - eraser_radius, ey - eraser_radius, eraser_size, eraser_size).intersected(widget_rect);
+}
+
+void EraseTool::on_mousedown(Layer& layer, GUI::MouseEvent& event, GUI::MouseEvent&)
+{
+ if (event.button() != GUI::MouseButton::Left && event.button() != GUI::MouseButton::Right)
+ return;
+ Gfx::IntRect r = build_rect(event.position(), layer.rect());
+ GUI::Painter painter(layer.bitmap());
+ painter.clear_rect(r, get_color());
+ layer.did_modify_bitmap(*m_editor->image());
+}
+
+void EraseTool::on_mousemove(Layer& layer, GUI::MouseEvent& event, GUI::MouseEvent&)
+{
+ if (event.buttons() & GUI::MouseButton::Left || event.buttons() & GUI::MouseButton::Right) {
+ Gfx::IntRect r = build_rect(event.position(), layer.rect());
+ GUI::Painter painter(layer.bitmap());
+ painter.clear_rect(r, get_color());
+ layer.did_modify_bitmap(*m_editor->image());
+ }
+}
+
+void EraseTool::on_mouseup(Layer&, GUI::MouseEvent& event, GUI::MouseEvent&)
+{
+ if (event.button() != GUI::MouseButton::Left && event.button() != GUI::MouseButton::Right)
+ return;
+ m_editor->did_complete_action();
+}
+
+void EraseTool::on_tool_button_contextmenu(GUI::ContextMenuEvent& event)
+{
+ if (!m_context_menu) {
+ m_context_menu = GUI::Menu::construct();
+
+ auto eraser_color_toggler = GUI::Action::create_checkable("Use secondary color", [&](auto& action) {
+ m_use_secondary_color = action.is_checked();
+ });
+ eraser_color_toggler->set_checked(m_use_secondary_color);
+
+ m_context_menu->add_action(eraser_color_toggler);
+ m_context_menu->add_separator();
+
+ m_thickness_actions.set_exclusive(true);
+ auto insert_action = [&](int size, bool checked = false) {
+ auto action = GUI::Action::create_checkable(String::number(size), [this, size](auto&) {
+ m_thickness = size;
+ });
+ action->set_checked(checked);
+ m_thickness_actions.add_action(*action);
+ m_context_menu->add_action(move(action));
+ };
+ insert_action(1, true);
+ insert_action(2);
+ insert_action(3);
+ insert_action(4);
+ }
+
+ m_context_menu->popup(event.screen_position());
+}
+
+Color EraseTool::get_color() const
+{
+ if (m_use_secondary_color)
+ return m_editor->secondary_color();
+ return Color(255, 255, 255, 0);
+}
+
+}
diff --git a/Userland/Applications/PixelPaint/EraseTool.h b/Userland/Applications/PixelPaint/EraseTool.h
new file mode 100644
index 0000000000..16241e87f0
--- /dev/null
+++ b/Userland/Applications/PixelPaint/EraseTool.h
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "Tool.h"
+#include <LibGUI/ActionGroup.h>
+#include <LibGfx/Forward.h>
+#include <LibGfx/Point.h>
+
+namespace PixelPaint {
+
+class EraseTool final : public Tool {
+public:
+ EraseTool();
+ virtual ~EraseTool() override;
+
+ virtual void on_mousedown(Layer&, GUI::MouseEvent& layer_event, GUI::MouseEvent& image_event) override;
+ virtual void on_mousemove(Layer&, GUI::MouseEvent& layer_event, GUI::MouseEvent& image_event) override;
+ virtual void on_mouseup(Layer&, GUI::MouseEvent& layer_event, GUI::MouseEvent& image_event) override;
+ virtual void on_tool_button_contextmenu(GUI::ContextMenuEvent&) override;
+
+private:
+ Gfx::Color get_color() const;
+ Gfx::IntRect build_rect(const Gfx::IntPoint& pos, const Gfx::IntRect& widget_rect);
+ RefPtr<GUI::Menu> m_context_menu;
+
+ bool m_use_secondary_color { false };
+ int m_thickness { 1 };
+ GUI::ActionGroup m_thickness_actions;
+};
+
+}
diff --git a/Userland/Applications/PixelPaint/FilterParams.h b/Userland/Applications/PixelPaint/FilterParams.h
new file mode 100644
index 0000000000..398caecf9f
--- /dev/null
+++ b/Userland/Applications/PixelPaint/FilterParams.h
@@ -0,0 +1,191 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <LibGUI/Action.h>
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/Button.h>
+#include <LibGUI/CheckBox.h>
+#include <LibGUI/Dialog.h>
+#include <LibGUI/Menu.h>
+#include <LibGUI/Painter.h>
+#include <LibGUI/TextBox.h>
+#include <LibGfx/Filters/BoxBlurFilter.h>
+#include <LibGfx/Filters/GenericConvolutionFilter.h>
+#include <LibGfx/Filters/LaplacianFilter.h>
+#include <LibGfx/Filters/SharpenFilter.h>
+#include <LibGfx/Filters/SpatialGaussianBlurFilter.h>
+
+namespace PixelPaint {
+
+template<typename Filter>
+struct FilterParameters {
+};
+
+template<size_t N>
+class GenericConvolutionFilterInputDialog : public GUI::Dialog {
+ C_OBJECT(GenericConvolutionFilterInputDialog);
+
+public:
+ const Matrix<N, float>& matrix() const { return m_matrix; }
+ bool should_wrap() const { return m_should_wrap; }
+
+private:
+ explicit GenericConvolutionFilterInputDialog(GUI::Window* parent_window)
+ : Dialog(parent_window)
+ {
+ // FIXME: Help! Make this GUI less ugly.
+ StringBuilder builder;
+ builder.appendf("%zux%zu", N, N);
+ builder.append(" Convolution");
+ set_title(builder.string_view());
+
+ resize(200, 250);
+ auto& main_widget = set_main_widget<GUI::Frame>();
+ main_widget.set_frame_shape(Gfx::FrameShape::Container);
+ main_widget.set_frame_shadow(Gfx::FrameShadow::Raised);
+ main_widget.set_fill_with_background_color(true);
+ auto& layout = main_widget.template set_layout<GUI::VerticalBoxLayout>();
+ layout.set_margins({ 4, 4, 4, 4 });
+
+ size_t index = 0;
+ size_t columns = N;
+ size_t rows = N;
+
+ for (size_t row = 0; row < rows; ++row) {
+ auto& horizontal_container = main_widget.template add<GUI::Widget>();
+ horizontal_container.template set_layout<GUI::HorizontalBoxLayout>();
+ for (size_t column = 0; column < columns; ++column) {
+ if (index < columns * rows) {
+ auto& textbox = horizontal_container.template add<GUI::TextBox>();
+ textbox.on_change = [&, row = row, column = column] {
+ auto& element = m_matrix.elements()[row][column];
+ char* endptr = nullptr;
+ auto value = strtof(textbox.text().characters(), &endptr);
+ if (endptr != nullptr)
+ element = value;
+ else
+ textbox.set_text("");
+ };
+ } else {
+ horizontal_container.template add<GUI::Widget>();
+ }
+ }
+ }
+
+ auto& norm_checkbox = main_widget.template add<GUI::CheckBox>("Normalize");
+ norm_checkbox.set_checked(false);
+
+ auto& wrap_checkbox = main_widget.template add<GUI::CheckBox>("Wrap");
+ wrap_checkbox.set_checked(m_should_wrap);
+
+ auto& button = main_widget.template add<GUI::Button>("Done");
+ button.on_click = [&](auto) {
+ m_should_wrap = wrap_checkbox.is_checked();
+ if (norm_checkbox.is_checked())
+ normalize(m_matrix);
+ done(ExecOK);
+ };
+ }
+
+ Matrix<N, float> m_matrix {};
+ bool m_should_wrap { false };
+};
+
+template<size_t N>
+struct FilterParameters<Gfx::SpatialGaussianBlurFilter<N>> {
+ static OwnPtr<typename Gfx::SpatialGaussianBlurFilter<N>::Parameters> get()
+ {
+ constexpr static ssize_t offset = N / 2;
+ Matrix<N, float> kernel;
+ auto sigma = 1.0f;
+ auto s = 2.0f * sigma * sigma;
+
+ for (auto x = -offset; x <= offset; x++) {
+ for (auto y = -offset; y <= offset; y++) {
+ auto r = sqrt(x * x + y * y);
+ kernel.elements()[x + offset][y + offset] = (exp(-(r * r) / s)) / (M_PI * s);
+ }
+ }
+
+ normalize(kernel);
+
+ return make<typename Gfx::GenericConvolutionFilter<N>::Parameters>(kernel);
+ }
+};
+
+template<>
+struct FilterParameters<Gfx::SharpenFilter> {
+ static OwnPtr<Gfx::GenericConvolutionFilter<3>::Parameters> get()
+ {
+ return make<Gfx::GenericConvolutionFilter<3>::Parameters>(Matrix<3, float>(0, -1, 0, -1, 5, -1, 0, -1, 0));
+ }
+};
+
+template<>
+struct FilterParameters<Gfx::LaplacianFilter> {
+ static OwnPtr<Gfx::GenericConvolutionFilter<3>::Parameters> get(bool diagonal)
+ {
+ if (diagonal)
+ return make<Gfx::GenericConvolutionFilter<3>::Parameters>(Matrix<3, float>(-1, -1, -1, -1, 8, -1, -1, -1, -1));
+
+ return make<Gfx::GenericConvolutionFilter<3>::Parameters>(Matrix<3, float>(0, -1, 0, -1, 4, -1, 0, -1, 0));
+ }
+};
+
+template<size_t N>
+struct FilterParameters<Gfx::GenericConvolutionFilter<N>> {
+ static OwnPtr<typename Gfx::GenericConvolutionFilter<N>::Parameters> get(GUI::Window* parent_window)
+ {
+ auto input = GenericConvolutionFilterInputDialog<N>::construct(parent_window);
+ input->exec();
+ if (input->result() == GUI::Dialog::ExecOK)
+ return make<typename Gfx::GenericConvolutionFilter<N>::Parameters>(input->matrix(), input->should_wrap());
+
+ return {};
+ }
+};
+
+template<size_t N>
+struct FilterParameters<Gfx::BoxBlurFilter<N>> {
+ static OwnPtr<typename Gfx::GenericConvolutionFilter<N>::Parameters> get()
+ {
+ Matrix<N, float> kernel;
+
+ for (size_t i = 0; i < N; ++i) {
+ for (size_t j = 0; j < N; ++j) {
+ kernel.elements()[i][j] = 1;
+ }
+ }
+
+ normalize(kernel);
+
+ return make<typename Gfx::GenericConvolutionFilter<N>::Parameters>(kernel);
+ }
+};
+
+}
diff --git a/Userland/Applications/PixelPaint/Image.cpp b/Userland/Applications/PixelPaint/Image.cpp
new file mode 100644
index 0000000000..09a075dfe0
--- /dev/null
+++ b/Userland/Applications/PixelPaint/Image.cpp
@@ -0,0 +1,331 @@
+/*
+ * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "Image.h"
+#include "Layer.h"
+#include <AK/Base64.h>
+#include <AK/JsonObject.h>
+#include <AK/JsonObjectSerializer.h>
+#include <AK/JsonValue.h>
+#include <AK/StringBuilder.h>
+#include <LibGUI/Painter.h>
+#include <LibGfx/BMPWriter.h>
+#include <LibGfx/ImageDecoder.h>
+#include <stdio.h>
+
+//#define PAINT_DEBUG
+
+namespace PixelPaint {
+
+RefPtr<Image> Image::create_with_size(const Gfx::IntSize& size)
+{
+ if (size.is_empty())
+ return nullptr;
+
+ if (size.width() > 16384 || size.height() > 16384)
+ return nullptr;
+
+ return adopt(*new Image(size));
+}
+
+Image::Image(const Gfx::IntSize& size)
+ : m_size(size)
+{
+}
+
+void Image::paint_into(GUI::Painter& painter, const Gfx::IntRect& dest_rect)
+{
+ float scale = (float)dest_rect.width() / (float)rect().width();
+ Gfx::PainterStateSaver saver(painter);
+ painter.add_clip_rect(dest_rect);
+ for (auto& layer : m_layers) {
+ if (!layer.is_visible())
+ continue;
+ auto target = dest_rect.translated(layer.location().x() * scale, layer.location().y() * scale);
+ target.set_size(layer.size().width() * scale, layer.size().height() * scale);
+ painter.draw_scaled_bitmap(target, layer.bitmap(), layer.rect(), (float)layer.opacity_percent() / 100.0f);
+ }
+}
+
+RefPtr<Image> Image::create_from_file(const String& file_path)
+{
+ auto file = fopen(file_path.characters(), "r");
+ fseek(file, 0L, SEEK_END);
+ auto length = ftell(file);
+ rewind(file);
+
+ auto buffer = ByteBuffer::create_uninitialized(length);
+ fread(buffer.data(), sizeof(u8), length, file);
+ fclose(file);
+
+ auto json_or_error = JsonValue::from_string(String::copy(buffer));
+ if (!json_or_error.has_value())
+ return nullptr;
+
+ auto json = json_or_error.value().as_object();
+ auto image = create_with_size({ json.get("width").to_i32(), json.get("height").to_i32() });
+ json.get("layers").as_array().for_each([&](JsonValue json_layer) {
+ auto json_layer_object = json_layer.as_object();
+ auto width = json_layer_object.get("width").to_i32();
+ auto height = json_layer_object.get("height").to_i32();
+ auto name = json_layer_object.get("name").as_string();
+ auto layer = Layer::create_with_size(*image, { width, height }, name);
+ layer->set_location({ json_layer_object.get("locationx").to_i32(), json_layer_object.get("locationy").to_i32() });
+ layer->set_opacity_percent(json_layer_object.get("opacity_percent").to_i32());
+ layer->set_visible(json_layer_object.get("visible").as_bool());
+ layer->set_selected(json_layer_object.get("selected").as_bool());
+
+ auto bitmap_base64_encoded = json_layer_object.get("bitmap").as_string();
+ auto bitmap_data = decode_base64(bitmap_base64_encoded);
+ auto image_decoder = Gfx::ImageDecoder::create(bitmap_data);
+ layer->set_bitmap(*image_decoder->bitmap());
+ image->add_layer(*layer);
+ });
+
+ return image;
+}
+
+void Image::save(const String& file_path) const
+{
+ // Build json file
+ StringBuilder builder;
+ JsonObjectSerializer json(builder);
+ json.add("width", m_size.width());
+ json.add("height", m_size.height());
+ {
+ auto json_layers = json.add_array("layers");
+ for (const auto& layer : m_layers) {
+ Gfx::BMPWriter bmp_dumber;
+ auto json_layer = json_layers.add_object();
+ json_layer.add("width", layer.size().width());
+ json_layer.add("height", layer.size().height());
+ json_layer.add("name", layer.name());
+ json_layer.add("locationx", layer.location().x());
+ json_layer.add("locationy", layer.location().y());
+ json_layer.add("opacity_percent", layer.opacity_percent());
+ json_layer.add("visible", layer.is_visible());
+ json_layer.add("selected", layer.is_selected());
+ json_layer.add("bitmap", encode_base64(bmp_dumber.dump(layer.bitmap())));
+ }
+ }
+ json.finish();
+
+ // Write json to disk
+ auto file = fopen(file_path.characters(), "w");
+ auto byte_buffer = builder.to_byte_buffer();
+ fwrite(byte_buffer.data(), sizeof(u8), byte_buffer.size(), file);
+ fclose(file);
+}
+
+void Image::export_bmp(const String& file_path)
+{
+ auto bitmap = Gfx::Bitmap::create(Gfx::BitmapFormat::RGB32, m_size);
+ GUI::Painter painter(*bitmap);
+ paint_into(painter, { 0, 0, m_size.width(), m_size.height() });
+
+ Gfx::BMPWriter dumper;
+ auto bmp = dumper.dump(bitmap);
+ auto file = fopen(file_path.characters(), "wb");
+ fwrite(bmp.data(), sizeof(u8), bmp.size(), file);
+ fclose(file);
+}
+
+void Image::add_layer(NonnullRefPtr<Layer> layer)
+{
+ for (auto& existing_layer : m_layers) {
+ ASSERT(&existing_layer != layer.ptr());
+ }
+ m_layers.append(move(layer));
+
+ for (auto* client : m_clients)
+ client->image_did_add_layer(m_layers.size() - 1);
+
+ did_modify_layer_stack();
+}
+
+RefPtr<Image> Image::take_snapshot() const
+{
+ auto snapshot = create_with_size(m_size);
+ for (const auto& layer : m_layers)
+ snapshot->add_layer(*Layer::create_snapshot(*snapshot, layer));
+ return snapshot;
+}
+
+void Image::restore_snapshot(const Image& snapshot)
+{
+ m_layers.clear();
+ select_layer(nullptr);
+ for (const auto& snapshot_layer : snapshot.m_layers) {
+ auto layer = Layer::create_snapshot(*this, snapshot_layer);
+ if (layer->is_selected())
+ select_layer(layer.ptr());
+ add_layer(*layer);
+ }
+
+ did_modify_layer_stack();
+}
+
+size_t Image::index_of(const Layer& layer) const
+{
+ for (size_t i = 0; i < m_layers.size(); ++i) {
+ if (&m_layers.at(i) == &layer)
+ return i;
+ }
+ ASSERT_NOT_REACHED();
+}
+
+void Image::move_layer_to_back(Layer& layer)
+{
+ NonnullRefPtr<Layer> protector(layer);
+ auto index = index_of(layer);
+ m_layers.remove(index);
+ m_layers.prepend(layer);
+
+ did_modify_layer_stack();
+}
+
+void Image::move_layer_to_front(Layer& layer)
+{
+ NonnullRefPtr<Layer> protector(layer);
+ auto index = index_of(layer);
+ m_layers.remove(index);
+ m_layers.append(layer);
+
+ did_modify_layer_stack();
+}
+
+void Image::move_layer_down(Layer& layer)
+{
+ NonnullRefPtr<Layer> protector(layer);
+ auto index = index_of(layer);
+ if (!index)
+ return;
+ m_layers.remove(index);
+ m_layers.insert(index - 1, layer);
+
+ did_modify_layer_stack();
+}
+
+void Image::move_layer_up(Layer& layer)
+{
+ NonnullRefPtr<Layer> protector(layer);
+ auto index = index_of(layer);
+ if (index == m_layers.size() - 1)
+ return;
+ m_layers.remove(index);
+ m_layers.insert(index + 1, layer);
+
+ did_modify_layer_stack();
+}
+
+void Image::change_layer_index(size_t old_index, size_t new_index)
+{
+ ASSERT(old_index < m_layers.size());
+ ASSERT(new_index < m_layers.size());
+ auto layer = m_layers.take(old_index);
+ m_layers.insert(new_index, move(layer));
+ did_modify_layer_stack();
+}
+
+void Image::did_modify_layer_stack()
+{
+ for (auto* client : m_clients)
+ client->image_did_modify_layer_stack();
+
+ did_change();
+}
+
+void Image::remove_layer(Layer& layer)
+{
+ NonnullRefPtr<Layer> protector(layer);
+ auto index = index_of(layer);
+ m_layers.remove(index);
+
+ for (auto* client : m_clients)
+ client->image_did_remove_layer(index);
+
+ did_modify_layer_stack();
+}
+
+void Image::select_layer(Layer* layer)
+{
+ for (auto* client : m_clients)
+ client->image_select_layer(layer);
+}
+
+void Image::add_client(ImageClient& client)
+{
+ ASSERT(!m_clients.contains(&client));
+ m_clients.set(&client);
+}
+
+void Image::remove_client(ImageClient& client)
+{
+ ASSERT(m_clients.contains(&client));
+ m_clients.remove(&client);
+}
+
+void Image::layer_did_modify_bitmap(Badge<Layer>, const Layer& layer)
+{
+ auto layer_index = index_of(layer);
+ for (auto* client : m_clients)
+ client->image_did_modify_layer(layer_index);
+
+ did_change();
+}
+
+void Image::layer_did_modify_properties(Badge<Layer>, const Layer& layer)
+{
+ auto layer_index = index_of(layer);
+ for (auto* client : m_clients)
+ client->image_did_modify_layer(layer_index);
+
+ did_change();
+}
+
+void Image::did_change()
+{
+ for (auto* client : m_clients)
+ client->image_did_change();
+}
+
+ImageUndoCommand::ImageUndoCommand(Image& image)
+ : m_snapshot(image.take_snapshot())
+ , m_image(image)
+{
+}
+
+void ImageUndoCommand::undo()
+{
+ m_image.restore_snapshot(*m_snapshot);
+}
+
+void ImageUndoCommand::redo()
+{
+ undo();
+}
+
+}
diff --git a/Userland/Applications/PixelPaint/Image.h b/Userland/Applications/PixelPaint/Image.h
new file mode 100644
index 0000000000..63184b3ad3
--- /dev/null
+++ b/Userland/Applications/PixelPaint/Image.h
@@ -0,0 +1,114 @@
+/*
+ * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/HashTable.h>
+#include <AK/NonnullRefPtrVector.h>
+#include <AK/RefCounted.h>
+#include <AK/RefPtr.h>
+#include <AK/Vector.h>
+#include <LibGUI/Command.h>
+#include <LibGUI/Forward.h>
+#include <LibGfx/Forward.h>
+#include <LibGfx/Rect.h>
+#include <LibGfx/Size.h>
+
+namespace PixelPaint {
+
+class Layer;
+
+class ImageClient {
+public:
+ virtual void image_did_add_layer(size_t) { }
+ virtual void image_did_remove_layer(size_t) { }
+ virtual void image_did_modify_layer(size_t) { }
+ virtual void image_did_modify_layer_stack() { }
+ virtual void image_did_change() { }
+ virtual void image_select_layer(Layer*) { }
+};
+
+class Image : public RefCounted<Image> {
+public:
+ static RefPtr<Image> create_with_size(const Gfx::IntSize&);
+ static RefPtr<Image> create_from_file(const String& file_path);
+
+ size_t layer_count() const { return m_layers.size(); }
+ const Layer& layer(size_t index) const { return m_layers.at(index); }
+ Layer& layer(size_t index) { return m_layers.at(index); }
+
+ const Gfx::IntSize& size() const { return m_size; }
+ Gfx::IntRect rect() const { return { {}, m_size }; }
+
+ void add_layer(NonnullRefPtr<Layer>);
+ RefPtr<Image> take_snapshot() const;
+ void restore_snapshot(const Image&);
+
+ void paint_into(GUI::Painter&, const Gfx::IntRect& dest_rect);
+ void save(const String& file_path) const;
+ void export_bmp(const String& file_path);
+
+ void move_layer_to_front(Layer&);
+ void move_layer_to_back(Layer&);
+ void move_layer_up(Layer&);
+ void move_layer_down(Layer&);
+ void change_layer_index(size_t old_index, size_t new_index);
+ void remove_layer(Layer&);
+ void select_layer(Layer*);
+
+ void add_client(ImageClient&);
+ void remove_client(ImageClient&);
+
+ void layer_did_modify_bitmap(Badge<Layer>, const Layer&);
+ void layer_did_modify_properties(Badge<Layer>, const Layer&);
+
+ size_t index_of(const Layer&) const;
+
+private:
+ explicit Image(const Gfx::IntSize&);
+
+ void did_change();
+ void did_modify_layer_stack();
+
+ Gfx::IntSize m_size;
+ NonnullRefPtrVector<Layer> m_layers;
+
+ HashTable<ImageClient*> m_clients;
+};
+
+class ImageUndoCommand : public GUI::Command {
+public:
+ ImageUndoCommand(Image& image);
+
+ virtual void undo() override;
+ virtual void redo() override;
+
+private:
+ RefPtr<Image> m_snapshot;
+ Image& m_image;
+};
+
+}
diff --git a/Userland/Applications/PixelPaint/ImageEditor.cpp b/Userland/Applications/PixelPaint/ImageEditor.cpp
new file mode 100644
index 0000000000..f29551c050
--- /dev/null
+++ b/Userland/Applications/PixelPaint/ImageEditor.cpp
@@ -0,0 +1,418 @@
+/*
+ * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "ImageEditor.h"
+#include "Image.h"
+#include "Layer.h"
+#include "MoveTool.h"
+#include "Tool.h"
+#include <LibGUI/Command.h>
+#include <LibGUI/Painter.h>
+#include <LibGfx/Palette.h>
+#include <LibGfx/Rect.h>
+
+namespace PixelPaint {
+
+ImageEditor::ImageEditor()
+ : m_undo_stack(make<GUI::UndoStack>())
+{
+ set_focus_policy(GUI::FocusPolicy::StrongFocus);
+}
+
+ImageEditor::~ImageEditor()
+{
+ if (m_image)
+ m_image->remove_client(*this);
+}
+
+void ImageEditor::set_image(RefPtr<Image> image)
+{
+ if (m_image)
+ m_image->remove_client(*this);
+
+ m_image = move(image);
+ m_active_layer = nullptr;
+ m_undo_stack = make<GUI::UndoStack>();
+ m_undo_stack->push(make<ImageUndoCommand>(*m_image));
+ update();
+ relayout();
+
+ if (m_image)
+ m_image->add_client(*this);
+}
+
+void ImageEditor::did_complete_action()
+{
+ if (!m_image)
+ return;
+ m_undo_stack->finalize_current_combo();
+ m_undo_stack->push(make<ImageUndoCommand>(*m_image));
+}
+
+bool ImageEditor::undo()
+{
+ if (!m_image)
+ return false;
+ if (m_undo_stack->can_undo()) {
+ m_undo_stack->undo();
+ layers_did_change();
+ return true;
+ }
+ return false;
+}
+
+bool ImageEditor::redo()
+{
+ if (!m_image)
+ return false;
+ if (m_undo_stack->can_redo()) {
+ m_undo_stack->redo();
+ layers_did_change();
+ return true;
+ }
+ return false;
+}
+
+void ImageEditor::paint_event(GUI::PaintEvent& event)
+{
+ GUI::Frame::paint_event(event);
+
+ GUI::Painter painter(*this);
+ painter.add_clip_rect(event.rect());
+ painter.add_clip_rect(frame_inner_rect());
+
+ Gfx::StylePainter::paint_transparency_grid(painter, rect(), palette());
+
+ if (m_image) {
+ painter.draw_rect(m_editor_image_rect.inflated(2, 2), Color::Black);
+ m_image->paint_into(painter, m_editor_image_rect);
+ }
+
+ if (m_active_layer) {
+ painter.draw_rect(enclosing_int_rect(image_rect_to_editor_rect(m_active_layer->relative_rect())).inflated(2, 2), Color::Black);
+ }
+}
+
+Gfx::FloatRect ImageEditor::layer_rect_to_editor_rect(const Layer& layer, const Gfx::IntRect& layer_rect) const
+{
+ return image_rect_to_editor_rect(layer_rect.translated(layer.location()));
+}
+
+Gfx::FloatRect ImageEditor::image_rect_to_editor_rect(const Gfx::IntRect& image_rect) const
+{
+ Gfx::FloatRect editor_rect;
+ editor_rect.set_location(image_position_to_editor_position(image_rect.location()));
+ editor_rect.set_width((float)image_rect.width() * m_scale);
+ editor_rect.set_height((float)image_rect.height() * m_scale);
+ return editor_rect;
+}
+
+Gfx::FloatRect ImageEditor::editor_rect_to_image_rect(const Gfx::IntRect& editor_rect) const
+{
+ Gfx::FloatRect image_rect;
+ image_rect.set_location(editor_position_to_image_position(editor_rect.location()));
+ image_rect.set_width((float)editor_rect.width() / m_scale);
+ image_rect.set_height((float)editor_rect.height() / m_scale);
+ return image_rect;
+}
+
+Gfx::FloatPoint ImageEditor::layer_position_to_editor_position(const Layer& layer, const Gfx::IntPoint& layer_position) const
+{
+ return image_position_to_editor_position(layer_position.translated(layer.location()));
+}
+
+Gfx::FloatPoint ImageEditor::image_position_to_editor_position(const Gfx::IntPoint& image_position) const
+{
+ Gfx::FloatPoint editor_position;
+ editor_position.set_x(m_editor_image_rect.x() + ((float)image_position.x() * m_scale));
+ editor_position.set_y(m_editor_image_rect.y() + ((float)image_position.y() * m_scale));
+ return editor_position;
+}
+
+Gfx::FloatPoint ImageEditor::editor_position_to_image_position(const Gfx::IntPoint& editor_position) const
+{
+ Gfx::FloatPoint image_position;
+ image_position.set_x(((float)editor_position.x() - m_editor_image_rect.x()) / m_scale);
+ image_position.set_y(((float)editor_position.y() - m_editor_image_rect.y()) / m_scale);
+ return image_position;
+}
+
+void ImageEditor::second_paint_event(GUI::PaintEvent& event)
+{
+ if (m_active_tool && m_active_layer)
+ m_active_tool->on_second_paint(*m_active_layer, event);
+}
+
+GUI::MouseEvent ImageEditor::event_with_pan_and_scale_applied(const GUI::MouseEvent& event) const
+{
+ auto image_position = editor_position_to_image_position(event.position());
+ return {
+ static_cast<GUI::Event::Type>(event.type()),
+ Gfx::IntPoint(image_position.x(), image_position.y()),
+ event.buttons(),
+ event.button(),
+ event.modifiers(),
+ event.wheel_delta()
+ };
+}
+
+GUI::MouseEvent ImageEditor::event_adjusted_for_layer(const GUI::MouseEvent& event, const Layer& layer) const
+{
+ auto image_position = editor_position_to_image_position(event.position());
+ image_position.move_by(-layer.location().x(), -layer.location().y());
+ return {
+ static_cast<GUI::Event::Type>(event.type()),
+ Gfx::IntPoint(image_position.x(), image_position.y()),
+ event.buttons(),
+ event.button(),
+ event.modifiers(),
+ event.wheel_delta()
+ };
+}
+
+void ImageEditor::mousedown_event(GUI::MouseEvent& event)
+{
+ if (event.button() == GUI::MouseButton::Middle) {
+ m_click_position = event.position();
+ m_saved_pan_origin = m_pan_origin;
+ return;
+ }
+
+ if (!m_active_tool)
+ return;
+
+ if (is<MoveTool>(*m_active_tool)) {
+ if (auto* other_layer = layer_at_editor_position(event.position())) {
+ set_active_layer(other_layer);
+ }
+ }
+
+ if (!m_active_layer)
+ return;
+
+ auto layer_event = event_adjusted_for_layer(event, *m_active_layer);
+ auto image_event = event_with_pan_and_scale_applied(event);
+ m_active_tool->on_mousedown(*m_active_layer, layer_event, image_event);
+}
+
+void ImageEditor::mousemove_event(GUI::MouseEvent& event)
+{
+ if (event.buttons() & GUI::MouseButton::Middle) {
+ auto delta = event.position() - m_click_position;
+ m_pan_origin = m_saved_pan_origin.translated(
+ -delta.x() / m_scale,
+ -delta.y() / m_scale);
+
+ relayout();
+ return;
+ }
+
+ if (!m_active_layer || !m_active_tool)
+ return;
+ auto layer_event = event_adjusted_for_layer(event, *m_active_layer);
+ auto image_event = event_with_pan_and_scale_applied(event);
+
+ m_active_tool->on_mousemove(*m_active_layer, layer_event, image_event);
+}
+
+void ImageEditor::mouseup_event(GUI::MouseEvent& event)
+{
+ if (!m_active_layer || !m_active_tool)
+ return;
+ auto layer_event = event_adjusted_for_layer(event, *m_active_layer);
+ auto image_event = event_with_pan_and_scale_applied(event);
+ m_active_tool->on_mouseup(*m_active_layer, layer_event, image_event);
+}
+
+void ImageEditor::mousewheel_event(GUI::MouseEvent& event)
+{
+ auto old_scale = m_scale;
+
+ m_scale += -event.wheel_delta() * 0.1f;
+ if (m_scale < 0.1f)
+ m_scale = 0.1f;
+ if (m_scale > 100.0f)
+ m_scale = 100.0f;
+
+ auto focus_point = Gfx::FloatPoint(
+ m_pan_origin.x() - ((float)event.x() - (float)width() / 2.0) / old_scale,
+ m_pan_origin.y() - ((float)event.y() - (float)height() / 2.0) / old_scale);
+
+ m_pan_origin = Gfx::FloatPoint(
+ focus_point.x() - m_scale / old_scale * (focus_point.x() - m_pan_origin.x()),
+ focus_point.y() - m_scale / old_scale * (focus_point.y() - m_pan_origin.y()));
+
+ if (old_scale != m_scale)
+ relayout();
+}
+
+void ImageEditor::context_menu_event(GUI::ContextMenuEvent& event)
+{
+ if (!m_active_layer || !m_active_tool)
+ return;
+ m_active_tool->on_context_menu(*m_active_layer, event);
+}
+
+void ImageEditor::resize_event(GUI::ResizeEvent& event)
+{
+ relayout();
+ GUI::Frame::resize_event(event);
+}
+
+void ImageEditor::keydown_event(GUI::KeyEvent& event)
+{
+ if (m_active_tool)
+ m_active_tool->on_keydown(event);
+}
+
+void ImageEditor::keyup_event(GUI::KeyEvent& event)
+{
+ if (m_active_tool)
+ m_active_tool->on_keyup(event);
+}
+
+void ImageEditor::set_active_layer(Layer* layer)
+{
+ if (m_active_layer == layer)
+ return;
+ m_active_layer = layer;
+
+ if (m_active_layer) {
+ size_t index = 0;
+ for (; index < m_image->layer_count(); ++index) {
+ if (&m_image->layer(index) == layer)
+ break;
+ }
+ if (on_active_layer_change)
+ on_active_layer_change(layer);
+ } else {
+ if (on_active_layer_change)
+ on_active_layer_change({});
+ }
+
+ layers_did_change();
+}
+
+void ImageEditor::set_active_tool(Tool* tool)
+{
+ if (m_active_tool == tool)
+ return;
+
+ if (m_active_tool)
+ m_active_tool->clear();
+
+ m_active_tool = tool;
+
+ if (m_active_tool)
+ m_active_tool->setup(*this);
+}
+
+void ImageEditor::layers_did_change()
+{
+ update();
+}
+
+Color ImageEditor::color_for(GUI::MouseButton button) const
+{
+ if (button == GUI::MouseButton::Left)
+ return m_primary_color;
+ if (button == GUI::MouseButton::Right)
+ return m_secondary_color;
+ ASSERT_NOT_REACHED();
+}
+
+Color ImageEditor::color_for(const GUI::MouseEvent& event) const
+{
+ if (event.buttons() & GUI::MouseButton::Left)
+ return m_primary_color;
+ if (event.buttons() & GUI::MouseButton::Right)
+ return m_secondary_color;
+ ASSERT_NOT_REACHED();
+}
+
+void ImageEditor::set_primary_color(Color color)
+{
+ if (m_primary_color == color)
+ return;
+ m_primary_color = color;
+ if (on_primary_color_change)
+ on_primary_color_change(color);
+}
+
+void ImageEditor::set_secondary_color(Color color)
+{
+ if (m_secondary_color == color)
+ return;
+ m_secondary_color = color;
+ if (on_secondary_color_change)
+ on_secondary_color_change(color);
+}
+
+Layer* ImageEditor::layer_at_editor_position(const Gfx::IntPoint& editor_position)
+{
+ if (!m_image)
+ return nullptr;
+ auto image_position = editor_position_to_image_position(editor_position);
+ for (ssize_t i = m_image->layer_count() - 1; i >= 0; --i) {
+ auto& layer = m_image->layer(i);
+ if (!layer.is_visible())
+ continue;
+ if (layer.relative_rect().contains(Gfx::IntPoint(image_position.x(), image_position.y())))
+ return const_cast<Layer*>(&layer);
+ }
+ return nullptr;
+}
+
+void ImageEditor::relayout()
+{
+ if (!image())
+ return;
+ auto& image = *this->image();
+
+ Gfx::IntSize new_size;
+ new_size.set_width(image.size().width() * m_scale);
+ new_size.set_height(image.size().height() * m_scale);
+ m_editor_image_rect.set_size(new_size);
+
+ Gfx::IntPoint new_location;
+ new_location.set_x((width() / 2) - (new_size.width() / 2) - (m_pan_origin.x() * m_scale));
+ new_location.set_y((height() / 2) - (new_size.height() / 2) - (m_pan_origin.y() * m_scale));
+ m_editor_image_rect.set_location(new_location);
+
+ update();
+}
+
+void ImageEditor::image_did_change()
+{
+ update();
+}
+
+void ImageEditor::image_select_layer(Layer* layer)
+{
+ set_active_layer(layer);
+}
+
+}
diff --git a/Userland/Applications/PixelPaint/ImageEditor.h b/Userland/Applications/PixelPaint/ImageEditor.h
new file mode 100644
index 0000000000..b6cdc8fb62
--- /dev/null
+++ b/Userland/Applications/PixelPaint/ImageEditor.h
@@ -0,0 +1,125 @@
+/*
+ * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "Image.h"
+#include <LibGUI/Frame.h>
+#include <LibGUI/UndoStack.h>
+#include <LibGfx/Point.h>
+
+namespace PixelPaint {
+
+class Layer;
+class Tool;
+
+class ImageEditor final
+ : public GUI::Frame
+ , public ImageClient {
+ C_OBJECT(ImageEditor);
+
+public:
+ virtual ~ImageEditor() override;
+
+ const Image* image() const { return m_image; }
+ Image* image() { return m_image; }
+
+ void set_image(RefPtr<Image>);
+
+ Layer* active_layer() { return m_active_layer; }
+ void set_active_layer(Layer*);
+
+ Tool* active_tool() { return m_active_tool; }
+ void set_active_tool(Tool*);
+
+ void did_complete_action();
+ bool undo();
+ bool redo();
+
+ void layers_did_change();
+
+ Layer* layer_at_editor_position(const Gfx::IntPoint&);
+
+ Color primary_color() const { return m_primary_color; }
+ void set_primary_color(Color);
+
+ Color secondary_color() const { return m_secondary_color; }
+ void set_secondary_color(Color);
+
+ Color color_for(const GUI::MouseEvent&) const;
+ Color color_for(GUI::MouseButton) const;
+
+ Function<void(Color)> on_primary_color_change;
+ Function<void(Color)> on_secondary_color_change;
+
+ Function<void(Layer*)> on_active_layer_change;
+
+ Gfx::FloatRect layer_rect_to_editor_rect(const Layer&, const Gfx::IntRect&) const;
+ Gfx::FloatRect image_rect_to_editor_rect(const Gfx::IntRect&) const;
+ Gfx::FloatRect editor_rect_to_image_rect(const Gfx::IntRect&) const;
+ Gfx::FloatPoint layer_position_to_editor_position(const Layer&, const Gfx::IntPoint&) const;
+ Gfx::FloatPoint image_position_to_editor_position(const Gfx::IntPoint&) const;
+ Gfx::FloatPoint editor_position_to_image_position(const Gfx::IntPoint&) const;
+
+private:
+ ImageEditor();
+
+ virtual void paint_event(GUI::PaintEvent&) override;
+ virtual void second_paint_event(GUI::PaintEvent&) override;
+ virtual void mousedown_event(GUI::MouseEvent&) override;
+ virtual void mousemove_event(GUI::MouseEvent&) override;
+ virtual void mouseup_event(GUI::MouseEvent&) override;
+ virtual void mousewheel_event(GUI::MouseEvent&) override;
+ virtual void keydown_event(GUI::KeyEvent&) override;
+ virtual void keyup_event(GUI::KeyEvent&) override;
+ virtual void context_menu_event(GUI::ContextMenuEvent&) override;
+ virtual void resize_event(GUI::ResizeEvent&) override;
+
+ virtual void image_did_change() override;
+ virtual void image_select_layer(Layer*) override;
+
+ GUI::MouseEvent event_adjusted_for_layer(const GUI::MouseEvent&, const Layer&) const;
+ GUI::MouseEvent event_with_pan_and_scale_applied(const GUI::MouseEvent&) const;
+
+ void relayout();
+
+ RefPtr<Image> m_image;
+ RefPtr<Layer> m_active_layer;
+ OwnPtr<GUI::UndoStack> m_undo_stack;
+
+ Tool* m_active_tool { nullptr };
+
+ Color m_primary_color { Color::Black };
+ Color m_secondary_color { Color::White };
+
+ Gfx::IntRect m_editor_image_rect;
+ float m_scale { 1 };
+ Gfx::FloatPoint m_pan_origin;
+ Gfx::FloatPoint m_saved_pan_origin;
+ Gfx::IntPoint m_click_position;
+};
+
+}
diff --git a/Userland/Applications/PixelPaint/Layer.cpp b/Userland/Applications/PixelPaint/Layer.cpp
new file mode 100644
index 0000000000..e607113c32
--- /dev/null
+++ b/Userland/Applications/PixelPaint/Layer.cpp
@@ -0,0 +1,108 @@
+/*
+ * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "Layer.h"
+#include "Image.h"
+#include <LibGfx/Bitmap.h>
+
+namespace PixelPaint {
+
+RefPtr<Layer> Layer::create_with_size(Image& image, const Gfx::IntSize& size, const String& name)
+{
+ if (size.is_empty())
+ return nullptr;
+
+ if (size.width() > 16384 || size.height() > 16384)
+ return nullptr;
+
+ return adopt(*new Layer(image, size, name));
+}
+
+RefPtr<Layer> Layer::create_with_bitmap(Image& image, const Gfx::Bitmap& bitmap, const String& name)
+{
+ if (bitmap.size().is_empty())
+ return nullptr;
+
+ if (bitmap.size().width() > 16384 || bitmap.size().height() > 16384)
+ return nullptr;
+
+ return adopt(*new Layer(image, bitmap, name));
+}
+
+RefPtr<Layer> Layer::create_snapshot(Image& image, const Layer& layer)
+{
+ auto snapshot = create_with_bitmap(image, *layer.bitmap().clone(), layer.name());
+ snapshot->set_opacity_percent(layer.opacity_percent());
+ snapshot->set_visible(layer.is_visible());
+ snapshot->set_selected(layer.is_selected());
+ snapshot->set_location(layer.location());
+ return snapshot;
+}
+
+Layer::Layer(Image& image, const Gfx::IntSize& size, const String& name)
+ : m_image(image)
+ , m_name(name)
+{
+ m_bitmap = Gfx::Bitmap::create(Gfx::BitmapFormat::RGBA32, size);
+}
+
+Layer::Layer(Image& image, const Gfx::Bitmap& bitmap, const String& name)
+ : m_image(image)
+ , m_name(name)
+ , m_bitmap(bitmap)
+{
+}
+
+void Layer::did_modify_bitmap(Image& image)
+{
+ image.layer_did_modify_bitmap({}, *this);
+}
+
+void Layer::set_visible(bool visible)
+{
+ if (m_visible == visible)
+ return;
+ m_visible = visible;
+ m_image.layer_did_modify_properties({}, *this);
+}
+
+void Layer::set_opacity_percent(int opacity_percent)
+{
+ if (m_opacity_percent == opacity_percent)
+ return;
+ m_opacity_percent = opacity_percent;
+ m_image.layer_did_modify_properties({}, *this);
+}
+
+void Layer::set_name(const String& name)
+{
+ if (m_name == name)
+ return;
+ m_name = name;
+ m_image.layer_did_modify_properties({}, *this);
+}
+
+}
diff --git a/Userland/Applications/PixelPaint/Layer.h b/Userland/Applications/PixelPaint/Layer.h
new file mode 100644
index 0000000000..b3b7d4d603
--- /dev/null
+++ b/Userland/Applications/PixelPaint/Layer.h
@@ -0,0 +1,95 @@
+/*
+ * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/Noncopyable.h>
+#include <AK/RefCounted.h>
+#include <AK/String.h>
+#include <AK/Weakable.h>
+#include <LibGfx/Bitmap.h>
+
+namespace PixelPaint {
+
+class Image;
+
+class Layer
+ : public RefCounted<Layer>
+ , public Weakable<Layer> {
+
+ AK_MAKE_NONCOPYABLE(Layer);
+ AK_MAKE_NONMOVABLE(Layer);
+
+public:
+ static RefPtr<Layer> create_with_size(Image&, const Gfx::IntSize&, const String& name);
+ static RefPtr<Layer> create_with_bitmap(Image&, const Gfx::Bitmap&, const String& name);
+ static RefPtr<Layer> create_snapshot(Image&, const Layer&);
+
+ ~Layer() { }
+
+ const Gfx::IntPoint& location() const { return m_location; }
+ void set_location(const Gfx::IntPoint& location) { m_location = location; }
+
+ const Gfx::Bitmap& bitmap() const { return *m_bitmap; }
+ Gfx::Bitmap& bitmap() { return *m_bitmap; }
+ Gfx::IntSize size() const { return bitmap().size(); }
+
+ Gfx::IntRect relative_rect() const { return { location(), size() }; }
+ Gfx::IntRect rect() const { return { {}, size() }; }
+
+ const String& name() const { return m_name; }
+ void set_name(const String&);
+
+ void set_bitmap(Gfx::Bitmap& bitmap) { m_bitmap = bitmap; }
+
+ void did_modify_bitmap(Image&);
+
+ void set_selected(bool selected) { m_selected = selected; }
+ bool is_selected() const { return m_selected; }
+
+ bool is_visible() const { return m_visible; }
+ void set_visible(bool visible);
+
+ int opacity_percent() const { return m_opacity_percent; }
+ void set_opacity_percent(int);
+
+private:
+ Layer(Image&, const Gfx::IntSize&, const String& name);
+ Layer(Image&, const Gfx::Bitmap&, const String& name);
+
+ Image& m_image;
+
+ String m_name;
+ Gfx::IntPoint m_location;
+ RefPtr<Gfx::Bitmap> m_bitmap;
+
+ bool m_selected { false };
+ bool m_visible { true };
+
+ int m_opacity_percent { 100 };
+};
+
+}
diff --git a/Userland/Applications/PixelPaint/LayerListWidget.cpp b/Userland/Applications/PixelPaint/LayerListWidget.cpp
new file mode 100644
index 0000000000..756772bb87
--- /dev/null
+++ b/Userland/Applications/PixelPaint/LayerListWidget.cpp
@@ -0,0 +1,285 @@
+/*
+ * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "LayerListWidget.h"
+#include "Image.h"
+#include "ImageEditor.h"
+#include "Layer.h"
+#include <LibGUI/Painter.h>
+#include <LibGfx/Palette.h>
+
+namespace PixelPaint {
+
+LayerListWidget::LayerListWidget()
+{
+}
+
+LayerListWidget::~LayerListWidget()
+{
+ if (m_image)
+ m_image->remove_client(*this);
+}
+
+void LayerListWidget::set_image(Image* image)
+{
+ if (m_image == image)
+ return;
+ if (m_image)
+ m_image->remove_client(*this);
+ m_image = image;
+ if (m_image)
+ m_image->add_client(*this);
+
+ rebuild_gadgets();
+}
+
+void LayerListWidget::rebuild_gadgets()
+{
+ m_gadgets.clear();
+ if (m_image) {
+ for (size_t layer_index = 0; layer_index < m_image->layer_count(); ++layer_index) {
+ m_gadgets.append({ layer_index, {}, {}, false, {} });
+ }
+ }
+ relayout_gadgets();
+}
+
+void LayerListWidget::resize_event(GUI::ResizeEvent& event)
+{
+ Widget::resize_event(event);
+ relayout_gadgets();
+}
+
+void LayerListWidget::paint_event(GUI::PaintEvent& event)
+{
+ GUI::Painter painter(*this);
+ painter.add_clip_rect(event.rect());
+
+ painter.fill_rect(event.rect(), palette().button());
+
+ if (!m_image)
+ return;
+
+ painter.fill_rect(event.rect(), palette().button());
+
+ auto paint_gadget = [&](auto& gadget) {
+ auto& layer = m_image->layer(gadget.layer_index);
+
+ auto adjusted_rect = gadget.rect;
+
+ if (gadget.is_moving) {
+ adjusted_rect.move_by(0, gadget.movement_delta.y());
+ }
+
+ if (gadget.is_moving) {
+ painter.fill_rect(adjusted_rect, palette().selection().lightened(1.5f));
+ } else if (layer.is_selected()) {
+ painter.fill_rect(adjusted_rect, palette().selection());
+ }
+
+ painter.draw_rect(adjusted_rect, Color::Black);
+
+ Gfx::IntRect thumbnail_rect { adjusted_rect.x(), adjusted_rect.y(), adjusted_rect.height(), adjusted_rect.height() };
+ thumbnail_rect.shrink(8, 8);
+ painter.draw_scaled_bitmap(thumbnail_rect, layer.bitmap(), layer.bitmap().rect());
+
+ Gfx::IntRect text_rect { thumbnail_rect.right() + 10, adjusted_rect.y(), adjusted_rect.width(), adjusted_rect.height() };
+ text_rect.intersect(adjusted_rect);
+
+ painter.draw_text(text_rect, layer.name(), Gfx::TextAlignment::CenterLeft, layer.is_selected() ? palette().selection_text() : palette().button_text());
+ };
+
+ for (auto& gadget : m_gadgets) {
+ if (!gadget.is_moving)
+ paint_gadget(gadget);
+ }
+
+ if (m_moving_gadget_index.has_value())
+ paint_gadget(m_gadgets[m_moving_gadget_index.value()]);
+}
+
+Optional<size_t> LayerListWidget::gadget_at(const Gfx::IntPoint& position)
+{
+ for (size_t i = 0; i < m_gadgets.size(); ++i) {
+ if (m_gadgets[i].rect.contains(position))
+ return i;
+ }
+ return {};
+}
+
+void LayerListWidget::mousedown_event(GUI::MouseEvent& event)
+{
+ if (!m_image)
+ return;
+ if (event.button() != GUI::MouseButton::Left)
+ return;
+ auto gadget_index = gadget_at(event.position());
+ if (!gadget_index.has_value()) {
+ if (on_layer_select)
+ on_layer_select(nullptr);
+ return;
+ }
+ m_moving_gadget_index = gadget_index;
+ m_moving_event_origin = event.position();
+ auto& gadget = m_gadgets[m_moving_gadget_index.value()];
+ auto& layer = m_image->layer(gadget_index.value());
+ set_selected_layer(&layer);
+ gadget.is_moving = true;
+ gadget.movement_delta = {};
+ update();
+}
+
+void LayerListWidget::mousemove_event(GUI::MouseEvent& event)
+{
+ if (!m_image)
+ return;
+ if (!m_moving_gadget_index.has_value())
+ return;
+
+ auto delta = event.position() - m_moving_event_origin;
+ auto& gadget = m_gadgets[m_moving_gadget_index.value()];
+ ASSERT(gadget.is_moving);
+ gadget.movement_delta = delta;
+ relayout_gadgets();
+}
+
+void LayerListWidget::mouseup_event(GUI::MouseEvent& event)
+{
+ if (!m_image)
+ return;
+ if (event.button() != GUI::MouseButton::Left)
+ return;
+ if (!m_moving_gadget_index.has_value())
+ return;
+
+ size_t old_index = m_moving_gadget_index.value();
+ size_t new_index = hole_index_during_move();
+ if (new_index >= m_image->layer_count())
+ new_index = m_image->layer_count() - 1;
+
+ m_moving_gadget_index = {};
+ m_image->change_layer_index(old_index, new_index);
+}
+
+void LayerListWidget::image_did_add_layer(size_t layer_index)
+{
+ if (m_moving_gadget_index.has_value()) {
+ m_gadgets[m_moving_gadget_index.value()].is_moving = false;
+ m_moving_gadget_index = {};
+ }
+ Gadget gadget { layer_index, {}, {}, false, {} };
+ m_gadgets.insert(layer_index, move(gadget));
+ relayout_gadgets();
+}
+
+void LayerListWidget::image_did_remove_layer(size_t layer_index)
+{
+ if (m_moving_gadget_index.has_value()) {
+ m_gadgets[m_moving_gadget_index.value()].is_moving = false;
+ m_moving_gadget_index = {};
+ }
+ m_gadgets.remove(layer_index);
+ relayout_gadgets();
+}
+
+void LayerListWidget::image_did_modify_layer(size_t layer_index)
+{
+ update(m_gadgets[layer_index].rect);
+}
+
+void LayerListWidget::image_did_modify_layer_stack()
+{
+ rebuild_gadgets();
+}
+
+static constexpr int gadget_height = 30;
+static constexpr int gadget_spacing = 1;
+static constexpr int vertical_step = gadget_height + gadget_spacing;
+
+size_t LayerListWidget::hole_index_during_move() const
+{
+ ASSERT(is_moving_gadget());
+ auto& moving_gadget = m_gadgets[m_moving_gadget_index.value()];
+ int center_y_of_moving_gadget = moving_gadget.rect.translated(0, moving_gadget.movement_delta.y()).center().y();
+ return center_y_of_moving_gadget / vertical_step;
+}
+
+void LayerListWidget::select_bottom_layer()
+{
+ if (!m_image || !m_image->layer_count())
+ return;
+ set_selected_layer(&m_image->layer(0));
+}
+
+void LayerListWidget::select_top_layer()
+{
+ if (!m_image || !m_image->layer_count())
+ return;
+ set_selected_layer(&m_image->layer(m_image->layer_count() - 1));
+}
+
+void LayerListWidget::move_selection(int delta)
+{
+ if (!m_image || !m_image->layer_count())
+ return;
+ int new_layer_index = min(max(0, (int)m_image->layer_count() + delta), (int)m_image->layer_count() - 1);
+ set_selected_layer(&m_image->layer(new_layer_index));
+}
+
+void LayerListWidget::relayout_gadgets()
+{
+ int y = 0;
+
+ Optional<size_t> hole_index;
+ if (is_moving_gadget())
+ hole_index = hole_index_during_move();
+
+ size_t index = 0;
+ for (auto& gadget : m_gadgets) {
+ if (gadget.is_moving)
+ continue;
+ if (hole_index.has_value() && index == hole_index.value())
+ y += vertical_step;
+ gadget.rect = { 0, y, width(), gadget_height };
+ y += vertical_step;
+ ++index;
+ }
+
+ update();
+}
+
+void LayerListWidget::set_selected_layer(Layer* layer)
+{
+ if (!m_image)
+ return;
+ for (size_t i = 0; i < m_image->layer_count(); ++i)
+ m_image->layer(i).set_selected(layer == &m_image->layer(i));
+ if (on_layer_select)
+ on_layer_select(layer);
+ update();
+}
+
+}
diff --git a/Userland/Applications/PixelPaint/LayerListWidget.h b/Userland/Applications/PixelPaint/LayerListWidget.h
new file mode 100644
index 0000000000..9550913b8e
--- /dev/null
+++ b/Userland/Applications/PixelPaint/LayerListWidget.h
@@ -0,0 +1,89 @@
+/*
+ * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "Image.h"
+#include <LibGUI/Widget.h>
+
+namespace PixelPaint {
+
+class LayerListWidget final
+ : public GUI::Widget
+ , ImageClient {
+ C_OBJECT(LayerListWidget);
+
+public:
+ virtual ~LayerListWidget() override;
+
+ void set_image(Image*);
+
+ void set_selected_layer(Layer*);
+ Function<void(Layer*)> on_layer_select;
+
+ void select_bottom_layer();
+ void select_top_layer();
+ void move_selection(int delta);
+
+private:
+ explicit LayerListWidget();
+
+ virtual void paint_event(GUI::PaintEvent&) override;
+ virtual void mousedown_event(GUI::MouseEvent&) override;
+ virtual void mousemove_event(GUI::MouseEvent&) override;
+ virtual void mouseup_event(GUI::MouseEvent&) override;
+ virtual void resize_event(GUI::ResizeEvent&) override;
+
+ virtual void image_did_add_layer(size_t) override;
+ virtual void image_did_remove_layer(size_t) override;
+ virtual void image_did_modify_layer(size_t) override;
+ virtual void image_did_modify_layer_stack() override;
+
+ void rebuild_gadgets();
+ void relayout_gadgets();
+
+ size_t hole_index_during_move() const;
+
+ struct Gadget {
+ size_t layer_index { 0 };
+ Gfx::IntRect rect;
+ Gfx::IntRect temporary_rect_during_move;
+ bool is_moving { false };
+ Gfx::IntPoint movement_delta;
+ };
+
+ bool is_moving_gadget() const { return m_moving_gadget_index.has_value(); }
+
+ Optional<size_t> gadget_at(const Gfx::IntPoint&);
+
+ Vector<Gadget> m_gadgets;
+ RefPtr<Image> m_image;
+
+ Optional<size_t> m_moving_gadget_index;
+ Gfx::IntPoint m_moving_event_origin;
+};
+
+}
diff --git a/Userland/Applications/PixelPaint/LayerPropertiesWidget.cpp b/Userland/Applications/PixelPaint/LayerPropertiesWidget.cpp
new file mode 100644
index 0000000000..cfdee65c0a
--- /dev/null
+++ b/Userland/Applications/PixelPaint/LayerPropertiesWidget.cpp
@@ -0,0 +1,107 @@
+/*
+ * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "LayerPropertiesWidget.h"
+#include "Layer.h"
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/CheckBox.h>
+#include <LibGUI/GroupBox.h>
+#include <LibGUI/Label.h>
+#include <LibGUI/OpacitySlider.h>
+#include <LibGUI/TextBox.h>
+#include <LibGfx/Font.h>
+
+namespace PixelPaint {
+
+LayerPropertiesWidget::LayerPropertiesWidget()
+{
+ set_layout<GUI::VerticalBoxLayout>();
+
+ auto& group_box = add<GUI::GroupBox>("Layer properties");
+ auto& layout = group_box.set_layout<GUI::VerticalBoxLayout>();
+
+ layout.set_margins({ 10, 20, 10, 10 });
+
+ auto& name_container = group_box.add<GUI::Widget>();
+ name_container.set_fixed_height(20);
+ name_container.set_layout<GUI::HorizontalBoxLayout>();
+
+ auto& name_label = name_container.add<GUI::Label>("Name:");
+ name_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
+ name_label.set_fixed_size(80, 20);
+
+ m_name_textbox = name_container.add<GUI::TextBox>();
+ m_name_textbox->set_fixed_height(20);
+ m_name_textbox->on_change = [this] {
+ if (m_layer)
+ m_layer->set_name(m_name_textbox->text());
+ };
+
+ auto& opacity_container = group_box.add<GUI::Widget>();
+ opacity_container.set_fixed_height(20);
+ opacity_container.set_layout<GUI::HorizontalBoxLayout>();
+
+ auto& opacity_label = opacity_container.add<GUI::Label>("Opacity:");
+ opacity_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
+ opacity_label.set_fixed_size(80, 20);
+
+ m_opacity_slider = opacity_container.add<GUI::OpacitySlider>();
+ m_opacity_slider->set_range(0, 100);
+ m_opacity_slider->on_change = [this](int value) {
+ if (m_layer)
+ m_layer->set_opacity_percent(value);
+ };
+
+ m_visibility_checkbox = group_box.add<GUI::CheckBox>("Visible");
+ m_visibility_checkbox->set_fixed_height(20);
+ m_visibility_checkbox->on_checked = [this](bool checked) {
+ if (m_layer)
+ m_layer->set_visible(checked);
+ };
+}
+
+LayerPropertiesWidget::~LayerPropertiesWidget()
+{
+}
+
+void LayerPropertiesWidget::set_layer(Layer* layer)
+{
+ if (m_layer == layer)
+ return;
+
+ if (layer) {
+ m_layer = layer->make_weak_ptr();
+ m_name_textbox->set_text(layer->name());
+ m_opacity_slider->set_value(layer->opacity_percent());
+ m_visibility_checkbox->set_checked(layer->is_visible());
+ set_enabled(true);
+ } else {
+ m_layer = nullptr;
+ set_enabled(false);
+ }
+}
+
+}
diff --git a/Userland/Applications/PixelPaint/LayerPropertiesWidget.h b/Userland/Applications/PixelPaint/LayerPropertiesWidget.h
new file mode 100644
index 0000000000..e07e02ec93
--- /dev/null
+++ b/Userland/Applications/PixelPaint/LayerPropertiesWidget.h
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <LibGUI/Widget.h>
+
+namespace PixelPaint {
+
+class Layer;
+
+class LayerPropertiesWidget final : public GUI::Widget {
+ C_OBJECT(LayerPropertiesWidget);
+
+public:
+ virtual ~LayerPropertiesWidget() override;
+
+ void set_layer(Layer*);
+
+private:
+ LayerPropertiesWidget();
+
+ RefPtr<GUI::CheckBox> m_visibility_checkbox;
+ RefPtr<GUI::OpacitySlider> m_opacity_slider;
+ RefPtr<GUI::TextBox> m_name_textbox;
+
+ WeakPtr<Layer> m_layer;
+};
+
+}
diff --git a/Userland/Applications/PixelPaint/LineTool.cpp b/Userland/Applications/PixelPaint/LineTool.cpp
new file mode 100644
index 0000000000..7c3e37e836
--- /dev/null
+++ b/Userland/Applications/PixelPaint/LineTool.cpp
@@ -0,0 +1,156 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "LineTool.h"
+#include "ImageEditor.h"
+#include "Layer.h"
+#include <LibGUI/Action.h>
+#include <LibGUI/Menu.h>
+#include <LibGUI/Painter.h>
+#include <math.h>
+
+namespace PixelPaint {
+
+static Gfx::IntPoint constrain_line_angle(const Gfx::IntPoint& start_pos, const Gfx::IntPoint& end_pos, float angle_increment)
+{
+ float current_angle = atan2(end_pos.y() - start_pos.y(), end_pos.x() - start_pos.x()) + M_PI * 2.;
+
+ float constrained_angle = ((int)((current_angle + angle_increment / 2.) / angle_increment)) * angle_increment;
+
+ auto diff = end_pos - start_pos;
+ float line_length = sqrt(diff.x() * diff.x() + diff.y() * diff.y());
+
+ return { start_pos.x() + (int)(cos(constrained_angle) * line_length),
+ start_pos.y() + (int)(sin(constrained_angle) * line_length) };
+}
+
+LineTool::LineTool()
+{
+}
+
+LineTool::~LineTool()
+{
+}
+
+void LineTool::on_mousedown(Layer&, GUI::MouseEvent& layer_event, GUI::MouseEvent&)
+{
+ if (layer_event.button() != GUI::MouseButton::Left && layer_event.button() != GUI::MouseButton::Right)
+ return;
+
+ if (m_drawing_button != GUI::MouseButton::None)
+ return;
+
+ m_drawing_button = layer_event.button();
+
+ m_line_start_position = layer_event.position();
+ m_line_end_position = layer_event.position();
+
+ m_editor->update();
+}
+
+void LineTool::on_mouseup(Layer& layer, GUI::MouseEvent& event, GUI::MouseEvent&)
+{
+ if (event.button() == m_drawing_button) {
+ GUI::Painter painter(layer.bitmap());
+ painter.draw_line(m_line_start_position, m_line_end_position, m_editor->color_for(m_drawing_button), m_thickness);
+ m_drawing_button = GUI::MouseButton::None;
+ layer.did_modify_bitmap(*m_editor->image());
+ m_editor->did_complete_action();
+ }
+}
+
+void LineTool::on_mousemove(Layer&, GUI::MouseEvent& layer_event, GUI::MouseEvent&)
+{
+ if (m_drawing_button == GUI::MouseButton::None)
+ return;
+
+ if (!m_constrain_angle) {
+ m_line_end_position = layer_event.position();
+ } else {
+ const float ANGLE_STEP = M_PI / 8.0f;
+ m_line_end_position = constrain_line_angle(m_line_start_position, layer_event.position(), ANGLE_STEP);
+ }
+ m_editor->update();
+}
+
+void LineTool::on_second_paint(const Layer& layer, GUI::PaintEvent& event)
+{
+ if (m_drawing_button == GUI::MouseButton::None)
+ return;
+
+ GUI::Painter painter(*m_editor);
+ painter.add_clip_rect(event.rect());
+ auto preview_start = m_editor->layer_position_to_editor_position(layer, m_line_start_position).to_type<int>();
+ auto preview_end = m_editor->layer_position_to_editor_position(layer, m_line_end_position).to_type<int>();
+ painter.draw_line(preview_start, preview_end, m_editor->color_for(m_drawing_button), m_thickness);
+}
+
+void LineTool::on_keydown(GUI::KeyEvent& event)
+{
+ if (event.key() == Key_Escape && m_drawing_button != GUI::MouseButton::None) {
+ m_drawing_button = GUI::MouseButton::None;
+ m_editor->update();
+ event.accept();
+ }
+
+ if (event.key() == Key_Shift) {
+ m_constrain_angle = true;
+ m_editor->update();
+ event.accept();
+ }
+}
+
+void LineTool::on_keyup(GUI::KeyEvent& event)
+{
+ if (event.key() == Key_Shift) {
+ m_constrain_angle = false;
+ m_editor->update();
+ event.accept();
+ }
+}
+
+void LineTool::on_tool_button_contextmenu(GUI::ContextMenuEvent& event)
+{
+ if (!m_context_menu) {
+ m_context_menu = GUI::Menu::construct();
+ m_thickness_actions.set_exclusive(true);
+ auto insert_action = [&](int size, bool checked = false) {
+ auto action = GUI::Action::create_checkable(String::number(size), [this, size](auto&) {
+ m_thickness = size;
+ });
+ action->set_checked(checked);
+ m_thickness_actions.add_action(*action);
+ m_context_menu->add_action(move(action));
+ };
+ insert_action(1, true);
+ insert_action(2);
+ insert_action(3);
+ insert_action(4);
+ }
+ m_context_menu->popup(event.screen_position());
+}
+
+}
diff --git a/Userland/Applications/PixelPaint/LineTool.h b/Userland/Applications/PixelPaint/LineTool.h
new file mode 100644
index 0000000000..aa8757655c
--- /dev/null
+++ b/Userland/Applications/PixelPaint/LineTool.h
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "Tool.h"
+#include <LibGUI/ActionGroup.h>
+#include <LibGfx/Point.h>
+
+namespace PixelPaint {
+
+class LineTool final : public Tool {
+public:
+ LineTool();
+ virtual ~LineTool() override;
+
+ virtual void on_mousedown(Layer&, GUI::MouseEvent& layer_event, GUI::MouseEvent& image_event) override;
+ virtual void on_mousemove(Layer&, GUI::MouseEvent& layer_event, GUI::MouseEvent& image_event) override;
+ virtual void on_mouseup(Layer&, GUI::MouseEvent& layer_event, GUI::MouseEvent& image_event) override;
+ virtual void on_tool_button_contextmenu(GUI::ContextMenuEvent&) override;
+ virtual void on_second_paint(const Layer&, GUI::PaintEvent&) override;
+ virtual void on_keydown(GUI::KeyEvent&) override;
+ virtual void on_keyup(GUI::KeyEvent&) override;
+
+private:
+ GUI::MouseButton m_drawing_button { GUI::MouseButton::None };
+ Gfx::IntPoint m_line_start_position;
+ Gfx::IntPoint m_line_end_position;
+
+ RefPtr<GUI::Menu> m_context_menu;
+ GUI::ActionGroup m_thickness_actions;
+ int m_thickness { 1 };
+ bool m_constrain_angle { false };
+};
+
+}
diff --git a/Userland/Applications/PixelPaint/MoveTool.cpp b/Userland/Applications/PixelPaint/MoveTool.cpp
new file mode 100644
index 0000000000..ebeba5ba89
--- /dev/null
+++ b/Userland/Applications/PixelPaint/MoveTool.cpp
@@ -0,0 +1,139 @@
+/*
+ * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "MoveTool.h"
+#include "Image.h"
+#include "ImageEditor.h"
+#include "Layer.h"
+#include <LibGUI/Action.h>
+#include <LibGUI/Menu.h>
+#include <LibGUI/Window.h>
+#include <LibGfx/Bitmap.h>
+
+namespace PixelPaint {
+
+MoveTool::MoveTool()
+{
+}
+
+MoveTool::~MoveTool()
+{
+}
+
+void MoveTool::on_mousedown(Layer& layer, GUI::MouseEvent& event, GUI::MouseEvent& image_event)
+{
+ if (event.button() != GUI::MouseButton::Left)
+ return;
+ if (!layer.rect().contains(event.position()))
+ return;
+ m_layer_being_moved = layer;
+ m_event_origin = image_event.position();
+ m_layer_origin = layer.location();
+ m_editor->window()->set_cursor(Gfx::StandardCursor::Move);
+}
+
+void MoveTool::on_mousemove(Layer&, GUI::MouseEvent&, GUI::MouseEvent& image_event)
+{
+ if (!m_layer_being_moved)
+ return;
+ auto delta = image_event.position() - m_event_origin;
+ m_layer_being_moved->set_location(m_layer_origin.translated(delta));
+ m_editor->layers_did_change();
+}
+
+void MoveTool::on_mouseup(Layer&, GUI::MouseEvent& event, GUI::MouseEvent&)
+{
+ if (event.button() != GUI::MouseButton::Left)
+ return;
+ m_layer_being_moved = nullptr;
+ m_editor->window()->set_cursor(Gfx::StandardCursor::None);
+ m_editor->did_complete_action();
+}
+
+void MoveTool::on_keydown(GUI::KeyEvent& event)
+{
+ if (event.modifiers() != 0)
+ return;
+
+ auto* layer = m_editor->active_layer();
+ if (!layer)
+ return;
+
+ auto new_location = layer->location();
+
+ switch (event.key()) {
+ case Key_Up:
+ new_location.move_by(0, -1);
+ break;
+ case Key_Down:
+ new_location.move_by(0, 1);
+ break;
+ case Key_Left:
+ new_location.move_by(-1, 0);
+ break;
+ case Key_Right:
+ new_location.move_by(1, 0);
+ break;
+ default:
+ return;
+ }
+
+ layer->set_location(new_location);
+ m_editor->layers_did_change();
+}
+
+void MoveTool::on_context_menu(Layer& layer, GUI::ContextMenuEvent& event)
+{
+ if (!m_context_menu) {
+ m_context_menu = GUI::Menu::construct();
+ m_context_menu->add_action(GUI::CommonActions::make_move_to_front_action(
+ [this](auto&) {
+ m_editor->image()->move_layer_to_front(*m_context_menu_layer);
+ m_editor->layers_did_change();
+ },
+ m_editor));
+ m_context_menu->add_action(GUI::CommonActions::make_move_to_back_action(
+ [this](auto&) {
+ m_editor->image()->move_layer_to_back(*m_context_menu_layer);
+ m_editor->layers_did_change();
+ },
+ m_editor));
+ m_context_menu->add_separator();
+ m_context_menu->add_action(GUI::Action::create(
+ "Delete layer", Gfx::Bitmap::load_from_file("/res/icons/16x16/delete.png"), [this](auto&) {
+ m_editor->image()->remove_layer(*m_context_menu_layer);
+ // FIXME: This should not be done imperatively here. Perhaps a Image::Client interface that ImageEditor can implement?
+ if (m_editor->active_layer() == m_context_menu_layer)
+ m_editor->set_active_layer(nullptr);
+ m_editor->layers_did_change();
+ },
+ m_editor));
+ }
+ m_context_menu_layer = layer;
+ m_context_menu->popup(event.screen_position());
+}
+
+}
diff --git a/Userland/Applications/PixelPaint/MoveTool.h b/Userland/Applications/PixelPaint/MoveTool.h
new file mode 100644
index 0000000000..a0c16e5282
--- /dev/null
+++ b/Userland/Applications/PixelPaint/MoveTool.h
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "Tool.h"
+
+namespace PixelPaint {
+
+class MoveTool final : public Tool {
+public:
+ MoveTool();
+ virtual ~MoveTool() override;
+
+ virtual void on_mousedown(Layer&, GUI::MouseEvent& layer_event, GUI::MouseEvent& image_event) override;
+ virtual void on_mousemove(Layer&, GUI::MouseEvent& layer_event, GUI::MouseEvent& image_event) override;
+ virtual void on_mouseup(Layer&, GUI::MouseEvent& layer_event, GUI::MouseEvent& image_event) override;
+ virtual void on_keydown(GUI::KeyEvent&) override;
+ virtual void on_context_menu(Layer&, GUI::ContextMenuEvent&) override;
+
+private:
+ RefPtr<Layer> m_layer_being_moved;
+ Gfx::IntPoint m_event_origin;
+ Gfx::IntPoint m_layer_origin;
+ RefPtr<GUI::Menu> m_context_menu;
+ RefPtr<Layer> m_context_menu_layer;
+};
+
+}
diff --git a/Userland/Applications/PixelPaint/PaletteWidget.cpp b/Userland/Applications/PixelPaint/PaletteWidget.cpp
new file mode 100644
index 0000000000..52620b4b31
--- /dev/null
+++ b/Userland/Applications/PixelPaint/PaletteWidget.cpp
@@ -0,0 +1,178 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "PaletteWidget.h"
+#include "ImageEditor.h"
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/ColorPicker.h>
+#include <LibGfx/Palette.h>
+
+namespace PixelPaint {
+
+class ColorWidget : public GUI::Frame {
+ C_OBJECT(ColorWidget);
+
+public:
+ explicit ColorWidget(Color color, PaletteWidget& palette_widget)
+ : m_palette_widget(palette_widget)
+ , m_color(color)
+ {
+ }
+
+ virtual ~ColorWidget() override
+ {
+ }
+
+ virtual void mousedown_event(GUI::MouseEvent& event) override
+ {
+ if (event.modifiers() & KeyModifier::Mod_Ctrl && event.button() == GUI::MouseButton::Left) {
+ auto dialog = GUI::ColorPicker::construct(m_color, window());
+ if (dialog->exec() == GUI::Dialog::ExecOK) {
+ m_color = dialog->color();
+ auto pal = palette();
+ pal.set_color(ColorRole::Background, m_color);
+ set_palette(pal);
+ update();
+ }
+ return;
+ }
+
+ if (event.button() == GUI::MouseButton::Left)
+ m_palette_widget.set_primary_color(m_color);
+ else if (event.button() == GUI::MouseButton::Right)
+ m_palette_widget.set_secondary_color(m_color);
+ }
+
+private:
+ PaletteWidget& m_palette_widget;
+ Color m_color;
+};
+
+PaletteWidget::PaletteWidget(ImageEditor& editor)
+ : m_editor(editor)
+{
+ set_frame_shape(Gfx::FrameShape::Panel);
+ set_frame_shadow(Gfx::FrameShadow::Raised);
+ set_frame_thickness(0);
+ set_fill_with_background_color(true);
+
+ set_fixed_height(34);
+
+ m_secondary_color_widget = add<GUI::Frame>();
+ m_secondary_color_widget->set_relative_rect({ 2, 2, 60, 31 });
+ m_secondary_color_widget->set_fill_with_background_color(true);
+ set_secondary_color(m_editor.secondary_color());
+
+ m_primary_color_widget = add<GUI::Frame>();
+ Gfx::IntRect rect { 0, 0, 38, 15 };
+ rect.center_within(m_secondary_color_widget->relative_rect());
+ m_primary_color_widget->set_relative_rect(rect);
+ m_primary_color_widget->set_fill_with_background_color(true);
+ set_primary_color(m_editor.primary_color());
+
+ m_editor.on_primary_color_change = [this](Color color) {
+ set_primary_color(color);
+ };
+
+ m_editor.on_secondary_color_change = [this](Color color) {
+ set_secondary_color(color);
+ };
+
+ auto& color_container = add<GUI::Widget>();
+ color_container.set_relative_rect(m_secondary_color_widget->relative_rect().right() + 2, 2, 500, 32);
+ color_container.set_layout<GUI::VerticalBoxLayout>();
+ color_container.layout()->set_spacing(1);
+
+ auto& top_color_container = color_container.add<GUI::Widget>();
+ top_color_container.set_layout<GUI::HorizontalBoxLayout>();
+ top_color_container.layout()->set_spacing(1);
+
+ auto& bottom_color_container = color_container.add<GUI::Widget>();
+ bottom_color_container.set_layout<GUI::HorizontalBoxLayout>();
+ bottom_color_container.layout()->set_spacing(1);
+
+ auto add_color_widget = [&](GUI::Widget& container, Color color) {
+ auto& color_widget = container.add<ColorWidget>(color, *this);
+ color_widget.set_fill_with_background_color(true);
+ auto pal = color_widget.palette();
+ pal.set_color(ColorRole::Background, color);
+ color_widget.set_palette(pal);
+ };
+
+ add_color_widget(top_color_container, Color::from_rgb(0x000000));
+ add_color_widget(top_color_container, Color::from_rgb(0x808080));
+ add_color_widget(top_color_container, Color::from_rgb(0x800000));
+ add_color_widget(top_color_container, Color::from_rgb(0x808000));
+ add_color_widget(top_color_container, Color::from_rgb(0x008000));
+ add_color_widget(top_color_container, Color::from_rgb(0x008080));
+ add_color_widget(top_color_container, Color::from_rgb(0x000080));
+ add_color_widget(top_color_container, Color::from_rgb(0x800080));
+ add_color_widget(top_color_container, Color::from_rgb(0x808040));
+ add_color_widget(top_color_container, Color::from_rgb(0x004040));
+ add_color_widget(top_color_container, Color::from_rgb(0x0080ff));
+ add_color_widget(top_color_container, Color::from_rgb(0x004080));
+ add_color_widget(top_color_container, Color::from_rgb(0x8000ff));
+ add_color_widget(top_color_container, Color::from_rgb(0x804000));
+
+ add_color_widget(bottom_color_container, Color::from_rgb(0xffffff));
+ add_color_widget(bottom_color_container, Color::from_rgb(0xc0c0c0));
+ add_color_widget(bottom_color_container, Color::from_rgb(0xff0000));
+ add_color_widget(bottom_color_container, Color::from_rgb(0xffff00));
+ add_color_widget(bottom_color_container, Color::from_rgb(0x00ff00));
+ add_color_widget(bottom_color_container, Color::from_rgb(0x00ffff));
+ add_color_widget(bottom_color_container, Color::from_rgb(0x0000ff));
+ add_color_widget(bottom_color_container, Color::from_rgb(0xff00ff));
+ add_color_widget(bottom_color_container, Color::from_rgb(0xffff80));
+ add_color_widget(bottom_color_container, Color::from_rgb(0x00ff80));
+ add_color_widget(bottom_color_container, Color::from_rgb(0x80ffff));
+ add_color_widget(bottom_color_container, Color::from_rgb(0x8080ff));
+ add_color_widget(bottom_color_container, Color::from_rgb(0xff0080));
+ add_color_widget(bottom_color_container, Color::from_rgb(0xff8040));
+}
+
+PaletteWidget::~PaletteWidget()
+{
+}
+
+void PaletteWidget::set_primary_color(Color color)
+{
+ m_editor.set_primary_color(color);
+ auto pal = m_primary_color_widget->palette();
+ pal.set_color(ColorRole::Background, color);
+ m_primary_color_widget->set_palette(pal);
+ m_primary_color_widget->update();
+}
+
+void PaletteWidget::set_secondary_color(Color color)
+{
+ m_editor.set_secondary_color(color);
+ auto pal = m_secondary_color_widget->palette();
+ pal.set_color(ColorRole::Background, color);
+ m_secondary_color_widget->set_palette(pal);
+ m_secondary_color_widget->update();
+}
+
+}
diff --git a/Userland/Applications/PixelPaint/PaletteWidget.h b/Userland/Applications/PixelPaint/PaletteWidget.h
new file mode 100644
index 0000000000..02c843f038
--- /dev/null
+++ b/Userland/Applications/PixelPaint/PaletteWidget.h
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <LibGUI/Frame.h>
+
+namespace PixelPaint {
+
+class ImageEditor;
+
+class PaletteWidget final : public GUI::Frame {
+ C_OBJECT(PaletteWidget);
+
+public:
+ virtual ~PaletteWidget() override;
+
+ void set_primary_color(Color);
+ void set_secondary_color(Color);
+
+private:
+ explicit PaletteWidget(ImageEditor&);
+
+ ImageEditor& m_editor;
+ RefPtr<GUI::Frame> m_primary_color_widget;
+ RefPtr<GUI::Frame> m_secondary_color_widget;
+};
+
+}
diff --git a/Userland/Applications/PixelPaint/PenTool.cpp b/Userland/Applications/PixelPaint/PenTool.cpp
new file mode 100644
index 0000000000..579231edf7
--- /dev/null
+++ b/Userland/Applications/PixelPaint/PenTool.cpp
@@ -0,0 +1,128 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "PenTool.h"
+#include "ImageEditor.h"
+#include "Layer.h"
+#include <LibGUI/Action.h>
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/Label.h>
+#include <LibGUI/Menu.h>
+#include <LibGUI/Painter.h>
+#include <LibGUI/Slider.h>
+
+namespace PixelPaint {
+
+PenTool::PenTool()
+{
+}
+
+PenTool::~PenTool()
+{
+}
+
+void PenTool::on_mousedown(Layer& layer, GUI::MouseEvent& event, GUI::MouseEvent&)
+{
+ if (event.button() != GUI::MouseButton::Left && event.button() != GUI::MouseButton::Right)
+ return;
+
+ GUI::Painter painter(layer.bitmap());
+ painter.draw_line(event.position(), event.position(), m_editor->color_for(event), m_thickness);
+ layer.did_modify_bitmap(*m_editor->image());
+ m_last_drawing_event_position = event.position();
+}
+
+void PenTool::on_mouseup(Layer&, GUI::MouseEvent& event, GUI::MouseEvent&)
+{
+ if (event.button() == GUI::MouseButton::Left || event.button() == GUI::MouseButton::Right) {
+ m_last_drawing_event_position = { -1, -1 };
+ m_editor->did_complete_action();
+ }
+}
+
+void PenTool::on_mousemove(Layer& layer, GUI::MouseEvent& event, GUI::MouseEvent&)
+{
+ if (!(event.buttons() & GUI::MouseButton::Left || event.buttons() & GUI::MouseButton::Right))
+ return;
+ GUI::Painter painter(layer.bitmap());
+
+ if (m_last_drawing_event_position != Gfx::IntPoint(-1, -1))
+ painter.draw_line(m_last_drawing_event_position, event.position(), m_editor->color_for(event), m_thickness);
+ else
+ painter.draw_line(event.position(), event.position(), m_editor->color_for(event), m_thickness);
+ layer.did_modify_bitmap(*m_editor->image());
+
+ m_last_drawing_event_position = event.position();
+}
+
+void PenTool::on_tool_button_contextmenu(GUI::ContextMenuEvent& event)
+{
+ if (!m_context_menu) {
+ m_context_menu = GUI::Menu::construct();
+ m_thickness_actions.set_exclusive(true);
+ auto insert_action = [&](int size, bool checked = false) {
+ auto action = GUI::Action::create_checkable(String::number(size), [this, size](auto&) {
+ m_thickness = size;
+ });
+ action->set_checked(checked);
+ m_thickness_actions.add_action(*action);
+ m_context_menu->add_action(move(action));
+ };
+ insert_action(1, true);
+ insert_action(2);
+ insert_action(3);
+ insert_action(4);
+ }
+ m_context_menu->popup(event.screen_position());
+}
+
+GUI::Widget* PenTool::get_properties_widget()
+{
+ if (!m_properties_widget) {
+ m_properties_widget = GUI::Widget::construct();
+ m_properties_widget->set_layout<GUI::VerticalBoxLayout>();
+
+ auto& thickness_container = m_properties_widget->add<GUI::Widget>();
+ thickness_container.set_fixed_height(20);
+ thickness_container.set_layout<GUI::HorizontalBoxLayout>();
+
+ auto& thickness_label = thickness_container.add<GUI::Label>("Thickness:");
+ thickness_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
+ thickness_label.set_fixed_size(80, 20);
+
+ auto& thickness_slider = thickness_container.add<GUI::HorizontalSlider>();
+ thickness_slider.set_fixed_height(20);
+ thickness_slider.set_range(1, 20);
+ thickness_slider.set_value(m_thickness);
+ thickness_slider.on_change = [this](int value) {
+ m_thickness = value;
+ };
+ }
+
+ return m_properties_widget.ptr();
+}
+
+}
diff --git a/Userland/Applications/PixelPaint/PenTool.h b/Userland/Applications/PixelPaint/PenTool.h
new file mode 100644
index 0000000000..137e58251e
--- /dev/null
+++ b/Userland/Applications/PixelPaint/PenTool.h
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "Tool.h"
+#include <LibGUI/ActionGroup.h>
+#include <LibGfx/Point.h>
+
+namespace PixelPaint {
+
+class PenTool final : public Tool {
+public:
+ PenTool();
+ virtual ~PenTool() override;
+
+ virtual void on_mousedown(Layer&, GUI::MouseEvent& layer_event, GUI::MouseEvent& image_event) override;
+ virtual void on_mousemove(Layer&, GUI::MouseEvent& layer_event, GUI::MouseEvent& image_event) override;
+ virtual void on_mouseup(Layer&, GUI::MouseEvent& layer_event, GUI::MouseEvent& image_event) override;
+ virtual void on_tool_button_contextmenu(GUI::ContextMenuEvent&) override;
+ virtual GUI::Widget* get_properties_widget() override;
+
+private:
+ Gfx::IntPoint m_last_drawing_event_position { -1, -1 };
+ RefPtr<GUI::Menu> m_context_menu;
+ RefPtr<GUI::Widget> m_properties_widget;
+ int m_thickness { 1 };
+ GUI::ActionGroup m_thickness_actions;
+};
+
+}
diff --git a/Userland/Applications/PixelPaint/PickerTool.cpp b/Userland/Applications/PixelPaint/PickerTool.cpp
new file mode 100644
index 0000000000..86a7acf961
--- /dev/null
+++ b/Userland/Applications/PixelPaint/PickerTool.cpp
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "PickerTool.h"
+#include "ImageEditor.h"
+#include "Layer.h"
+#include <LibGfx/Bitmap.h>
+
+namespace PixelPaint {
+
+PickerTool::PickerTool()
+{
+}
+
+PickerTool::~PickerTool()
+{
+}
+
+void PickerTool::on_mousedown(Layer& layer, GUI::MouseEvent& event, GUI::MouseEvent&)
+{
+ if (!layer.rect().contains(event.position()))
+ return;
+ auto color = layer.bitmap().get_pixel(event.position());
+ if (event.button() == GUI::MouseButton::Left)
+ m_editor->set_primary_color(color);
+ else if (event.button() == GUI::MouseButton::Right)
+ m_editor->set_secondary_color(color);
+}
+
+}
diff --git a/Userland/Applications/PixelPaint/PickerTool.h b/Userland/Applications/PixelPaint/PickerTool.h
new file mode 100644
index 0000000000..163a0aeeee
--- /dev/null
+++ b/Userland/Applications/PixelPaint/PickerTool.h
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "Tool.h"
+
+namespace PixelPaint {
+
+class PickerTool final : public Tool {
+public:
+ PickerTool();
+ virtual ~PickerTool() override;
+
+ virtual void on_mousedown(Layer&, GUI::MouseEvent& layer_event, GUI::MouseEvent& image_event) override;
+};
+
+}
diff --git a/Userland/Applications/PixelPaint/RectangleTool.cpp b/Userland/Applications/PixelPaint/RectangleTool.cpp
new file mode 100644
index 0000000000..a5b023f18b
--- /dev/null
+++ b/Userland/Applications/PixelPaint/RectangleTool.cpp
@@ -0,0 +1,137 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "RectangleTool.h"
+#include "ImageEditor.h"
+#include "Layer.h"
+#include <LibGUI/Action.h>
+#include <LibGUI/Menu.h>
+#include <LibGUI/Painter.h>
+#include <LibGfx/Rect.h>
+#include <math.h>
+
+namespace PixelPaint {
+
+RectangleTool::RectangleTool()
+{
+}
+
+RectangleTool::~RectangleTool()
+{
+}
+
+void RectangleTool::draw_using(GUI::Painter& painter, const Gfx::IntRect& rect)
+{
+ switch (m_mode) {
+ case Mode::Fill:
+ painter.fill_rect(rect, m_editor->color_for(m_drawing_button));
+ break;
+ case Mode::Outline:
+ painter.draw_rect(rect, m_editor->color_for(m_drawing_button));
+ break;
+ case Mode::Gradient:
+ painter.fill_rect_with_gradient(rect, m_editor->primary_color(), m_editor->secondary_color());
+ break;
+ default:
+ ASSERT_NOT_REACHED();
+ }
+}
+
+void RectangleTool::on_mousedown(Layer&, GUI::MouseEvent& event, GUI::MouseEvent&)
+{
+ if (event.button() != GUI::MouseButton::Left && event.button() != GUI::MouseButton::Right)
+ return;
+
+ if (m_drawing_button != GUI::MouseButton::None)
+ return;
+
+ m_drawing_button = event.button();
+ m_rectangle_start_position = event.position();
+ m_rectangle_end_position = event.position();
+ m_editor->update();
+}
+
+void RectangleTool::on_mouseup(Layer& layer, GUI::MouseEvent& event, GUI::MouseEvent&)
+{
+ if (event.button() == m_drawing_button) {
+ GUI::Painter painter(layer.bitmap());
+ auto rect = Gfx::IntRect::from_two_points(m_rectangle_start_position, m_rectangle_end_position);
+ draw_using(painter, rect);
+ m_drawing_button = GUI::MouseButton::None;
+ layer.did_modify_bitmap(*m_editor->image());
+ m_editor->did_complete_action();
+ }
+}
+
+void RectangleTool::on_mousemove(Layer&, GUI::MouseEvent& event, GUI::MouseEvent&)
+{
+ if (m_drawing_button == GUI::MouseButton::None)
+ return;
+
+ m_rectangle_end_position = event.position();
+ m_editor->update();
+}
+
+void RectangleTool::on_second_paint(const Layer& layer, GUI::PaintEvent& event)
+{
+ if (m_drawing_button == GUI::MouseButton::None)
+ return;
+
+ GUI::Painter painter(*m_editor);
+ painter.add_clip_rect(event.rect());
+ auto rect = Gfx::IntRect::from_two_points(
+ m_editor->layer_position_to_editor_position(layer, m_rectangle_start_position).to_type<int>(),
+ m_editor->layer_position_to_editor_position(layer, m_rectangle_end_position).to_type<int>());
+ draw_using(painter, rect);
+}
+
+void RectangleTool::on_keydown(GUI::KeyEvent& event)
+{
+ if (event.key() == Key_Escape && m_drawing_button != GUI::MouseButton::None) {
+ m_drawing_button = GUI::MouseButton::None;
+ m_editor->update();
+ event.accept();
+ }
+}
+
+void RectangleTool::on_tool_button_contextmenu(GUI::ContextMenuEvent& event)
+{
+ if (!m_context_menu) {
+ m_context_menu = GUI::Menu::construct();
+ m_context_menu->add_action(GUI::Action::create("Fill", [this](auto&) {
+ m_mode = Mode::Fill;
+ }));
+ m_context_menu->add_action(GUI::Action::create("Outline", [this](auto&) {
+ m_mode = Mode::Outline;
+ }));
+ m_context_menu->add_action(GUI::Action::create("Gradient", [this](auto&) {
+ m_mode = Mode::Gradient;
+ }));
+ }
+ m_context_menu->popup(event.screen_position());
+}
+
+}
diff --git a/Userland/Applications/PixelPaint/RectangleTool.h b/Userland/Applications/PixelPaint/RectangleTool.h
new file mode 100644
index 0000000000..f7d8c53273
--- /dev/null
+++ b/Userland/Applications/PixelPaint/RectangleTool.h
@@ -0,0 +1,63 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "Tool.h"
+#include <LibGUI/Forward.h>
+#include <LibGfx/Point.h>
+
+namespace PixelPaint {
+
+class RectangleTool final : public Tool {
+public:
+ RectangleTool();
+ virtual ~RectangleTool() override;
+
+ virtual void on_mousedown(Layer&, GUI::MouseEvent& layer_event, GUI::MouseEvent& image_event) override;
+ virtual void on_mousemove(Layer&, GUI::MouseEvent& layer_event, GUI::MouseEvent& image_event) override;
+ virtual void on_mouseup(Layer&, GUI::MouseEvent& layer_event, GUI::MouseEvent& image_event) override;
+ virtual void on_tool_button_contextmenu(GUI::ContextMenuEvent&) override;
+ virtual void on_second_paint(const Layer&, GUI::PaintEvent&) override;
+ virtual void on_keydown(GUI::KeyEvent&) override;
+
+private:
+ enum class Mode {
+ Outline,
+ Fill,
+ Gradient,
+ };
+
+ void draw_using(GUI::Painter&, const Gfx::IntRect&);
+
+ GUI::MouseButton m_drawing_button { GUI::MouseButton::None };
+ Gfx::IntPoint m_rectangle_start_position;
+ Gfx::IntPoint m_rectangle_end_position;
+ RefPtr<GUI::Menu> m_context_menu;
+ Mode m_mode { Mode::Outline };
+};
+
+}
diff --git a/Userland/Applications/PixelPaint/SprayTool.cpp b/Userland/Applications/PixelPaint/SprayTool.cpp
new file mode 100644
index 0000000000..9997c29674
--- /dev/null
+++ b/Userland/Applications/PixelPaint/SprayTool.cpp
@@ -0,0 +1,176 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "SprayTool.h"
+#include "ImageEditor.h"
+#include "Layer.h"
+#include <AK/Queue.h>
+#include <LibGUI/Action.h>
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/Label.h>
+#include <LibGUI/Menu.h>
+#include <LibGUI/Painter.h>
+#include <LibGUI/Slider.h>
+#include <LibGfx/Bitmap.h>
+#include <math.h>
+#include <stdio.h>
+
+namespace PixelPaint {
+
+SprayTool::SprayTool()
+{
+ m_timer = Core::Timer::construct();
+ m_timer->on_timeout = [&]() {
+ paint_it();
+ };
+ m_timer->set_interval(200);
+}
+
+SprayTool::~SprayTool()
+{
+}
+
+static double nrand()
+{
+ return double(rand()) / double(RAND_MAX);
+}
+
+void SprayTool::paint_it()
+{
+ auto* layer = m_editor->active_layer();
+ if (!layer)
+ return;
+
+ auto& bitmap = layer->bitmap();
+ GUI::Painter painter(bitmap);
+ ASSERT(bitmap.bpp() == 32);
+ m_editor->update();
+ const double minimal_radius = 2;
+ const double base_radius = minimal_radius * m_thickness;
+ for (int i = 0; i < M_PI * base_radius * base_radius * (m_density / 100.0f); i++) {
+ double radius = base_radius * nrand();
+ double angle = 2 * M_PI * nrand();
+ const int xpos = m_last_pos.x() + radius * cos(angle);
+ const int ypos = m_last_pos.y() - radius * sin(angle);
+ if (xpos < 0 || xpos >= bitmap.width())
+ continue;
+ if (ypos < 0 || ypos >= bitmap.height())
+ continue;
+ bitmap.set_pixel<Gfx::StorageFormat::RGBA32>(xpos, ypos, m_color);
+ }
+
+ layer->did_modify_bitmap(*m_editor->image());
+}
+
+void SprayTool::on_mousedown(Layer&, GUI::MouseEvent& event, GUI::MouseEvent&)
+{
+ m_color = m_editor->color_for(event);
+ m_last_pos = event.position();
+ m_timer->start();
+ paint_it();
+}
+
+void SprayTool::on_mousemove(Layer&, GUI::MouseEvent& event, GUI::MouseEvent&)
+{
+ m_last_pos = event.position();
+ if (m_timer->is_active()) {
+ paint_it();
+ m_timer->restart(m_timer->interval());
+ }
+}
+
+void SprayTool::on_mouseup(Layer&, GUI::MouseEvent&, GUI::MouseEvent&)
+{
+ if (m_timer->is_active()) {
+ m_timer->stop();
+ m_editor->did_complete_action();
+ }
+}
+
+void SprayTool::on_tool_button_contextmenu(GUI::ContextMenuEvent& event)
+{
+ if (!m_context_menu) {
+ m_context_menu = GUI::Menu::construct();
+ m_thickness_actions.set_exclusive(true);
+ auto insert_action = [&](int size, bool checked = false) {
+ auto action = GUI::Action::create_checkable(String::number(size), [this, size](auto&) {
+ m_thickness = size;
+ });
+ action->set_checked(checked);
+ m_thickness_actions.add_action(*action);
+ m_context_menu->add_action(move(action));
+ };
+ insert_action(1, true);
+ insert_action(2);
+ insert_action(3);
+ insert_action(4);
+ }
+ m_context_menu->popup(event.screen_position());
+}
+
+GUI::Widget* SprayTool::get_properties_widget()
+{
+ if (!m_properties_widget) {
+ m_properties_widget = GUI::Widget::construct();
+ m_properties_widget->set_layout<GUI::VerticalBoxLayout>();
+
+ auto& thickness_container = m_properties_widget->add<GUI::Widget>();
+ thickness_container.set_fixed_height(20);
+ thickness_container.set_layout<GUI::HorizontalBoxLayout>();
+
+ auto& thickness_label = thickness_container.add<GUI::Label>("Thickness:");
+ thickness_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
+ thickness_label.set_fixed_size(80, 20);
+
+ auto& thickness_slider = thickness_container.add<GUI::HorizontalSlider>();
+ thickness_slider.set_fixed_height(20);
+ thickness_slider.set_range(1, 20);
+ thickness_slider.set_value(m_thickness);
+ thickness_slider.on_change = [this](int value) {
+ m_thickness = value;
+ };
+
+ auto& density_container = m_properties_widget->add<GUI::Widget>();
+ density_container.set_fixed_height(20);
+ density_container.set_layout<GUI::HorizontalBoxLayout>();
+
+ auto& density_label = density_container.add<GUI::Label>("Density:");
+ density_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
+ density_label.set_fixed_size(80, 20);
+
+ auto& density_slider = density_container.add<GUI::HorizontalSlider>();
+ density_slider.set_fixed_height(30);
+ density_slider.set_range(1, 100);
+ density_slider.set_value(m_density);
+ density_slider.on_change = [this](int value) {
+ m_density = value;
+ };
+ }
+
+ return m_properties_widget.ptr();
+}
+
+}
diff --git a/Userland/Applications/PixelPaint/SprayTool.h b/Userland/Applications/PixelPaint/SprayTool.h
new file mode 100644
index 0000000000..a3e55efd52
--- /dev/null
+++ b/Userland/Applications/PixelPaint/SprayTool.h
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "Tool.h"
+#include <LibCore/Timer.h>
+#include <LibGUI/ActionGroup.h>
+#include <LibGUI/Painter.h>
+
+namespace PixelPaint {
+
+class SprayTool final : public Tool {
+public:
+ SprayTool();
+ virtual ~SprayTool() override;
+
+ virtual void on_mousedown(Layer&, GUI::MouseEvent& layer_event, GUI::MouseEvent& image_event) override;
+ virtual void on_mouseup(Layer&, GUI::MouseEvent& layer_event, GUI::MouseEvent& image_event) override;
+ virtual void on_mousemove(Layer&, GUI::MouseEvent& layer_event, GUI::MouseEvent& image_event) override;
+ virtual void on_tool_button_contextmenu(GUI::ContextMenuEvent&) override;
+ virtual GUI::Widget* get_properties_widget() override;
+
+private:
+ void paint_it();
+
+ RefPtr<GUI::Widget> m_properties_widget;
+ RefPtr<Core::Timer> m_timer;
+ Gfx::IntPoint m_last_pos;
+ Color m_color;
+ RefPtr<GUI::Menu> m_context_menu;
+ GUI::ActionGroup m_thickness_actions;
+ int m_thickness { 10 };
+ int m_density { 40 };
+};
+
+}
diff --git a/Userland/Applications/PixelPaint/Tool.cpp b/Userland/Applications/PixelPaint/Tool.cpp
new file mode 100644
index 0000000000..846e017410
--- /dev/null
+++ b/Userland/Applications/PixelPaint/Tool.cpp
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "Tool.h"
+#include "ImageEditor.h"
+#include <LibGUI/Action.h>
+
+namespace PixelPaint {
+
+Tool::Tool()
+{
+}
+
+Tool::~Tool()
+{
+}
+
+void Tool::setup(ImageEditor& editor)
+{
+ m_editor = editor;
+}
+
+void Tool::set_action(GUI::Action* action)
+{
+ m_action = action;
+}
+
+}
diff --git a/Userland/Applications/PixelPaint/Tool.h b/Userland/Applications/PixelPaint/Tool.h
new file mode 100644
index 0000000000..e7deb5f1cb
--- /dev/null
+++ b/Userland/Applications/PixelPaint/Tool.h
@@ -0,0 +1,63 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <LibGUI/Event.h>
+#include <LibGUI/Forward.h>
+
+namespace PixelPaint {
+
+class ImageEditor;
+class Layer;
+
+class Tool {
+public:
+ virtual ~Tool();
+
+ virtual void on_mousedown(Layer&, GUI::MouseEvent&, GUI::MouseEvent&) { }
+ virtual void on_mousemove(Layer&, GUI::MouseEvent&, GUI::MouseEvent&) { }
+ virtual void on_mouseup(Layer&, GUI::MouseEvent&, GUI::MouseEvent&) { }
+ virtual void on_context_menu(Layer&, GUI::ContextMenuEvent&) { }
+ virtual void on_tool_button_contextmenu(GUI::ContextMenuEvent&) { }
+ virtual void on_second_paint(const Layer&, GUI::PaintEvent&) { }
+ virtual void on_keydown(GUI::KeyEvent&) { }
+ virtual void on_keyup(GUI::KeyEvent&) { }
+ virtual GUI::Widget* get_properties_widget() { return nullptr; }
+
+ void clear() { m_editor = nullptr; }
+ void setup(ImageEditor&);
+
+ GUI::Action* action() { return m_action; }
+ void set_action(GUI::Action*);
+
+protected:
+ Tool();
+ WeakPtr<ImageEditor> m_editor;
+ RefPtr<GUI::Action> m_action;
+};
+
+}
diff --git a/Userland/Applications/PixelPaint/ToolPropertiesWidget.cpp b/Userland/Applications/PixelPaint/ToolPropertiesWidget.cpp
new file mode 100644
index 0000000000..a27103d3e7
--- /dev/null
+++ b/Userland/Applications/PixelPaint/ToolPropertiesWidget.cpp
@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2020, Ben Jilks <benjyjilks@gmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "ToolPropertiesWidget.h"
+#include "Tool.h"
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/GroupBox.h>
+
+namespace PixelPaint {
+
+ToolPropertiesWidget::ToolPropertiesWidget()
+{
+ set_layout<GUI::VerticalBoxLayout>();
+
+ m_group_box = add<GUI::GroupBox>("Tool properties");
+ auto& layout = m_group_box->set_layout<GUI::VerticalBoxLayout>();
+ layout.set_margins({ 10, 20, 10, 10 });
+}
+
+void ToolPropertiesWidget::set_active_tool(Tool* tool)
+{
+ if (tool == m_active_tool)
+ return;
+
+ if (m_active_tool_widget != nullptr)
+ m_group_box->remove_child(*m_active_tool_widget);
+
+ m_active_tool = tool;
+ m_active_tool_widget = tool->get_properties_widget();
+ if (m_active_tool_widget != nullptr)
+ m_group_box->add_child(*m_active_tool_widget);
+}
+
+ToolPropertiesWidget::~ToolPropertiesWidget()
+{
+}
+
+}
diff --git a/Userland/Applications/PixelPaint/ToolPropertiesWidget.h b/Userland/Applications/PixelPaint/ToolPropertiesWidget.h
new file mode 100644
index 0000000000..41c1675cfa
--- /dev/null
+++ b/Userland/Applications/PixelPaint/ToolPropertiesWidget.h
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2020, Ben Jilks <benjyjilks@gmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/RefPtr.h>
+#include <LibGUI/Forward.h>
+#include <LibGUI/Widget.h>
+
+namespace PixelPaint {
+
+class Tool;
+
+class ToolPropertiesWidget final : public GUI::Widget {
+ C_OBJECT(ToolPropertiesWidget);
+
+public:
+ virtual ~ToolPropertiesWidget() override;
+
+ void set_active_tool(Tool*);
+
+private:
+ ToolPropertiesWidget();
+
+ RefPtr<GUI::GroupBox> m_group_box;
+
+ Tool* m_active_tool { nullptr };
+ GUI::Widget* m_active_tool_widget { nullptr };
+};
+
+}
diff --git a/Userland/Applications/PixelPaint/ToolboxWidget.cpp b/Userland/Applications/PixelPaint/ToolboxWidget.cpp
new file mode 100644
index 0000000000..c746901887
--- /dev/null
+++ b/Userland/Applications/PixelPaint/ToolboxWidget.cpp
@@ -0,0 +1,140 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "ToolboxWidget.h"
+#include "BrushTool.h"
+#include "BucketTool.h"
+#include "EllipseTool.h"
+#include "EraseTool.h"
+#include "LineTool.h"
+#include "MoveTool.h"
+#include "PenTool.h"
+#include "PickerTool.h"
+#include "RectangleTool.h"
+#include "SprayTool.h"
+#include <AK/StringBuilder.h>
+#include <LibGUI/Action.h>
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/Button.h>
+#include <LibGUI/Window.h>
+
+namespace PixelPaint {
+
+class ToolButton final : public GUI::Button {
+ C_OBJECT(ToolButton)
+public:
+ ToolButton(ToolboxWidget& toolbox, const String& name, const GUI::Shortcut& shortcut, OwnPtr<Tool> tool)
+ : m_toolbox(toolbox)
+ , m_tool(move(tool))
+ {
+ StringBuilder builder;
+ builder.append(name);
+ builder.append(" (");
+ builder.append(shortcut.to_string());
+ builder.append(")");
+ set_tooltip(builder.to_string());
+
+ m_action = GUI::Action::create_checkable(
+ name, shortcut, [this](auto& action) {
+ if (action.is_checked())
+ m_toolbox.on_tool_selection(m_tool);
+ else
+ m_toolbox.on_tool_selection(nullptr);
+ },
+ toolbox.window());
+
+ m_tool->set_action(m_action);
+ set_action(*m_action);
+ m_toolbox.m_action_group.add_action(*m_action);
+ }
+
+ const Tool& tool() const { return *m_tool; }
+ Tool& tool() { return *m_tool; }
+
+ virtual bool is_uncheckable() const override { return false; }
+
+ virtual void context_menu_event(GUI::ContextMenuEvent& event) override
+ {
+ m_action->activate();
+ m_tool->on_tool_button_contextmenu(event);
+ }
+
+private:
+ ToolboxWidget& m_toolbox;
+ OwnPtr<Tool> m_tool;
+ RefPtr<GUI::Action> m_action;
+};
+
+ToolboxWidget::ToolboxWidget()
+{
+ set_fill_with_background_color(true);
+
+ set_frame_thickness(1);
+ set_frame_shape(Gfx::FrameShape::Panel);
+ set_frame_shadow(Gfx::FrameShadow::Raised);
+
+ set_fixed_width(48);
+
+ set_layout<GUI::VerticalBoxLayout>();
+ layout()->set_margins({ 4, 4, 4, 4 });
+
+ m_action_group.set_exclusive(true);
+ m_action_group.set_unchecking_allowed(false);
+
+ deferred_invoke([this](auto&) {
+ setup_tools();
+ });
+}
+
+ToolboxWidget::~ToolboxWidget()
+{
+}
+
+void ToolboxWidget::setup_tools()
+{
+ auto add_tool = [&](const StringView& name, const StringView& icon_name, const GUI::Shortcut& shortcut, NonnullOwnPtr<Tool> tool) -> ToolButton& {
+ m_tools.append(tool.ptr());
+ auto& button = add<ToolButton>(*this, name, shortcut, move(tool));
+ button.set_focus_policy(GUI::FocusPolicy::TabFocus);
+ button.set_fixed_height(32);
+ button.set_checkable(true);
+ button.set_icon(Gfx::Bitmap::load_from_file(String::formatted("/res/icons/pixelpaint/{}.png", icon_name)));
+ return button;
+ };
+
+ add_tool("Move", "move", { 0, Key_M }, make<MoveTool>());
+ add_tool("Pen", "pen", { 0, Key_N }, make<PenTool>());
+ add_tool("Brush", "brush", { 0, Key_P }, make<BrushTool>());
+ add_tool("Bucket Fill", "bucket", { Mod_Shift, Key_B }, make<BucketTool>());
+ add_tool("Spray", "spray", { Mod_Shift, Key_S }, make<SprayTool>());
+ add_tool("Color Picker", "picker", { 0, Key_O }, make<PickerTool>());
+ add_tool("Erase", "eraser", { Mod_Shift, Key_E }, make<EraseTool>());
+ add_tool("Line", "line", { Mod_Ctrl | Mod_Shift, Key_L }, make<LineTool>());
+ add_tool("Rectangle", "rectangle", { Mod_Ctrl | Mod_Shift, Key_R }, make<RectangleTool>());
+ add_tool("Ellipse", "circle", { Mod_Ctrl | Mod_Shift, Key_E }, make<EllipseTool>());
+}
+
+}
diff --git a/Userland/Applications/PixelPaint/ToolboxWidget.h b/Userland/Applications/PixelPaint/ToolboxWidget.h
new file mode 100644
index 0000000000..23794179f6
--- /dev/null
+++ b/Userland/Applications/PixelPaint/ToolboxWidget.h
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <LibGUI/ActionGroup.h>
+#include <LibGUI/Frame.h>
+
+namespace PixelPaint {
+
+class Tool;
+
+class ToolboxWidget final : public GUI::Frame {
+ C_OBJECT(ToolboxWidget)
+public:
+ virtual ~ToolboxWidget() override;
+
+ Function<void(Tool*)> on_tool_selection;
+
+ template<typename Callback>
+ void for_each_tool(Callback callback)
+ {
+ for (auto& tool : m_tools)
+ callback(*tool);
+ }
+
+private:
+ friend class ToolButton;
+
+ void setup_tools();
+
+ explicit ToolboxWidget();
+ GUI::ActionGroup m_action_group;
+ Vector<Tool*> m_tools;
+};
+
+}
diff --git a/Userland/Applications/PixelPaint/main.cpp b/Userland/Applications/PixelPaint/main.cpp
new file mode 100644
index 0000000000..02164a52ce
--- /dev/null
+++ b/Userland/Applications/PixelPaint/main.cpp
@@ -0,0 +1,387 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "CreateNewImageDialog.h"
+#include "CreateNewLayerDialog.h"
+#include "FilterParams.h"
+#include "Image.h"
+#include "ImageEditor.h"
+#include "Layer.h"
+#include "LayerListWidget.h"
+#include "LayerPropertiesWidget.h"
+#include "PaletteWidget.h"
+#include "Tool.h"
+#include "ToolPropertiesWidget.h"
+#include "ToolboxWidget.h"
+#include <LibGUI/Action.h>
+#include <LibGUI/Application.h>
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/Clipboard.h>
+#include <LibGUI/FilePicker.h>
+#include <LibGUI/Icon.h>
+#include <LibGUI/Menu.h>
+#include <LibGUI/MenuBar.h>
+#include <LibGUI/MessageBox.h>
+#include <LibGUI/TableView.h>
+#include <LibGUI/Window.h>
+#include <LibGfx/Bitmap.h>
+#include <LibGfx/Matrix4x4.h>
+#include <stdio.h>
+
+int main(int argc, char** argv)
+{
+ if (pledge("stdio thread shared_buffer accept rpath unix wpath cpath fattr", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ auto app = GUI::Application::construct(argc, argv);
+
+ if (pledge("stdio thread shared_buffer accept rpath wpath cpath", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ auto app_icon = GUI::Icon::default_icon("app-pixel-paint");
+
+ auto window = GUI::Window::construct();
+ window->set_title("PixelPaint");
+ window->resize(950, 570);
+ window->set_icon(app_icon.bitmap_for_size(16));
+
+ auto& horizontal_container = window->set_main_widget<GUI::Widget>();
+ horizontal_container.set_layout<GUI::HorizontalBoxLayout>();
+ horizontal_container.layout()->set_spacing(0);
+
+ auto& toolbox = horizontal_container.add<PixelPaint::ToolboxWidget>();
+
+ auto& vertical_container = horizontal_container.add<GUI::Widget>();
+ vertical_container.set_layout<GUI::VerticalBoxLayout>();
+ vertical_container.layout()->set_spacing(0);
+
+ auto& image_editor = vertical_container.add<PixelPaint::ImageEditor>();
+ image_editor.set_focus(true);
+
+ vertical_container.add<PixelPaint::PaletteWidget>(image_editor);
+
+ auto& right_panel = horizontal_container.add<GUI::Widget>();
+ right_panel.set_fill_with_background_color(true);
+ right_panel.set_fixed_width(230);
+ right_panel.set_layout<GUI::VerticalBoxLayout>();
+
+ auto& layer_list_widget = right_panel.add<PixelPaint::LayerListWidget>();
+
+ auto& layer_properties_widget = right_panel.add<PixelPaint::LayerPropertiesWidget>();
+
+ auto& tool_properties_widget = right_panel.add<PixelPaint::ToolPropertiesWidget>();
+
+ toolbox.on_tool_selection = [&](auto* tool) {
+ image_editor.set_active_tool(tool);
+ tool_properties_widget.set_active_tool(tool);
+ };
+
+ window->show();
+
+ auto menubar = GUI::MenuBar::construct();
+ auto& app_menu = menubar->add_menu("PixelPaint");
+
+ app_menu.add_action(
+ GUI::Action::create(
+ "New", [&](auto&) {
+ auto dialog = PixelPaint::CreateNewImageDialog::construct(window);
+ if (dialog->exec() == GUI::Dialog::ExecOK) {
+ auto image = PixelPaint::Image::create_with_size(dialog->image_size());
+ auto bg_layer = PixelPaint::Layer::create_with_size(*image, image->size(), "Background");
+ image->add_layer(*bg_layer);
+ bg_layer->bitmap().fill(Color::White);
+
+ image_editor.set_image(image);
+ layer_list_widget.set_image(image);
+ image_editor.set_active_layer(bg_layer);
+ }
+ },
+ window));
+ app_menu.add_action(GUI::CommonActions::make_open_action([&](auto&) {
+ Optional<String> open_path = GUI::FilePicker::get_open_filepath(window);
+
+ if (!open_path.has_value())
+ return;
+
+ auto image = PixelPaint::Image::create_from_file(open_path.value());
+ image_editor.set_image(image);
+ layer_list_widget.set_image(image);
+ }));
+ app_menu.add_action(GUI::CommonActions::make_save_as_action([&](auto&) {
+ if (!image_editor.image())
+ return;
+
+ Optional<String> save_path = GUI::FilePicker::get_save_filepath(window, "untitled", "pp");
+
+ if (!save_path.has_value())
+ return;
+
+ image_editor.image()->save(save_path.value());
+ }));
+ auto& export_submenu = app_menu.add_submenu("Export");
+ export_submenu.add_action(
+ GUI::Action::create(
+ "As BMP", [&](auto&) {
+ if (!image_editor.image())
+ return;
+
+ Optional<String> save_path = GUI::FilePicker::get_save_filepath(window, "untitled", "bmp");
+
+ if (!save_path.has_value())
+ return;
+
+ image_editor.image()->export_bmp(save_path.value());
+ },
+ window));
+
+ app_menu.add_separator();
+ app_menu.add_action(GUI::CommonActions::make_quit_action([](auto&) {
+ GUI::Application::the()->quit();
+ return;
+ }));
+
+ auto& edit_menu = menubar->add_menu("Edit");
+ auto paste_action = GUI::CommonActions::make_paste_action([&](auto&) {
+ ASSERT(image_editor.image());
+ auto bitmap = GUI::Clipboard::the().bitmap();
+ if (!bitmap)
+ return;
+
+ auto layer = PixelPaint::Layer::create_with_bitmap(*image_editor.image(), *bitmap, "Pasted layer");
+ image_editor.image()->add_layer(layer.release_nonnull());
+ });
+ GUI::Clipboard::the().on_change = [&](auto& mime_type) {
+ paste_action->set_enabled(mime_type == "image/x-serenityos");
+ };
+ paste_action->set_enabled(GUI::Clipboard::the().mime_type() == "image/x-serenityos");
+
+ edit_menu.add_action(paste_action);
+
+ auto undo_action = GUI::CommonActions::make_undo_action([&](auto&) {
+ ASSERT(image_editor.image());
+ image_editor.undo();
+ });
+ edit_menu.add_action(undo_action);
+
+ auto redo_action = GUI::CommonActions::make_redo_action([&](auto&) {
+ ASSERT(image_editor.image());
+ image_editor.redo();
+ });
+ edit_menu.add_action(redo_action);
+
+ auto& tool_menu = menubar->add_menu("Tool");
+ toolbox.for_each_tool([&](auto& tool) {
+ if (tool.action())
+ tool_menu.add_action(*tool.action());
+ return IterationDecision::Continue;
+ });
+
+ auto& layer_menu = menubar->add_menu("Layer");
+ layer_menu.add_action(GUI::Action::create(
+ "Create new layer...", { Mod_Ctrl | Mod_Shift, Key_N }, [&](auto&) {
+ auto dialog = PixelPaint::CreateNewLayerDialog::construct(image_editor.image()->size(), window);
+ if (dialog->exec() == GUI::Dialog::ExecOK) {
+ auto layer = PixelPaint::Layer::create_with_size(*image_editor.image(), dialog->layer_size(), dialog->layer_name());
+ if (!layer) {
+ GUI::MessageBox::show_error(window, String::formatted("Unable to create layer with size {}", dialog->size().to_string()));
+ return;
+ }
+ image_editor.image()->add_layer(layer.release_nonnull());
+ image_editor.layers_did_change();
+ }
+ },
+ window));
+
+ layer_menu.add_separator();
+ layer_menu.add_action(GUI::Action::create(
+ "Select previous layer", { 0, Key_PageUp }, [&](auto&) {
+ layer_list_widget.move_selection(1);
+ },
+ window));
+ layer_menu.add_action(GUI::Action::create(
+ "Select next layer", { 0, Key_PageDown }, [&](auto&) {
+ layer_list_widget.move_selection(-1);
+ },
+ window));
+ layer_menu.add_action(GUI::Action::create(
+ "Select top layer", { 0, Key_Home }, [&](auto&) {
+ layer_list_widget.select_top_layer();
+ },
+ window));
+ layer_menu.add_action(GUI::Action::create(
+ "Select bottom layer", { 0, Key_End }, [&](auto&) {
+ layer_list_widget.select_bottom_layer();
+ },
+ window));
+ layer_menu.add_separator();
+ layer_menu.add_action(GUI::Action::create(
+ "Move active layer up", { Mod_Ctrl, Key_PageUp }, [&](auto&) {
+ auto active_layer = image_editor.active_layer();
+ if (!active_layer)
+ return;
+ image_editor.image()->move_layer_up(*active_layer);
+ },
+ window));
+ layer_menu.add_action(GUI::Action::create(
+ "Move active layer down", { Mod_Ctrl, Key_PageDown }, [&](auto&) {
+ auto active_layer = image_editor.active_layer();
+ if (!active_layer)
+ return;
+ image_editor.image()->move_layer_down(*active_layer);
+ },
+ window));
+ layer_menu.add_separator();
+ layer_menu.add_action(GUI::Action::create(
+ "Remove active layer", { Mod_Ctrl, Key_D }, [&](auto&) {
+ auto active_layer = image_editor.active_layer();
+ if (!active_layer)
+ return;
+ image_editor.image()->remove_layer(*active_layer);
+ image_editor.set_active_layer(nullptr);
+ },
+ window));
+
+ auto& filter_menu = menubar->add_menu("Filter");
+ auto& spatial_filters_menu = filter_menu.add_submenu("Spatial");
+
+ auto& edge_detect_submenu = spatial_filters_menu.add_submenu("Edge Detect");
+ edge_detect_submenu.add_action(GUI::Action::create("Laplacian (cardinal)", [&](auto&) {
+ if (auto* layer = image_editor.active_layer()) {
+ Gfx::LaplacianFilter filter;
+ if (auto parameters = PixelPaint::FilterParameters<Gfx::LaplacianFilter>::get(false)) {
+ filter.apply(layer->bitmap(), layer->rect(), layer->bitmap(), layer->rect(), *parameters);
+ image_editor.did_complete_action();
+ }
+ }
+ }));
+ edge_detect_submenu.add_action(GUI::Action::create("Laplacian (diagonal)", [&](auto&) {
+ if (auto* layer = image_editor.active_layer()) {
+ Gfx::LaplacianFilter filter;
+ if (auto parameters = PixelPaint::FilterParameters<Gfx::LaplacianFilter>::get(true)) {
+ filter.apply(layer->bitmap(), layer->rect(), layer->bitmap(), layer->rect(), *parameters);
+ image_editor.did_complete_action();
+ }
+ }
+ }));
+ auto& blur_submenu = spatial_filters_menu.add_submenu("Blur and Sharpen");
+ blur_submenu.add_action(GUI::Action::create("Gaussian Blur (3x3)", [&](auto&) {
+ if (auto* layer = image_editor.active_layer()) {
+ Gfx::SpatialGaussianBlurFilter<3> filter;
+ if (auto parameters = PixelPaint::FilterParameters<Gfx::SpatialGaussianBlurFilter<3>>::get()) {
+ filter.apply(layer->bitmap(), layer->rect(), layer->bitmap(), layer->rect(), *parameters);
+ image_editor.did_complete_action();
+ }
+ }
+ }));
+ blur_submenu.add_action(GUI::Action::create("Gaussian Blur (5x5)", [&](auto&) {
+ if (auto* layer = image_editor.active_layer()) {
+ Gfx::SpatialGaussianBlurFilter<5> filter;
+ if (auto parameters = PixelPaint::FilterParameters<Gfx::SpatialGaussianBlurFilter<5>>::get()) {
+ filter.apply(layer->bitmap(), layer->rect(), layer->bitmap(), layer->rect(), *parameters);
+ image_editor.did_complete_action();
+ }
+ }
+ }));
+ blur_submenu.add_action(GUI::Action::create("Box Blur (3x3)", [&](auto&) {
+ if (auto* layer = image_editor.active_layer()) {
+ Gfx::BoxBlurFilter<3> filter;
+ if (auto parameters = PixelPaint::FilterParameters<Gfx::BoxBlurFilter<3>>::get()) {
+ filter.apply(layer->bitmap(), layer->rect(), layer->bitmap(), layer->rect(), *parameters);
+ image_editor.did_complete_action();
+ }
+ }
+ }));
+ blur_submenu.add_action(GUI::Action::create("Box Blur (5x5)", [&](auto&) {
+ if (auto* layer = image_editor.active_layer()) {
+ Gfx::BoxBlurFilter<5> filter;
+ if (auto parameters = PixelPaint::FilterParameters<Gfx::BoxBlurFilter<5>>::get()) {
+ filter.apply(layer->bitmap(), layer->rect(), layer->bitmap(), layer->rect(), *parameters);
+ image_editor.did_complete_action();
+ }
+ }
+ }));
+ blur_submenu.add_action(GUI::Action::create("Sharpen", [&](auto&) {
+ if (auto* layer = image_editor.active_layer()) {
+ Gfx::SharpenFilter filter;
+ if (auto parameters = PixelPaint::FilterParameters<Gfx::SharpenFilter>::get()) {
+ filter.apply(layer->bitmap(), layer->rect(), layer->bitmap(), layer->rect(), *parameters);
+ image_editor.did_complete_action();
+ }
+ }
+ }));
+
+ spatial_filters_menu.add_separator();
+ spatial_filters_menu.add_action(GUI::Action::create("Generic 5x5 Convolution", [&](auto&) {
+ if (auto* layer = image_editor.active_layer()) {
+ Gfx::GenericConvolutionFilter<5> filter;
+ if (auto parameters = PixelPaint::FilterParameters<Gfx::GenericConvolutionFilter<5>>::get(window)) {
+ filter.apply(layer->bitmap(), layer->rect(), layer->bitmap(), layer->rect(), *parameters);
+ image_editor.did_complete_action();
+ }
+ }
+ }));
+
+ auto& help_menu = menubar->add_menu("Help");
+ help_menu.add_action(GUI::CommonActions::make_about_action("PixelPaint", app_icon, window));
+
+ app->set_menubar(move(menubar));
+
+ image_editor.on_active_layer_change = [&](auto* layer) {
+ layer_list_widget.set_selected_layer(layer);
+ layer_properties_widget.set_layer(layer);
+ };
+
+ auto image = PixelPaint::Image::create_with_size({ 640, 480 });
+
+ auto bg_layer = PixelPaint::Layer::create_with_size(*image, { 640, 480 }, "Background");
+ image->add_layer(*bg_layer);
+ bg_layer->bitmap().fill(Color::White);
+
+ auto fg_layer1 = PixelPaint::Layer::create_with_size(*image, { 200, 200 }, "FG Layer 1");
+ fg_layer1->set_location({ 50, 50 });
+ image->add_layer(*fg_layer1);
+ fg_layer1->bitmap().fill(Color::Yellow);
+
+ auto fg_layer2 = PixelPaint::Layer::create_with_size(*image, { 100, 100 }, "FG Layer 2");
+ fg_layer2->set_location({ 300, 300 });
+ image->add_layer(*fg_layer2);
+ fg_layer2->bitmap().fill(Color::Blue);
+
+ layer_list_widget.on_layer_select = [&](auto* layer) {
+ image_editor.set_active_layer(layer);
+ };
+
+ layer_list_widget.set_image(image);
+
+ image_editor.set_image(image);
+ image_editor.set_active_layer(bg_layer);
+
+ return app->exec();
+}