Serenity Operating System
1/*
2 * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
3 * Copyright (c) 2021, Mustafa Quraish <mustafa@serenityos.org>
4 * Copyright (c) 2022, the SerenityOS developers.
5 *
6 * SPDX-License-Identifier: BSD-2-Clause
7 */
8
9#include "EllipseTool.h"
10#include "../ImageEditor.h"
11#include "../Layer.h"
12#include <LibGUI/Action.h>
13#include <LibGUI/BoxLayout.h>
14#include <LibGUI/CheckBox.h>
15#include <LibGUI/Label.h>
16#include <LibGUI/Menu.h>
17#include <LibGUI/Painter.h>
18#include <LibGUI/RadioButton.h>
19#include <LibGUI/TextBox.h>
20#include <LibGUI/ValueSlider.h>
21#include <LibGfx/AntiAliasingPainter.h>
22#include <LibGfx/Rect.h>
23
24namespace PixelPaint {
25
26void EllipseTool::draw_using(GUI::Painter& painter, Gfx::IntPoint start_position, Gfx::IntPoint end_position, int thickness)
27{
28 Gfx::IntRect ellipse_intersecting_rect;
29 if (m_draw_mode == DrawMode::FromCenter) {
30 auto delta = end_position - start_position;
31 ellipse_intersecting_rect = Gfx::IntRect::from_two_points(start_position - delta, end_position);
32 } else {
33 ellipse_intersecting_rect = Gfx::IntRect::from_two_points(start_position, end_position);
34 }
35
36 Gfx::AntiAliasingPainter aa_painter { painter };
37
38 switch (m_fill_mode) {
39 case FillMode::Outline:
40 if (m_antialias_enabled)
41 aa_painter.draw_ellipse(ellipse_intersecting_rect, m_editor->color_for(m_drawing_button), thickness);
42 else
43 painter.draw_ellipse_intersecting(ellipse_intersecting_rect, m_editor->color_for(m_drawing_button), thickness);
44 break;
45 case FillMode::Fill:
46 if (m_antialias_enabled)
47 aa_painter.fill_ellipse(ellipse_intersecting_rect, m_editor->color_for(m_drawing_button));
48 else
49 painter.fill_ellipse(ellipse_intersecting_rect, m_editor->color_for(m_drawing_button));
50 break;
51 default:
52 VERIFY_NOT_REACHED();
53 }
54}
55
56void EllipseTool::on_mousedown(Layer* layer, MouseEvent& event)
57{
58 if (!layer)
59 return;
60
61 auto& layer_event = event.layer_event();
62 if (layer_event.button() != GUI::MouseButton::Primary && layer_event.button() != GUI::MouseButton::Secondary)
63 return;
64
65 if (m_drawing_button != GUI::MouseButton::None)
66 return;
67
68 m_drawing_button = layer_event.button();
69 m_ellipse_start_position = layer_event.position();
70 m_ellipse_end_position = layer_event.position();
71 m_editor->update();
72}
73
74void EllipseTool::on_mouseup(Layer* layer, MouseEvent& event)
75{
76 if (!layer)
77 return;
78
79 if (event.layer_event().button() == m_drawing_button) {
80 GUI::Painter painter(layer->get_scratch_edited_bitmap());
81 draw_using(painter, m_ellipse_start_position, m_ellipse_end_position, m_thickness);
82 m_drawing_button = GUI::MouseButton::None;
83 layer->did_modify_bitmap(layer->get_scratch_edited_bitmap().rect());
84 m_editor->update();
85 m_editor->did_complete_action(tool_name());
86 }
87}
88
89void EllipseTool::on_mousemove(Layer*, MouseEvent& event)
90{
91 if (m_drawing_button == GUI::MouseButton::None)
92 return;
93
94 m_draw_mode = event.layer_event().alt() ? DrawMode::FromCenter : DrawMode::FromCorner;
95
96 if (event.layer_event().shift())
97 m_ellipse_end_position = m_ellipse_start_position.end_point_for_aspect_ratio(event.layer_event().position(), 1.0);
98 else if (m_aspect_ratio.has_value())
99 m_ellipse_end_position = m_ellipse_start_position.end_point_for_aspect_ratio(event.layer_event().position(), m_aspect_ratio.value());
100 else
101 m_ellipse_end_position = event.layer_event().position();
102
103 m_editor->update();
104}
105
106void EllipseTool::on_second_paint(Layer const* layer, GUI::PaintEvent& event)
107{
108 if (!layer || m_drawing_button == GUI::MouseButton::None)
109 return;
110
111 GUI::Painter painter(*m_editor);
112 painter.add_clip_rect(event.rect());
113 painter.translate(editor_layer_location(*layer));
114 auto preview_start = m_editor->content_to_frame_position(m_ellipse_start_position).to_type<int>();
115 auto preview_end = m_editor->content_to_frame_position(m_ellipse_end_position).to_type<int>();
116 draw_using(painter, preview_start, preview_end, AK::max(m_thickness * m_editor->scale(), 1));
117}
118
119bool EllipseTool::on_keydown(GUI::KeyEvent& event)
120{
121 if (event.key() == Key_Escape && m_drawing_button != GUI::MouseButton::None) {
122 m_drawing_button = GUI::MouseButton::None;
123 m_editor->update();
124 return true;
125 }
126 return Tool::on_keydown(event);
127}
128
129ErrorOr<GUI::Widget*> EllipseTool::get_properties_widget()
130{
131 if (!m_properties_widget) {
132 auto properties_widget = TRY(GUI::Widget::try_create());
133 (void)TRY(properties_widget->try_set_layout<GUI::VerticalBoxLayout>());
134
135 auto thickness_container = TRY(properties_widget->try_add<GUI::Widget>());
136 thickness_container->set_fixed_height(20);
137 (void)TRY(thickness_container->try_set_layout<GUI::HorizontalBoxLayout>());
138
139 auto thickness_label = TRY(thickness_container->try_add<GUI::Label>("Thickness:"));
140 thickness_label->set_text_alignment(Gfx::TextAlignment::CenterLeft);
141 thickness_label->set_fixed_size(80, 20);
142
143 auto thickness_slider = TRY(thickness_container->try_add<GUI::ValueSlider>(Orientation::Horizontal, "px"_short_string));
144 thickness_slider->set_range(1, 10);
145 thickness_slider->set_value(m_thickness);
146
147 thickness_slider->on_change = [this](int value) {
148 m_thickness = value;
149 };
150 set_primary_slider(thickness_slider);
151
152 auto mode_container = TRY(properties_widget->try_add<GUI::Widget>());
153 mode_container->set_fixed_height(70);
154 (void)TRY(mode_container->try_set_layout<GUI::HorizontalBoxLayout>());
155 auto mode_label = TRY(mode_container->try_add<GUI::Label>("Mode:"));
156 mode_label->set_text_alignment(Gfx::TextAlignment::CenterLeft);
157
158 auto mode_radio_container = TRY(mode_container->try_add<GUI::Widget>());
159 (void)TRY(mode_radio_container->try_set_layout<GUI::VerticalBoxLayout>());
160 auto outline_mode_radio = TRY(mode_radio_container->try_add<GUI::RadioButton>("Outline"_short_string));
161 auto fill_mode_radio = TRY(mode_radio_container->try_add<GUI::RadioButton>("Fill"_short_string));
162 auto aa_enable_checkbox = TRY(mode_radio_container->try_add<GUI::CheckBox>(TRY("Anti-alias"_string)));
163
164 aa_enable_checkbox->on_checked = [this](bool checked) {
165 m_antialias_enabled = checked;
166 };
167 outline_mode_radio->on_checked = [this](bool checked) {
168 if (checked)
169 m_fill_mode = FillMode::Outline;
170 };
171 fill_mode_radio->on_checked = [this](bool checked) {
172 if (checked)
173 m_fill_mode = FillMode::Fill;
174 };
175
176 aa_enable_checkbox->set_checked(true);
177 outline_mode_radio->set_checked(true);
178
179 auto aspect_container = TRY(properties_widget->try_add<GUI::Widget>());
180 aspect_container->set_fixed_height(20);
181 (void)TRY(aspect_container->try_set_layout<GUI::HorizontalBoxLayout>());
182
183 auto aspect_label = TRY(aspect_container->try_add<GUI::Label>("Aspect Ratio:"));
184 aspect_label->set_text_alignment(Gfx::TextAlignment::CenterLeft);
185 aspect_label->set_fixed_size(80, 20);
186
187 m_aspect_w_textbox = TRY(aspect_container->try_add<GUI::TextBox>());
188 m_aspect_w_textbox->set_fixed_height(20);
189 m_aspect_w_textbox->set_fixed_width(25);
190 m_aspect_w_textbox->on_change = [this] {
191 auto x = m_aspect_w_textbox->text().to_int().value_or(0);
192 auto y = m_aspect_h_textbox->text().to_int().value_or(0);
193 if (x > 0 && y > 0) {
194 m_aspect_ratio = (float)x / (float)y;
195 } else {
196 m_aspect_ratio = {};
197 }
198 };
199
200 auto multiply_label = TRY(aspect_container->try_add<GUI::Label>("x"));
201 multiply_label->set_text_alignment(Gfx::TextAlignment::Center);
202 multiply_label->set_fixed_size(10, 20);
203
204 m_aspect_h_textbox = TRY(aspect_container->try_add<GUI::TextBox>());
205 m_aspect_h_textbox->set_fixed_height(20);
206 m_aspect_h_textbox->set_fixed_width(25);
207 m_aspect_h_textbox->on_change = [this] { m_aspect_w_textbox->on_change(); };
208 m_properties_widget = properties_widget;
209 }
210
211 return m_properties_widget.ptr();
212}
213
214}