瀏覽代碼

Merge pull request #53185 from KoBeWi/viewing_pan

Rémi Verschelde 3 年之前
父節點
當前提交
b5495783b2

+ 11 - 0
doc/classes/GraphEdit.xml

@@ -214,6 +214,9 @@
 		<member name="minimap_size" type="Vector2" setter="set_minimap_size" getter="get_minimap_size" default="Vector2(240, 160)">
 			The size of the minimap rectangle. The map itself is based on the size of the grid area and is scaled to fit this rectangle.
 		</member>
+		<member name="panning_scheme" type="int" setter="set_panning_scheme" getter="get_panning_scheme" enum="GraphEdit.PanningScheme" default="0">
+			Defines the control scheme for panning with mouse wheel.
+		</member>
 		<member name="rect_clip_content" type="bool" setter="set_clip_contents" getter="is_clipping_contents" overrides="Control" default="true" />
 		<member name="right_disconnects" type="bool" setter="set_right_disconnects" getter="is_right_disconnects_enabled" default="false">
 			If [code]true[/code], enables disconnection of existing connections in the GraphEdit by dragging the right end.
@@ -345,6 +348,14 @@
 			</description>
 		</signal>
 	</signals>
+	<constants>
+		<constant name="SCROLL_ZOOMS" value="0" enum="PanningScheme">
+			[kbd]Mouse Wheel[/kbd] will zoom, [kbd]Ctrl + Mouse Wheel[/kbd] will move the view.
+		</constant>
+		<constant name="SCROLL_PANS" value="1" enum="PanningScheme">
+			[kbd]Mouse Wheel[/kbd] will move the view, [kbd]Ctrl + Mouse Wheel[/kbd] will zoom.
+		</constant>
+	</constants>
 	<theme_items>
 		<theme_item name="activity" data_type="color" type="Color" default="Color(1, 1, 1, 1)">
 		</theme_item>

+ 3 - 0
editor/editor_node.cpp

@@ -6038,6 +6038,9 @@ EditorNode::EditorNode() {
 	EDITOR_DEF("interface/inspector/default_color_picker_shape", (int32_t)ColorPicker::SHAPE_VHS_CIRCLE);
 	EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::INT, "interface/inspector/default_color_picker_shape", PROPERTY_HINT_ENUM, "HSV Rectangle,HSV Rectangle Wheel,VHS Circle", PROPERTY_USAGE_DEFAULT));
 	EDITOR_DEF("run/auto_save/save_before_running", true);
