1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-07-27 19:57:44 +00:00

PixelPaint: Move Tools to it's own subdirectory

The PixelPaint source directory was getting a bit large, let's move all
the Tools to it's own subdirectory. Also remove some unused includes.
This commit is contained in:
Marcus Nilsson 2021-09-17 09:54:55 +02:00 committed by Brian Gianforcaro
parent 61ad239ee0
commit f9e0815c3b
35 changed files with 67 additions and 69 deletions

View file

@ -0,0 +1,183 @@
/*
* Copyright (c) 2020, Ben Jilks <benjyjilks@gmail.com>
* Copyright (c) 2021, Mustafa Quraish <mustafa@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#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/ValueSlider.h>
#include <LibGfx/Color.h>
#include <LibGfx/Rect.h>
namespace PixelPaint {
BrushTool::BrushTool()
{
}
BrushTool::~BrushTool()
{
}
void BrushTool::on_mousedown(Layer* layer, MouseEvent& event)
{
if (!layer)
return;
auto& layer_event = event.layer_event();
if (layer_event.button() != GUI::MouseButton::Left && layer_event.button() != GUI::MouseButton::Right)
return;
// Shift+Click draws a line from the last position to current one.
if (layer_event.shift() && m_has_clicked) {
draw_line(layer->bitmap(), color_for(layer_event), m_last_position, layer_event.position());
auto modified_rect = Gfx::IntRect::from_two_points(m_last_position, layer_event.position()).inflated(m_size * 2, m_size * 2);
layer->did_modify_bitmap(modified_rect);
m_last_position = layer_event.position();
return;
}
const int first_draw_opacity = 10;
for (int i = 0; i < first_draw_opacity; ++i)
draw_point(layer->bitmap(), color_for(layer_event), layer_event.position());
layer->did_modify_bitmap(Gfx::IntRect::centered_on(layer_event.position(), Gfx::IntSize { m_size * 2, m_size * 2 }));
m_last_position = layer_event.position();
m_has_clicked = true;
}
void BrushTool::on_mousemove(Layer* layer, MouseEvent& event)
{
if (!layer)
return;
auto& layer_event = event.layer_event();
if (!(layer_event.buttons() & GUI::MouseButton::Left || layer_event.buttons() & GUI::MouseButton::Right))
return;
draw_line(layer->bitmap(), color_for(layer_event), m_last_position, layer_event.position());
auto modified_rect = Gfx::IntRect::from_two_points(m_last_position, layer_event.position()).inflated(m_size * 2, m_size * 2);
layer->did_modify_bitmap(modified_rect);
m_last_position = layer_event.position();
m_was_drawing = true;
}
void BrushTool::on_mouseup(Layer*, MouseEvent&)
{
if (m_was_drawing) {
m_editor->did_complete_action();
m_was_drawing = false;
}
}
Color BrushTool::color_for(GUI::MouseEvent const& event)
{
return m_editor->color_for(event);
}
void BrushTool::draw_point(Gfx::Bitmap& bitmap, Gfx::Color const& color, Gfx::IntPoint const& point)
{
for (int y = point.y() - size(); y < point.y() + size(); y++) {
for (int x = point.x() - size(); x < point.x() + size(); x++) {
auto distance = point.distance_from({ x, y });
if (x < 0 || x >= bitmap.width() || y < 0 || y >= bitmap.height())
continue;
if (distance >= size())
continue;
auto falloff = (1.0 - double { distance / size() }) * (1.0 / (100 - 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, Gfx::Color const& color, Gfx::IntPoint const& start, Gfx::IntPoint const& 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::ValueSlider>(Orientation::Horizontal, "px");
size_slider.set_range(1, 100);
size_slider.set_value(m_size);
size_slider.on_change = [&](int value) {
set_size(value);
};
set_primary_slider(&size_slider);
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::ValueSlider>(Orientation::Horizontal, "%");
hardness_slider.set_range(1, 99);
hardness_slider.set_value(m_hardness);
hardness_slider.on_change = [&](int value) {
set_hardness(value);
};
set_secondary_slider(&hardness_slider);
}
return m_properties_widget.ptr();
}
}

View file

@ -0,0 +1,45 @@
/*
* Copyright (c) 2020, Ben Jilks <benjyjilks@gmail.com>
* Copyright (c) 2021, Mustafa Quraish <mustafa@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include "Tool.h"
namespace PixelPaint {
class BrushTool : public Tool {
public:
BrushTool();
virtual ~BrushTool() override;
virtual void on_mousedown(Layer*, MouseEvent&) override;
virtual void on_mousemove(Layer*, MouseEvent&) override;
virtual void on_mouseup(Layer*, MouseEvent&) override;
virtual GUI::Widget* get_properties_widget() override;
virtual Gfx::StandardCursor cursor() override { return Gfx::StandardCursor::Crosshair; }
void set_size(int size) { m_size = size; }
int size() const { return m_size; }
void set_hardness(int hardness) { m_hardness = hardness; }
int hardness() const { return m_hardness; }
protected:
virtual Color color_for(GUI::MouseEvent const& event);
virtual void draw_point(Gfx::Bitmap& bitmap, Gfx::Color const& color, Gfx::IntPoint const& point);
virtual void draw_line(Gfx::Bitmap& bitmap, Gfx::Color const& color, Gfx::IntPoint const& start, Gfx::IntPoint const& end);
private:
RefPtr<GUI::Widget> m_properties_widget;
int m_size { 20 };
int m_hardness { 80 };
bool m_was_drawing { false };
bool m_has_clicked { false };
Gfx::IntPoint m_last_position;
};
}

View file

@ -0,0 +1,123 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "BucketTool.h"
#include "../ImageEditor.h"
#include "../Layer.h"
#include <AK/HashTable.h>
#include <AK/Queue.h>
#include <LibGUI/BoxLayout.h>
#include <LibGUI/Label.h>
#include <LibGUI/Painter.h>
#include <LibGUI/ValueSlider.h>
#include <LibGfx/Bitmap.h>
#include <LibGfx/Rect.h>
namespace PixelPaint {
BucketTool::BucketTool()
{
}
BucketTool::~BucketTool()
{
}
static float color_distance_squared(Gfx::Color const& lhs, Gfx::Color const& 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, Gfx::IntPoint const& start_position, Color target_color, Color fill_color, int threshold)
{
VERIFY(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);
HashTable<Gfx::IntPoint> visited;
while (!queue.is_empty()) {
auto position = queue.dequeue();
if (visited.contains(position))
continue;
visited.set(position);
auto pixel_color = bitmap.get_pixel<Gfx::StorageFormat::BGRA8888>(position.x(), position.y());
if (color_distance_squared(pixel_color, target_color) > threshold_normalized_squared)
continue;
bitmap.set_pixel<Gfx::StorageFormat::BGRA8888>(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, MouseEvent& event)
{
if (!layer)
return;
auto& layer_event = event.layer_event();
if (!layer->rect().contains(layer_event.position()))
return;
GUI::Painter painter(layer->bitmap());
auto target_color = layer->bitmap().get_pixel(layer_event.x(), layer_event.y());
flood_fill(layer->bitmap(), layer_event.position(), target_color, m_editor->color_for(layer_event), m_threshold);
layer->did_modify_bitmap();
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::ValueSlider>(Orientation::Horizontal, "%");
threshold_slider.set_range(0, 100);
threshold_slider.set_value(m_threshold);
threshold_slider.on_change = [&](int value) {
m_threshold = value;
};
set_primary_slider(&threshold_slider);
}
return m_properties_widget.ptr();
}
}

View file

@ -0,0 +1,26 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include "Tool.h"
namespace PixelPaint {
class BucketTool final : public Tool {
public:
BucketTool();
virtual ~BucketTool() override;
virtual void on_mousedown(Layer*, MouseEvent&) override;
virtual GUI::Widget* get_properties_widget() override;
private:
RefPtr<GUI::Widget> m_properties_widget;
int m_threshold { 0 };
};
}

View file

@ -0,0 +1,179 @@
/*
* Copyright (c) 2021, Mustafa Quraish <mustafa@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "CloneTool.h"
#include "../ImageEditor.h"
#include "../Layer.h"
#include <LibGUI/BoxLayout.h>
#include <LibGUI/Label.h>
#include <LibGUI/Menu.h>
#include <LibGUI/Painter.h>
#include <LibGUI/ValueSlider.h>
#include <LibGfx/Bitmap.h>
namespace PixelPaint {
void CloneTool::draw_point(Gfx::Bitmap& bitmap, Gfx::Color const&, Gfx::IntPoint const& point)
{
if (!m_sample_location.has_value())
return;
auto source_point = point - m_cursor_offset.value();
for (int y = -size(); y < size(); y++) {
for (int x = -size(); x < size(); x++) {
auto target_x = point.x() + x;
auto target_y = point.y() + y;
auto distance = point.distance_from({ target_x, target_y });
if (target_x < 0 || target_x >= bitmap.width() || target_y < 0 || target_y >= bitmap.height())
continue;
if (distance >= size())
continue;
auto source_x = source_point.x() + x;
auto source_y = source_point.y() + y;
if (source_x < 0 || source_x >= bitmap.width() || source_y < 0 || source_y >= bitmap.height())
continue;
auto falloff = (1.0 - double { distance / size() }) * (1.0 / (100 - hardness()));
auto pixel_color = bitmap.get_pixel(source_x, source_y);
pixel_color.set_alpha(falloff * 255);
bitmap.set_pixel(target_x, target_y, bitmap.get_pixel(target_x, target_y).blend(pixel_color));
}
}
}
void CloneTool::draw_line(Gfx::Bitmap& bitmap, Gfx::Color const& color, Gfx::IntPoint const& start, Gfx::IntPoint const& end)
{
if (!m_sample_location.has_value())
return;
BrushTool::draw_line(bitmap, color, start, end);
}
Gfx::StandardCursor CloneTool::cursor()
{
if (m_is_selecting_location)
return Gfx::StandardCursor::Eyedropper;
return Gfx::StandardCursor::Crosshair;
}
void CloneTool::on_mousemove(Layer* layer, MouseEvent& event)
{
auto& image_event = event.image_event();
if (image_event.alt())
return;
if (m_cursor_offset.has_value()) {
m_sample_location = image_event.position() - m_cursor_offset.value();
// FIXME: This is a really inefficient way to update the marker's location
m_editor->update();
}
BrushTool::on_mousemove(layer, event);
}
void CloneTool::on_mousedown(Layer* layer, MouseEvent& event)
{
auto& image_event = event.image_event();
if (image_event.alt()) {
m_sample_location = image_event.position();
m_cursor_offset = {};
// FIXME: This is a really dumb way to get the marker to show up
m_editor->update();
return;
}
if (!m_sample_location.has_value())
return;
if (!m_cursor_offset.has_value())
m_cursor_offset = event.image_event().position() - m_sample_location.value();
BrushTool::on_mousedown(layer, event);
}
void CloneTool::on_second_paint(Layer const*, GUI::PaintEvent& event)
{
if (!m_sample_location.has_value())
return;
GUI::Painter painter(*m_editor);
painter.add_clip_rect(event.rect());
auto sample_pos = m_editor->image_position_to_editor_position(m_sample_location.value());
// We don't want the marker to be a single pixel and hide the color.
auto offset = AK::max(2, size() / 2);
Gfx::IntRect rect = {
(int)sample_pos.x() - offset,
(int)sample_pos.y() - offset,
offset * 2,
offset * 2
};
painter.draw_ellipse_intersecting(rect, m_marker_color, 1);
}
void CloneTool::on_keydown(GUI::KeyEvent& event)
{
Tool::on_keydown(event);
if (event.key() == KeyCode::Key_Alt && !m_is_selecting_location) {
m_is_selecting_location = true;
m_editor->update_tool_cursor();
return;
}
}
void CloneTool::on_keyup(GUI::KeyEvent& event)
{
if (m_is_selecting_location && event.key() == KeyCode::Key_Alt) {
m_is_selecting_location = false;
m_editor->update_tool_cursor();
return;
}
}
GUI::Widget* CloneTool::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::ValueSlider>(Orientation::Horizontal, "px");
size_slider.set_range(1, 100);
size_slider.set_value(size());
size_slider.on_change = [&](int value) {
set_size(value);
};
set_primary_slider(&size_slider);
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::ValueSlider>(Orientation::Horizontal, "%");
hardness_slider.set_range(1, 99);
hardness_slider.on_change = [&](int value) {
set_hardness(value);
};
hardness_slider.set_value(99);
set_secondary_slider(&hardness_slider);
}
return m_properties_widget.ptr();
}
}

View file

@ -0,0 +1,41 @@
/*
* Copyright (c) 2021, Mustafa Quraish <mustafa@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include "BrushTool.h"
namespace PixelPaint {
class CloneTool : public BrushTool {
public:
CloneTool() = default;
virtual ~CloneTool() override = default;
virtual GUI::Widget* get_properties_widget() override;
virtual Gfx::StandardCursor cursor() override;
protected:
virtual void draw_point(Gfx::Bitmap& bitmap, Gfx::Color const& color, Gfx::IntPoint const& point) override;
virtual void draw_line(Gfx::Bitmap& bitmap, Gfx::Color const& color, Gfx::IntPoint const& start, Gfx::IntPoint const& end) override;
virtual void on_mousedown(Layer*, MouseEvent&) override;
virtual void on_mousemove(Layer*, MouseEvent&) override;
virtual void on_second_paint(Layer const*, GUI::PaintEvent&) override;
virtual void on_keydown(GUI::KeyEvent&) override;
virtual void on_keyup(GUI::KeyEvent&) override;
private:
RefPtr<GUI::Widget> m_properties_widget;
Optional<Gfx::IntPoint> m_sample_location;
Optional<Gfx::IntPoint> m_cursor_offset;
bool m_is_selecting_location { false };
Gfx::Color m_marker_color { Gfx::Color::Green };
};
}

View file

@ -0,0 +1,203 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* Copyright (c) 2021, Mustafa Quraish <mustafa@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "EllipseTool.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/RadioButton.h>
#include <LibGUI/TextBox.h>
#include <LibGUI/ValueSlider.h>
#include <LibGfx/Rect.h>
namespace PixelPaint {
EllipseTool::EllipseTool()
{
}
EllipseTool::~EllipseTool()
{
}
void EllipseTool::draw_using(GUI::Painter& painter, Gfx::IntPoint const& start_position, Gfx::IntPoint const& end_position, int thickness)
{
Gfx::IntRect ellipse_intersecting_rect;
if (m_draw_mode == DrawMode::FromCenter) {
auto delta = end_position - start_position;
ellipse_intersecting_rect = Gfx::IntRect::from_two_points(start_position - delta, end_position);
} else {
ellipse_intersecting_rect = Gfx::IntRect::from_two_points(start_position, end_position);
}
switch (m_fill_mode) {
case FillMode::Outline:
painter.draw_ellipse_intersecting(ellipse_intersecting_rect, m_editor->color_for(m_drawing_button), thickness);
break;
case FillMode::Fill:
painter.fill_ellipse(ellipse_intersecting_rect, m_editor->color_for(m_drawing_button));
break;
default:
VERIFY_NOT_REACHED();
}
}
void EllipseTool::on_mousedown(Layer* layer, MouseEvent& event)
{
if (!layer)
return;
auto& layer_event = event.layer_event();
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_ellipse_start_position = layer_event.position();
m_ellipse_end_position = layer_event.position();
m_editor->update();
}
void EllipseTool::on_mouseup(Layer* layer, MouseEvent& event)
{
if (!layer)
return;
if (event.layer_event().button() == m_drawing_button) {
GUI::Painter painter(layer->bitmap());
draw_using(painter, m_ellipse_start_position, m_ellipse_end_position, m_thickness);
m_drawing_button = GUI::MouseButton::None;
layer->did_modify_bitmap();
m_editor->update();
m_editor->did_complete_action();
}
}
void EllipseTool::on_mousemove(Layer*, MouseEvent& event)
{
if (m_drawing_button == GUI::MouseButton::None)
return;
m_draw_mode = event.layer_event().alt() ? DrawMode::FromCenter : DrawMode::FromCorner;
if (event.layer_event().shift())
m_ellipse_end_position = m_ellipse_start_position.end_point_for_aspect_ratio(event.layer_event().position(), 1.0);
else if (m_aspect_ratio.has_value())
m_ellipse_end_position = m_ellipse_start_position.end_point_for_aspect_ratio(event.layer_event().position(), m_aspect_ratio.value());
else
m_ellipse_end_position = event.layer_event().position();
m_editor->update();
}
void EllipseTool::on_second_paint(Layer const* layer, GUI::PaintEvent& event)
{
if (!layer || 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, preview_start, preview_end, AK::max(m_thickness * m_editor->scale(), 1));
}
void EllipseTool::on_keydown(GUI::KeyEvent& event)
{
Tool::on_keydown(event);
if (event.key() == Key_Escape && m_drawing_button != GUI::MouseButton::None) {
m_drawing_button = GUI::MouseButton::None;
m_editor->update();
event.accept();
}
}
GUI::Widget* EllipseTool::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::ValueSlider>(Orientation::Horizontal, "px");
thickness_slider.set_range(1, 10);
thickness_slider.set_value(m_thickness);
thickness_slider.on_change = [&](int value) {
m_thickness = value;
};
set_primary_slider(&thickness_slider);
auto& mode_container = m_properties_widget->add<GUI::Widget>();
mode_container.set_fixed_height(46);
mode_container.set_layout<GUI::HorizontalBoxLayout>();
auto& mode_label = mode_container.add<GUI::Label>("Mode:");
mode_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
mode_label.set_fixed_size(80, 20);
auto& mode_radio_container = mode_container.add<GUI::Widget>();
mode_radio_container.set_layout<GUI::VerticalBoxLayout>();
auto& outline_mode_radio = mode_radio_container.add<GUI::RadioButton>("Outline");
auto& fill_mode_radio = mode_radio_container.add<GUI::RadioButton>("Fill");
outline_mode_radio.on_checked = [&](bool) {
m_fill_mode = FillMode::Outline;
};
fill_mode_radio.on_checked = [&](bool) {
m_fill_mode = FillMode::Fill;
};
outline_mode_radio.set_checked(true);
auto& aspect_container = m_properties_widget->add<GUI::Widget>();
aspect_container.set_fixed_height(20);
aspect_container.set_layout<GUI::HorizontalBoxLayout>();
auto& aspect_label = aspect_container.add<GUI::Label>("Aspect Ratio:");
aspect_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
aspect_label.set_fixed_size(80, 20);
m_aspect_w_textbox = aspect_container.add<GUI::TextBox>();
m_aspect_w_textbox->set_fixed_height(20);
m_aspect_w_textbox->set_fixed_width(25);
m_aspect_w_textbox->on_change = [&] {
auto x = m_aspect_w_textbox->text().to_int().value_or(0);
auto y = m_aspect_h_textbox->text().to_int().value_or(0);
if (x > 0 && y > 0) {
m_aspect_ratio = (float)x / (float)y;
} else {
m_aspect_ratio = {};
}
};
auto& multiply_label = aspect_container.add<GUI::Label>("x");
multiply_label.set_text_alignment(Gfx::TextAlignment::Center);
multiply_label.set_fixed_size(10, 20);
m_aspect_h_textbox = aspect_container.add<GUI::TextBox>();
m_aspect_h_textbox->set_fixed_height(20);
m_aspect_h_textbox->set_fixed_width(25);
m_aspect_h_textbox->on_change = [&] { m_aspect_w_textbox->on_change(); };
}
return m_properties_widget.ptr();
}
}

View file

@ -0,0 +1,55 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* Copyright (c) 2021, Mustafa Quraish <mustafa@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include "Tool.h"
#include <LibGUI/Forward.h>
#include <LibGfx/Point.h>
namespace PixelPaint {
class EllipseTool final : public Tool {
public:
EllipseTool();
virtual ~EllipseTool() override;
virtual void on_mousedown(Layer*, MouseEvent&) override;
virtual void on_mousemove(Layer*, MouseEvent&) override;
virtual void on_mouseup(Layer*, MouseEvent&) override;
virtual void on_second_paint(Layer const*, GUI::PaintEvent&) override;
virtual void on_keydown(GUI::KeyEvent&) override;
virtual GUI::Widget* get_properties_widget() override;
virtual Gfx::StandardCursor cursor() override { return Gfx::StandardCursor::Crosshair; }
private:
enum class FillMode {
Outline,
Fill,
};
enum class DrawMode {
FromCenter,
FromCorner,
};
void draw_using(GUI::Painter&, Gfx::IntPoint const& start_position, Gfx::IntPoint const& end_position, int thickness);
RefPtr<GUI::Widget> m_properties_widget;
RefPtr<GUI::TextBox> m_aspect_w_textbox;
RefPtr<GUI::TextBox> m_aspect_h_textbox;
GUI::MouseButton m_drawing_button { GUI::MouseButton::None };
Gfx::IntPoint m_ellipse_start_position;
Gfx::IntPoint m_ellipse_end_position;
int m_thickness { 1 };
FillMode m_fill_mode { FillMode::Outline };
DrawMode m_draw_mode { DrawMode::FromCorner };
Optional<float> m_aspect_ratio;
};
}

View file

@ -0,0 +1,140 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* Copyright (c) 2021, Mustafa Quraish <mustafa@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "EraseTool.h"
#include "../ImageEditor.h"
#include "../Layer.h"
#include <LibGUI/Action.h>
#include <LibGUI/BoxLayout.h>
#include <LibGUI/CheckBox.h>
#include <LibGUI/Label.h>
#include <LibGUI/Menu.h>
#include <LibGUI/Painter.h>
#include <LibGUI/RadioButton.h>
#include <LibGUI/ValueSlider.h>
#include <LibGfx/Bitmap.h>
namespace PixelPaint {
EraseTool::EraseTool()
{
}
EraseTool::~EraseTool()
{
}
Color EraseTool::color_for(GUI::MouseEvent const&)
{
if (m_use_secondary_color)
return m_editor->secondary_color();
return Color(255, 255, 255, 0);
}
void EraseTool::draw_point(Gfx::Bitmap& bitmap, Gfx::Color const& color, Gfx::IntPoint const& point)
{
if (m_draw_mode == DrawMode::Pencil) {
int radius = size() / 2;
Gfx::IntRect rect { point.x() - radius, point.y() - radius, size(), size() };
GUI::Painter painter(bitmap);
painter.clear_rect(rect, color);
} else {
for (int y = point.y() - size(); y < point.y() + size(); y++) {
for (int x = point.x() - size(); x < point.x() + size(); x++) {
auto distance = point.distance_from({ x, y });
if (x < 0 || x >= bitmap.width() || y < 0 || y >= bitmap.height())
continue;
if (distance >= size())
continue;
auto old_color = bitmap.get_pixel(x, y);
auto falloff = (1.0 - double { distance / size() }) * (1.0 / (100 - hardness()));
auto new_color = old_color.interpolate(color, falloff);
bitmap.set_pixel(x, y, new_color);
}
}
}
}
GUI::Widget* EraseTool::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::ValueSlider>(Orientation::Horizontal, "px");
size_slider.set_range(1, 100);
size_slider.set_value(size());
size_slider.on_change = [&](int value) {
set_size(value);
};
set_primary_slider(&size_slider);
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::ValueSlider>(Orientation::Horizontal, "%");
hardness_slider.set_range(1, 99);
hardness_slider.set_value(hardness());
hardness_slider.on_change = [&](int value) {
set_hardness(value);
};
set_secondary_slider(&hardness_slider);
auto& secondary_color_container = m_properties_widget->add<GUI::Widget>();
secondary_color_container.set_fixed_height(20);
secondary_color_container.set_layout<GUI::HorizontalBoxLayout>();
auto& use_secondary_color_checkbox = secondary_color_container.add<GUI::CheckBox>();
use_secondary_color_checkbox.set_checked(m_use_secondary_color);
use_secondary_color_checkbox.set_text("Use secondary color");
use_secondary_color_checkbox.on_checked = [&](bool checked) {
m_use_secondary_color = checked;
};
auto& mode_container = m_properties_widget->add<GUI::Widget>();
mode_container.set_fixed_height(46);
mode_container.set_layout<GUI::HorizontalBoxLayout>();
auto& mode_label = mode_container.add<GUI::Label>("Draw Mode:");
mode_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
mode_label.set_fixed_size(80, 20);
auto& mode_radio_container = mode_container.add<GUI::Widget>();
mode_radio_container.set_layout<GUI::VerticalBoxLayout>();
auto& pencil_mode_radio = mode_radio_container.add<GUI::RadioButton>("Pencil");
auto& brush_mode_radio = mode_radio_container.add<GUI::RadioButton>("Brush");
pencil_mode_radio.on_checked = [&](bool) {
m_draw_mode = DrawMode::Pencil;
hardness_slider.set_enabled(false);
};
brush_mode_radio.on_checked = [&](bool) {
m_draw_mode = DrawMode::Brush;
hardness_slider.set_enabled(true);
};
pencil_mode_radio.set_checked(true);
}
return m_properties_widget.ptr();
}
}

View file

@ -0,0 +1,39 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* Copyright (c) 2021, Mustafa Quraish <mustafa@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include "BrushTool.h"
#include <LibGUI/ActionGroup.h>
#include <LibGfx/Forward.h>
#include <LibGfx/Point.h>
namespace PixelPaint {
class EraseTool final : public BrushTool {
public:
EraseTool();
virtual ~EraseTool() override;
virtual GUI::Widget* get_properties_widget() override;
protected:
virtual Color color_for(GUI::MouseEvent const& event) override;
virtual void draw_point(Gfx::Bitmap& bitmap, Gfx::Color const& color, Gfx::IntPoint const& point) override;
private:
RefPtr<GUI::Widget> m_properties_widget;
enum class DrawMode {
Pencil,
Brush,
};
DrawMode m_draw_mode { DrawMode::Brush };
bool m_use_secondary_color { false };
};
}

View file

@ -0,0 +1,214 @@
/*
* Copyright (c) 2021, Tobias Christiansen <tobyase@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "GuideTool.h"
#include "../EditGuideDialog.h"
#include <LibGUI/Application.h>
#include <LibGUI/BoxLayout.h>
#include <LibGUI/Label.h>
#include <LibGUI/Menu.h>
#include <LibGUI/ValueSlider.h>
#include <LibGUI/Widget.h>
namespace PixelPaint {
GuideTool::GuideTool()
{
}
GuideTool::~GuideTool()
{
}
RefPtr<Guide> GuideTool::closest_guide(const Gfx::IntPoint& point)
{
auto guides = editor()->guides();
Guide* closest_guide = nullptr;
int closest_guide_distance = NumericLimits<int>::max();
for (auto& guide : guides) {
int relevant_position = 0;
if (guide.orientation() == Guide::Orientation::Horizontal)
relevant_position = point.y();
else if (guide.orientation() == Guide::Orientation::Vertical)
relevant_position = point.x();
auto distance = abs(relevant_position - (int)guide.offset());
if (distance < closest_guide_distance) {
closest_guide = &guide;
closest_guide_distance = distance;
}
}
if (closest_guide_distance < 20)
return closest_guide;
return nullptr;
}
void GuideTool::on_mousedown(Layer*, MouseEvent& event)
{
if (!m_editor)
return;
auto& image_event = event.image_event();
if (image_event.button() != GUI::MouseButton::Left)
return;
m_editor->set_guide_visibility(true);
RefPtr<Guide> new_guide;
if (image_event.position().x() < 0 || image_event.position().x() > editor()->image().size().width()) {
new_guide = make_ref_counted<Guide>(Guide::Orientation::Vertical, image_event.position().x());
} else if (image_event.position().y() < 0 || image_event.position().y() > editor()->image().size().height()) {
new_guide = make_ref_counted<Guide>(Guide::Orientation::Horizontal, image_event.position().y());
}
if (new_guide) {
m_selected_guide = new_guide;
m_guide_origin = 0;
editor()->add_guide(new_guide.release_nonnull());
return;
}
m_event_origin = image_event.position();
m_selected_guide = closest_guide(image_event.position());
if (m_selected_guide) {
m_guide_origin = m_selected_guide->offset();
GUI::Application::the()->show_tooltip_immediately(String::formatted("{}", m_guide_origin), GUI::Application::the()->tooltip_source_widget());
}
}
void GuideTool::on_mouseup(Layer*, MouseEvent&)
{
m_guide_origin = 0;
m_event_origin = { 0, 0 };
GUI::Application::the()->hide_tooltip();
if (!m_selected_guide)
return;
if (m_selected_guide->offset() < 0
|| (m_selected_guide->orientation() == Guide::Orientation::Horizontal && m_selected_guide->offset() > editor()->image().size().height())
|| (m_selected_guide->orientation() == Guide::Orientation::Vertical && m_selected_guide->offset() > editor()->image().size().width())) {
editor()->remove_guide(*m_selected_guide);
editor()->layers_did_change();
}
m_selected_guide = nullptr;
}
void GuideTool::on_mousemove(Layer*, MouseEvent& event)
{
if (!m_selected_guide)
return;
auto& image_event = event.image_event();
auto delta = image_event.position() - m_event_origin;
auto relevant_offset = 0;
if (m_selected_guide->orientation() == Guide::Orientation::Horizontal)
relevant_offset = delta.y();
else if (m_selected_guide->orientation() == Guide::Orientation::Vertical)
relevant_offset = delta.x();
auto new_offset = (float)relevant_offset + m_guide_origin;
if (image_event.shift() && m_snap_size > 0) {
float snap_size_half = m_snap_size / 2.0;
new_offset -= fmodf(new_offset + snap_size_half, m_snap_size) - snap_size_half;
}
m_selected_guide->set_offset(new_offset);
GUI::Application::the()->show_tooltip_immediately(String::formatted("{}", new_offset), GUI::Application::the()->tooltip_source_widget());
editor()->layers_did_change();
}
void GuideTool::on_context_menu(Layer*, GUI::ContextMenuEvent& event)
{
if (!m_editor)
return;
m_editor->set_guide_visibility(true);
if (!m_context_menu) {
m_context_menu = GUI::Menu::construct();
m_context_menu->add_action(GUI::Action::create(
"Set &Offset", Gfx::Bitmap::try_load_from_file("/res/icons/16x16/gear.png"), [this](auto&) {
if (!m_context_menu_guide)
return;
auto dialog = EditGuideDialog::construct(
editor()->window(),
String::formatted("{}", m_context_menu_guide->offset()),
m_context_menu_guide->orientation());
if (dialog->exec() != GUI::Dialog::ExecOK)
return;
auto offset = dialog->offset_as_pixel(*editor());
if (!offset.has_value())
return;
m_context_menu_guide->set_offset(offset.release_value());
m_context_menu_guide->set_orientation(dialog->orientation());
editor()->layers_did_change();
},
editor()));
m_context_menu->add_action(GUI::Action::create(
"&Delete Guide", Gfx::Bitmap::try_load_from_file("/res/icons/16x16/delete.png"), [this](auto&) {
if (!m_context_menu_guide)
return;
editor()->remove_guide(*m_context_menu_guide);
m_selected_guide = nullptr;
m_guide_origin = 0;
editor()->layers_did_change();
},
editor()));
}
auto image_position = editor()->editor_position_to_image_position(event.position());
m_context_menu_guide = closest_guide({ (int)image_position.x(), (int)image_position.y() });
if (m_context_menu_guide)
m_context_menu->popup(event.screen_position());
}
void GuideTool::on_tool_activation()
{
if (m_editor)
m_editor->set_guide_visibility(true);
}
GUI::Widget* GuideTool::get_properties_widget()
{
if (!m_properties_widget) {
m_properties_widget = GUI::Widget::construct();
m_properties_widget->set_layout<GUI::VerticalBoxLayout>();
auto& snapping_container = m_properties_widget->add<GUI::Widget>();
snapping_container.set_fixed_height(20);
snapping_container.set_layout<GUI::HorizontalBoxLayout>();
auto& snapping_label = snapping_container.add<GUI::Label>("Snap offset:");
snapping_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
snapping_label.set_fixed_size(80, 20);
snapping_label.set_tooltip("Press Shift to snap");
auto& snapping_slider = snapping_container.add<GUI::ValueSlider>(Orientation::Horizontal, "px");
snapping_slider.set_range(0, 50);
snapping_slider.set_value(m_snap_size);
snapping_slider.on_change = [&](int value) {
m_snap_size = value;
};
set_primary_slider(&snapping_slider);
}
return m_properties_widget.ptr();
}
}

View file

@ -0,0 +1,43 @@
/*
* Copyright (c) 2021, Tobias Christiansen <tobyase@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include "../Guide.h"
#include "Tool.h"
#include <AK/RefPtr.h>
namespace PixelPaint {
class GuideTool final : public Tool {
public:
GuideTool();
virtual ~GuideTool() override;
virtual void on_mousedown(Layer*, MouseEvent&) override;
virtual void on_mousemove(Layer*, MouseEvent&) override;
virtual void on_mouseup(Layer*, MouseEvent&) override;
virtual void on_context_menu(Layer*, GUI::ContextMenuEvent&) override;
virtual void on_tool_activation() override;
virtual GUI::Widget* get_properties_widget() override;
virtual Gfx::StandardCursor cursor() override { return Gfx::StandardCursor::Crosshair; }
private:
RefPtr<Guide> closest_guide(Gfx::IntPoint const&);
RefPtr<GUI::Widget> m_properties_widget;
RefPtr<Guide> m_selected_guide;
RefPtr<Guide> m_context_menu_guide;
Gfx::IntPoint m_event_origin;
float m_guide_origin { 0 };
RefPtr<GUI::Menu> m_context_menu;
int m_snap_size { 10 };
};
}

View file

@ -0,0 +1,153 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* Copyright (c) 2021, Mustafa Quraish <mustafa@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "LineTool.h"
#include "../ImageEditor.h"
#include "../Layer.h"
#include <AK/Math.h>
#include <LibGUI/Action.h>
#include <LibGUI/BoxLayout.h>
#include <LibGUI/Label.h>
#include <LibGUI/Menu.h>
#include <LibGUI/Painter.h>
#include <LibGUI/ValueSlider.h>
namespace PixelPaint {
static Gfx::IntPoint constrain_line_angle(Gfx::IntPoint const& start_pos, Gfx::IntPoint const& end_pos, float angle_increment)
{
float current_angle = AK::atan2<float>(end_pos.y() - start_pos.y(), end_pos.x() - start_pos.x()) + float { 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 = AK::hypot<float>(diff.x(), diff.y());
return { start_pos.x() + (int)(AK::cos(constrained_angle) * line_length),
start_pos.y() + (int)(AK::sin(constrained_angle) * line_length) };
}
LineTool::LineTool()
{
}
LineTool::~LineTool()
{
}
void LineTool::on_mousedown(Layer* layer, MouseEvent& event)
{
if (!layer)
return;
auto& layer_event = event.layer_event();
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_drag_start_position = layer_event.position();
m_line_start_position = layer_event.position();
m_line_end_position = layer_event.position();
m_editor->update();
}
void LineTool::on_mouseup(Layer* layer, MouseEvent& event)
{
if (!layer)
return;
auto& layer_event = event.layer_event();
if (layer_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->update();
m_editor->did_complete_action();
}
}
void LineTool::on_mousemove(Layer* layer, MouseEvent& event)
{
if (!layer)
return;
auto& layer_event = event.layer_event();
if (m_drawing_button == GUI::MouseButton::None)
return;
if (layer_event.shift()) {
constexpr auto ANGLE_STEP = M_PI / 8;
m_line_end_position = constrain_line_angle(m_drag_start_position, layer_event.position(), ANGLE_STEP);
} else {
m_line_end_position = layer_event.position();
}
if (layer_event.alt()) {
m_line_start_position = m_drag_start_position + (m_drag_start_position - m_line_end_position);
} else {
m_line_start_position = m_drag_start_position;
}
m_editor->update();
}
void LineTool::on_second_paint(Layer const* layer, GUI::PaintEvent& event)
{
if (!layer || m_drawing_button == GUI::MouseButton::None)
return;
GUI::Painter painter(*m_editor);
painter.add_clip_rect(event.rect());
auto preview_start = editor_stroke_position(m_line_start_position, m_thickness);
auto preview_end = editor_stroke_position(m_line_end_position, m_thickness);
painter.draw_line(preview_start, preview_end, m_editor->color_for(m_drawing_button), AK::max(m_thickness * m_editor->scale(), 1));
}
void LineTool::on_keydown(GUI::KeyEvent& event)
{
Tool::on_keydown(event);
if (event.key() == Key_Escape && m_drawing_button != GUI::MouseButton::None) {
m_drawing_button = GUI::MouseButton::None;
m_editor->update();
event.accept();
}
}
GUI::Widget* LineTool::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::ValueSlider>(Orientation::Horizontal, "px");
thickness_slider.set_range(1, 10);
thickness_slider.set_value(m_thickness);
thickness_slider.on_change = [&](int value) {
m_thickness = value;
};
set_primary_slider(&thickness_slider);
}
return m_properties_widget.ptr();
}
}

View file

@ -0,0 +1,38 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#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*, MouseEvent&) override;
virtual void on_mousemove(Layer*, MouseEvent&) override;
virtual void on_mouseup(Layer*, MouseEvent&) override;
virtual void on_second_paint(Layer const*, GUI::PaintEvent&) override;
virtual void on_keydown(GUI::KeyEvent&) override;
virtual GUI::Widget* get_properties_widget() override;
virtual Gfx::StandardCursor cursor() override { return Gfx::StandardCursor::Crosshair; }
private:
RefPtr<GUI::Widget> m_properties_widget;
GUI::MouseButton m_drawing_button { GUI::MouseButton::None };
Gfx::IntPoint m_drag_start_position;
Gfx::IntPoint m_line_start_position;
Gfx::IntPoint m_line_end_position;
int m_thickness { 1 };
};
}

View file

@ -0,0 +1,121 @@
/*
* Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "MoveTool.h"
#include "../Image.h"
#include "../ImageEditor.h"
#include "../Layer.h"
#include <LibGUI/Action.h>
#include <LibGUI/Menu.h>
#include <LibGUI/Window.h>
namespace PixelPaint {
MoveTool::MoveTool()
{
}
MoveTool::~MoveTool()
{
}
void MoveTool::on_mousedown(Layer* layer, MouseEvent& event)
{
if (event.image_event().button() == GUI::MouseButton::Right && !m_is_panning) {
m_is_panning = true;
m_event_origin = event.raw_event().position();
m_saved_pan_origin = m_editor->pan_origin();
m_editor->set_override_cursor(Gfx::StandardCursor::Drag);
return;
}
if (!layer)
return;
auto& layer_event = event.layer_event();
auto& image_event = event.image_event();
if (layer_event.button() != GUI::MouseButton::Left)
return;
if (!layer->rect().contains(layer_event.position()))
return;
m_layer_being_moved = *layer;
m_event_origin = image_event.position();
m_layer_origin = layer->location();
}
void MoveTool::on_mousemove(Layer* layer, MouseEvent& event)
{
if (m_is_panning) {
auto& raw_event = event.raw_event();
auto delta = raw_event.position() - m_event_origin;
m_editor->set_pan_origin(m_saved_pan_origin.translated(
-delta.x(),
-delta.y()));
return;
}
if (!layer)
return;
auto& image_event = event.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* layer, MouseEvent& event)
{
if (event.image_event().button() == GUI::MouseButton::Right && m_is_panning) {
m_is_panning = false;
m_editor->set_override_cursor(cursor());
return;
}
if (!layer)
return;
auto& layer_event = event.layer_event();
if (layer_event.button() != GUI::MouseButton::Left)
return;
m_layer_being_moved = nullptr;
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.translate_by(0, -1);
break;
case Key_Down:
new_location.translate_by(0, 1);
break;
case Key_Left:
new_location.translate_by(-1, 0);
break;
case Key_Right:
new_location.translate_by(1, 0);
break;
default:
return;
}
layer->set_location(new_location);
m_editor->layers_did_change();
}
}

View file

@ -0,0 +1,33 @@
/*
* Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include "Tool.h"
namespace PixelPaint {
class MoveTool final : public Tool {
public:
MoveTool();
virtual ~MoveTool() override;
virtual void on_mousedown(Layer*, MouseEvent&) override;
virtual void on_mousemove(Layer*, MouseEvent&) override;
virtual void on_mouseup(Layer*, MouseEvent&) override;
virtual void on_keydown(GUI::KeyEvent&) override;
virtual Gfx::StandardCursor cursor() override { return Gfx::StandardCursor::Move; }
private:
RefPtr<Layer> m_layer_being_moved;
Gfx::IntPoint m_event_origin;
Gfx::IntPoint m_layer_origin;
bool m_is_panning { false };
Gfx::FloatPoint m_saved_pan_origin;
};
}

View file

@ -0,0 +1,68 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* Copyright (c) 2021, Mustafa Quraish <mustafa@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#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/ValueSlider.h>
namespace PixelPaint {
PenTool::PenTool()
{
set_size(1);
}
PenTool::~PenTool()
{
}
void PenTool::draw_point(Gfx::Bitmap& bitmap, Gfx::Color const& color, Gfx::IntPoint const& point)
{
GUI::Painter painter(bitmap);
painter.draw_line(point, point, color, size());
}
void PenTool::draw_line(Gfx::Bitmap& bitmap, Gfx::Color const& color, Gfx::IntPoint const& start, Gfx::IntPoint const& end)
{
GUI::Painter painter(bitmap);
painter.draw_line(start, end, color, size());
}
GUI::Widget* PenTool::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>("Thickness:");
size_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
size_label.set_fixed_size(80, 20);
auto& size_slider = size_container.add<GUI::ValueSlider>(Orientation::Horizontal, "px");
size_slider.set_range(1, 20);
size_slider.set_value(size());
size_slider.on_change = [&](int value) {
set_size(value);
};
set_primary_slider(&size_slider);
}
return m_properties_widget.ptr();
}
}

View file

@ -0,0 +1,31 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* Copyright (c) 2021, Mustafa Quraish <mustafa@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include "BrushTool.h"
#include <LibGUI/ActionGroup.h>
#include <LibGfx/Point.h>
namespace PixelPaint {
class PenTool final : public BrushTool {
public:
PenTool();
virtual ~PenTool() override;
virtual GUI::Widget* get_properties_widget() override;
protected:
virtual void draw_point(Gfx::Bitmap& bitmap, Gfx::Color const& color, Gfx::IntPoint const& point) override;
virtual void draw_line(Gfx::Bitmap& bitmap, Gfx::Color const& color, Gfx::IntPoint const& start, Gfx::IntPoint const& end) override;
private:
RefPtr<GUI::Widget> m_properties_widget;
};
}

View file

@ -0,0 +1,63 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* Copyright (c) 2021, Mustafa Quraish <mustafa@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "PickerTool.h"
#include "../ImageEditor.h"
#include "../Layer.h"
#include <LibGUI/BoxLayout.h>
#include <LibGUI/CheckBox.h>
namespace PixelPaint {
PickerTool::PickerTool()
{
}
PickerTool::~PickerTool()
{
}
void PickerTool::on_mousedown(Layer* layer, MouseEvent& event)
{
auto& position = event.layer_event().position();
Color color;
if (m_sample_all_layers) {
color = m_editor->image().color_at(position);
} else {
if (!layer || !layer->rect().contains(position))
return;
color = layer->bitmap().get_pixel(position);
}
// We picked a transparent pixel, do nothing.
if (!color.alpha())
return;
if (event.layer_event().button() == GUI::MouseButton::Left)
m_editor->set_primary_color(color);
else if (event.layer_event().button() == GUI::MouseButton::Right)
m_editor->set_secondary_color(color);
}
GUI::Widget* PickerTool::get_properties_widget()
{
if (!m_properties_widget) {
m_properties_widget = GUI::Widget::construct();
m_properties_widget->set_layout<GUI::VerticalBoxLayout>();
auto& sample_checkbox = m_properties_widget->add<GUI::CheckBox>("Sample all layers");
sample_checkbox.set_checked(m_sample_all_layers);
sample_checkbox.on_checked = [&](bool value) {
m_sample_all_layers = value;
};
}
return m_properties_widget.ptr();
}
}

View file

@ -0,0 +1,29 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* Copyright (c) 2021, Mustafa Quraish <mustafa@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include "Tool.h"
namespace PixelPaint {
class PickerTool final : public Tool {
public:
PickerTool();
virtual ~PickerTool() override;
virtual void on_mousedown(Layer*, MouseEvent&) override;
virtual GUI::Widget* get_properties_widget() override;
virtual Gfx::StandardCursor cursor() override { return Gfx::StandardCursor::Eyedropper; }
private:
RefPtr<GUI::Widget> m_properties_widget;
bool m_sample_all_layers { false };
};
}

View file

@ -0,0 +1,212 @@
/*
* Copyright (c) 2021, Andreas Kling <kling@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "RectangleSelectTool.h"
#include "../ImageEditor.h"
#include "../Layer.h"
#include <LibGUI/BoxLayout.h>
#include <LibGUI/Button.h>
#include <LibGUI/ComboBox.h>
#include <LibGUI/ItemListModel.h>
#include <LibGUI/Label.h>
#include <LibGUI/Model.h>
#include <LibGUI/Painter.h>
#include <LibGUI/ValueSlider.h>
namespace PixelPaint {
RectangleSelectTool::RectangleSelectTool()
{
}
RectangleSelectTool::~RectangleSelectTool()
{
}
void RectangleSelectTool::on_mousedown(Layer*, MouseEvent& event)
{
auto& image_event = event.image_event();
if (image_event.button() != GUI::MouseButton::Left)
return;
m_selecting = true;
m_editor->selection().begin_interactive_selection();
m_selection_start = image_event.position();
m_selection_end = image_event.position();
m_editor->update();
}
void RectangleSelectTool::on_mousemove(Layer*, MouseEvent& event)
{
auto& image_event = event.image_event();
if (!m_selecting)
return;
if (m_moving_mode != MovingMode::None) {
auto delta = m_selection_end - image_event.position();
if (m_moving_mode == MovingMode::MovingOrigin)
m_selection_start -= delta;
else if (m_moving_mode == MovingMode::AroundCenter)
m_selection_start += delta;
}
m_selection_end = image_event.position();
m_editor->update();
}
void RectangleSelectTool::on_mouseup(Layer*, MouseEvent& event)
{
auto& image_event = event.image_event();
if (!m_selecting || image_event.button() != GUI::MouseButton::Left)
return;
m_selecting = false;
m_editor->selection().end_interactive_selection();
m_editor->update();
auto rect_in_image = Gfx::IntRect::from_two_points(m_selection_start, m_selection_end);
auto mask = Mask::full(rect_in_image);
auto feathering = ((mask.bounding_rect().size().to_type<float>() * .5f) * m_edge_feathering).to_type<int>();
// Multiply the alpha instead of setting it to ensure corners are feathered correctly
auto multiply_alpha = [&mask](int x, int y, float alpha) {
Gfx::IntPoint point { x, y };
point += mask.bounding_rect().top_left();
float old_alpha = mask.getf(point);
mask.setf(point, old_alpha * alpha);
};
// Horizontal feathering
for (int offset = 0; offset < feathering.width(); offset++) {
// Add 1 to offset before dividing to ensure the first pixel won't always be transparent
float alpha = (float)(offset + 1) / (float)feathering.width();
for (int y = 0; y < mask.bounding_rect().height(); y++) {
multiply_alpha(offset, y, alpha);
multiply_alpha(mask.bounding_rect().width() - offset - 1, y, alpha);
}
}
// Vertical feathering
for (int offset = 0; offset < feathering.height(); offset++) {
// Add 1 to offset before dividing to ensure the first pixel won't always be transparent
float alpha = (float)(offset + 1) / (float)feathering.height();
for (int x = 0; x < mask.bounding_rect().width(); x++) {
multiply_alpha(x, offset, alpha);
multiply_alpha(x, mask.bounding_rect().height() - offset - 1, alpha);
}
}
m_editor->selection().merge(mask, m_merge_mode);
}
void RectangleSelectTool::on_keydown(GUI::KeyEvent& key_event)
{
Tool::on_keydown(key_event);
if (key_event.key() == KeyCode::Key_Space)
m_moving_mode = MovingMode::MovingOrigin;
else if (key_event.key() == KeyCode::Key_Control)
m_moving_mode = MovingMode::AroundCenter;
}
void RectangleSelectTool::on_keyup(GUI::KeyEvent& key_event)
{
if (key_event.key() == KeyCode::Key_Space && m_moving_mode == MovingMode::MovingOrigin)
m_moving_mode = MovingMode::None;
else if (key_event.key() == KeyCode::Key_Control && m_moving_mode == MovingMode::AroundCenter)
m_moving_mode = MovingMode::None;
}
void RectangleSelectTool::on_second_paint(Layer const*, GUI::PaintEvent& event)
{
if (!m_selecting)
return;
GUI::Painter painter(*m_editor);
painter.add_clip_rect(event.rect());
auto rect_in_image = Gfx::IntRect::from_two_points(m_selection_start, m_selection_end);
auto rect_in_editor = m_editor->image_rect_to_editor_rect(rect_in_image);
m_editor->selection().draw_marching_ants(painter, rect_in_editor.to_type<int>());
}
GUI::Widget* RectangleSelectTool::get_properties_widget()
{
if (m_properties_widget) {
return m_properties_widget.ptr();
}
m_properties_widget = GUI::Widget::construct();
m_properties_widget->set_layout<GUI::VerticalBoxLayout>();
auto& feather_container = m_properties_widget->add<GUI::Widget>();
feather_container.set_fixed_height(20);
feather_container.set_layout<GUI::HorizontalBoxLayout>();
auto& feather_label = feather_container.add<GUI::Label>();
feather_label.set_text("Feather:");
feather_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
feather_label.set_fixed_size(80, 20);
const int feather_slider_max = 100;
auto& feather_slider = feather_container.add<GUI::ValueSlider>(Orientation::Horizontal, "%");
feather_slider.set_range(0, feather_slider_max);
feather_slider.set_value((int)floorf(m_edge_feathering * (float)feather_slider_max));
feather_slider.on_change = [&](int value) {
m_edge_feathering = (float)value / (float)feather_slider_max;
};
set_primary_slider(&feather_slider);
auto& mode_container = m_properties_widget->add<GUI::Widget>();
mode_container.set_fixed_height(20);
mode_container.set_layout<GUI::HorizontalBoxLayout>();
auto& mode_label = mode_container.add<GUI::Label>();
mode_label.set_text("Mode:");
mode_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
mode_label.set_fixed_size(80, 20);
for (int i = 0; i < (int)Selection::MergeMode::__Count; i++) {
switch ((Selection::MergeMode)i) {
case Selection::MergeMode::Set:
m_merge_mode_names.append("Set");
break;
case Selection::MergeMode::Add:
m_merge_mode_names.append("Add");
break;
case Selection::MergeMode::Subtract:
m_merge_mode_names.append("Subtract");
break;
case Selection::MergeMode::Intersect:
m_merge_mode_names.append("Intersect");
break;
default:
VERIFY_NOT_REACHED();
}
}
auto& mode_combo = mode_container.add<GUI::ComboBox>();
mode_combo.set_only_allow_values_from_model(true);
mode_combo.set_model(*GUI::ItemListModel<String>::create(m_merge_mode_names));
mode_combo.set_selected_index((int)m_merge_mode);
mode_combo.on_change = [this](auto&&, GUI::ModelIndex const& index) {
VERIFY(index.row() >= 0);
VERIFY(index.row() < (int)Selection::MergeMode::__Count);
m_merge_mode = (Selection::MergeMode)index.row();
};
return m_properties_widget.ptr();
}
}

View file

@ -0,0 +1,48 @@
/*
* Copyright (c) 2021, Andreas Kling <kling@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include "../Selection.h"
#include "Tool.h"
#include <AK/Vector.h>
#include <LibGUI/Widget.h>
namespace PixelPaint {
class RectangleSelectTool final : public Tool {
public:
RectangleSelectTool();
virtual ~RectangleSelectTool();
virtual void on_mousedown(Layer*, MouseEvent& event) override;
virtual void on_mousemove(Layer*, MouseEvent& event) override;
virtual void on_mouseup(Layer*, MouseEvent& event) override;
virtual void on_keydown(GUI::KeyEvent&) override;
virtual void on_keyup(GUI::KeyEvent&) override;
virtual void on_second_paint(Layer const*, GUI::PaintEvent&) override;
virtual GUI::Widget* get_properties_widget() override;
virtual Gfx::StandardCursor cursor() override { return Gfx::StandardCursor::Crosshair; }
private:
enum class MovingMode {
MovingOrigin,
AroundCenter,
None,
};
RefPtr<GUI::Widget> m_properties_widget;
Vector<String> m_merge_mode_names {};
Selection::MergeMode m_merge_mode { Selection::MergeMode::Set };
float m_edge_feathering { 0.0f };
bool m_selecting { false };
MovingMode m_moving_mode { MovingMode::None };
Gfx::IntPoint m_selection_start;
Gfx::IntPoint m_selection_end;
};
}

View file

@ -0,0 +1,213 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* Copyright (c) 2021, Mustafa Quraish <mustafa@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "RectangleTool.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/RadioButton.h>
#include <LibGUI/TextBox.h>
#include <LibGUI/ValueSlider.h>
#include <LibGfx/Rect.h>
namespace PixelPaint {
RectangleTool::RectangleTool()
{
}
RectangleTool::~RectangleTool()
{
}
void RectangleTool::draw_using(GUI::Painter& painter, Gfx::IntPoint const& start_position, Gfx::IntPoint const& end_position, int thickness)
{
Gfx::IntRect rect;
if (m_draw_mode == DrawMode::FromCenter) {
auto delta = end_position - start_position;
rect = Gfx::IntRect::from_two_points(start_position - delta, end_position);
} else {
rect = Gfx::IntRect::from_two_points(start_position, end_position);
}
switch (m_fill_mode) {
case FillMode::Fill:
painter.fill_rect(rect, m_editor->color_for(m_drawing_button));
break;
case FillMode::Outline:
painter.draw_rect_with_thickness(rect, m_editor->color_for(m_drawing_button), thickness);
break;
case FillMode::Gradient:
painter.fill_rect_with_gradient(rect, m_editor->primary_color(), m_editor->secondary_color());
break;
default:
VERIFY_NOT_REACHED();
}
}
void RectangleTool::on_mousedown(Layer* layer, MouseEvent& event)
{
if (!layer)
return;
auto& layer_event = event.layer_event();
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_rectangle_start_position = layer_event.position();
m_rectangle_end_position = layer_event.position();
m_editor->update();
}
void RectangleTool::on_mouseup(Layer* layer, MouseEvent& event)
{
if (!layer)
return;
if (event.layer_event().button() == m_drawing_button) {
GUI::Painter painter(layer->bitmap());
draw_using(painter, m_rectangle_start_position, m_rectangle_end_position, m_thickness);
m_drawing_button = GUI::MouseButton::None;
layer->did_modify_bitmap();
m_editor->update();
m_editor->did_complete_action();
}
}
void RectangleTool::on_mousemove(Layer* layer, MouseEvent& event)
{
if (!layer)
return;
if (m_drawing_button == GUI::MouseButton::None)
return;
m_draw_mode = event.layer_event().alt() ? DrawMode::FromCenter : DrawMode::FromCorner;
if (event.layer_event().shift())
m_rectangle_end_position = m_rectangle_start_position.end_point_for_aspect_ratio(event.layer_event().position(), 1.0);
else if (m_aspect_ratio.has_value())
m_rectangle_end_position = m_rectangle_start_position.end_point_for_aspect_ratio(event.layer_event().position(), m_aspect_ratio.value());
else
m_rectangle_end_position = event.layer_event().position();
m_editor->update();
}
void RectangleTool::on_second_paint(Layer const* layer, GUI::PaintEvent& event)
{
if (!layer || m_drawing_button == GUI::MouseButton::None)
return;
GUI::Painter painter(*m_editor);
painter.add_clip_rect(event.rect());
auto start_position = editor_stroke_position(m_rectangle_start_position, m_thickness);
auto end_position = editor_stroke_position(m_rectangle_end_position, m_thickness);
draw_using(painter, start_position, end_position, AK::max(m_thickness * m_editor->scale(), 1));
}
void RectangleTool::on_keydown(GUI::KeyEvent& event)
{
Tool::on_keydown(event);
if (event.key() == Key_Escape && m_drawing_button != GUI::MouseButton::None) {
m_drawing_button = GUI::MouseButton::None;
m_editor->update();
event.accept();
}
}
GUI::Widget* RectangleTool::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::ValueSlider>(Orientation::Horizontal, "px");
thickness_slider.set_range(1, 10);
thickness_slider.set_value(m_thickness);
thickness_slider.on_change = [&](int value) {
m_thickness = value;
};
set_primary_slider(&thickness_slider);
auto& mode_container = m_properties_widget->add<GUI::Widget>();
mode_container.set_fixed_height(70);
mode_container.set_layout<GUI::HorizontalBoxLayout>();
auto& mode_label = mode_container.add<GUI::Label>("Mode:");
mode_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
mode_label.set_fixed_size(80, 20);
auto& mode_radio_container = mode_container.add<GUI::Widget>();
mode_radio_container.set_layout<GUI::VerticalBoxLayout>();
auto& outline_mode_radio = mode_radio_container.add<GUI::RadioButton>("Outline");
auto& fill_mode_radio = mode_radio_container.add<GUI::RadioButton>("Fill");
auto& gradient_mode_radio = mode_radio_container.add<GUI::RadioButton>("Gradient");
outline_mode_radio.on_checked = [&](bool) {
m_fill_mode = FillMode::Outline;
};
fill_mode_radio.on_checked = [&](bool) {
m_fill_mode = FillMode::Fill;
};
gradient_mode_radio.on_checked = [&](bool) {
m_fill_mode = FillMode::Gradient;
};
outline_mode_radio.set_checked(true);
auto& aspect_container = m_properties_widget->add<GUI::Widget>();
aspect_container.set_fixed_height(20);
aspect_container.set_layout<GUI::HorizontalBoxLayout>();
auto& aspect_label = aspect_container.add<GUI::Label>("Aspect Ratio:");
aspect_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
aspect_label.set_fixed_size(80, 20);
m_aspect_w_textbox = aspect_container.add<GUI::TextBox>();
m_aspect_w_textbox->set_fixed_height(20);
m_aspect_w_textbox->set_fixed_width(25);
m_aspect_w_textbox->on_change = [&] {
auto x = m_aspect_w_textbox->text().to_int().value_or(0);
auto y = m_aspect_h_textbox->text().to_int().value_or(0);
if (x > 0 && y > 0) {
m_aspect_ratio = (float)x / (float)y;
} else {
m_aspect_ratio = {};
}
};
auto& multiply_label = aspect_container.add<GUI::Label>("x");
multiply_label.set_text_alignment(Gfx::TextAlignment::Center);
multiply_label.set_fixed_size(10, 20);
m_aspect_h_textbox = aspect_container.add<GUI::TextBox>();
m_aspect_h_textbox->set_fixed_height(20);
m_aspect_h_textbox->set_fixed_width(25);
m_aspect_h_textbox->on_change = [&] { m_aspect_w_textbox->on_change(); };
}
return m_properties_widget.ptr();
}
}

View file

@ -0,0 +1,56 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* Copyright (c) 2021, Mustafa Quraish <mustafa@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#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*, MouseEvent&) override;
virtual void on_mousemove(Layer*, MouseEvent&) override;
virtual void on_mouseup(Layer*, MouseEvent&) override;
virtual void on_second_paint(Layer const*, GUI::PaintEvent&) override;
virtual void on_keydown(GUI::KeyEvent&) override;
virtual GUI::Widget* get_properties_widget() override;
virtual Gfx::StandardCursor cursor() override { return Gfx::StandardCursor::Crosshair; }
private:
enum class FillMode {
Outline,
Fill,
Gradient,
};
enum class DrawMode {
FromCenter,
FromCorner,
};
void draw_using(GUI::Painter&, Gfx::IntPoint const& start_position, Gfx::IntPoint const& end_position, int thickness);
RefPtr<GUI::Widget> m_properties_widget;
RefPtr<GUI::TextBox> m_aspect_w_textbox;
RefPtr<GUI::TextBox> m_aspect_h_textbox;
GUI::MouseButton m_drawing_button { GUI::MouseButton::None };
Gfx::IntPoint m_rectangle_start_position;
Gfx::IntPoint m_rectangle_end_position;
FillMode m_fill_mode { FillMode::Outline };
DrawMode m_draw_mode { DrawMode::FromCorner };
int m_thickness { 1 };
Optional<float> m_aspect_ratio;
};
}

View file

@ -0,0 +1,142 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "SprayTool.h"
#include "../ImageEditor.h"
#include "../Layer.h"
#include <AK/Math.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/ValueSlider.h>
#include <LibGfx/Bitmap.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);
VERIFY(bitmap.bpp() == 32);
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.0); i++) {
double radius = base_radius * nrand();
double angle = 2 * M_PI * nrand();
const int xpos = m_last_pos.x() + radius * AK::cos(angle);
const int ypos = m_last_pos.y() - radius * AK::sin(angle);
if (xpos < 0 || xpos >= bitmap.width())
continue;
if (ypos < 0 || ypos >= bitmap.height())
continue;
bitmap.set_pixel<Gfx::StorageFormat::BGRA8888>(xpos, ypos, m_color);
}
layer->did_modify_bitmap(Gfx::IntRect::centered_on(m_last_pos, Gfx::IntSize(base_radius * 2, base_radius * 2)));
}
void SprayTool::on_mousedown(Layer* layer, MouseEvent& event)
{
if (!layer)
return;
auto& layer_event = event.layer_event();
m_color = m_editor->color_for(layer_event);
m_last_pos = layer_event.position();
m_timer->start();
paint_it();
}
void SprayTool::on_mousemove(Layer* layer, MouseEvent& event)
{
if (!layer)
return;
m_last_pos = event.layer_event().position();
if (m_timer->is_active()) {
paint_it();
m_timer->restart(m_timer->interval());
}
}
void SprayTool::on_mouseup(Layer*, MouseEvent&)
{
if (m_timer->is_active()) {
m_timer->stop();
m_editor->did_complete_action();
}
}
GUI::Widget* SprayTool::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::ValueSlider>(Orientation::Horizontal, "px");
size_slider.set_range(1, 20);
size_slider.set_value(m_thickness);
size_slider.on_change = [&](int value) {
m_thickness = value;
};
set_primary_slider(&size_slider);
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::ValueSlider>(Orientation::Horizontal, "%");
density_slider.set_range(1, 100);
density_slider.set_value(m_density);
density_slider.on_change = [&](int value) {
m_density = value;
};
set_secondary_slider(&density_slider);
}
return m_properties_widget.ptr();
}
}

View file

@ -0,0 +1,38 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#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*, MouseEvent&) override;
virtual void on_mouseup(Layer*, MouseEvent&) override;
virtual void on_mousemove(Layer*, MouseEvent&) override;
virtual GUI::Widget* get_properties_widget() override;
virtual Gfx::StandardCursor cursor() override { return Gfx::StandardCursor::Crosshair; }
private:
void paint_it();
RefPtr<GUI::Widget> m_properties_widget;
RefPtr<Core::Timer> m_timer;
Gfx::IntPoint m_last_pos;
Color m_color;
int m_thickness { 10 };
int m_density { 40 };
};
}

View file

@ -0,0 +1,64 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* Copyright (c) 2021, Mustafa Quraish <mustafa@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#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;
}
void Tool::on_keydown(GUI::KeyEvent& event)
{
switch (event.key()) {
case KeyCode::Key_LeftBracket:
if (m_primary_slider)
m_primary_slider->set_value(m_primary_slider->value() - 1);
break;
case KeyCode::Key_RightBracket:
if (m_primary_slider)
m_primary_slider->set_value(m_primary_slider->value() + 1);
break;
case KeyCode::Key_LeftBrace:
if (m_secondary_slider)
m_secondary_slider->set_value(m_secondary_slider->value() - 1);
break;
case KeyCode::Key_RightBrace:
if (m_secondary_slider)
m_secondary_slider->set_value(m_secondary_slider->value() + 1);
break;
default:
break;
}
}
Gfx::IntPoint Tool::editor_stroke_position(Gfx::IntPoint const& pixel_coords, int stroke_thickness) const
{
auto position = m_editor->image_position_to_editor_position(pixel_coords);
auto offset = (stroke_thickness % 2 == 0) ? m_editor->scale() : m_editor->scale() / 2;
position = position.translated(offset, offset);
return position.to_type<int>();
}
}

View file

@ -0,0 +1,88 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* Copyright (c) 2021, Mustafa Quraish <mustafa@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibGUI/Event.h>
#include <LibGUI/Forward.h>
#include <LibGUI/ValueSlider.h>
#include <LibGfx/StandardCursor.h>
namespace PixelPaint {
class ImageEditor;
class Layer;
class Tool {
public:
virtual ~Tool();
class MouseEvent {
public:
enum class Action {
MouseDown,
MouseMove,
MouseUp
};
MouseEvent(Action action, GUI::MouseEvent& layer_event, GUI::MouseEvent& image_event, GUI::MouseEvent& raw_event)
: m_action(action)
, m_layer_event(layer_event)
, m_image_event(image_event)
, m_raw_event(raw_event)
{
}
Action action() const { return m_action; }
GUI::MouseEvent const& layer_event() const { return m_layer_event; }
GUI::MouseEvent const& image_event() const { return m_image_event; }
GUI::MouseEvent const& raw_event() const { return m_raw_event; }
private:
Action m_action;
GUI::MouseEvent& m_layer_event;
GUI::MouseEvent& m_image_event;
GUI::MouseEvent& m_raw_event;
};
virtual void on_mousedown(Layer*, MouseEvent&) { }
virtual void on_mousemove(Layer*, MouseEvent&) { }
virtual void on_mouseup(Layer*, MouseEvent&) { }
virtual void on_context_menu(Layer*, GUI::ContextMenuEvent&) { }
virtual void on_tool_button_contextmenu(GUI::ContextMenuEvent&) { }
virtual void on_second_paint(Layer const*, GUI::PaintEvent&) { }
virtual void on_keydown(GUI::KeyEvent&);
virtual void on_keyup(GUI::KeyEvent&) { }
virtual void on_tool_activation() { }
virtual GUI::Widget* get_properties_widget() { return nullptr; }
virtual Gfx::StandardCursor cursor() { return Gfx::StandardCursor::None; }
void clear() { m_editor = nullptr; }
void setup(ImageEditor&);
ImageEditor const* editor() const { return m_editor; }
ImageEditor* editor() { return m_editor; }
GUI::Action* action() { return m_action; }
void set_action(GUI::Action*);
protected:
Tool();
WeakPtr<ImageEditor> m_editor;
RefPtr<GUI::Action> m_action;
virtual Gfx::IntPoint editor_stroke_position(Gfx::IntPoint const& pixel_coords, int stroke_thickness) const;
void set_primary_slider(GUI::ValueSlider* primary) { m_primary_slider = primary; }
void set_secondary_slider(GUI::ValueSlider* secondary) { m_secondary_slider = secondary; }
GUI::ValueSlider* m_primary_slider { nullptr };
GUI::ValueSlider* m_secondary_slider { nullptr };
};
}

View file

@ -0,0 +1,60 @@
/*
* Copyright (c) 2021, the SerenityOS developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "ZoomTool.h"
#include "../ImageEditor.h"
#include <LibGUI/BoxLayout.h>
#include <LibGUI/Label.h>
#include <LibGUI/ValueSlider.h>
namespace PixelPaint {
ZoomTool::ZoomTool()
{
}
ZoomTool::~ZoomTool()
{
}
void ZoomTool::on_mousedown(Layer*, MouseEvent& event)
{
auto& raw_event = event.raw_event();
if (raw_event.button() != GUI::MouseButton::Left && raw_event.button() != GUI::MouseButton::Right)
return;
auto scale_factor = (raw_event.button() == GUI::MouseButton::Left) ? m_sensitivity : -m_sensitivity;
m_editor->scale_centered_on_position(raw_event.position(), scale_factor);
}
GUI::Widget* ZoomTool::get_properties_widget()
{
if (!m_properties_widget) {
m_properties_widget = GUI::Widget::construct();
m_properties_widget->set_layout<GUI::VerticalBoxLayout>();
auto& sensitivity_container = m_properties_widget->add<GUI::Widget>();
sensitivity_container.set_fixed_height(20);
sensitivity_container.set_layout<GUI::HorizontalBoxLayout>();
auto& sensitivity_label = sensitivity_container.add<GUI::Label>("Sensitivity:");
sensitivity_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
sensitivity_label.set_fixed_size(80, 20);
auto& sensitivity_slider = sensitivity_container.add<GUI::ValueSlider>(Orientation::Horizontal, "%");
sensitivity_slider.set_range(1, 100);
sensitivity_slider.set_value(100 * m_sensitivity);
sensitivity_slider.on_change = [&](int value) {
m_sensitivity = (double)value / 100.0;
};
set_primary_slider(&sensitivity_slider);
}
return m_properties_widget.ptr();
}
}

View file

@ -0,0 +1,29 @@
/*
* Copyright (c) 2021, the SerenityOS developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include "Tool.h"
#include <LibGUI/ActionGroup.h>
#include <LibGfx/Point.h>
namespace PixelPaint {
class ZoomTool final : public Tool {
public:
ZoomTool();
virtual ~ZoomTool() override;
virtual void on_mousedown(Layer*, MouseEvent&) override;
virtual GUI::Widget* get_properties_widget() override;
virtual Gfx::StandardCursor cursor() override { return Gfx::StandardCursor::Zoom; }
private:
RefPtr<GUI::Widget> m_properties_widget;
double m_sensitivity { 0.5 };
};
}