diff options
author | Andreas Kling <kling@serenityos.org> | 2021-01-12 12:05:23 +0100 |
---|---|---|
committer | Andreas Kling <kling@serenityos.org> | 2021-01-12 12:05:23 +0100 |
commit | dc28c07fa526841e05e16161c74a6c23984f1dd5 (patch) | |
tree | d68796bc7708eba33fbf7247e1a92188ac5acf6f /Userland/Applications/PixelPaint | |
parent | aa939c4b4b8a7eb1d22b166ebb5fb737d6e66714 (diff) | |
download | serenity-dc28c07fa526841e05e16161c74a6c23984f1dd5.zip |
Applications: Move to Userland/Applications/
Diffstat (limited to 'Userland/Applications/PixelPaint')
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(); +} |