+	EDITOR_DEF("interface/editors/sub_editor_panning_scheme", 0);
+	// Should be in sync with ControlScheme in ViewPanner.
+	EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::INT, "interface/editors/sub_editor_panning_scheme", PROPERTY_HINT_ENUM, "Scroll Zooms,Scroll Pans", PROPERTY_USAGE_DEFAULT));
 
 	const Vector<String> textfile_ext = ((String)(EditorSettings::get_singleton()->get("docks/filesystem/textfile_extensions"))).split(",", false);
 	for (const String &E : textfile_ext) {

+ 25 - 43
editor/plugins/tiles/tile_atlas_view.cpp

@@ -37,57 +37,31 @@
 #include "scene/gui/label.h"
 #include "scene/gui/panel.h"
 #include "scene/gui/texture_rect.h"
+#include "scene/gui/view_panner.h"
 
 #include "editor/editor_scale.h"
 #include "editor/editor_settings.h"
 
 void TileAtlasView::gui_input(const Ref<InputEvent> &p_event) {
-	Ref<InputEventMouseButton> mb = p_event;
-	if (mb.is_valid()) {
-		drag_type = DRAG_TYPE_NONE;
-
-		Vector2i scroll_vec = Vector2((mb->get_button_index() == MouseButton::WHEEL_LEFT) - (mb->get_button_index() == MouseButton::WHEEL_RIGHT), (mb->get_button_index() == MouseButton::WHEEL_UP) - (mb->get_button_index() == MouseButton::WHEEL_DOWN));
-		if (scroll_vec != Vector2()) {
-			if (mb->is_ctrl_pressed()) {
-				if (mb->is_shift_pressed()) {
-					panning.x += 32 * mb->get_factor() * scroll_vec.y;
-					panning.y += 32 * mb->get_factor() * scroll_vec.x;
-				} else {
-					panning.y += 32 * mb->get_factor() * scroll_vec.y;
-					panning.x += 32 * mb->get_factor() * scroll_vec.x;
-				}
-
-				emit_signal(SNAME("transform_changed"), zoom_widget->get_zoom(), panning);
-				_update_zoom_and_panning(true);
-				accept_event();
+	if (panner->gui_input(p_event)) {
+		accept_event();
+	}
+}
 
-			} else if (!mb->is_shift_pressed()) {
-				zoom_widget->set_zoom_by_increments(scroll_vec.y * 2);
-				emit_signal(SNAME("transform_changed"), zoom_widget->get_zoom(), panning);
-				_update_zoom_and_panning(true);
-				accept_event();
-			}
-		}
+void TileAtlasView::_scroll_callback(Vector2 p_scroll_vec) {
+	_pan_callback(-p_scroll_vec * 32);
+}
 
-		if (mb->get_button_index() == MouseButton::MIDDLE || mb->get_button_index() == MouseButton::RIGHT) {
-			if (mb->is_pressed()) {
-				drag_type = DRAG_TYPE_PAN;
-			} else {
-				drag_type = DRAG_TYPE_NONE;
-			}
-			accept_event();
-		}
-	}
+void TileAtlasView::_pan_callback(Vector2 p_scroll_vec) {
+	panning += p_scroll_vec;
+	emit_signal(SNAME("transform_changed"), zoom_widget->get_zoom(), panning);
+	_update_zoom_and_panning(true);
+}
 
-	Ref<InputEventMouseMotion> mm = p_event;
-	if (mm.is_valid()) {
-		if (drag_type == DRAG_TYPE_PAN) {
-			panning += mm->get_relative();
-			_update_zoom_and_panning();
-			emit_signal(SNAME("transform_changed"), zoom_widget->get_zoom(), panning);
-			accept_event();
-		}
-	}
+void TileAtlasView::_zoom_callback(Vector2 p_scroll_vec, Vector2 p_origin) {
+	zoom_widget->set_zoom_by_increments(-p_scroll_vec.y * 2);
+	emit_signal(SNAME("transform_changed"), zoom_widget->get_zoom(), panning);
+	_update_zoom_and_panning(true);
 }
 
 Size2i TileAtlasView::_compute_base_tiles_control_size() {
@@ -548,6 +522,11 @@ void TileAtlasView::update() {
 
 void TileAtlasView::_notification(int p_what) {
 	switch (p_what) {
+		case NOTIFICATION_ENTER_TREE:
+		case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED:
+			panner->set_control_scheme((ViewPanner::ControlScheme)EDITOR_GET("interface/editors/sub_editor_panning_scheme").operator int());
+			break;
+
 		case NOTIFICATION_READY:
 			button_center_view->set_icon(get_theme_icon(SNAME("CenterView"), SNAME("EditorIcons")));
 			break;
@@ -561,6 +540,9 @@ void TileAtlasView::_bind_methods() {
 TileAtlasView::TileAtlasView() {
 	set_texture_filter(CanvasItem::TEXTURE_FILTER_NEAREST);
 
+	panner.instantiate();
+	panner->set_callbacks(callable_mp(this, &TileAtlasView::_scroll_callback), callable_mp(this, &TileAtlasView::_pan_callback), callable_mp(this, &TileAtlasView::_zoom_callback));
+
 	Panel *panel = memnew(Panel);
 	panel->set_clip_contents(true);
 	panel->set_mouse_filter(Control::MOUSE_FILTER_IGNORE);

+ 7 - 0
editor/plugins/tiles/tile_atlas_view.h

@@ -41,6 +41,8 @@
 #include "scene/gui/texture_rect.h"
 #include "scene/resources/tile_set.h"
 
+class ViewPanner;
+
 class TileAtlasView : public Control {
 	GDCLASS(TileAtlasView, Control);
 
@@ -64,6 +66,11 @@ private:
 	void _center_view();
 	virtual void gui_input(const Ref<InputEvent> &p_event) override;
 
+	Ref<ViewPanner> panner;
+	void _scroll_callback(Vector2 p_scroll_vec);
+	void _pan_callback(Vector2 p_scroll_vec);
+	void _zoom_callback(Vector2 p_scroll_vec, Vector2 p_origin);
+
 	Map<Vector2, Map<int, Rect2i>> alternative_tiles_rect_cache;
 	void _update_alternative_tiles_rect_cache();
 

+ 4 - 0
editor/plugins/visual_shader_editor_plugin.cpp

@@ -3209,6 +3209,10 @@ void VisualShaderEditor::_notification(int p_what) {
 		}
 	}
 
+	if (p_what == NOTIFICATION_ENTER_TREE || p_what == EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED) {
+		graph->set_panning_scheme((GraphEdit::PanningScheme)EDITOR_GET("interface/editors/sub_editor_panning_scheme").operator int());
+	}
+
 	if (p_what == NOTIFICATION_DRAG_BEGIN) {
 		Dictionary dd = get_viewport()->gui_get_drag_data();
 		if (members->is_visible_in_tree() && dd.has("id")) {

+ 5 - 0
modules/visual_script/editor/visual_script_editor.cpp

@@ -3753,6 +3753,11 @@ void VisualScriptEditor::_toggle_scripts_pressed() {
 
 void VisualScriptEditor::_notification(int p_what) {
 	switch (p_what) {
+		case NOTIFICATION_ENTER_TREE:
+		case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: {
+			graph->set_panning_scheme((GraphEdit::PanningScheme)EDITOR_GET("interface/editors/sub_editor_panning_scheme").operator int());
+		} break;
+
 		case NOTIFICATION_READY: {
 			variable_editor->connect("changed", callable_mp(this, &VisualScriptEditor::_update_members));
 			variable_editor->connect("changed", callable_mp(this, &VisualScriptEditor::_update_graph), varray(-1), CONNECT_DEFERRED);

+ 39 - 21
scene/gui/graph_edit.cpp

@@ -35,6 +35,7 @@
 #include "core/os/keyboard.h"
 #include "scene/gui/box_container.h"
 #include "scene/gui/button.h"
+#include "scene/gui/view_panner.h"
 
 constexpr int MINIMAP_OFFSET = 12;
 constexpr int MINIMAP_PADDING = 5;
@@ -1069,13 +1070,9 @@ void GraphEdit::set_selected(Node *p_child) {
 
 void GraphEdit::gui_input(const Ref<InputEvent> &p_ev) {
 	ERR_FAIL_COND(p_ev.is_null());
+	panner->gui_input(p_ev);
 
 	Ref<InputEventMouseMotion> mm = p_ev;
-	if (mm.is_valid() && ((mm->get_button_mask() & MouseButton::MASK_MIDDLE) != MouseButton::NONE || ((mm->get_button_mask() & MouseButton::MASK_LEFT) != MouseButton::NONE && Input::get_singleton()->is_key_pressed(Key::SPACE)))) {
-		Vector2i relative = Input::get_singleton()->warp_mouse_motion(mm, get_global_rect());
-		h_scroll->set_value(h_scroll->get_value() - relative.x);
-		v_scroll->set_value(v_scroll->get_value() - relative.y);
-	}
 
 	if (mm.is_valid() && dragging) {
 		if (!moving_selection) {
@@ -1327,22 +1324,6 @@ void GraphEdit::gui_input(const Ref<InputEvent> &p_ev) {
 			top_layer->update();
 			minimap->update();
 		}
-
-		int scroll_direction = (b->get_button_index() == MouseButton::WHEEL_DOWN) - (b->get_button_index() == MouseButton::WHEEL_UP);
-		if (scroll_direction != 0) {
-			if (b->is_ctrl_pressed()) {
-				if (b->is_shift_pressed()) {
-					// Horizontal scrolling.
-					h_scroll->set_value(h_scroll->get_value() + (h_scroll->get_page() * b->get_factor() / 8) * scroll_direction);
-				} else {
-					// Vertical scrolling.
-					v_scroll->set_value(v_scroll->get_value() + (v_scroll->get_page() * b->get_factor() / 8) * scroll_direction);
-				}
-			} else {
-				// Zooming.
-				set_zoom_custom(scroll_direction < 0 ? zoom * zoom_step : zoom / zoom_step, b->get_position());
-			}
-		}
 	}
 
 	if (p_ev->is_pressed()) {
@@ -1373,6 +1354,23 @@ void GraphEdit::gui_input(const Ref<InputEvent> &p_ev) {
 	}
 }
 
+void GraphEdit::_scroll_callback(Vector2 p_scroll_vec) {
+	if (p_scroll_vec.x != 0) {
+		h_scroll->set_value(h_scroll->get_value() + (h_scroll->get_page() * Math::abs(p_scroll_vec.x) / 8) * SIGN(p_scroll_vec.x));
+	} else {
+		v_scroll->set_value(v_scroll->get_value() + (v_scroll->get_page() * Math::abs(p_scroll_vec.y) / 8) * SIGN(p_scroll_vec.y));
+	}
+}
+
+void GraphEdit::_pan_callback(Vector2 p_scroll_vec) {
+	h_scroll->set_value(h_scroll->get_value() - p_scroll_vec.x);
+	v_scroll->set_value(v_scroll->get_value() - p_scroll_vec.y);
+}
+
+void GraphEdit::_zoom_callback(Vector2 p_scroll_vec, Vector2 p_origin) {
+	set_zoom_custom(p_scroll_vec.y < 0 ? zoom * zoom_step : zoom / zoom_step, p_origin);
+}
+
 void GraphEdit::set_connection_activity(const StringName &p_from, int p_from_port, const StringName &p_to, int p_to_port, float p_activity) {
 	for (Connection &E : connections) {
 		if (E.from == p_from && E.from_port == p_from_port && E.to == p_to && E.to_port == p_to_port) {
@@ -1406,6 +1404,15 @@ void GraphEdit::force_connection_drag_end() {
 	emit_signal(SNAME("connection_drag_ended"));
 }
 
+void GraphEdit::set_panning_scheme(PanningScheme p_scheme) {
+	panning_scheme = p_scheme;
+	panner->set_control_scheme((ViewPanner::ControlScheme)p_scheme);
+}
+
+GraphEdit::PanningScheme GraphEdit::get_panning_scheme() const {
+	return panning_scheme;
+}
+
 void GraphEdit::set_zoom(float p_zoom) {
 	set_zoom_custom(p_zoom, get_size() / 2);
 }
@@ -2190,6 +2197,9 @@ void GraphEdit::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("is_valid_connection_type", "from_type", "to_type"), &GraphEdit::is_valid_connection_type);
 	ClassDB::bind_method(D_METHOD("get_connection_line", "from", "to"), &GraphEdit::get_connection_line);
 
+	ClassDB::bind_method(D_METHOD("set_panning_scheme", "scheme"), &GraphEdit::set_panning_scheme);
+	ClassDB::bind_method(D_METHOD("get_panning_scheme"), &GraphEdit::get_panning_scheme);
+
 	ClassDB::bind_method(D_METHOD("set_zoom", "zoom"), &GraphEdit::set_zoom);
 	ClassDB::bind_method(D_METHOD("get_zoom"), &GraphEdit::get_zoom);
 
@@ -2244,6 +2254,7 @@ void GraphEdit::_bind_methods() {
 	ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "scroll_offset"), "set_scroll_ofs", "get_scroll_ofs");
 	ADD_PROPERTY(PropertyInfo(Variant::INT, "snap_distance"), "set_snap", "get_snap");
 	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "use_snap"), "set_use_snap", "is_using_snap");
+	ADD_PROPERTY(PropertyInfo(Variant::INT, "panning_scheme", PROPERTY_HINT_ENUM, "Scroll Zooms,Scroll Pans"), "set_panning_scheme", "get_panning_scheme");
 
 	ADD_GROUP("Connection Lines", "connection_lines");
 	ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "connection_lines_thickness"), "set_connection_lines_thickness", "get_connection_lines_thickness");
@@ -2277,6 +2288,9 @@ void GraphEdit::_bind_methods() {
 	ADD_SIGNAL(MethodInfo("scroll_offset_changed", PropertyInfo(Variant::VECTOR2, "ofs")));
 	ADD_SIGNAL(MethodInfo("connection_drag_started", PropertyInfo(Variant::STRING, "from"), PropertyInfo(Variant::STRING, "slot"), PropertyInfo(Variant::BOOL, "is_output")));
 	ADD_SIGNAL(MethodInfo("connection_drag_ended"));
+
+	BIND_ENUM_CONSTANT(SCROLL_ZOOMS);
+	BIND_ENUM_CONSTANT(SCROLL_PANS);
 }
 
 GraphEdit::GraphEdit() {
@@ -2289,6 +2303,10 @@ GraphEdit::GraphEdit() {
 	// Allow zooming 4 times from the default zoom level.
 	zoom_max = (1 * Math::pow(zoom_step, 4));
 
+	panner.instantiate();
+	panner->set_callbacks(callable_mp(this, &GraphEdit::_scroll_callback), callable_mp(this, &GraphEdit::_pan_callback), callable_mp(this, &GraphEdit::_zoom_callback));
+	panner->set_disable_rmb(true);
+
 	top_layer = memnew(GraphEditFilter(this));
 	add_child(top_layer, false, INTERNAL_MODE_BACK);
 	top_layer->set_mouse_filter(MOUSE_FILTER_PASS);

+ 18 - 0
scene/gui/graph_edit.h

@@ -41,6 +41,7 @@
 #include "scene/gui/texture_rect.h"
 
 class GraphEdit;
+class ViewPanner;
 
 class GraphEditFilter : public Control {
 	GDCLASS(GraphEditFilter, Control);
@@ -103,6 +104,12 @@ public:
 		float activity = 0.0;
 	};
 
+	// Should be in sync with ControlScheme in ViewPanner.
+	enum PanningScheme {
+		SCROLL_ZOOMS,
+		SCROLL_PANS,
+	};
+
 private:
 	Label *zoom_label;
 	Button *zoom_minus;
@@ -122,6 +129,11 @@ private:
 	float port_grab_distance_horizontal = 0.0;
 	float port_grab_distance_vertical;
 
+	Ref<ViewPanner> panner;
+	void _scroll_callback(Vector2 p_scroll_vec);
+	void _pan_callback(Vector2 p_scroll_vec);
+	void _zoom_callback(Vector2 p_scroll_vec, Vector2 p_origin);
+
 	bool connecting = false;
 	String connecting_from;
 	bool connecting_out = false;
@@ -136,6 +148,7 @@ private:
 	bool connecting_valid = false;
 	Vector2 click_pos;
 
+	PanningScheme panning_scheme = SCROLL_ZOOMS;
 	bool dragging = false;
 	bool just_selected = false;
 	bool moving_selection = false;
@@ -277,6 +290,9 @@ public:
 	void remove_valid_connection_type(int p_type, int p_with_type);
 	bool is_valid_connection_type(int p_type, int p_with_type) const;
 
+	void set_panning_scheme(PanningScheme p_scheme);
+	PanningScheme get_panning_scheme() const;
+
 	void set_zoom(float p_zoom);
 	void set_zoom_custom(float p_zoom, const Vector2 &p_center);
 	float get_zoom() const;
@@ -338,4 +354,6 @@ public:
 	GraphEdit();
 };
 
+VARIANT_ENUM_CAST(GraphEdit::PanningScheme);
+
 #endif // GRAPHEdit_H

+ 136 - 0
scene/gui/view_panner.cpp

@@ -0,0 +1,136 @@
+/*************************************************************************/
+/*  view_panner.cpp                                                      */
+/*************************************************************************/
+/*                       This file is part of:                           */
+/*                           GODOT ENGINE                                */
+/*                      https://godotengine.org                          */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur.                 */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md).   */
+/*                                                                       */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the       */
+/* "Software"), to deal in the Software without restriction, including   */
+/* without limitation the rights to use, copy, modify, merge, publish,   */
+/* distribute, sublicense, and/or sell copies of the Software, and to    */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions:                                             */
+/*                                                                       */
+/* The above copyright notice and this permission notice shall be        */
+/* included in all copies or substantial portions of the Software.       */
+/*                                                                       */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY  */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,  */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
+/*************************************************************************/
+
+#include "view_panner.h"
+
+#include "core/input/input.h"
+#include "core/os/keyboard.h"
+
+bool ViewPanner::gui_input(const Ref<InputEvent> &p_event, Rect2 p_canvas_rect) {
+	Ref<InputEventMouseButton> mb = p_event;
+	if (mb.is_valid()) {
+		Vector2i scroll_vec = Vector2((mb->get_button_index() == MouseButton::WHEEL_RIGHT) - (mb->get_button_index() == MouseButton::WHEEL_LEFT), (mb->get_button_index() == MouseButton::WHEEL_DOWN) - (mb->get_button_index() == MouseButton::WHEEL_UP));
+		if (scroll_vec != Vector2()) {
+			if (control_scheme == SCROLL_PANS) {
+				if (mb->is_ctrl_pressed()) {
+					callback_helper(zoom_callback, scroll_vec, mb->get_position());
+				} else {
+					Vector2 panning;
+					if (mb->is_shift_pressed()) {
+						panning.x += mb->get_factor() * scroll_vec.y;
+						panning.y += mb->get_factor() * scroll_vec.x;
+					} else {
+						panning.y += mb->get_factor() * scroll_vec.y;
+						panning.x += mb->get_factor() * scroll_vec.x;
+					}
+					callback_helper(scroll_callback, panning);
+
+					return true;
+				}
+			} else {
+				if (mb->is_ctrl_pressed()) {
+					Vector2 panning;
+					if (mb->is_shift_pressed()) {
+						panning.x += mb->get_factor() * scroll_vec.y;
+						panning.y += mb->get_factor() * scroll_vec.x;
+					} else {
+						panning.y += mb->get_factor() * scroll_vec.y;
+						panning.x += mb->get_factor() * scroll_vec.x;
+					}
+					callback_helper(scroll_callback, panning);
+
+					return true;
+				} else if (!mb->is_shift_pressed()) {
+					callback_helper(zoom_callback, scroll_vec, mb->get_position());
+					return true;
+				}
+			}
+		}
+
+		if (mb->get_button_index() == MouseButton::MIDDLE || (mb->get_button_index() == MouseButton::RIGHT && !disable_rmb) || (mb->get_button_index() == MouseButton::LEFT && (Input::get_singleton()->is_key_pressed(Key::SPACE) || !mb->is_pressed()))) {
+			if (mb->is_pressed()) {
+				is_dragging = true;
+			} else {
+				is_dragging = false;
+			}
+			return true;
+		}
+	}
+
+	Ref<InputEventMouseMotion> mm = p_event;
+	if (mm.is_valid()) {
+		if (is_dragging) {
+			if (p_canvas_rect != Rect2()) {
+				callback_helper(pan_callback, Input::get_singleton()->warp_mouse_motion(mm, p_canvas_rect));
+			} else {
+				callback_helper(pan_callback, mm->get_relative());
+			}
+			return true;
+		}
+	}
+
+	return false;
+}
+
+void ViewPanner::callback_helper(Callable p_callback, Vector2 p_arg1, Vector2 p_arg2) {
+	if (p_callback == zoom_callback) {
+		const Variant **argptr = (const Variant **)alloca(sizeof(Variant *) * 2);
+		Variant var1 = p_arg1;
+		argptr[0] = &var1;
+		Variant var2 = p_arg2;
+		argptr[1] = &var2;
+
+		Variant result;
+		Callable::CallError ce;
+		p_callback.call(argptr, 2, result, ce);
+	} else {
+		const Variant **argptr = (const Variant **)alloca(sizeof(Variant *));
+		Variant var = p_arg1;
+		argptr[0] = &var;
+
+		Variant result;
+		Callable::CallError ce;
+		p_callback.call(argptr, 1, result, ce);
+	}
+}
+
+void ViewPanner::set_callbacks(Callable p_scroll_callback, Callable p_pan_callback, Callable p_zoom_callback) {
+	scroll_callback = p_scroll_callback;
+	pan_callback = p_pan_callback;
+	zoom_callback = p_zoom_callback;
+}
+
+void ViewPanner::set_control_scheme(ControlScheme p_scheme) {
+	control_scheme = p_scheme;
+}
+
+void ViewPanner::set_disable_rmb(bool p_disable) {
+	disable_rmb = p_disable;
+}

+ 63 - 0
scene/gui/view_panner.h

@@ -0,0 +1,63 @@
+/*************************************************************************/
+/*  view_panner.h                                                        */
+/*************************************************************************/
+/*                       This file is part of:                           */
+/*                           GODOT ENGINE                                */
+/*                      https://godotengine.org                          */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur.                 */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md).   */
+/*                                                                       */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the       */
+/* "Software"), to deal in the Software without restriction, including   */
+/* without limitation the rights to use, copy, modify, merge, publish,   */
+/* distribute, sublicense, and/or sell copies of the Software, and to    */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions:                                             */
+/*                                                                       */
+/* The above copyright notice and this permission notice shall be        */
+/* included in all copies or substantial portions of the Software.       */
+/*                                                                       */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY  */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,  */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
+/*************************************************************************/
+
+#ifndef VIEW_PANNER_H
+#define VIEW_PANNER_H
+
+#include "core/object/ref_counted.h"
+
+class InputEvent;
+
+class ViewPanner : public RefCounted {
+	GDCLASS(ViewPanner, RefCounted);
+
+	bool is_dragging = false;
+	bool disable_rmb = false;
+
+	Callable scroll_callback;
+	Callable pan_callback;
+	Callable zoom_callback;
+
+	void callback_helper(Callable p_callback, Vector2 p_arg1, Vector2 p_arg2 = Vector2());
+
+public:
+	enum ControlScheme {
+		SCROLL_ZOOMS,
+		SCROLL_PANS,
+	};
+	ControlScheme control_scheme = SCROLL_ZOOMS;
+
+	void set_callbacks(Callable p_scroll_callback, Callable p_pan_callback, Callable p_zoom_callback);
+	void set_control_scheme(ControlScheme p_scheme);
+	void set_disable_rmb(bool p_disable);
+	bool gui_input(const Ref<InputEvent> &p_ev, Rect2 p_canvas_rect = Rect2());
+};
+
+#endif // VIEW_PANNER_H