1/**************************************************************************/
2/* gradient_texture_2d_editor_plugin.cpp */
3/**************************************************************************/
4/* This file is part of: */
5/* GODOT ENGINE */
6/* https://godotengine.org */
7/**************************************************************************/
8/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
9/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
10/* */
11/* Permission is hereby granted, free of charge, to any person obtaining */
12/* a copy of this software and associated documentation files (the */
13/* "Software"), to deal in the Software without restriction, including */
14/* without limitation the rights to use, copy, modify, merge, publish, */
15/* distribute, sublicense, and/or sell copies of the Software, and to */
16/* permit persons to whom the Software is furnished to do so, subject to */
17/* the following conditions: */
18/* */
19/* The above copyright notice and this permission notice shall be */
20/* included in all copies or substantial portions of the Software. */
21/* */
22/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
23/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
24/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
25/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
26/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
27/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
28/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
29/**************************************************************************/
30
31#include "gradient_texture_2d_editor_plugin.h"
32
33#include "editor/editor_node.h"
34#include "editor/editor_scale.h"
35#include "editor/editor_undo_redo_manager.h"
36#include "editor/gui/editor_spin_slider.h"
37#include "scene/gui/box_container.h"
38#include "scene/gui/button.h"
39#include "scene/gui/flow_container.h"
40#include "scene/gui/separator.h"
41#include "scene/resources/gradient_texture.h"
42
43Point2 GradientTexture2DEdit::_get_handle_pos(const Handle p_handle) {
44 // Get the handle's mouse position in pixels relative to offset.
45 return (p_handle == HANDLE_FROM ? texture->get_fill_from() : texture->get_fill_to()).clamp(Vector2(), Vector2(1, 1)) * size;
46}
47
48GradientTexture2DEdit::Handle GradientTexture2DEdit::get_handle_at(const Vector2 &p_pos) {
49 Point2 from_pos = _get_handle_pos(HANDLE_FROM);
50 Point2 to_pos = _get_handle_pos(HANDLE_TO);
51 // If both handles are at the position, grab the one that's closer.
52 if (p_pos.distance_squared_to(from_pos) < p_pos.distance_squared_to(to_pos)) {
53 return Rect2(from_pos.round() - handle_size / 2, handle_size).has_point(p_pos) ? HANDLE_FROM : HANDLE_NONE;
54 } else {
55 return Rect2(to_pos.round() - handle_size / 2, handle_size).has_point(p_pos) ? HANDLE_TO : HANDLE_NONE;
56 }
57}
58
59void GradientTexture2DEdit::set_fill_pos(const Vector2 &p_pos) {
60 if (p_pos.is_equal_approx(initial_grab_pos)) {
61 return;
62 }
63
64 const StringName property_name = (grabbed == HANDLE_FROM) ? "fill_from" : "fill_to";
65 EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
66 undo_redo->create_action(TTR("Move GradientTexture2D Fill Point"));
67 undo_redo->add_do_property(texture.ptr(), property_name, p_pos);
68 undo_redo->add_undo_property(texture.ptr(), property_name, initial_grab_pos);
69 undo_redo->commit_action();
70}
71
72void GradientTexture2DEdit::gui_input(const Ref<InputEvent> &p_event) {
73 const Ref<InputEventMouseButton> mb = p_event;
74 if (mb.is_valid()) {
75 if (mb->get_button_index() == MouseButton::LEFT) {
76 if (mb->is_pressed()) {
77 grabbed = get_handle_at(mb->get_position() - offset);
78
79 if (grabbed != HANDLE_NONE) {
80 initial_grab_pos = _get_handle_pos(grabbed) / size;
81 queue_redraw();
82 }
83 } else {
84 // Release the handle.
85 if (grabbed != HANDLE_NONE) {
86 set_fill_pos(_get_handle_pos(grabbed) / size);
87 grabbed = HANDLE_NONE;
88 queue_redraw();
89 }
90 }
91 }
92
93 if (grabbed != HANDLE_NONE && mb->is_pressed() && mb->get_button_index() == MouseButton::RIGHT) {
94 texture->set((grabbed == HANDLE_FROM) ? SNAME("fill_from") : SNAME("fill_to"), initial_grab_pos);
95 grabbed = HANDLE_NONE;
96 queue_redraw();
97 }
98 }
99
100 // Move handle.
101 const Ref<InputEventMouseMotion> mm = p_event;
102 if (mm.is_valid()) {
103 Vector2 mpos = mm->get_position() - offset;
104
105 Handle handle_at_mpos = get_handle_at(mpos);
106 if (hovered != handle_at_mpos) {
107 hovered = handle_at_mpos;
108 queue_redraw();
109 }
110
111 if (grabbed == HANDLE_NONE) {
112 return;
113 }
114
115 Vector2 new_pos = (mpos / size).clamp(Vector2(0, 0), Vector2(1, 1));
116 if (snap_enabled || mm->is_ctrl_pressed()) {
117 new_pos = new_pos.snapped(Vector2(1.0 / snap_count, 1.0 / snap_count));
118 }
119
120 // Allow to snap to an axis with Shift.
121 if (mm->is_shift_pressed()) {
122 Vector2 initial_mpos = initial_grab_pos * size;
123 if (Math::abs(mpos.x - initial_mpos.x) > Math::abs(mpos.y - initial_mpos.y)) {
124 new_pos.y = initial_grab_pos.y;
125 } else {
126 new_pos.x = initial_grab_pos.x;
127 }
128 }
129 // Do it directly from the texture so there's no undo/redo until the handle is released.
130 texture->set((grabbed == HANDLE_FROM) ? SNAME("fill_from") : SNAME("fill_to"), new_pos);
131 }
132}
133
134void GradientTexture2DEdit::set_texture(Ref<GradientTexture2D> &p_texture) {
135 texture = p_texture;
136 texture->connect_changed(callable_mp((CanvasItem *)this, &CanvasItem::queue_redraw));
137}
138
139void GradientTexture2DEdit::set_snap_enabled(bool p_snap_enabled) {
140 snap_enabled = p_snap_enabled;
141 queue_redraw();
142 if (texture.is_valid()) {
143 if (snap_enabled) {
144 texture->set_meta(SNAME("_snap_enabled"), true);
145 } else {
146 texture->remove_meta(SNAME("_snap_enabled"));
147 }
148 }
149}
150
151void GradientTexture2DEdit::set_snap_count(int p_snap_count) {
152 snap_count = p_snap_count;
153 queue_redraw();
154 if (texture.is_valid()) {
155 if (snap_count != GradientTexture2DEditor::DEFAULT_SNAP) {
156 texture->set_meta(SNAME("_snap_count"), snap_count);
157 } else {
158 texture->remove_meta(SNAME("_snap_count"));
159 }
160 }
161}
162
163void GradientTexture2DEdit::_notification(int p_what) {
164 switch (p_what) {
165 case NOTIFICATION_MOUSE_EXIT: {
166 if (hovered != HANDLE_NONE) {
167 hovered = HANDLE_NONE;
168 queue_redraw();
169 }
170 } break;
171 case NOTIFICATION_THEME_CHANGED: {
172 checkerboard->set_texture(get_editor_theme_icon(SNAME("GuiMiniCheckerboard")));
173 } break;
174 case NOTIFICATION_DRAW: {
175 _draw();
176 } break;
177 }
178}
179
180void GradientTexture2DEdit::_draw() {
181 if (texture.is_null()) {
182 return;
183 }
184
185 const Ref<Texture2D> fill_from_icon = get_editor_theme_icon(SNAME("EditorPathSmoothHandle"));
186 const Ref<Texture2D> fill_to_icon = get_editor_theme_icon(SNAME("EditorPathSharpHandle"));
187 handle_size = fill_from_icon->get_size();
188
189 Size2 rect_size = get_size();
190
191 // Get the size and position to draw the texture and handles at.
192 // Subtract handle sizes so they stay inside the preview, but keep the texture's aspect ratio.
193 Size2 available_size = rect_size - handle_size;
194 Size2 ratio = available_size / texture->get_size();
195 size = MIN(ratio.x, ratio.y) * texture->get_size();
196 offset = ((rect_size - size) / 2).round();
197
198 checkerboard->set_rect(Rect2(offset, size));
199
200 draw_set_transform(offset);
201 draw_texture_rect(texture, Rect2(Point2(), size));
202
203 // Draw grid snap lines.
204 if (snap_enabled || (Input::get_singleton()->is_key_pressed(Key::CTRL) && grabbed != HANDLE_NONE)) {
205 const Color line_color = Color(0.5, 0.5, 0.5, 0.5);
206
207 for (int idx = 0; idx < snap_count + 1; idx++) {
208 float x = float(idx * size.width) / snap_count;
209 float y = float(idx * size.height) / snap_count;
210 draw_line(Point2(x, 0), Point2(x, size.height), line_color);
211 draw_line(Point2(0, y), Point2(size.width, y), line_color);
212 }
213 }
214
215 // Draw handles.
216 const Color focus_modulate = Color(0.5, 1, 2);
217 bool modulate_handle_from = grabbed == HANDLE_FROM || hovered == HANDLE_FROM;
218 bool modulate_handle_to = grabbed == HANDLE_TO || hovered == HANDLE_TO;
219 draw_texture(fill_from_icon, (_get_handle_pos(HANDLE_FROM) - handle_size / 2).round(), modulate_handle_from ? focus_modulate : Color(1, 1, 1));
220 draw_texture(fill_to_icon, (_get_handle_pos(HANDLE_TO) - handle_size / 2).round(), modulate_handle_to ? focus_modulate : Color(1, 1, 1));
221}
222
223GradientTexture2DEdit::GradientTexture2DEdit() {
224 checkerboard = memnew(TextureRect);
225 checkerboard->set_stretch_mode(TextureRect::STRETCH_TILE);
226 checkerboard->set_expand_mode(TextureRect::EXPAND_IGNORE_SIZE);
227 checkerboard->set_draw_behind_parent(true);
228 add_child(checkerboard, false, INTERNAL_MODE_FRONT);
229
230 set_custom_minimum_size(Size2(0, 250 * EDSCALE));
231}
232
233///////////////////////
234
235const int GradientTexture2DEditor::DEFAULT_SNAP = 10;
236
237void GradientTexture2DEditor::_reverse_button_pressed() {
238 EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
239 undo_redo->create_action(TTR("Swap GradientTexture2D Fill Points"));
240 undo_redo->add_do_property(texture.ptr(), "fill_from", texture->get_fill_to());
241 undo_redo->add_do_property(texture.ptr(), "fill_to", texture->get_fill_from());
242 undo_redo->add_undo_property(texture.ptr(), "fill_from", texture->get_fill_from());
243 undo_redo->add_undo_property(texture.ptr(), "fill_to", texture->get_fill_to());
244 undo_redo->commit_action();
245}
246
247void GradientTexture2DEditor::_set_snap_enabled(bool p_enabled) {
248 texture_editor_rect->set_snap_enabled(p_enabled);
249 snap_count_edit->set_visible(p_enabled);
250}
251
252void GradientTexture2DEditor::_set_snap_count(int p_snap_count) {
253 texture_editor_rect->set_snap_count(p_snap_count);
254}
255
256void GradientTexture2DEditor::set_texture(Ref<GradientTexture2D> &p_texture) {
257 texture = p_texture;
258 texture_editor_rect->set_texture(p_texture);
259}
260
261void GradientTexture2DEditor::_notification(int p_what) {
262 switch (p_what) {
263 case NOTIFICATION_ENTER_TREE:
264 case NOTIFICATION_THEME_CHANGED: {
265 reverse_button->set_icon(get_editor_theme_icon(SNAME("ReverseGradient")));
266 snap_button->set_icon(get_editor_theme_icon(SNAME("SnapGrid")));
267 } break;
268 case NOTIFICATION_READY: {
269 if (texture.is_valid()) {
270 // Set snapping settings based on the texture's meta.
271 snap_button->set_pressed(texture->get_meta("_snap_enabled", false));
272 snap_count_edit->set_value(texture->get_meta("_snap_count", DEFAULT_SNAP));
273 }
274 } break;
275 }
276}
277
278GradientTexture2DEditor::GradientTexture2DEditor() {
279 HFlowContainer *toolbar = memnew(HFlowContainer);
280 add_child(toolbar);
281
282 reverse_button = memnew(Button);
283 reverse_button->set_tooltip_text(TTR("Swap Gradient Fill Points"));
284 toolbar->add_child(reverse_button);
285 reverse_button->connect("pressed", callable_mp(this, &GradientTexture2DEditor::_reverse_button_pressed));
286
287 toolbar->add_child(memnew(VSeparator));
288
289 snap_button = memnew(Button);
290 snap_button->set_tooltip_text(TTR("Toggle Grid Snap"));
291 snap_button->set_toggle_mode(true);
292 toolbar->add_child(snap_button);
293 snap_button->connect("toggled", callable_mp(this, &GradientTexture2DEditor::_set_snap_enabled));
294
295 snap_count_edit = memnew(EditorSpinSlider);
296 snap_count_edit->set_min(2);
297 snap_count_edit->set_max(100);
298 snap_count_edit->set_value(DEFAULT_SNAP);
299 snap_count_edit->set_custom_minimum_size(Size2(65 * EDSCALE, 0));
300 toolbar->add_child(snap_count_edit);
301 snap_count_edit->connect("value_changed", callable_mp(this, &GradientTexture2DEditor::_set_snap_count));
302
303 texture_editor_rect = memnew(GradientTexture2DEdit);
304 add_child(texture_editor_rect);
305
306 set_mouse_filter(MOUSE_FILTER_STOP);
307 _set_snap_enabled(snap_button->is_pressed());
308 _set_snap_count(snap_count_edit->get_value());
309}
310
311///////////////////////
312
313bool EditorInspectorPluginGradientTexture2D::can_handle(Object *p_object) {
314 return Object::cast_to<GradientTexture2D>(p_object) != nullptr;
315}
316
317void EditorInspectorPluginGradientTexture2D::parse_begin(Object *p_object) {
318 GradientTexture2D *texture = Object::cast_to<GradientTexture2D>(p_object);
319 if (!texture) {
320 return;
321 }
322 Ref<GradientTexture2D> t(texture);
323
324 GradientTexture2DEditor *editor = memnew(GradientTexture2DEditor);
325 editor->set_texture(t);
326 add_custom_control(editor);
327}
328
329///////////////////////
330
331GradientTexture2DEditorPlugin::GradientTexture2DEditorPlugin() {
332 Ref<EditorInspectorPluginGradientTexture2D> plugin;
333 plugin.instantiate();
334 add_inspector_plugin(plugin);
335}
336