summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--core/input/input.cpp4
-rw-r--r--core/math/geometry_2d.h26
-rw-r--r--doc/classes/GraphEdit.xml39
-rw-r--r--editor/editor_node.cpp3
-rw-r--r--editor/themes/editor_theme_manager.cpp11
-rw-r--r--misc/extension_api_validation/4.2-stable.expected7
-rw-r--r--modules/multiplayer/scene_cache_interface.cpp164
-rw-r--r--modules/multiplayer/scene_cache_interface.h27
-rw-r--r--modules/openxr/editor/openxr_editor_plugin.cpp5
-rw-r--r--modules/openxr/editor/openxr_editor_plugin.h4
-rw-r--r--modules/openxr/editor/openxr_select_runtime.cpp132
-rw-r--r--modules/openxr/editor/openxr_select_runtime.h51
-rw-r--r--platform/android/export/export.cpp2
-rw-r--r--platform/android/export/export_plugin.cpp54
-rw-r--r--platform/android/export/export_plugin.h2
-rw-r--r--platform/android/java/app/build.gradle9
-rw-r--r--platform/android/java/app/config.gradle2
-rw-r--r--scene/gui/button.cpp481
-rw-r--r--scene/gui/button.h4
-rw-r--r--scene/gui/graph_edit.compat.inc5
-rw-r--r--scene/gui/graph_edit.cpp710
-rw-r--r--scene/gui/graph_edit.h82
-rw-r--r--scene/gui/graph_edit_arranger.cpp64
-rw-r--r--scene/gui/option_button.cpp4
-rw-r--r--scene/register_scene_types.cpp5
-rw-r--r--scene/theme/default_theme.cpp19
26 files changed, 1352 insertions, 564 deletions
diff --git a/core/input/input.cpp b/core/input/input.cpp
index 2ba4b1d1e8..4e33d3087d 100644
--- a/core/input/input.cpp
+++ b/core/input/input.cpp
@@ -865,6 +865,8 @@ Point2i Input::warp_mouse_motion(const Ref<InputEventMouseMotion> &p_motion, con
}
void Input::action_press(const StringName &p_action, float p_strength) {
+ ERR_FAIL_COND_MSG(!InputMap::get_singleton()->has_action(p_action), InputMap::get_singleton()->suggest_actions(p_action));
+
// Create or retrieve existing action.
ActionState &action_state = action_states[p_action];
@@ -879,6 +881,8 @@ void Input::action_press(const StringName &p_action, float p_strength) {
}
void Input::action_release(const StringName &p_action) {
+ ERR_FAIL_COND_MSG(!InputMap::get_singleton()->has_action(p_action), InputMap::get_singleton()->suggest_actions(p_action));
+
// Create or retrieve existing action.
ActionState &action_state = action_states[p_action];
action_state.cache.pressed = 0;
diff --git a/core/math/geometry_2d.h b/core/math/geometry_2d.h
index b37fce9e9c..9907d579a5 100644
--- a/core/math/geometry_2d.h
+++ b/core/math/geometry_2d.h
@@ -119,6 +119,10 @@ public:
}
}
+ static real_t get_distance_to_segment(const Vector2 &p_point, const Vector2 *p_segment) {
+ return p_point.distance_to(get_closest_point_to_segment(p_point, p_segment));
+ }
+
static bool is_point_in_triangle(const Vector2 &s, const Vector2 &a, const Vector2 &b, const Vector2 &c) {
Vector2 an = a - s;
Vector2 bn = b - s;
@@ -249,6 +253,28 @@ public:
return -1;
}
+ static bool segment_intersects_rect(const Vector2 &p_from, const Vector2 &p_to, const Rect2 &p_rect) {
+ if (p_rect.has_point(p_from) || p_rect.has_point(p_to)) {
+ return true;
+ }
+
+ const Vector2 rect_points[4] = {
+ p_rect.position,
+ p_rect.position + Vector2(p_rect.size.x, 0),
+ p_rect.position + p_rect.size,
+ p_rect.position + Vector2(0, p_rect.size.y)
+ };
+
+ // Check if any of the rect's edges intersect the segment.
+ for (int i = 0; i < 4; i++) {
+ if (segment_intersects_segment(p_from, p_to, rect_points[i], rect_points[(i + 1) % 4], nullptr)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
enum PolyBooleanOperation {
OPERATION_UNION,
OPERATION_DIFFERENCE,
diff --git a/doc/classes/GraphEdit.xml b/doc/classes/GraphEdit.xml
index 95e760be9f..e5952d9f71 100644
--- a/doc/classes/GraphEdit.xml
+++ b/doc/classes/GraphEdit.xml
@@ -143,7 +143,22 @@
[b]Note:[/b] This method suppresses any other connection request signals apart from [signal connection_drag_ended].
</description>
</method>
- <method name="get_connection_line">
+ <method name="get_closest_connection_at_point" qualifiers="const">
+ <return type="Dictionary" />
+ <param index="0" name="point" type="Vector2" />
+ <param index="1" name="max_distance" type="float" default="4.0" />
+ <description>
+ Returns the closest connection to the given point in screen space. If no connection is found within [param max_distance] pixels, an empty [Dictionary] is returned.
+ A connection consists in a structure of the form [code]{ from_port: 0, from_node: "GraphNode name 0", to_port: 1, to_node: "GraphNode name 1" }[/code].
+ For example, getting a connection at a given mouse position can be achieved like this:
+ [codeblocks]
+ [gdscript]
+ var connection = get_closest_connection_at_point(mouse_event.get_position())
+ [/gdscript]
+ [/codeblocks]
+ </description>
+ </method>
+ <method name="get_connection_line" qualifiers="const">
<return type="PackedVector2Array" />
<param index="0" name="from_node" type="Vector2" />
<param index="1" name="to_node" type="Vector2" />
@@ -154,7 +169,14 @@
<method name="get_connection_list" qualifiers="const">
<return type="Dictionary[]" />
<description>
- Returns an Array containing the list of connections. A connection consists in a structure of the form [code]{ from_port: 0, from_node: "GraphNode name 0", to_port: 1, to_node: "GraphNode name 1" }[/code].
+ Returns an [Array] containing the list of connections. A connection consists in a structure of the form [code]{ from_port: 0, from_node: "GraphNode name 0", to_port: 1, to_node: "GraphNode name 1" }[/code].
+ </description>
+ </method>
+ <method name="get_connections_intersecting_with_rect" qualifiers="const">
+ <return type="Dictionary[]" />
+ <param index="0" name="rect" type="Rect2" />
+ <description>
+ Returns an [Array] containing the list of connections that intersect with the given [Rect2]. A connection consists in a structure of the form [code]{ from_port: 0, from_node: "GraphNode name 0", to_port: 1, to_node: "GraphNode name 1" }[/code].
</description>
</method>
<method name="get_menu_hbox">
@@ -233,7 +255,7 @@
<member name="connection_lines_curvature" type="float" setter="set_connection_lines_curvature" getter="get_connection_lines_curvature" default="0.5">
The curvature of the lines between the nodes. 0 results in straight lines.
</member>
- <member name="connection_lines_thickness" type="float" setter="set_connection_lines_thickness" getter="get_connection_lines_thickness" default="2.0">
+ <member name="connection_lines_thickness" type="float" setter="set_connection_lines_thickness" getter="get_connection_lines_thickness" default="4.0">
The thickness of the lines between the nodes.
</member>
<member name="focus_mode" type="int" setter="set_focus_mode" getter="get_focus_mode" overrides="Control" enum="Control.FocusMode" default="2" />
@@ -417,7 +439,16 @@
</constants>
<theme_items>
<theme_item name="activity" data_type="color" type="Color" default="Color(1, 1, 1, 1)">
- Color of the connection's activity (see [method set_connection_activity]).
+ Color the connection line is interpolated to based on the activity value of a connection (see [method set_connection_activity]).
+ </theme_item>
+ <theme_item name="connection_hover_tint_color" data_type="color" type="Color" default="Color(0, 0, 0, 0.3)">
+ Color which is blended with the connection line when the mouse is hovering over it.
+ </theme_item>
+ <theme_item name="connection_rim_color" data_type="color" type="Color" default="Color(0.1, 0.1, 0.1, 0.6)">
+ Color of the rim around each connection line used for making intersecting lines more distinguishable.
+ </theme_item>
+ <theme_item name="connection_valid_target_tint_color" data_type="color" type="Color" default="Color(1, 1, 1, 0.4)">
+ Color which is blended with the connection line when the currently dragged connection is hovering over a valid target port.
</theme_item>
<theme_item name="grid_major" data_type="color" type="Color" default="Color(1, 1, 1, 0.2)">
Color of major grid lines/dots.
diff --git a/editor/editor_node.cpp b/editor/editor_node.cpp
index 87d5531806..8cffc2b4b0 100644
--- a/editor/editor_node.cpp
+++ b/editor/editor_node.cpp
@@ -6936,10 +6936,9 @@ EditorNode::EditorNode() {
renderer = memnew(OptionButton);
renderer->set_visible(true);
renderer->set_flat(true);
+ renderer->set_theme_type_variation("TopBarOptionButton");
renderer->set_fit_to_longest_item(false);
renderer->set_focus_mode(Control::FOCUS_NONE);
- renderer->add_theme_font_override("font", theme->get_font(SNAME("bold"), EditorStringName(EditorFonts)));
- renderer->add_theme_font_size_override("font_size", theme->get_font_size(SNAME("bold_size"), EditorStringName(EditorFonts)));
renderer->set_tooltip_text(TTR("Choose a rendering method.\n\nNotes:\n- On mobile platforms, the Mobile rendering method is used if Forward+ is selected here.\n- On the web platform, the Compatibility rendering method is always used."));
right_menu_hb->add_child(renderer);
diff --git a/editor/themes/editor_theme_manager.cpp b/editor/themes/editor_theme_manager.cpp
index 5eb287cc43..886f105efc 100644
--- a/editor/themes/editor_theme_manager.cpp
+++ b/editor/themes/editor_theme_manager.cpp
@@ -1442,6 +1442,10 @@ void EditorThemeManager::_populate_standard_styles(const Ref<EditorTheme> &p_the
p_theme->set_color("selection_stroke", "GraphEdit", p_theme->get_color(SNAME("box_selection_stroke_color"), EditorStringName(Editor)));
p_theme->set_color("activity", "GraphEdit", p_config.accent_color);
+ p_theme->set_color("connection_hover_tint_color", "GraphEdit", p_config.dark_theme ? Color(0, 0, 0, 0.3) : Color(1, 1, 1, 0.3));
+ p_theme->set_color("connection_valid_target_tint_color", "GraphEdit", p_config.dark_theme ? Color(1, 1, 1, 0.4) : Color(0, 0, 0, 0.4));
+ p_theme->set_color("connection_rim_color", "GraphEdit", p_config.tree_panel_style->get_bg_color());
+
p_theme->set_icon("zoom_out", "GraphEdit", p_theme->get_icon(SNAME("ZoomLess"), EditorStringName(EditorIcons)));
p_theme->set_icon("zoom_in", "GraphEdit", p_theme->get_icon(SNAME("ZoomMore"), EditorStringName(EditorIcons)));
p_theme->set_icon("zoom_reset", "GraphEdit", p_theme->get_icon(SNAME("ZoomReset"), EditorStringName(EditorIcons)));
@@ -1765,6 +1769,13 @@ void EditorThemeManager::_populate_editor_styles(const Ref<EditorTheme> &p_theme
p_theme->set_stylebox("pressed", "EditorLogFilterButton", editor_log_button_pressed);
}
+ // Top bar selectors.
+ {
+ p_theme->set_type_variation("TopBarOptionButton", "OptionButton");
+ p_theme->set_font("font", "TopBarOptionButton", p_theme->get_font(SNAME("bold"), EditorStringName(EditorFonts)));
+ p_theme->set_font_size("font_size", "TopBarOptionButton", p_theme->get_font_size(SNAME("bold_size"), EditorStringName(EditorFonts)));
+ }
+
// Complex editor windows.
{
Ref<StyleBoxFlat> style_complex_window = p_config.window_style->duplicate();
diff --git a/misc/extension_api_validation/4.2-stable.expected b/misc/extension_api_validation/4.2-stable.expected
index 25094dda77..2c18b43948 100644
--- a/misc/extension_api_validation/4.2-stable.expected
+++ b/misc/extension_api_validation/4.2-stable.expected
@@ -59,3 +59,10 @@ Validate extension JSON: Error: Field 'classes/TileMap/methods/get_collision_vis
Validate extension JSON: Error: Field 'classes/TileMap/methods/get_navigation_visibility_mode': is_const changed value in new API, from false to true.
Two TileMap getters were made const. No adjustments should be necessary.
+
+
+GH-86158
+--------
+Validate extension JSON: Error: Field 'classes/GraphEdit/methods/get_connection_line': is_const changed value in new API, from false to true.
+
+get_connection_line was made const.
diff --git a/modules/multiplayer/scene_cache_interface.cpp b/modules/multiplayer/scene_cache_interface.cpp
index 56cd0bec18..33b05d4cc2 100644
--- a/modules/multiplayer/scene_cache_interface.cpp
+++ b/modules/multiplayer/scene_cache_interface.cpp
@@ -35,25 +35,61 @@
#include "core/io/marshalls.h"
#include "scene/main/node.h"
#include "scene/main/window.h"
+#include "scene/scene_string_names.h"
+
+SceneCacheInterface::NodeCache &SceneCacheInterface::_track(Node *p_node) {
+ const ObjectID oid = p_node->get_instance_id();
+ NodeCache *nc = nodes_cache.getptr(oid);
+ if (!nc) {
+ nodes_cache[oid] = NodeCache();
+ p_node->connect(SceneStringNames::get_singleton()->tree_exited, callable_mp(this, &SceneCacheInterface::_remove_node_cache).bind(oid), Object::CONNECT_ONE_SHOT);
+ }
+ return nodes_cache[oid];
+}
+
+void SceneCacheInterface::_remove_node_cache(ObjectID p_oid) {
+ NodeCache *nc = nodes_cache.getptr(p_oid);
+ if (!nc) {
+ return;
+ }
+ for (KeyValue<int, int> &E : nc->recv_ids) {
+ PeerInfo *pinfo = peers_info.getptr(E.key);
+ ERR_CONTINUE(!pinfo);
+ pinfo->recv_nodes.erase(E.value);
+ }
+ for (KeyValue<int, bool> &E : nc->confirmed_peers) {
+ PeerInfo *pinfo = peers_info.getptr(E.key);
+ ERR_CONTINUE(!pinfo);
+ pinfo->sent_nodes.erase(p_oid);
+ }
+ nodes_cache.erase(p_oid);
+}
void SceneCacheInterface::on_peer_change(int p_id, bool p_connected) {
if (p_connected) {
- path_get_cache.insert(p_id, PathGetCache());
+ peers_info.insert(p_id, PeerInfo());
} else {
- // Cleanup get cache.
- path_get_cache.erase(p_id);
- // Cleanup sent cache.
- // Some refactoring is needed to make this faster and do paths GC.
- for (KeyValue<ObjectID, PathSentCache> &E : path_send_cache) {
- E.value.confirmed_peers.erase(p_id);
+ PeerInfo *pinfo = peers_info.getptr(p_id);
+ ERR_FAIL_NULL(pinfo); // Bug.
+ for (KeyValue<int, ObjectID> E : pinfo->recv_nodes) {
+ NodeCache *nc = nodes_cache.getptr(E.value);
+ ERR_CONTINUE(!nc);
+ nc->recv_ids.erase(E.key);
+ }
+ for (const ObjectID &oid : pinfo->sent_nodes) {
+ NodeCache *nc = nodes_cache.getptr(oid);
+ ERR_CONTINUE(!nc);
+ nc->confirmed_peers.erase(p_id);
}
+ peers_info.erase(p_id);
}
}
void SceneCacheInterface::process_simplify_path(int p_from, const uint8_t *p_packet, int p_packet_len) {
+ ERR_FAIL_COND(!peers_info.has(p_from)); // Bug.
+ ERR_FAIL_COND_MSG(p_packet_len < 38, "Invalid packet received. Size too small.");
Node *root_node = SceneTree::get_singleton()->get_root()->get_node(multiplayer->get_root_path());
ERR_FAIL_NULL(root_node);
- ERR_FAIL_COND_MSG(p_packet_len < 38, "Invalid packet received. Size too small.");
int ofs = 1;
String methods_md5;
@@ -63,15 +99,13 @@ void SceneCacheInterface::process_simplify_path(int p_from, const uint8_t *p_pac
int id = decode_uint32(&p_packet[ofs]);
ofs += 4;
+ ERR_FAIL_COND_MSG(peers_info[p_from].recv_nodes.has(id), vformat("Duplicate remote cache ID %d for peer %d", id, p_from));
+
String paths;
paths.parse_utf8((const char *)(p_packet + ofs), p_packet_len - ofs);
const NodePath path = paths;
- if (!path_get_cache.has(p_from)) {
- path_get_cache[p_from] = PathGetCache();
- }
-
Node *node = root_node->get_node(path);
ERR_FAIL_NULL(node);
const bool valid_rpc_checksum = multiplayer->get_rpc_md5(node) == methods_md5;
@@ -79,10 +113,9 @@ void SceneCacheInterface::process_simplify_path(int p_from, const uint8_t *p_pac
ERR_PRINT("The rpc node checksum failed. Make sure to have the same methods on both nodes. Node path: " + path);
}
- PathGetCache::NodeInfo ni;
- ni.path = node->get_path();
-
- path_get_cache[p_from].nodes[id] = ni;
+ peers_info[p_from].recv_nodes.insert(id, node->get_instance_id());
+ NodeCache &cache = _track(node);
+ cache.recv_ids.insert(p_from, id);
// Encode path to send ack.
CharString pname = String(path).utf8();
@@ -122,15 +155,15 @@ void SceneCacheInterface::process_confirm_path(int p_from, const uint8_t *p_pack
Node *node = root_node->get_node(path);
ERR_FAIL_NULL(node);
- PathSentCache *psc = path_send_cache.getptr(node->get_instance_id());
- ERR_FAIL_NULL_MSG(psc, "Invalid packet received. Tries to confirm a path which was not found in cache.");
+ NodeCache *cache = nodes_cache.getptr(node->get_instance_id());
+ ERR_FAIL_NULL_MSG(cache, "Invalid packet received. Tries to confirm a node which was not requested.");
- HashMap<int, bool>::Iterator E = psc->confirmed_peers.find(p_from);
- ERR_FAIL_COND_MSG(!E, "Invalid packet received. Source peer was not found in cache for the given path.");
- E->value = true;
+ bool *confirmed = cache->confirmed_peers.getptr(p_from);
+ ERR_FAIL_NULL_MSG(confirmed, "Invalid packet received. Tries to confirm a node which was not requested.");
+ *confirmed = true;
}
-Error SceneCacheInterface::_send_confirm_path(Node *p_node, PathSentCache *psc, const List<int> &p_peers) {
+Error SceneCacheInterface::_send_confirm_path(Node *p_node, NodeCache &p_cache, const List<int> &p_peers) {
// Encode function name.
const CharString path = String(multiplayer->get_root_path().rel_path_to(p_node->get_path())).utf8();
const int path_len = encode_cstring(path.get_data(), nullptr);
@@ -148,7 +181,7 @@ Error SceneCacheInterface::_send_confirm_path(Node *p_node, PathSentCache *psc,
ofs += encode_cstring(methods_md5.utf8().get_data(), &packet.write[ofs]);
- ofs += encode_uint32(psc->id, &packet.write[ofs]);
+ ofs += encode_uint32(p_cache.cache_id, &packet.write[ofs]);
ofs += encode_cstring(path.get_data(), &packet.write[ofs]);
@@ -162,80 +195,74 @@ Error SceneCacheInterface::_send_confirm_path(Node *p_node, PathSentCache *psc,
err = multiplayer->send_command(peer_id, packet.ptr(), packet.size());
ERR_FAIL_COND_V(err != OK, err);
// Insert into confirmed, but as false since it was not confirmed.
- psc->confirmed_peers.insert(peer_id, false);
+ p_cache.confirmed_peers.insert(peer_id, false);
+ ERR_CONTINUE(!peers_info.has(peer_id));
+ peers_info[peer_id].sent_nodes.insert(p_node->get_instance_id());
}
return err;
}
bool SceneCacheInterface::is_cache_confirmed(Node *p_node, int p_peer) {
ERR_FAIL_NULL_V(p_node, false);
- const PathSentCache *psc = path_send_cache.getptr(p_node->get_instance_id());
- ERR_FAIL_NULL_V(psc, false);
- HashMap<int, bool>::ConstIterator F = psc->confirmed_peers.find(p_peer);
- ERR_FAIL_COND_V(!F, false); // Should never happen.
- return F->value;
+ const ObjectID oid = p_node->get_instance_id();
+ NodeCache *cache = nodes_cache.getptr(oid);
+ bool *confirmed = cache ? cache->confirmed_peers.getptr(p_peer) : nullptr;
+ return confirmed && *confirmed;
}
int SceneCacheInterface::make_object_cache(Object *p_obj) {
Node *node = Object::cast_to<Node>(p_obj);
ERR_FAIL_NULL_V(node, -1);
- const ObjectID oid = node->get_instance_id();
- // See if the path is cached.
- PathSentCache *psc = path_send_cache.getptr(oid);
- if (!psc) {
- // Path is not cached, create.
- path_send_cache[oid] = PathSentCache();
- psc = path_send_cache.getptr(oid);
- psc->id = last_send_cache_id++;
+ NodeCache &cache = _track(node);
+ if (cache.cache_id == 0) {
+ cache.cache_id = last_send_cache_id++;
}
- return psc->id;
+ return cache.cache_id;
}
bool SceneCacheInterface::send_object_cache(Object *p_obj, int p_peer_id, int &r_id) {
Node *node = Object::cast_to<Node>(p_obj);
ERR_FAIL_NULL_V(node, false);
- const ObjectID oid = node->get_instance_id();
// See if the path is cached.
- PathSentCache *psc = path_send_cache.getptr(oid);
- if (!psc) {
- // Path is not cached, create.
- path_send_cache[oid] = PathSentCache();
- psc = path_send_cache.getptr(oid);
- psc->id = last_send_cache_id++;
+ NodeCache &cache = _track(node);
+ if (cache.cache_id == 0) {
+ cache.cache_id = last_send_cache_id++;
}
- r_id = psc->id;
+ r_id = cache.cache_id;
bool has_all_peers = true;
List<int> peers_to_add; // If one is missing, take note to add it.
if (p_peer_id > 0) {
// Fast single peer check.
- HashMap<int, bool>::Iterator F = psc->confirmed_peers.find(p_peer_id);
- if (!F) {
+ ERR_FAIL_COND_V_MSG(!peers_info.has(p_peer_id), false, "Peer doesn't exist: " + itos(p_peer_id));
+
+ bool *confirmed = cache.confirmed_peers.getptr(p_peer_id);
+ if (!confirmed) {
peers_to_add.push_back(p_peer_id); // Need to also be notified.
has_all_peers = false;
- } else if (!F->value) {
+ } else if (!(*confirmed)) {
has_all_peers = false;
}
} else {
// Long and painful.
- for (const int &E : multiplayer->get_connected_peers()) {
- if (p_peer_id < 0 && E == -p_peer_id) {
+ for (KeyValue<int, PeerInfo> &E : peers_info) {
+ if (p_peer_id < 0 && E.key == -p_peer_id) {
continue; // Continue, excluded.
}
- HashMap<int, bool>::Iterator F = psc->confirmed_peers.find(E);
- if (!F) {
- peers_to_add.push_back(E); // Need to also be notified.
+ bool *confirmed = cache.confirmed_peers.getptr(E.key);
+ if (!confirmed) {
+ peers_to_add.push_back(E.key); // Need to also be notified.
has_all_peers = false;
- } else if (!F->value) {
+ } else if (!(*confirmed)) {
has_all_peers = false;
}
}
}
if (peers_to_add.size()) {
- _send_confirm_path(node, psc, peers_to_add);
+ _send_confirm_path(node, cache, peers_to_add);
}
return has_all_peers;
@@ -244,22 +271,23 @@ bool SceneCacheInterface::send_object_cache(Object *p_obj, int p_peer_id, int &r
Object *SceneCacheInterface::get_cached_object(int p_from, uint32_t p_cache_id) {
Node *root_node = SceneTree::get_singleton()->get_root()->get_node(multiplayer->get_root_path());
ERR_FAIL_NULL_V(root_node, nullptr);
- HashMap<int, PathGetCache>::Iterator E = path_get_cache.find(p_from);
- ERR_FAIL_COND_V_MSG(!E, nullptr, vformat("No cache found for peer %d.", p_from));
-
- HashMap<int, PathGetCache::NodeInfo>::Iterator F = E->value.nodes.find(p_cache_id);
- ERR_FAIL_COND_V_MSG(!F, nullptr, vformat("ID %d not found in cache of peer %d.", p_cache_id, p_from));
+ PeerInfo *pinfo = peers_info.getptr(p_from);
+ ERR_FAIL_NULL_V(pinfo, nullptr);
- PathGetCache::NodeInfo *ni = &F->value;
- Node *node = root_node->get_node(ni->path);
- if (!node) {
- ERR_PRINT("Failed to get cached path: " + String(ni->path) + ".");
- }
+ const ObjectID *oid = pinfo->recv_nodes.getptr(p_cache_id);
+ ERR_FAIL_NULL_V_MSG(oid, nullptr, vformat("ID %d not found in cache of peer %d.", p_cache_id, p_from));
+ Node *node = Object::cast_to<Node>(ObjectDB::get_instance(*oid));
+ ERR_FAIL_NULL_V_MSG(node, nullptr, vformat("Failed to get cached node from peer %d with cache ID %d.", p_from, p_cache_id));
return node;
}
void SceneCacheInterface::clear() {
- path_get_cache.clear();
- path_send_cache.clear();
+ for (KeyValue<ObjectID, NodeCache> &E : nodes_cache) {
+ Object *obj = ObjectDB::get_instance(E.key);
+ ERR_CONTINUE(!obj);
+ obj->disconnect(SceneStringNames::get_singleton()->tree_exited, callable_mp(this, &SceneCacheInterface::_remove_node_cache));
+ }
+ peers_info.clear();
+ nodes_cache.clear();
last_send_cache_id = 1;
}
diff --git a/modules/multiplayer/scene_cache_interface.h b/modules/multiplayer/scene_cache_interface.h
index e63beb5f84..ab4a20c078 100644
--- a/modules/multiplayer/scene_cache_interface.h
+++ b/modules/multiplayer/scene_cache_interface.h
@@ -43,27 +43,26 @@ private:
SceneMultiplayer *multiplayer = nullptr;
//path sent caches
- struct PathSentCache {
- HashMap<int, bool> confirmed_peers;
- int id;
+ struct NodeCache {
+ int cache_id;
+ HashMap<int, int> recv_ids; // peer id, remote cache id
+ HashMap<int, bool> confirmed_peers; // peer id, confirmed
};
- //path get caches
- struct PathGetCache {
- struct NodeInfo {
- NodePath path;
- ObjectID instance;
- };
-
- HashMap<int, NodeInfo> nodes;
+ struct PeerInfo {
+ HashMap<int, ObjectID> recv_nodes; // remote cache id, ObjectID
+ HashSet<ObjectID> sent_nodes;
};
- HashMap<ObjectID, PathSentCache> path_send_cache;
- HashMap<int, PathGetCache> path_get_cache;
+ HashMap<ObjectID, NodeCache> nodes_cache;
+ HashMap<int, PeerInfo> peers_info;
int last_send_cache_id = 1;
+ void _remove_node_cache(ObjectID p_oid);
+ NodeCache &_track(Node *p_node);
+
protected:
- Error _send_confirm_path(Node *p_node, PathSentCache *psc, const List<int> &p_peers);
+ Error _send_confirm_path(Node *p_node, NodeCache &p_cache, const List<int> &p_peers);
public:
void clear();
diff --git a/modules/openxr/editor/openxr_editor_plugin.cpp b/modules/openxr/editor/openxr_editor_plugin.cpp
index 51ebbcf44a..559890ecb3 100644
--- a/modules/openxr/editor/openxr_editor_plugin.cpp
+++ b/modules/openxr/editor/openxr_editor_plugin.cpp
@@ -53,6 +53,11 @@ void OpenXREditorPlugin::make_visible(bool p_visible) {
OpenXREditorPlugin::OpenXREditorPlugin() {
action_map_editor = memnew(OpenXRActionMapEditor);
EditorNode::get_singleton()->add_bottom_panel_item(TTR("OpenXR Action Map"), action_map_editor);
+
+#ifndef ANDROID_ENABLED
+ select_runtime = memnew(OpenXRSelectRuntime);
+ add_control_to_container(CONTAINER_TOOLBAR, select_runtime);
+#endif
}
OpenXREditorPlugin::~OpenXREditorPlugin() {
diff --git a/modules/openxr/editor/openxr_editor_plugin.h b/modules/openxr/editor/openxr_editor_plugin.h
index 9764f8fe21..b80f20d049 100644
--- a/modules/openxr/editor/openxr_editor_plugin.h
+++ b/modules/openxr/editor/openxr_editor_plugin.h
@@ -32,6 +32,7 @@
#define OPENXR_EDITOR_PLUGIN_H
#include "openxr_action_map_editor.h"
+#include "openxr_select_runtime.h"
#include "editor/editor_plugin.h"
@@ -39,6 +40,9 @@ class OpenXREditorPlugin : public EditorPlugin {
GDCLASS(OpenXREditorPlugin, EditorPlugin);
OpenXRActionMapEditor *action_map_editor = nullptr;
+#ifndef ANDROID_ENABLED
+ OpenXRSelectRuntime *select_runtime = nullptr;
+#endif
public:
virtual String get_name() const override { return "OpenXRPlugin"; }
diff --git a/modules/openxr/editor/openxr_select_runtime.cpp b/modules/openxr/editor/openxr_select_runtime.cpp
new file mode 100644
index 0000000000..f6aa157907
--- /dev/null
+++ b/modules/openxr/editor/openxr_select_runtime.cpp
@@ -0,0 +1,132 @@
+/**************************************************************************/
+/* openxr_select_runtime.cpp */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* 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 "openxr_select_runtime.h"
+
+#include "core/io/dir_access.h"
+#include "core/os/os.h"
+#include "editor/editor_settings.h"
+#include "editor/editor_string_names.h"
+
+void OpenXRSelectRuntime::_bind_methods() {
+}
+
+void OpenXRSelectRuntime::_update_items() {
+ Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
+ OS *os = OS::get_singleton();
+ Dictionary runtimes = EDITOR_GET("xr/openxr/runtime_paths");
+
+ int current_runtime = 0;
+ int index = 0;
+ String current_path = os->get_environment("XR_RUNTIME_JSON");
+
+ // Parse the user's home folder.
+ String home_folder = os->get_environment("HOME");
+ if (home_folder.is_empty()) {
+ home_folder = os->get_environment("HOMEDRIVE") + os->get_environment("HOMEPATH");
+ }
+
+ clear();
+ add_item("Default", -1);
+ set_item_metadata(index, "");
+ index++;
+
+ Array keys = runtimes.keys();
+ for (int i = 0; i < keys.size(); i++) {
+ String key = keys[i];
+ String path = runtimes[key];
+ String adj_path = path.replace("~", home_folder);
+
+ if (da->file_exists(adj_path)) {
+ add_item(key, index);
+ set_item_metadata(index, adj_path);
+
+ if (current_path == adj_path) {
+ current_runtime = index;
+ }
+ index++;
+ }
+ }
+
+ select(current_runtime);
+}
+
+void OpenXRSelectRuntime::_item_selected(int p_which) {
+ OS *os = OS::get_singleton();
+
+ if (p_which == 0) {
+ // Return to default runtime
+ os->set_environment("XR_RUNTIME_JSON", "");
+ } else {
+ // Select the runtime we want
+ String runtime_path = get_item_metadata(p_which);
+ os->set_environment("XR_RUNTIME_JSON", runtime_path);
+ }
+}
+
+void OpenXRSelectRuntime::_notification(int p_notification) {
+ switch (p_notification) {
+ case NOTIFICATION_ENTER_TREE: {
+ // Update dropdown
+ _update_items();
+
+ // Connect signal
+ connect("item_selected", callable_mp(this, &OpenXRSelectRuntime::_item_selected));
+ } break;
+ case NOTIFICATION_EXIT_TREE: {
+ // Disconnect signal
+ disconnect("item_selected", callable_mp(this, &OpenXRSelectRuntime::_item_selected));
+ } break;
+ }
+}
+
+OpenXRSelectRuntime::OpenXRSelectRuntime() {
+ Dictionary default_runtimes;
+
+ // Add known common runtimes by default.
+#ifdef WINDOWS_ENABLED
+ default_runtimes["Meta"] = "C:\\Program Files\\Oculus\\Support\\oculus-runtime\\oculus_openxr_64.json";
+ default_runtimes["SteamVR"] = "C:\\Program Files (x86)\\Steam\\steamapps\\common\\SteamVR\\steamxr_win64.json";
+ default_runtimes["Varjo"] = "C:\\Program Files\\Varjo\\varjo-openxr\\VarjoOpenXR.json";
+ default_runtimes["WMR"] = "C:\\WINDOWS\\system32\\MixedRealityRuntime.json";
+#endif
+#ifdef LINUXBSD_ENABLED
+ default_runtimes["Monado"] = "/usr/share/openxr/1/openxr_monado.json";
+ default_runtimes["SteamVR"] = "~/.steam/steam/steamapps/common/SteamVR/steamxr_linux64.json";
+#endif
+
+ EDITOR_DEF_RST("xr/openxr/runtime_paths", default_runtimes);
+
+ set_flat(true);
+ set_theme_type_variation("TopBarOptionButton");
+ set_fit_to_longest_item(false);
+ set_focus_mode(Control::FOCUS_NONE);
+ set_tooltip_text(TTR("Choose an XR runtime."));
+}
diff --git a/modules/openxr/editor/openxr_select_runtime.h b/modules/openxr/editor/openxr_select_runtime.h
new file mode 100644
index 0000000000..60b5137f67
--- /dev/null
+++ b/modules/openxr/editor/openxr_select_runtime.h
@@ -0,0 +1,51 @@
+/**************************************************************************/
+/* openxr_select_runtime.h */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* 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 OPENXR_SELECT_RUNTIME_H
+#define OPENXR_SELECT_RUNTIME_H
+
+#include "scene/gui/option_button.h"
+
+class OpenXRSelectRuntime : public OptionButton {
+ GDCLASS(OpenXRSelectRuntime, OptionButton);
+
+public:
+ OpenXRSelectRuntime();
+
+protected:
+ static void _bind_methods();
+ void _notification(int p_notification);
+
+private:
+ void _update_items();
+ void _item_selected(int p_which);
+};
+
+#endif // OPENXR_SELECT_RUNTIME_H
diff --git a/platform/android/export/export.cpp b/platform/android/export/export.cpp
index e276048d40..138714634f 100644
--- a/platform/android/export/export.cpp
+++ b/platform/android/export/export.cpp
@@ -42,6 +42,8 @@ void register_android_exporter_types() {
void register_android_exporter() {
#ifndef ANDROID_ENABLED
+ EDITOR_DEF("export/android/java_sdk_path", OS::get_singleton()->get_environment("JAVA_HOME"));
+ EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/android/java_sdk_path", PROPERTY_HINT_GLOBAL_DIR));
EDITOR_DEF("export/android/android_sdk_path", OS::get_singleton()->get_environment("ANDROID_HOME"));
EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/android/android_sdk_path", PROPERTY_HINT_GLOBAL_DIR));
EDITOR_DEF("export/android/debug_keystore", "");
diff --git a/platform/android/export/export_plugin.cpp b/platform/android/export/export_plugin.cpp
index 653e7bfe6f..5e42d8a967 100644
--- a/platform/android/export/export_plugin.cpp
+++ b/platform/android/export/export_plugin.cpp
@@ -2115,8 +2115,17 @@ Ref<Texture2D> EditorExportPlatformAndroid::get_run_icon() const {
return run_icon;
}
+String EditorExportPlatformAndroid::get_java_path() {
+ String exe_ext;
+ if (OS::get_singleton()->get_name() == "Windows") {
+ exe_ext = ".exe";
+ }
+ String java_sdk_path = EDITOR_GET("export/android/java_sdk_path");
+ return java_sdk_path.path_join("bin/java" + exe_ext);
+}
+
String EditorExportPlatformAndroid::get_adb_path() {
- String exe_ext = "";
+ String exe_ext;
if (OS::get_singleton()->get_name() == "Windows") {
exe_ext = ".exe";
}
@@ -2128,13 +2137,13 @@ String EditorExportPlatformAndroid::get_apksigner_path(int p_target_sdk, bool p_
if (p_target_sdk == -1) {
p_target_sdk = DEFAULT_TARGET_SDK_VERSION;
}
- String exe_ext = "";
+ String exe_ext;
if (OS::get_singleton()->get_name() == "Windows") {
exe_ext = ".bat";
}
String apksigner_command_name = "apksigner" + exe_ext;
String sdk_path = EDITOR_GET("export/android/android_sdk_path");
- String apksigner_path = "";
+ String apksigner_path;
Error errn;
String build_tools_dir = sdk_path.path_join("build-tools");
@@ -2381,6 +2390,32 @@ bool EditorExportPlatformAndroid::has_valid_export_configuration(const Ref<Edito
err += TTR("Release keystore incorrectly configured in the export preset.") + "\n";
}
+ String java_sdk_path = EDITOR_GET("export/android/java_sdk_path");
+ if (java_sdk_path.is_empty()) {
+ err += TTR("A valid Java SDK path is required in Editor Settings.") + "\n";
+ valid = false;
+ } else {
+ // Validate the given path by checking that `java` is present under the `bin` directory.
+ Error errn;
+ // Check for the bin directory.
+ Ref<DirAccess> da = DirAccess::open(java_sdk_path.path_join("bin"), &errn);
+ if (errn != OK) {
+ err += TTR("Invalid Java SDK path in Editor Settings.");
+ err += TTR("Missing 'bin' directory!");
+ err += "\n";
+ valid = false;
+ } else {
+ // Check for the `java` command.
+ String java_path = get_java_path();
+ if (!FileAccess::exists(java_path)) {
+ err += TTR("Unable to find 'java' command using the Java SDK path.");
+ err += TTR("Please check the Java SDK directory specified in Editor Settings.");
+ err += "\n";
+ valid = false;
+ }
+ }
+ }
+
String sdk_path = EDITOR_GET("export/android/android_sdk_path");
if (sdk_path.is_empty()) {
err += TTR("A valid Android SDK path is required in Editor Settings.") + "\n";
@@ -2918,6 +2953,13 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
}
}
const String assets_directory = get_assets_directory(p_preset, export_format);
+ String java_sdk_path = EDITOR_GET("export/android/java_sdk_path");
+ if (java_sdk_path.is_empty()) {
+ add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Java SDK path must be configured in Editor Settings at 'export/android/java_sdk_path'."));
+ return ERR_UNCONFIGURED;
+ }
+ print_verbose("Java sdk path: " + java_sdk_path);
+
String sdk_path = EDITOR_GET("export/android/android_sdk_path");
if (sdk_path.is_empty()) {
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Android SDK path must be configured in Editor Settings at 'export/android/android_sdk_path'."));
@@ -2968,8 +3010,11 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
print_verbose("Storing command line flags...");
store_file_at_path(assets_directory + "/_cl_", command_line_flags);
+ print_verbose("Updating JAVA_HOME environment to " + java_sdk_path);
+ OS::get_singleton()->set_environment("JAVA_HOME", java_sdk_path);
+
print_verbose("Updating ANDROID_HOME environment to " + sdk_path);
- OS::get_singleton()->set_environment("ANDROID_HOME", sdk_path); //set and overwrite if required
+ OS::get_singleton()->set_environment("ANDROID_HOME", sdk_path);
String build_command;
#ifdef WINDOWS_ENABLED
@@ -3032,6 +3077,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
String combined_android_dependencies_maven_repos = String("|").join(android_dependencies_maven_repos);
List<String> cmdline;
+ cmdline.push_back("validateJavaVersion");
if (clean_build_required) {
cmdline.push_back("clean");
}
diff --git a/platform/android/export/export_plugin.h b/platform/android/export/export_plugin.h
index 5b585581b0..c282055fba 100644
--- a/platform/android/export/export_plugin.h
+++ b/platform/android/export/export_plugin.h
@@ -226,6 +226,8 @@ public:
static String get_apksigner_path(int p_target_sdk = -1, bool p_check_executes = false);
+ static String get_java_path();
+
virtual bool has_valid_export_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates, bool p_debug = false) const override;
virtual bool has_valid_project_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error) const override;
static bool has_valid_username_and_password(const Ref<EditorExportPreset> &p_preset, String &r_error);
diff --git a/platform/android/java/app/build.gradle b/platform/android/java/app/build.gradle
index 01b148aeef..8a543a8550 100644
--- a/platform/android/java/app/build.gradle
+++ b/platform/android/java/app/build.gradle
@@ -241,3 +241,12 @@ task copyAndRenameReleaseAab(type: Copy) {
into getExportPath()
rename "build-release.aab", getExportFilename()
}
+
+/**
+ * Used to validate the version of the Java SDK used for the Godot gradle builds.
+ */
+task validateJavaVersion {
+ if (JavaVersion.current() != versions.javaVersion) {
+ throw new GradleException("Invalid Java version ${JavaVersion.current()}. Version ${versions.javaVersion} is the required Java version for Godot gradle builds.")
+ }
+}
diff --git a/platform/android/java/app/config.gradle b/platform/android/java/app/config.gradle
index a91e7bc7ce..bf091098b4 100644
--- a/platform/android/java/app/config.gradle
+++ b/platform/android/java/app/config.gradle
@@ -9,7 +9,7 @@ ext.versions = [
kotlinVersion : '1.7.0',
fragmentVersion : '1.3.6',
nexusPublishVersion: '1.1.0',
- javaVersion : 17,
+ javaVersion : JavaVersion.VERSION_17,
// Also update 'platform/android/detect.py#get_ndk_version()' when this is updated.
ndkVersion : '23.2.8568313'
diff --git a/scene/gui/button.cpp b/scene/gui/button.cpp
index 23a581c5f6..222cdd15e4 100644
--- a/scene/gui/button.cpp
+++ b/scene/gui/button.cpp
@@ -50,6 +50,63 @@ void Button::_set_internal_margin(Side p_side, float p_value) {
void Button::_queue_update_size_cache() {
}
+void Button::_set_h_separation_is_valid_when_no_text(bool p_h_separation_is_valid_when_no_text) {
+ h_separation_is_valid_when_no_text = p_h_separation_is_valid_when_no_text;
+}
+
+Ref<StyleBox> Button::_get_current_stylebox() const {
+ Ref<StyleBox> stylebox = theme_cache.normal;
+ const bool rtl = is_layout_rtl();
+
+ switch (get_draw_mode()) {
+ case DRAW_NORMAL: {
+ if (rtl && has_theme_stylebox(SNAME("normal_mirrored"))) {
+ stylebox = theme_cache.normal_mirrored;
+ } else {
+ stylebox = theme_cache.normal;
+ }
+ } break;
+
+ case DRAW_HOVER_PRESSED: {
+ // Edge case for CheckButton and CheckBox.
+ if (has_theme_stylebox("hover_pressed")) {
+ if (rtl && has_theme_stylebox(SNAME("hover_pressed_mirrored"))) {
+ stylebox = theme_cache.hover_pressed_mirrored;
+ } else {
+ stylebox = theme_cache.hover_pressed;
+ }
+ break;
+ }
+ }
+ [[fallthrough]];
+ case DRAW_PRESSED: {
+ if (rtl && has_theme_stylebox(SNAME("pressed_mirrored"))) {
+ stylebox = theme_cache.pressed_mirrored;
+ } else {
+ stylebox = theme_cache.pressed;
+ }
+ } break;
+
+ case DRAW_HOVER: {
+ if (rtl && has_theme_stylebox(SNAME("hover_mirrored"))) {
+ stylebox = theme_cache.hover_mirrored;
+ } else {
+ stylebox = theme_cache.hover;
+ }
+ } break;
+
+ case DRAW_DISABLED: {
+ if (rtl && has_theme_stylebox(SNAME("disabled_mirrored"))) {
+ stylebox = theme_cache.disabled_mirrored;
+ } else {
+ stylebox = theme_cache.disabled;
+ }
+ } break;
+ }
+
+ return stylebox;
+}
+
void Button::_notification(int p_what) {
switch (p_what) {
case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: {
@@ -72,287 +129,265 @@ void Button::_notification(int p_what) {
} break;
case NOTIFICATION_DRAW: {
- RID ci = get_canvas_item();
- Size2 size = get_size();
- Color color;
- Color color_icon(1, 1, 1, 1);
+ const RID ci = get_canvas_item();
+ const Size2 size = get_size();
- Ref<StyleBox> style = theme_cache.normal;
- bool rtl = is_layout_rtl();
- const bool is_clipped = clip_text || overrun_behavior != TextServer::OVERRUN_NO_TRIMMING;
+ const Ref<StyleBox> style = _get_current_stylebox();
+ { // Draws the stylebox in the current state.
+ if (!flat) {
+ style->draw(ci, Rect2(Point2(), size));
+ }
- switch (get_draw_mode()) {
- case DRAW_NORMAL: {
- if (rtl && has_theme_stylebox(SNAME("normal_mirrored"))) {
- style = theme_cache.normal_mirrored;
- } else {
- style = theme_cache.normal;
+ if (has_focus()) {
+ Ref<StyleBox> style2 = theme_cache.focus;
+ style2->draw(ci, Rect2(Point2(), size));
+ }
+ }
+
+ Ref<Texture2D> _icon = icon;
+ if (_icon.is_null() && has_theme_icon(SNAME("icon"))) {
+ _icon = theme_cache.icon;
+ }
+
+ if (xl_text.is_empty() && _icon.is_null()) {
+ break;
+ }
+
+ const float style_margin_left = style->get_margin(SIDE_LEFT);
+ const float style_margin_right = style->get_margin(SIDE_RIGHT);
+ const float style_margin_top = style->get_margin(SIDE_TOP);
+ const float style_margin_bottom = style->get_margin(SIDE_BOTTOM);
+
+ Size2 drawable_size_remained = size;
+
+ { // The size after the stelybox is stripped.
+ drawable_size_remained.width -= style_margin_left + style_margin_right;
+ drawable_size_remained.height -= style_margin_top + style_margin_bottom;
+ }
+
+ const int h_separation = MAX(0, theme_cache.h_separation);
+
+ { // The width reserved for internal element in derived classes (and h_separation if need).
+ float internal_margin = _internal_margin[SIDE_LEFT] + _internal_margin[SIDE_RIGHT];
+
+ if (!xl_text.is_empty() || h_separation_is_valid_when_no_text) {
+ if (_internal_margin[SIDE_LEFT] > 0.0f) {
+ internal_margin += h_separation;
}
- if (!flat) {
- style->draw(ci, Rect2(Point2(0, 0), size));
+ if (_internal_margin[SIDE_RIGHT] > 0.0f) {
+ internal_margin += h_separation;
}
+ }
+
+ drawable_size_remained.width -= internal_margin; // The size after the internal element is stripped.
+ }
+
+ HorizontalAlignment icon_align_rtl_checked = horizontal_icon_alignment;
+ HorizontalAlignment align_rtl_checked = alignment;
+ // Swap icon and text alignment sides if right-to-left layout is set.
+ if (is_layout_rtl()) {
+ if (horizontal_icon_alignment == HORIZONTAL_ALIGNMENT_RIGHT) {
+ icon_align_rtl_checked = HORIZONTAL_ALIGNMENT_LEFT;
+ } else if (horizontal_icon_alignment == HORIZONTAL_ALIGNMENT_LEFT) {
+ icon_align_rtl_checked = HORIZONTAL_ALIGNMENT_RIGHT;
+ }
+ if (alignment == HORIZONTAL_ALIGNMENT_RIGHT) {
+ align_rtl_checked = HORIZONTAL_ALIGNMENT_LEFT;
+ } else if (alignment == HORIZONTAL_ALIGNMENT_LEFT) {
+ align_rtl_checked = HORIZONTAL_ALIGNMENT_RIGHT;
+ }
+ }
+ Color font_color;
+ Color icon_modulate_color(1, 1, 1, 1);
+ // Get the font color and icon modulate color in the current state.
+ switch (get_draw_mode()) {
+ case DRAW_NORMAL: {
// Focus colors only take precedence over normal state.
if (has_focus()) {
- color = theme_cache.font_focus_color;
+ font_color = theme_cache.font_focus_color;
if (has_theme_color(SNAME("icon_focus_color"))) {
- color_icon = theme_cache.icon_focus_color;
+ icon_modulate_color = theme_cache.icon_focus_color;
}
} else {
- color = theme_cache.font_color;
+ font_color = theme_cache.font_color;
if (has_theme_color(SNAME("icon_normal_color"))) {
- color_icon = theme_cache.icon_normal_color;
+ icon_modulate_color = theme_cache.icon_normal_color;
}
}
} break;
case DRAW_HOVER_PRESSED: {
// Edge case for CheckButton and CheckBox.
if (has_theme_stylebox("hover_pressed")) {
- if (rtl && has_theme_stylebox(SNAME("hover_pressed_mirrored"))) {
- style = theme_cache.hover_pressed_mirrored;
- } else {
- style = theme_cache.hover_pressed;
- }
-
- if (!flat) {
- style->draw(ci, Rect2(Point2(0, 0), size));
- }
if (has_theme_color(SNAME("font_hover_pressed_color"))) {
- color = theme_cache.font_hover_pressed_color;
+ font_color = theme_cache.font_hover_pressed_color;
}
if (has_theme_color(SNAME("icon_hover_pressed_color"))) {
- color_icon = theme_cache.icon_hover_pressed_color;
+ icon_modulate_color = theme_cache.icon_hover_pressed_color;
}
break;
}
- [[fallthrough]];
}
+ [[fallthrough]];
case DRAW_PRESSED: {
- if (rtl && has_theme_stylebox(SNAME("pressed_mirrored"))) {
- style = theme_cache.pressed_mirrored;
- } else {
- style = theme_cache.pressed;
- }
-
- if (!flat) {
- style->draw(ci, Rect2(Point2(0, 0), size));
- }
if (has_theme_color(SNAME("font_pressed_color"))) {
- color = theme_cache.font_pressed_color;
+ font_color = theme_cache.font_pressed_color;
} else {
- color = theme_cache.font_color;
+ font_color = theme_cache.font_color;
}
if (has_theme_color(SNAME("icon_pressed_color"))) {
- color_icon = theme_cache.icon_pressed_color;
+ icon_modulate_color = theme_cache.icon_pressed_color;
}
} break;
case DRAW_HOVER: {
- if (rtl && has_theme_stylebox(SNAME("hover_mirrored"))) {
- style = theme_cache.hover_mirrored;
- } else {
- style = theme_cache.hover;
- }
-
- if (!flat) {
- style->draw(ci, Rect2(Point2(0, 0), size));
- }
- color = theme_cache.font_hover_color;
+ font_color = theme_cache.font_hover_color;
if (has_theme_color(SNAME("icon_hover_color"))) {
- color_icon = theme_cache.icon_hover_color;
+ icon_modulate_color = theme_cache.icon_hover_color;
}
} break;
case DRAW_DISABLED: {
- if (rtl && has_theme_stylebox(SNAME("disabled_mirrored"))) {
- style = theme_cache.disabled_mirrored;
- } else {
- style = theme_cache.disabled;
- }
-
- if (!flat) {
- style->draw(ci, Rect2(Point2(0, 0), size));
- }
- color = theme_cache.font_disabled_color;
+ font_color = theme_cache.font_disabled_color;
if (has_theme_color(SNAME("icon_disabled_color"))) {
- color_icon = theme_cache.icon_disabled_color;
+ icon_modulate_color = theme_cache.icon_disabled_color;
} else {
- color_icon.a = 0.4;
+ icon_modulate_color.a = 0.4;
}
} break;
}
- if (has_focus()) {
- Ref<StyleBox> style2 = theme_cache.focus;
- style2->draw(ci, Rect2(Point2(), size));
- }
-
- Ref<Texture2D> _icon;
- if (icon.is_null() && has_theme_icon(SNAME("icon"))) {
- _icon = theme_cache.icon;
- } else {
- _icon = icon;
- }
-
- Rect2 icon_region;
- HorizontalAlignment icon_align_rtl_checked = horizontal_icon_alignment;
- HorizontalAlignment align_rtl_checked = alignment;
- // Swap icon and text alignment sides if right-to-left layout is set.
- if (rtl) {
- if (horizontal_icon_alignment == HORIZONTAL_ALIGNMENT_RIGHT) {
- icon_align_rtl_checked = HORIZONTAL_ALIGNMENT_LEFT;
- } else if (horizontal_icon_alignment == HORIZONTAL_ALIGNMENT_LEFT) {
- icon_align_rtl_checked = HORIZONTAL_ALIGNMENT_RIGHT;
- }
- if (alignment == HORIZONTAL_ALIGNMENT_RIGHT) {
- align_rtl_checked = HORIZONTAL_ALIGNMENT_LEFT;
- } else if (alignment == HORIZONTAL_ALIGNMENT_LEFT) {
- align_rtl_checked = HORIZONTAL_ALIGNMENT_RIGHT;
- }
- }
- if (!_icon.is_null()) {
- int valign = size.height - style->get_minimum_size().y;
+ const bool is_clipped = clip_text || overrun_behavior != TextServer::OVERRUN_NO_TRIMMING;
+ const Size2 custom_element_size = drawable_size_remained;
+
+ // Draw the icon.
+ if (_icon.is_valid()) {
+ Size2 icon_size;
+
+ { // Calculate the drawing size of the icon.
+ icon_size = _icon->get_size();
+
+ if (expand_icon) {
+ const Size2 text_buf_size = text_buf->get_size();
+ Size2 _size = custom_element_size;
+ if (!is_clipped && icon_align_rtl_checked != HORIZONTAL_ALIGNMENT_CENTER && text_buf_size.width > 0.0f) {
+ // If there is not enough space for icon and h_separation, h_separation will occupy the space first,
+ // so the icon's width may be negative. Keep it negative to make it easier to calculate the space
+ // reserved for text later.
+ _size.width -= text_buf_size.width + h_separation;
+ }
+ if (vertical_icon_alignment != VERTICAL_ALIGNMENT_CENTER) {
+ _size.height -= text_buf_size.height;
+ }
- int voffset = 0;
- Size2 icon_size = _icon->get_size();
+ float icon_width = icon_size.width * _size.height / icon_size.height;
+ float icon_height = _size.height;
- // Fix vertical size.
- if (vertical_icon_alignment != VERTICAL_ALIGNMENT_CENTER) {
- valign -= text_buf->get_size().height;
- }
+ if (icon_width > _size.width) {
+ icon_width = _size.width;
+ icon_height = icon_size.height * icon_width / icon_size.width;
+ }
- float icon_ofs_region = 0.0;
- Point2 style_offset;
- if (icon_align_rtl_checked == HORIZONTAL_ALIGNMENT_LEFT) {
- style_offset.x = style->get_margin(SIDE_LEFT);
- if (_internal_margin[SIDE_LEFT] > 0) {
- icon_ofs_region = _internal_margin[SIDE_LEFT] + theme_cache.h_separation;
- }
- } else if (icon_align_rtl_checked == HORIZONTAL_ALIGNMENT_CENTER) {
- style_offset.x = 0.0;
- } else if (icon_align_rtl_checked == HORIZONTAL_ALIGNMENT_RIGHT) {
- style_offset.x = -style->get_margin(SIDE_RIGHT);
- if (_internal_margin[SIDE_RIGHT] > 0) {
- icon_ofs_region = -_internal_margin[SIDE_RIGHT] - theme_cache.h_separation;
+ icon_size = Size2(icon_width, icon_height);
}
+ icon_size = _fit_icon_size(icon_size);
}
- style_offset.y = style->get_margin(SIDE_TOP);
-
- if (expand_icon) {
- Size2 _size = get_size() - style->get_offset() * 2;
- int icon_text_separation = text.is_empty() ? 0 : theme_cache.h_separation;
- _size.width -= icon_text_separation + icon_ofs_region;
- if (!is_clipped && icon_align_rtl_checked != HORIZONTAL_ALIGNMENT_CENTER) {
- _size.width -= text_buf->get_size().width;
- }
- if (vertical_icon_alignment != VERTICAL_ALIGNMENT_CENTER) {
- _size.height -= text_buf->get_size().height;
- }
- float icon_width = _icon->get_width() * _size.height / _icon->get_height();
- float icon_height = _size.height;
- if (icon_width > _size.width) {
- icon_width = _size.width;
- icon_height = _icon->get_height() * icon_width / _icon->get_width();
+ if (icon_size.width > 0.0f) {
+ // Calculate the drawing position of the icon.
+ Point2 icon_ofs;
+
+ switch (icon_align_rtl_checked) {
+ case HORIZONTAL_ALIGNMENT_CENTER: {
+ icon_ofs.x = (custom_element_size.width - icon_size.width) / 2.0f;
+ }
+ [[fallthrough]];
+ case HORIZONTAL_ALIGNMENT_FILL:
+ case HORIZONTAL_ALIGNMENT_LEFT: {
+ icon_ofs.x += style_margin_left;
+ icon_ofs.x += _internal_margin[SIDE_LEFT];
+ } break;
+
+ case HORIZONTAL_ALIGNMENT_RIGHT: {
+ icon_ofs.x = size.x - style_margin_right;
+ icon_ofs.x -= _internal_margin[SIDE_RIGHT];
+ icon_ofs.x -= icon_size.width;
+ } break;
}
- icon_size = Size2(icon_width, icon_height);
- }
- icon_size = _fit_icon_size(icon_size);
+ switch (vertical_icon_alignment) {
+ case VERTICAL_ALIGNMENT_CENTER: {
+ icon_ofs.y = (custom_element_size.height - icon_size.height) / 2.0f;
+ }
+ [[fallthrough]];
+ case VERTICAL_ALIGNMENT_FILL:
+ case VERTICAL_ALIGNMENT_TOP: {
+ icon_ofs.y += style_margin_top;
+ } break;
+
+ case VERTICAL_ALIGNMENT_BOTTOM: {
+ icon_ofs.y = size.y - style_margin_bottom - icon_size.height;
+ } break;
+ }
- if (vertical_icon_alignment == VERTICAL_ALIGNMENT_TOP) {
- voffset = -(valign - icon_size.y) / 2;
- }
- if (vertical_icon_alignment == VERTICAL_ALIGNMENT_BOTTOM) {
- voffset = (valign - icon_size.y) / 2 + text_buf->get_size().y;
+ Rect2 icon_region = Rect2(icon_ofs, icon_size);
+ draw_texture_rect(_icon, icon_region, false, icon_modulate_color);
}
- if (icon_align_rtl_checked == HORIZONTAL_ALIGNMENT_LEFT) {
- icon_region = Rect2(style_offset + Point2(icon_ofs_region, voffset + Math::floor((valign - icon_size.y) * 0.5)), icon_size);
- } else if (icon_align_rtl_checked == HORIZONTAL_ALIGNMENT_CENTER) {
- icon_region = Rect2(style_offset + Point2(icon_ofs_region + Math::floor((size.x - icon_size.x) * 0.5), voffset + Math::floor((valign - icon_size.y) * 0.5)), icon_size);
- } else {
- icon_region = Rect2(style_offset + Point2(icon_ofs_region + size.x - icon_size.x, voffset + Math::floor((valign - icon_size.y) * 0.5)), icon_size);
- }
+ if (!xl_text.is_empty()) {
+ // Update the size after the icon is stripped. Stripping only when the icon alignments are not center.
+ if (icon_align_rtl_checked != HORIZONTAL_ALIGNMENT_CENTER) {
+ // Subtract the space's width occupied by icon and h_separation together.
+ drawable_size_remained.width -= icon_size.width + h_separation;
+ }
- if (icon_region.size.width > 0) {
- Rect2 icon_region_rounded = Rect2(icon_region.position.round(), icon_region.size.round());
- draw_texture_rect(_icon, icon_region_rounded, false, color_icon);
+ if (vertical_icon_alignment != VERTICAL_ALIGNMENT_CENTER) {
+ drawable_size_remained.height -= icon_size.height;
+ }
}
}
- Point2 icon_ofs = !_icon.is_null() ? Point2(icon_region.size.width + theme_cache.h_separation, 0) : Point2();
- if (align_rtl_checked == HORIZONTAL_ALIGNMENT_CENTER && icon_align_rtl_checked == HORIZONTAL_ALIGNMENT_CENTER) {
- icon_ofs.x = 0.0;
- }
-
- int text_clip = size.width - style->get_minimum_size().width - icon_ofs.width;
- if (_internal_margin[SIDE_LEFT] > 0) {
- text_clip -= _internal_margin[SIDE_LEFT] + theme_cache.h_separation;
- }
- if (_internal_margin[SIDE_RIGHT] > 0) {
- text_clip -= _internal_margin[SIDE_RIGHT] + theme_cache.h_separation;
- }
-
- text_buf->set_width(is_clipped ? text_clip : -1);
+ // Draw the text.
+ if (!xl_text.is_empty()) {
+ text_buf->set_alignment(align_rtl_checked);
- int text_width = MAX(1, is_clipped ? MIN(text_clip, text_buf->get_size().x) : text_buf->get_size().x);
+ float text_buf_width = MAX(1.0f, drawable_size_remained.width); // The space's width filled by the text_buf.
+ text_buf->set_width(text_buf_width);
- Point2 text_ofs = (size - style->get_minimum_size() - icon_ofs - text_buf->get_size() - Point2(_internal_margin[SIDE_RIGHT] - _internal_margin[SIDE_LEFT], 0)) / 2.0;
-
- if (vertical_icon_alignment == VERTICAL_ALIGNMENT_TOP) {
- text_ofs.y += icon_region.size.height / 2;
- }
- if (vertical_icon_alignment == VERTICAL_ALIGNMENT_BOTTOM) {
- text_ofs.y -= icon_region.size.height / 2;
- }
+ Point2 text_ofs;
- text_buf->set_alignment(align_rtl_checked);
- text_buf->set_width(text_width);
- switch (align_rtl_checked) {
- case HORIZONTAL_ALIGNMENT_FILL:
- case HORIZONTAL_ALIGNMENT_LEFT: {
- if (icon_align_rtl_checked != HORIZONTAL_ALIGNMENT_LEFT) {
- icon_ofs.x = 0.0;
- }
- if (_internal_margin[SIDE_LEFT] > 0) {
- text_ofs.x = style->get_margin(SIDE_LEFT) + icon_ofs.x + _internal_margin[SIDE_LEFT] + theme_cache.h_separation;
- } else {
- text_ofs.x = style->get_margin(SIDE_LEFT) + icon_ofs.x;
- }
- text_ofs.y += style->get_offset().y;
- } break;
- case HORIZONTAL_ALIGNMENT_CENTER: {
- if (text_ofs.x < 0) {
- text_ofs.x = 0;
- }
- if (icon_align_rtl_checked == HORIZONTAL_ALIGNMENT_LEFT) {
- text_ofs += icon_ofs;
- }
- text_ofs += style->get_offset();
- } break;
- case HORIZONTAL_ALIGNMENT_RIGHT: {
- if (_internal_margin[SIDE_RIGHT] > 0) {
- text_ofs.x = size.x - style->get_margin(SIDE_RIGHT) - text_width - _internal_margin[SIDE_RIGHT] - theme_cache.h_separation;
- } else {
- text_ofs.x = size.x - style->get_margin(SIDE_RIGHT) - text_width;
- }
- text_ofs.y += style->get_offset().y;
- if (icon_align_rtl_checked == HORIZONTAL_ALIGNMENT_RIGHT) {
- text_ofs.x -= icon_ofs.x;
+ switch (align_rtl_checked) {
+ case HORIZONTAL_ALIGNMENT_CENTER: {
+ text_ofs.x = (drawable_size_remained.width - text_buf_width) / 2.0f;
}
- } break;
- }
+ [[fallthrough]];
+ case HORIZONTAL_ALIGNMENT_FILL:
+ case HORIZONTAL_ALIGNMENT_LEFT:
+ case HORIZONTAL_ALIGNMENT_RIGHT: {
+ text_ofs.x += style_margin_left;
+ text_ofs.x += _internal_margin[SIDE_LEFT];
+ if (icon_align_rtl_checked == HORIZONTAL_ALIGNMENT_LEFT) {
+ // Offset by the space's width that occupied by icon and h_separation together.
+ text_ofs.x += custom_element_size.width - drawable_size_remained.width;
+ }
+ } break;
+ }
+
+ text_ofs.y = (drawable_size_remained.height - text_buf->get_size().height) / 2.0f + style_margin_top;
+ if (vertical_icon_alignment == VERTICAL_ALIGNMENT_TOP) {
+ text_ofs.y += custom_element_size.height - drawable_size_remained.height; // Offset by the icon's height.
+ }
- Color font_outline_color = theme_cache.font_outline_color;
- int outline_size = theme_cache.outline_size;
- if (outline_size > 0 && font_outline_color.a > 0) {
- text_buf->draw_outline(ci, text_ofs, outline_size, font_outline_color);
+ Color font_outline_color = theme_cache.font_outline_color;
+ int outline_size = theme_cache.outline_size;
+ if (outline_size > 0 && font_outline_color.a > 0.0f) {
+ text_buf->draw_outline(ci, text_ofs, outline_size, font_outline_color);
+ }
+ text_buf->draw(ci, text_ofs, font_color);
}
- text_buf->draw(ci, text_ofs, color);
} break;
}
}
@@ -411,7 +446,7 @@ Size2 Button::get_minimum_size_for_text_and_icon(const String &p_text, Ref<Textu
}
}
- return theme_cache.normal->get_minimum_size() + minsize;
+ return _get_current_stylebox()->get_minimum_size() + minsize;
}
void Button::_shape(Ref<TextParagraph> p_paragraph, String p_text) {
@@ -443,9 +478,13 @@ void Button::_shape(Ref<TextParagraph> p_paragraph, String p_text) {
void Button::set_text_overrun_behavior(TextServer::OverrunBehavior p_behavior) {
if (overrun_behavior != p_behavior) {
+ bool need_update_cache = overrun_behavior == TextServer::OVERRUN_NO_TRIMMING || p_behavior == TextServer::OVERRUN_NO_TRIMMING;
overrun_behavior = p_behavior;
_shape();
+ if (need_update_cache) {
+ _queue_update_size_cache();
+ }
queue_redraw();
update_minimum_size();
}
@@ -550,6 +589,8 @@ bool Button::is_flat() const {
void Button::set_clip_text(bool p_enabled) {
if (clip_text != p_enabled) {
clip_text = p_enabled;
+
+ _queue_update_size_cache();
queue_redraw();
update_minimum_size();
}
@@ -571,13 +612,25 @@ HorizontalAlignment Button::get_text_alignment() const {
}
void Button::set_icon_alignment(HorizontalAlignment p_alignment) {
+ if (horizontal_icon_alignment == p_alignment) {
+ return;
+ }
+
horizontal_icon_alignment = p_alignment;
update_minimum_size();
queue_redraw();
}
void Button::set_vertical_icon_alignment(VerticalAlignment p_alignment) {
+ if (vertical_icon_alignment == p_alignment) {
+ return;
+ }
+ bool need_update_cache = vertical_icon_alignment == VERTICAL_ALIGNMENT_CENTER || p_alignment == VERTICAL_ALIGNMENT_CENTER;
vertical_icon_alignment = p_alignment;
+
+ if (need_update_cache) {
+ _queue_update_size_cache();
+ }
update_minimum_size();
queue_redraw();
}
diff --git a/scene/gui/button.h b/scene/gui/button.h
index ad7412b54e..d0243a9eeb 100644
--- a/scene/gui/button.h
+++ b/scene/gui/button.h
@@ -55,6 +55,8 @@ private:
VerticalAlignment vertical_icon_alignment = VERTICAL_ALIGNMENT_CENTER;
float _internal_margin[4] = {};
+ bool h_separation_is_valid_when_no_text = false;
+
struct ThemeCache {
Ref<StyleBox> normal;
Ref<StyleBox> normal_mirrored;
@@ -102,6 +104,8 @@ protected:
void _set_internal_margin(Side p_side, float p_value);
virtual void _queue_update_size_cache();
+ void _set_h_separation_is_valid_when_no_text(bool p_h_separation_is_valid_when_no_text);
+ Ref<StyleBox> _get_current_stylebox() const;
void _notification(int p_what);
static void _bind_methods();
diff --git a/scene/gui/graph_edit.compat.inc b/scene/gui/graph_edit.compat.inc
index 9059637a2a..7c2af20066 100644
--- a/scene/gui/graph_edit.compat.inc
+++ b/scene/gui/graph_edit.compat.inc
@@ -38,9 +38,14 @@ void GraphEdit::_set_arrange_nodes_button_hidden_bind_compat_81582(bool p_enable
set_show_arrange_button(!p_enable);
}
+PackedVector2Array GraphEdit::_get_connection_line_bind_compat_86158(const Vector2 &p_from, const Vector2 &p_to) {
+ return get_connection_line(p_from, p_to);
+}
+
void GraphEdit::_bind_compatibility_methods() {
ClassDB::bind_compatibility_method(D_METHOD("is_arrange_nodes_button_hidden"), &GraphEdit::_is_arrange_nodes_button_hidden_bind_compat_81582);
ClassDB::bind_compatibility_method(D_METHOD("set_arrange_nodes_button_hidden", "enable"), &GraphEdit::_set_arrange_nodes_button_hidden_bind_compat_81582);
+ ClassDB::bind_compatibility_method(D_METHOD("get_connection_line", "from_node", "to_node"), &GraphEdit::_get_connection_line_bind_compat_86158);
}
#endif
diff --git a/scene/gui/graph_edit.cpp b/scene/gui/graph_edit.cpp
index f5cf7eb59d..c23d21775f 100644
--- a/scene/gui/graph_edit.cpp
+++ b/scene/gui/graph_edit.cpp
@@ -32,8 +32,10 @@
#include "graph_edit.compat.inc"
#include "core/input/input.h"
+#include "core/math/geometry_2d.h"
#include "core/math/math_funcs.h"
#include "core/os/keyboard.h"
+#include "scene/2d/line_2d.h"
#include "scene/gui/box_container.h"
#include "scene/gui/button.h"
#include "scene/gui/graph_edit_arranger.h"
@@ -52,7 +54,6 @@ constexpr int MAX_CONNECTION_LINE_CURVE_TESSELATION_STAGES = 5;
constexpr int GRID_MINOR_STEPS_PER_MAJOR_LINE = 10;
constexpr int GRID_MIN_SNAPPING_DISTANCE = 2;
constexpr int GRID_MAX_SNAPPING_DISTANCE = 100;
-constexpr float CONNECTING_TARGET_LINE_COLOR_BRIGHTENING = 0.4;
bool GraphEditFilter::has_point(const Point2 &p_point) const {
return ge->_filter_input(p_point);
@@ -212,6 +213,36 @@ GraphEditMinimap::GraphEditMinimap(GraphEdit *p_edit) {
minimap_offset = minimap_padding + _convert_from_graph_position(graph_padding);
}
+Ref<Shader> GraphEdit::default_connections_shader;
+
+void GraphEdit::init_shaders() {
+ default_connections_shader.instantiate();
+ default_connections_shader->set_code(R"(
+// Connection lines shader.
+shader_type canvas_item;
+render_mode blend_mix;
+
+uniform vec4 rim_color : source_color;
+uniform int from_type;
+uniform int to_type;
+uniform float line_width;
+
+void fragment(){
+ float fake_aa_width = 1.5/line_width;
+ float rim_width = 1.5/line_width;
+
+ float dist = abs(UV.y - 0.5);
+ float alpha = smoothstep(0.5, 0.5-fake_aa_width, dist);
+ vec4 final_color = mix(rim_color, COLOR, smoothstep(0.5-rim_width, 0.5-fake_aa_width-rim_width, dist));
+ COLOR = vec4(final_color.rgb, final_color.a*alpha);
+}
+)");
+}
+
+void GraphEdit::finish_shaders() {
+ default_connections_shader.unref();
+}
+
Control::CursorShape GraphEdit::get_cursor_shape(const Point2 &p_pos) const {
if (moving_selection) {
return CURSOR_MOVE;
@@ -232,24 +263,48 @@ Error GraphEdit::connect_node(const StringName &p_from, int p_from_port, const S
if (is_node_connected(p_from, p_from_port, p_to, p_to_port)) {
return OK;
}
- Connection c;
- c.from_node = p_from;
- c.from_port = p_from_port;
- c.to_node = p_to;
- c.to_port = p_to_port;
- c.activity = 0;
+ Ref<Connection> c;
+ c.instantiate();
+ c->from_node = p_from;
+ c->from_port = p_from_port;
+ c->to_node = p_to;
+ c->to_port = p_to_port;
+ c->activity = 0;
connections.push_back(c);
- top_layer->queue_redraw();
+ connection_map[p_from].push_back(c);
+ connection_map[p_to].push_back(c);
+
+ Line2D *line = memnew(Line2D);
+ line->set_texture_mode(Line2D::LineTextureMode::LINE_TEXTURE_STRETCH);
+
+ Ref<ShaderMaterial> line_material;
+ line_material.instantiate();
+ line_material->set_shader(connections_shader);
+
+ float line_width = _get_shader_line_width();
+ line_material->set_shader_parameter("line_width", line_width);
+ line_material->set_shader_parameter("from_type", c->from_port);
+ line_material->set_shader_parameter("to_type", c->to_port);
+
+ Ref<StyleBoxFlat> bg_panel = theme_cache.panel;
+ Color connection_line_rim_color = bg_panel.is_valid() ? bg_panel->get_bg_color() : Color(0.0, 0.0, 0.0, 0.0);
+ line_material->set_shader_parameter("rim_color", connection_line_rim_color);
+ line->set_material(line_material);
+
+ connections_layer->add_child(line);
+ c->_cache.line = line;
+
minimap->queue_redraw();
queue_redraw();
connections_layer->queue_redraw();
+ callable_mp(this, &GraphEdit::_update_top_connection_layer).call_deferred();
return OK;
}
bool GraphEdit::is_node_connected(const StringName &p_from, int p_from_port, const StringName &p_to, int p_to_port) {
- for (const Connection &E : connections) {
- if (E.from_node == p_from && E.from_port == p_from_port && E.to_node == p_to && E.to_port == p_to_port) {
+ for (const Ref<Connection> &conn : connection_map[p_from]) {
+ if (conn->from_node == p_from && conn->from_port == p_from_port && conn->to_node == p_to && conn->to_port == p_to_port) {
return true;
}
}
@@ -258,20 +313,24 @@ bool GraphEdit::is_node_connected(const StringName &p_from, int p_from_port, con
}
void GraphEdit::disconnect_node(const StringName &p_from, int p_from_port, const StringName &p_to, int p_to_port) {
- for (const List<Connection>::Element *E = connections.front(); E; E = E->next()) {
- if (E->get().from_node == p_from && E->get().from_port == p_from_port && E->get().to_node == p_to && E->get().to_port == p_to_port) {
+ for (const List<Ref<Connection>>::Element *E = connections.front(); E; E = E->next()) {
+ if (E->get()->from_node == p_from && E->get()->from_port == p_from_port && E->get()->to_node == p_to && E->get()->to_port == p_to_port) {
+ connection_map[p_from].erase(E->get());
+ connection_map[p_to].erase(E->get());
+ E->get()->_cache.line->queue_free();
connections.erase(E);
- top_layer->queue_redraw();
+
minimap->queue_redraw();
queue_redraw();
connections_layer->queue_redraw();
+ callable_mp(this, &GraphEdit::_update_top_connection_layer).call_deferred();
return;
}
}
}
-void GraphEdit::get_connection_list(List<Connection> *r_connections) const {
- *r_connections = connections;
+const List<Ref<GraphEdit::Connection>> &GraphEdit::get_connection_list() const {
+ return connections;
}
void GraphEdit::set_scroll_offset(const Vector2 &p_offset) {
@@ -291,9 +350,9 @@ void GraphEdit::_scroll_moved(double) {
callable_mp(this, &GraphEdit::_update_scroll_offset).call_deferred();
awaiting_scroll_offset_update = true;
}
- top_layer->queue_redraw();
minimap->queue_redraw();
queue_redraw();
+ callable_mp(this, &GraphEdit::_update_top_connection_layer).call_deferred();
}
void GraphEdit::_update_scroll_offset() {
@@ -415,20 +474,34 @@ void GraphEdit::_graph_element_moved(Node *p_node) {
GraphElement *graph_element = Object::cast_to<GraphElement>(p_node);
ERR_FAIL_NULL(graph_element);
- top_layer->queue_redraw();
minimap->queue_redraw();
queue_redraw();
connections_layer->queue_redraw();
+ callable_mp(this, &GraphEdit::_update_top_connection_layer).call_deferred();
}
void GraphEdit::_graph_node_slot_updated(int p_index, Node *p_node) {
GraphNode *graph_node = Object::cast_to<GraphNode>(p_node);
ERR_FAIL_NULL(graph_node);
- top_layer->queue_redraw();
minimap->queue_redraw();
queue_redraw();
connections_layer->queue_redraw();
+ callable_mp(this, &GraphEdit::_update_top_connection_layer).call_deferred();
+}
+
+void GraphEdit::_graph_node_rect_changed(GraphNode *p_node) {
+ // Only invalidate the cache when zooming or the node is moved/resized in graph space.
+ if (panner->is_panning()) {
+ return;
+ }
+
+ for (Ref<Connection> &c : connection_map[p_node->get_name()]) {
+ c->_cache.dirty = true;
+ }
+
+ connections_layer->queue_redraw();
+ callable_mp(this, &GraphEdit::_update_top_connection_layer).call_deferred();
}
void GraphEdit::add_child_notify(Node *p_child) {
@@ -445,12 +518,12 @@ void GraphEdit::add_child_notify(Node *p_child) {
GraphNode *graph_node = Object::cast_to<GraphNode>(graph_element);
if (graph_node) {
- graph_element->connect("slot_updated", callable_mp(this, &GraphEdit::_graph_node_slot_updated).bind(graph_element));
+ graph_node->connect("slot_updated", callable_mp(this, &GraphEdit::_graph_node_slot_updated).bind(graph_element));
+ graph_node->connect("item_rect_changed", callable_mp(this, &GraphEdit::_graph_node_rect_changed).bind(graph_node));
}
graph_element->connect("raise_request", callable_mp(this, &GraphEdit::_graph_element_moved_to_front).bind(graph_element));
graph_element->connect("resize_request", callable_mp(this, &GraphEdit::_graph_element_resized).bind(graph_element));
- graph_element->connect("item_rect_changed", callable_mp((CanvasItem *)connections_layer, &CanvasItem::queue_redraw));
graph_element->connect("item_rect_changed", callable_mp((CanvasItem *)minimap, &GraphEditMinimap::queue_redraw));
graph_element->set_scale(Vector2(zoom, zoom));
@@ -482,16 +555,20 @@ void GraphEdit::remove_child_notify(Node *p_child) {
GraphNode *graph_node = Object::cast_to<GraphNode>(graph_element);
if (graph_node) {
- graph_element->disconnect("slot_updated", callable_mp(this, &GraphEdit::_graph_node_slot_updated));
+ graph_node->disconnect("slot_updated", callable_mp(this, &GraphEdit::_graph_node_slot_updated));
+ graph_node->disconnect("item_rect_changed", callable_mp(this, &GraphEdit::_graph_node_rect_changed));
+
+ // Invalidate all adjacent connections, so that they are removed before the next redraw.
+ for (const Ref<Connection> &conn : connection_map[graph_node->get_name()]) {
+ conn->_cache.dirty = true;
+ }
+ connections_layer->queue_redraw();
}
graph_element->disconnect("raise_request", callable_mp(this, &GraphEdit::_graph_element_moved_to_front));
graph_element->disconnect("resize_request", callable_mp(this, &GraphEdit::_graph_element_resized));
// In case of the whole GraphEdit being destroyed these references can already be freed.
- if (connections_layer != nullptr && connections_layer->is_inside_tree()) {
- graph_element->disconnect("item_rect_changed", callable_mp((CanvasItem *)connections_layer, &CanvasItem::queue_redraw));
- }
if (minimap != nullptr && minimap->is_inside_tree()) {
graph_element->disconnect("item_rect_changed", callable_mp((CanvasItem *)minimap, &GraphEditMinimap::queue_redraw));
}
@@ -520,7 +597,6 @@ void GraphEdit::_notification(int p_what) {
menu_panel->add_theme_style_override("panel", theme_cache.menu_panel);
} break;
-
case NOTIFICATION_READY: {
Size2 hmin = h_scrollbar->get_combined_minimum_size();
Size2 vmin = v_scrollbar->get_combined_minimum_size();
@@ -535,7 +611,6 @@ void GraphEdit::_notification(int p_what) {
v_scrollbar->set_anchor_and_offset(SIDE_TOP, ANCHOR_BEGIN, 0);
v_scrollbar->set_anchor_and_offset(SIDE_BOTTOM, ANCHOR_END, 0);
} break;
-
case NOTIFICATION_DRAW: {
// Draw background fill.
draw_style_box(theme_cache.panel, Rect2(Point2(), get_size()));
@@ -547,8 +622,8 @@ void GraphEdit::_notification(int p_what) {
} break;
case NOTIFICATION_RESIZED: {
_update_scroll();
- top_layer->queue_redraw();
minimap->queue_redraw();
+ callable_mp(this, &GraphEdit::_update_top_connection_layer).call_deferred();
} break;
}
}
@@ -593,7 +668,7 @@ bool GraphEdit::_filter_input(const Point2 &p_point) {
return false;
}
-void GraphEdit::_top_layer_input(const Ref<InputEvent> &p_ev) {
+void GraphEdit::_top_connection_layer_input(const Ref<InputEvent> &p_ev) {
Ref<InputEventMouseButton> mb = p_ev;
if (mb.is_valid() && mb->get_button_index() == MouseButton::LEFT && mb->is_pressed()) {
connecting_valid = false;
@@ -618,26 +693,26 @@ void GraphEdit::_top_layer_input(const Ref<InputEvent> &p_ev) {
if (is_in_output_hotzone(graph_node, j, click_pos, port_size)) {
if (valid_left_disconnect_types.has(graph_node->get_output_port_type(j))) {
// Check disconnect.
- for (const Connection &E : connections) {
- if (E.from_node == graph_node->get_name() && E.from_port == j) {
- Node *to = get_node(NodePath(E.to_node));
+ for (const Ref<Connection> &conn : connection_map[graph_node->get_name()]) {
+ if (conn->from_node == graph_node->get_name() && conn->from_port == j) {
+ Node *to = get_node(NodePath(conn->to_node));
if (Object::cast_to<GraphNode>(to)) {
- connecting_from = E.to_node;
- connecting_index = E.to_port;
- connecting_out = false;
- connecting_type = Object::cast_to<GraphNode>(to)->get_input_port_type(E.to_port);
- connecting_color = Object::cast_to<GraphNode>(to)->get_input_port_color(E.to_port);
- connecting_target = false;
- connecting_to = pos;
+ connecting_from_node = conn->to_node;
+ connecting_from_port_index = conn->to_port;
+ connecting_from_output = false;
+ connecting_type = Object::cast_to<GraphNode>(to)->get_input_port_type(conn->to_port);
+ connecting_color = Object::cast_to<GraphNode>(to)->get_input_port_color(conn->to_port);
+ connecting_target_valid = false;
+ connecting_to_point = pos;
if (connecting_type >= 0) {
just_disconnected = true;
- emit_signal(SNAME("disconnection_request"), E.from_node, E.from_port, E.to_node, E.to_port);
- to = get_node(NodePath(connecting_from)); // Maybe it was erased.
+ emit_signal(SNAME("disconnection_request"), conn->from_node, conn->from_port, conn->to_node, conn->to_port);
+ to = get_node(NodePath(connecting_from_node)); // Maybe it was erased.
if (Object::cast_to<GraphNode>(to)) {
connecting = true;
- emit_signal(SNAME("connection_drag_started"), connecting_from, connecting_index, false);
+ emit_signal(SNAME("connection_drag_started"), connecting_from_node, connecting_from_port_index, false);
}
}
return;
@@ -646,17 +721,17 @@ void GraphEdit::_top_layer_input(const Ref<InputEvent> &p_ev) {
}
}
- connecting_from = graph_node->get_name();
- connecting_index = j;
- connecting_out = true;
+ connecting_from_node = graph_node->get_name();
+ connecting_from_port_index = j;
+ connecting_from_output = true;
connecting_type = graph_node->get_output_port_type(j);
connecting_color = graph_node->get_output_port_color(j);
- connecting_target = false;
- connecting_to = pos;
+ connecting_target_valid = false;
+ connecting_to_point = pos;
if (connecting_type >= 0) {
connecting = true;
just_disconnected = false;
- emit_signal(SNAME("connection_drag_started"), connecting_from, connecting_index, true);
+ emit_signal(SNAME("connection_drag_started"), connecting_from_node, connecting_from_port_index, true);
}
return;
}
@@ -675,25 +750,25 @@ void GraphEdit::_top_layer_input(const Ref<InputEvent> &p_ev) {
if (is_in_input_hotzone(graph_node, j, click_pos, port_size)) {
if (right_disconnects || valid_right_disconnect_types.has(graph_node->get_input_port_type(j))) {
// Check disconnect.
- for (const Connection &E : connections) {
- if (E.to_node == graph_node->get_name() && E.to_port == j) {
- Node *fr = get_node(NodePath(E.from_node));
+ for (const Ref<Connection> &conn : connection_map[graph_node->get_name()]) {
+ if (conn->to_node == graph_node->get_name() && conn->to_port == j) {
+ Node *fr = get_node(NodePath(conn->from_node));
if (Object::cast_to<GraphNode>(fr)) {
- connecting_from = E.from_node;
- connecting_index = E.from_port;
- connecting_out = true;
- connecting_type = Object::cast_to<GraphNode>(fr)->get_output_port_type(E.from_port);
- connecting_color = Object::cast_to<GraphNode>(fr)->get_output_port_color(E.from_port);
- connecting_target = false;
- connecting_to = pos;
+ connecting_from_node = conn->from_node;
+ connecting_from_port_index = conn->from_port;
+ connecting_from_output = true;
+ connecting_type = Object::cast_to<GraphNode>(fr)->get_output_port_type(conn->from_port);
+ connecting_color = Object::cast_to<GraphNode>(fr)->get_output_port_color(conn->from_port);
+ connecting_target_valid = false;
+ connecting_to_point = pos;
just_disconnected = true;
if (connecting_type >= 0) {
- emit_signal(SNAME("disconnection_request"), E.from_node, E.from_port, E.to_node, E.to_port);
- fr = get_node(NodePath(connecting_from));
+ emit_signal(SNAME("disconnection_request"), conn->from_node, conn->from_port, conn->to_node, conn->to_port);
+ fr = get_node(NodePath(connecting_from_node));
if (Object::cast_to<GraphNode>(fr)) {
connecting = true;
- emit_signal(SNAME("connection_drag_started"), connecting_from, connecting_index, true);
+ emit_signal(SNAME("connection_drag_started"), connecting_from_node, connecting_from_port_index, true);
}
}
return;
@@ -702,17 +777,17 @@ void GraphEdit::_top_layer_input(const Ref<InputEvent> &p_ev) {
}
}
- connecting_from = graph_node->get_name();
- connecting_index = j;
- connecting_out = false;
+ connecting_from_node = graph_node->get_name();
+ connecting_from_port_index = j;
+ connecting_from_output = false;
connecting_type = graph_node->get_input_port_type(j);
connecting_color = graph_node->get_input_port_color(j);
- connecting_target = false;
- connecting_to = pos;
+ connecting_target_valid = false;
+ connecting_to_point = pos;
if (connecting_type >= 0) {
connecting = true;
just_disconnected = false;
- emit_signal(SNAME("connection_drag_started"), connecting_from, connecting_index, false);
+ emit_signal(SNAME("connection_drag_started"), connecting_from_node, connecting_from_port_index, false);
}
return;
}
@@ -722,12 +797,11 @@ void GraphEdit::_top_layer_input(const Ref<InputEvent> &p_ev) {
Ref<InputEventMouseMotion> mm = p_ev;
if (mm.is_valid() && connecting) {
- connecting_to = mm->get_position();
- connecting_target = false;
- top_layer->queue_redraw();
+ connecting_to_point = mm->get_position();
minimap->queue_redraw();
+ callable_mp(this, &GraphEdit::_update_top_connection_layer).call_deferred();
- connecting_valid = just_disconnected || click_pos.distance_to(connecting_to / zoom) > MIN_DRAG_DISTANCE_FOR_VALID_CONNECTION;
+ connecting_valid = just_disconnected || click_pos.distance_to(connecting_to_point / zoom) > MIN_DRAG_DISTANCE_FOR_VALID_CONNECTION;
if (connecting_valid) {
Vector2 mpos = mm->get_position() / zoom;
@@ -739,7 +813,7 @@ void GraphEdit::_top_layer_input(const Ref<InputEvent> &p_ev) {
Ref<Texture2D> port_icon = graph_node->theme_cache.port;
- if (!connecting_out) {
+ if (!connecting_from_output) {
for (int j = 0; j < graph_node->get_output_port_count(); j++) {
Vector2 pos = graph_node->get_output_port_position(j) * zoom + graph_node->get_position();
Vector2i port_size = Vector2i(port_icon->get_width(), port_icon->get_height());
@@ -753,16 +827,17 @@ void GraphEdit::_top_layer_input(const Ref<InputEvent> &p_ev) {
if ((type == connecting_type ||
valid_connection_types.has(ConnectionType(type, connecting_type))) &&
is_in_output_hotzone(graph_node, j, mpos, port_size)) {
- if (!is_node_hover_valid(graph_node->get_name(), j, connecting_from, connecting_index)) {
+ if (!is_node_hover_valid(graph_node->get_name(), j, connecting_from_node, connecting_from_port_index)) {
continue;
}
- connecting_target = true;
- connecting_to = pos;
- connecting_target_to = graph_node->get_name();
- connecting_target_index = j;
+ connecting_target_valid = true;
+ connecting_to_point = pos;
+ connecting_target_node = graph_node->get_name();
+ connecting_target_port_index = j;
return;
}
}
+ connecting_target_valid = false;
} else {
for (int j = 0; j < graph_node->get_input_port_count(); j++) {
Vector2 pos = graph_node->get_input_port_position(j) * zoom + graph_node->get_position();
@@ -776,16 +851,17 @@ void GraphEdit::_top_layer_input(const Ref<InputEvent> &p_ev) {
int type = graph_node->get_input_port_type(j);
if ((type == connecting_type || valid_connection_types.has(ConnectionType(connecting_type, type))) &&
is_in_input_hotzone(graph_node, j, mpos, port_size)) {
- if (!is_node_hover_valid(connecting_from, connecting_index, graph_node->get_name(), j)) {
+ if (!is_node_hover_valid(connecting_from_node, connecting_from_port_index, graph_node->get_name(), j)) {
continue;
}
- connecting_target = true;
- connecting_to = pos;
- connecting_target_to = graph_node->get_name();
- connecting_target_index = j;
+ connecting_target_valid = true;
+ connecting_to_point = pos;
+ connecting_target_node = graph_node->get_name();
+ connecting_target_port_index = j;
return;
}
}
+ connecting_target_valid = false;
}
}
}
@@ -793,17 +869,17 @@ void GraphEdit::_top_layer_input(const Ref<InputEvent> &p_ev) {
if (mb.is_valid() && mb->get_button_index() == MouseButton::LEFT && !mb->is_pressed()) {
if (connecting_valid) {
- if (connecting && connecting_target) {
- if (connecting_out) {
- emit_signal(SNAME("connection_request"), connecting_from, connecting_index, connecting_target_to, connecting_target_index);
+ if (connecting && connecting_target_valid) {
+ if (connecting_from_output) {
+ emit_signal(SNAME("connection_request"), connecting_from_node, connecting_from_port_index, connecting_target_node, connecting_target_port_index);
} else {
- emit_signal(SNAME("connection_request"), connecting_target_to, connecting_target_index, connecting_from, connecting_index);
+ emit_signal(SNAME("connection_request"), connecting_target_node, connecting_target_port_index, connecting_from_node, connecting_from_port_index);
}
} else if (!just_disconnected) {
- if (connecting_out) {
- emit_signal(SNAME("connection_to_empty"), connecting_from, connecting_index, mb->get_position());
+ if (connecting_from_output) {
+ emit_signal(SNAME("connection_to_empty"), connecting_from_node, connecting_from_port_index, mb->get_position());
} else {
- emit_signal(SNAME("connection_from_empty"), connecting_from, connecting_index, mb->get_position());
+ emit_signal(SNAME("connection_from_empty"), connecting_from_node, connecting_from_port_index, mb->get_position());
}
}
}
@@ -905,7 +981,7 @@ bool GraphEdit::is_in_port_hotzone(const Vector2 &p_pos, const Vector2 &p_mouse_
return true;
}
-PackedVector2Array GraphEdit::get_connection_line(const Vector2 &p_from, const Vector2 &p_to) {
+PackedVector2Array GraphEdit::get_connection_line(const Vector2 &p_from, const Vector2 &p_to) const {
Vector<Vector2> ret;
if (GDVIRTUAL_CALL(_get_connection_line, p_from, p_to, ret)) {
return ret;
@@ -930,96 +1006,249 @@ PackedVector2Array GraphEdit::get_connection_line(const Vector2 &p_from, const V
}
}
-void GraphEdit::_draw_connection_line(CanvasItem *p_where, const Vector2 &p_from, const Vector2 &p_to, const Color &p_color, const Color &p_to_color, float p_width, float p_zoom) {
- Vector<Vector2> points = get_connection_line(p_from / p_zoom, p_to / p_zoom);
- Vector<Vector2> scaled_points;
- Vector<Color> colors;
- float length = (p_from / p_zoom).distance_to(p_to / p_zoom);
- for (int i = 0; i < points.size(); i++) {
- float d = (p_from / p_zoom).distance_to(points[i]) / length;
- colors.push_back(p_color.lerp(p_to_color, d));
- scaled_points.push_back(points[i] * p_zoom);
+Ref<GraphEdit::Connection> GraphEdit::get_closest_connection_at_point(const Vector2 &p_point, float p_max_distance) const {
+ Vector2 transformed_point = p_point + get_scroll_offset();
+
+ Ref<GraphEdit::Connection> closest_connection;
+ float closest_distance = p_max_distance;
+ for (const Ref<Connection> &c : connections) {
+ if (c->_cache.aabb.distance_to(transformed_point) > p_max_distance) {
+ continue;
+ }
+
+ Vector<Vector2> points = get_connection_line(c->_cache.from_pos * zoom, c->_cache.to_pos * zoom);
+ for (int i = 0; i < points.size() - 1; i++) {
+ float distance = Geometry2D::get_distance_to_segment(transformed_point, &points[i]);
+ if (distance <= lines_thickness * 0.5 + p_max_distance && distance < closest_distance) {
+ closest_connection = c;
+ closest_distance = distance;
+ }
+ }
}
- // Thickness below 0.5 doesn't look good on the graph or its minimap.
- p_where->draw_polyline_colors(scaled_points, colors, MAX(0.5, Math::floor(p_width * theme_cache.base_scale)), lines_antialiased);
+ return closest_connection;
}
-void GraphEdit::_connections_layer_draw() {
- // Draw connections.
- List<List<Connection>::Element *> to_erase;
- for (List<Connection>::Element *E = connections.front(); E; E = E->next()) {
- const Connection &c = E->get();
-
- Node *from = get_node(NodePath(c.from_node));
- GraphNode *gnode_from = Object::cast_to<GraphNode>(from);
+List<Ref<GraphEdit::Connection>> GraphEdit::get_connections_intersecting_with_rect(const Rect2 &p_rect) const {
+ Rect2 transformed_rect = p_rect;
+ transformed_rect.position += get_scroll_offset();
- if (!gnode_from) {
- to_erase.push_back(E);
+ List<Ref<Connection>> intersecting_connections;
+ for (const Ref<Connection> &c : connections) {
+ if (!c->_cache.aabb.intersects(transformed_rect)) {
continue;
}
- Node *to = get_node(NodePath(c.to_node));
- GraphNode *gnode_to = Object::cast_to<GraphNode>(to);
+ Vector<Vector2> points = get_connection_line(c->_cache.from_pos * zoom, c->_cache.to_pos * zoom);
+ for (int i = 0; i < points.size() - 1; i++) {
+ if (Geometry2D::segment_intersects_rect(points[i], points[i + 1], transformed_rect)) {
+ intersecting_connections.push_back(c);
+ break;
+ }
+ }
+ }
+ return intersecting_connections;
+}
+
+void GraphEdit::_draw_minimap_connection_line(CanvasItem *p_where, const Vector2 &p_from, const Vector2 &p_to, const Color &p_from_color, const Color &p_to_color) {
+ const Vector<Vector2> &points = get_connection_line(p_from, p_to);
+ LocalVector<Color> colors;
+ colors.reserve(points.size());
+
+ float length_inv = 1.0 / (p_from).distance_to(p_to);
+ for (const Vector2 &point : points) {
+ float normalized_curve_position = (p_from).distance_to(point) * length_inv;
+ colors.push_back(p_from_color.lerp(p_to_color, normalized_curve_position));
+ }
+
+ p_where->draw_polyline_colors(points, colors, 0.5, lines_antialiased);
+}
+
+void GraphEdit::_update_connections() {
+ // Collect all dead connections and remove them.
+ List<List<Ref<Connection>>::Element *> dead_connections;
+
+ for (List<Ref<Connection>>::Element *E = connections.front(); E; E = E->next()) {
+ Ref<Connection> &c = E->get();
+
+ if (c->_cache.dirty) {
+ Node *from = get_node_or_null(NodePath(c->from_node));
+ GraphNode *gnode_from = Object::cast_to<GraphNode>(from);
+ if (!gnode_from) {
+ dead_connections.push_back(E);
+ continue;
+ }
+ Node *to = get_node_or_null(NodePath(c->to_node));
+ GraphNode *gnode_to = Object::cast_to<GraphNode>(to);
+
+ if (!gnode_to) {
+ dead_connections.push_back(E);
+ continue;
+ }
- if (!gnode_to) {
- to_erase.push_back(E);
+ const Vector2 from_pos = gnode_from->get_output_port_position(c->from_port) + gnode_from->get_position_offset();
+ const Vector2 to_pos = gnode_to->get_input_port_position(c->to_port) + gnode_to->get_position_offset();
+
+ const Color from_color = gnode_from->get_output_port_color(c->from_port);
+ const Color to_color = gnode_to->get_input_port_color(c->to_port);
+
+ const int from_type = gnode_from->get_output_port_type(c->from_port);
+ const int to_type = gnode_to->get_input_port_type(c->to_port);
+
+ c->_cache.from_pos = from_pos;
+ c->_cache.to_pos = to_pos;
+ c->_cache.from_color = from_color;
+ c->_cache.to_color = to_color;
+
+ PackedVector2Array line_points = get_connection_line(from_pos * zoom, to_pos * zoom);
+ c->_cache.line->set_points(line_points);
+
+ Ref<ShaderMaterial> line_material = c->_cache.line->get_material();
+ if (line_material.is_null()) {
+ line_material.instantiate();
+ c->_cache.line->set_material(line_material);
+ }
+
+ float line_width = _get_shader_line_width();
+ line_material->set_shader_parameter("line_width", line_width);
+ line_material->set_shader_parameter("from_type", from_type);
+ line_material->set_shader_parameter("to_type", to_type);
+ line_material->set_shader_parameter("rim_color", theme_cache.connection_rim_color);
+
+ // Compute bounding box of the line, including the line width.
+ c->_cache.aabb = Rect2(line_points[0], Vector2());
+ for (int i = 0; i < line_points.size(); i++) {
+ c->_cache.aabb.expand_to(line_points[i]);
+ }
+ c->_cache.aabb.grow_by(lines_thickness * 0.5);
+
+ c->_cache.dirty = false;
+ }
+
+ // Skip updating/drawing connections that are not visible.
+ Rect2 viewport_rect = get_viewport_rect();
+ viewport_rect.position += get_scroll_offset();
+ if (!c->_cache.aabb.intersects(viewport_rect)) {
continue;
}
- Vector2 frompos = gnode_from->get_output_port_position(c.from_port) * zoom + gnode_from->get_position_offset() * zoom;
- Color color = gnode_from->get_output_port_color(c.from_port);
- Vector2 topos = gnode_to->get_input_port_position(c.to_port) * zoom + gnode_to->get_position_offset() * zoom;
- Color tocolor = gnode_to->get_input_port_color(c.to_port);
+ Color from_color = c->_cache.from_color;
+ Color to_color = c->_cache.to_color;
+
+ if (c->activity > 0) {
+ from_color = from_color.lerp(theme_cache.activity_color, c->activity);
+ to_color = to_color.lerp(theme_cache.activity_color, c->activity);
+ }
- if (c.activity > 0) {
- color = color.lerp(theme_cache.activity_color, c.activity);
- tocolor = tocolor.lerp(theme_cache.activity_color, c.activity);
+ if (c == hovered_connection) {
+ from_color = from_color.blend(theme_cache.connection_hover_tint_color);
+ to_color = to_color.blend(theme_cache.connection_hover_tint_color);
}
- _draw_connection_line(connections_layer, frompos, topos, color, tocolor, lines_thickness, zoom);
+
+ // Update Line2D node.
+ Ref<Gradient> line_gradient = memnew(Gradient);
+
+ float line_width = _get_shader_line_width();
+ c->_cache.line->set_width(line_width);
+ line_gradient->set_color(0, from_color);
+ line_gradient->set_color(1, to_color);
+
+ c->_cache.line->set_gradient(line_gradient);
}
- for (List<Connection>::Element *&E : to_erase) {
- connections.erase(E);
+ for (const List<Ref<Connection>>::Element *E : dead_connections) {
+ List<Ref<Connection>> &connections_from = connection_map[E->get()->from_node];
+ List<Ref<Connection>> &connections_to = connection_map[E->get()->to_node];
+ connections_from.erase(E->get());
+ connections_to.erase(E->get());
+ E->get()->_cache.line->queue_free();
+
+ connections.erase(E->get());
}
}
void GraphEdit::_top_layer_draw() {
+ if (!box_selecting) {
+ return;
+ }
+
+ top_layer->draw_rect(box_selecting_rect, theme_cache.selection_fill);
+ top_layer->draw_rect(box_selecting_rect, theme_cache.selection_stroke, false);
+}
+
+void GraphEdit::_update_top_connection_layer() {
_update_scroll();
- if (connecting) {
- Node *node_from = get_node_or_null(NodePath(connecting_from));
- ERR_FAIL_NULL(node_from);
- GraphNode *graph_node_from = Object::cast_to<GraphNode>(node_from);
- ERR_FAIL_NULL(graph_node_from);
- Vector2 pos;
- if (connecting_out) {
- pos = graph_node_from->get_output_port_position(connecting_index) * zoom;
- } else {
- pos = graph_node_from->get_input_port_position(connecting_index) * zoom;
- }
- pos += graph_node_from->get_position();
+ if (!connecting) {
+ dragged_connection_line->clear_points();
- Vector2 to_pos = connecting_to;
- Color line_color = connecting_color;
+ return;
+ }
- // Draw the line to the mouse cursor brighter when it's over a valid target port.
- if (connecting_target) {
- line_color.r += CONNECTING_TARGET_LINE_COLOR_BRIGHTENING;
- line_color.g += CONNECTING_TARGET_LINE_COLOR_BRIGHTENING;
- line_color.b += CONNECTING_TARGET_LINE_COLOR_BRIGHTENING;
- }
+ GraphNode *graph_node_from = Object::cast_to<GraphNode>(get_node_or_null(NodePath(connecting_from_node)));
+ ERR_FAIL_NULL(graph_node_from);
- if (!connecting_out) {
- SWAP(pos, to_pos);
+ Vector2 from_pos = graph_node_from->get_position() / zoom;
+ Vector2 to_pos = connecting_to_point / zoom;
+ int from_type;
+ int to_type = connecting_type;
+ Color from_color;
+ Color to_color = connecting_color;
+
+ if (connecting_from_output) {
+ from_pos += graph_node_from->get_output_port_position(connecting_from_port_index);
+ from_type = graph_node_from->get_output_port_type(connecting_from_port_index);
+ from_color = graph_node_from->get_output_port_color(connecting_from_port_index);
+ } else {
+ from_pos += graph_node_from->get_input_port_position(connecting_from_port_index);
+ from_type = graph_node_from->get_input_port_type(connecting_from_port_index);
+ from_color = graph_node_from->get_input_port_color(connecting_from_port_index);
+ }
+
+ if (connecting_target_valid) {
+ GraphNode *graph_node_to = Object::cast_to<GraphNode>(get_node_or_null(NodePath(connecting_target_node)));
+ ERR_FAIL_NULL(graph_node_to);
+ if (connecting_from_output) {
+ to_type = graph_node_to->get_input_port_type(connecting_target_port_index);
+ to_color = graph_node_to->get_input_port_color(connecting_target_port_index);
+ } else {
+ to_type = graph_node_to->get_output_port_type(connecting_target_port_index);
+ to_color = graph_node_to->get_output_port_color(connecting_target_port_index);
}
- _draw_connection_line(top_layer, pos, to_pos, line_color, line_color, lines_thickness, zoom);
+
+ // Highlight the line to the mouse cursor when it's over a valid target port.
+ from_color = from_color.blend(theme_cache.connection_valid_target_tint_color);
+ to_color = to_color.blend(theme_cache.connection_valid_target_tint_color);
+ }
+
+ if (!connecting_from_output) {
+ SWAP(from_pos, to_pos);
+ SWAP(from_type, to_type);
+ SWAP(from_color, to_color);
}
- if (box_selecting) {
- top_layer->draw_rect(box_selecting_rect, theme_cache.selection_fill);
- top_layer->draw_rect(box_selecting_rect, theme_cache.selection_stroke, false);
+ PackedVector2Array line_points = get_connection_line(from_pos * zoom, to_pos * zoom);
+ dragged_connection_line->set_points(line_points);
+
+ Ref<ShaderMaterial> line_material = dragged_connection_line->get_material();
+ if (line_material.is_null()) {
+ line_material.instantiate();
+ line_material->set_shader(connections_shader);
+ dragged_connection_line->set_material(line_material);
}
+
+ float line_width = _get_shader_line_width();
+ line_material->set_shader_parameter("line_width", line_width);
+ line_material->set_shader_parameter("from_type", from_type);
+ line_material->set_shader_parameter("to_type", to_type);
+ line_material->set_shader_parameter("rim_color", theme_cache.connection_rim_color);
+
+ Ref<Gradient> line_gradient = memnew(Gradient);
+ dragged_connection_line->set_width(line_width);
+ line_gradient->set_color(0, from_color);
+ line_gradient->set_color(1, to_color);
+
+ dragged_connection_line->set_gradient(line_gradient);
}
void GraphEdit::_minimap_draw() {
@@ -1060,31 +1289,17 @@ void GraphEdit::_minimap_draw() {
}
// Draw node connections.
- for (const Connection &E : connections) {
- Node *from = get_node(NodePath(E.from_node));
- GraphNode *graph_node_from = Object::cast_to<GraphNode>(from);
- if (!graph_node_from) {
- continue;
- }
-
- Node *node_to = get_node(NodePath(E.to_node));
- GraphNode *graph_node_to = Object::cast_to<GraphNode>(node_to);
- if (!graph_node_to) {
- continue;
- }
-
- Vector2 from_port_position = graph_node_from->get_position_offset() * zoom + graph_node_from->get_output_port_position(E.from_port) * zoom;
- Vector2 from_position = minimap->_convert_from_graph_position(from_port_position - graph_offset) + minimap_offset;
- Color from_color = graph_node_from->get_output_port_color(E.from_port);
- Vector2 to_port_position = graph_node_to->get_position_offset() * zoom + graph_node_to->get_input_port_position(E.to_port) * zoom;
- Vector2 to_position = minimap->_convert_from_graph_position(to_port_position - graph_offset) + minimap_offset;
- Color to_color = graph_node_to->get_input_port_color(E.to_port);
-
- if (E.activity > 0) {
- from_color = from_color.lerp(theme_cache.activity_color, E.activity);
- to_color = to_color.lerp(theme_cache.activity_color, E.activity);
+ for (const Ref<Connection> &c : connections) {
+ Vector2 from_position = minimap->_convert_from_graph_position(c->_cache.from_pos * zoom - graph_offset) + minimap_offset;
+ Vector2 to_position = minimap->_convert_from_graph_position(c->_cache.to_pos * zoom - graph_offset) + minimap_offset;
+ Color from_color = c->_cache.from_color;
+ Color to_color = c->_cache.to_color;
+
+ if (c->activity > 0) {
+ from_color = from_color.lerp(theme_cache.activity_color, c->activity);
+ to_color = to_color.lerp(theme_cache.activity_color, c->activity);
}
- _draw_connection_line(minimap, from_position, to_position, from_color, to_color, 0.5, minimap->_convert_from_graph_position(Vector2(zoom, zoom)).length());
+ _draw_minimap_connection_line(minimap, from_position, to_position, from_color, to_color);
}
// Draw the "camera" viewport.
@@ -1175,7 +1390,15 @@ void GraphEdit::gui_input(const Ref<InputEvent> &p_ev) {
return;
}
+ // Highlight the connection close to the mouse cursor.
Ref<InputEventMouseMotion> mm = p_ev;
+ if (mm.is_valid()) {
+ Ref<Connection> new_highlighted_connection = get_closest_connection_at_point(mm->get_position());
+ if (new_highlighted_connection != hovered_connection) {
+ connections_layer->queue_redraw();
+ }
+ hovered_connection = new_highlighted_connection;
+ }
if (mm.is_valid() && dragging) {
if (!moving_selection) {
@@ -1201,6 +1424,7 @@ void GraphEdit::gui_input(const Ref<InputEvent> &p_ev) {
}
}
+ // Box selection logic.
if (mm.is_valid() && box_selecting) {
box_selecting_to = mm->get_position();
@@ -1281,10 +1505,10 @@ void GraphEdit::gui_input(const Ref<InputEvent> &p_ev) {
dragging = false;
- top_layer->queue_redraw();
minimap->queue_redraw();
queue_redraw();
connections_layer->queue_redraw();
+ callable_mp(this, &GraphEdit::_update_top_connection_layer).call_deferred();
}
// Node selection logic.
@@ -1430,29 +1654,56 @@ void GraphEdit::gui_input(const Ref<InputEvent> &p_ev) {
void GraphEdit::_pan_callback(Vector2 p_scroll_vec, Ref<InputEvent> p_event) {
h_scrollbar->set_value(h_scrollbar->get_value() - p_scroll_vec.x);
v_scrollbar->set_value(v_scrollbar->get_value() - p_scroll_vec.y);
+
+ connections_layer->queue_redraw();
}
void GraphEdit::_zoom_callback(float p_zoom_factor, Vector2 p_origin, Ref<InputEvent> p_event) {
+ // We need to invalidate all connections since we don't know whether
+ // the user is zooming/panning at the same time.
+ _invalidate_connection_line_cache();
+
set_zoom_custom(zoom * p_zoom_factor, 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_node == p_from && E.from_port == p_from_port && E.to_node == p_to && E.to_port == p_to_port) {
- if (!Math::is_equal_approx(E.activity, p_activity)) {
+ for (Ref<Connection> &c : connection_map[p_from]) {
+ if (c->from_node == p_from && c->from_port == p_from_port && c->to_node == p_to && c->to_port == p_to_port) {
+ if (!Math::is_equal_approx(c->activity, p_activity)) {
// Update only if changed.
- top_layer->queue_redraw();
minimap->queue_redraw();
+ c->_cache.dirty = true;
connections_layer->queue_redraw();
+ callable_mp(this, &GraphEdit::_update_top_connection_layer).call_deferred();
}
- E.activity = p_activity;
+ c->activity = p_activity;
return;
}
}
}
+void GraphEdit::reset_all_connection_activity() {
+ bool changed = false;
+ for (Ref<Connection> &conn : connections) {
+ if (conn->activity > 0) {
+ changed = true;
+ conn->_cache.dirty = true;
+ }
+ conn->activity = 0;
+ }
+ if (changed) {
+ connections_layer->queue_redraw();
+ }
+}
+
void GraphEdit::clear_connections() {
+ for (Ref<Connection> &c : connections) {
+ c->_cache.line->queue_free();
+ }
+
connections.clear();
+ connection_map.clear();
+
minimap->queue_redraw();
queue_redraw();
connections_layer->queue_redraw();
@@ -1462,10 +1713,10 @@ void GraphEdit::force_connection_drag_end() {
ERR_FAIL_COND_MSG(!connecting, "Drag end requested without active drag!");
connecting = false;
connecting_valid = false;
- top_layer->queue_redraw();
minimap->queue_redraw();
queue_redraw();
connections_layer->queue_redraw();
+ callable_mp(this, &GraphEdit::_update_top_connection_layer).call_deferred();
emit_signal(SNAME("connection_drag_ended"));
}
@@ -1497,7 +1748,8 @@ void GraphEdit::set_zoom_custom(float p_zoom, const Vector2 &p_center) {
Vector2 scrollbar_offset = (Vector2(h_scrollbar->get_value(), v_scrollbar->get_value()) + p_center) / zoom;
zoom = p_zoom;
- top_layer->queue_redraw();
+
+ callable_mp(this, &GraphEdit::_update_top_connection_layer).call_deferred();
zoom_minus_button->set_disabled(zoom == zoom_min);
zoom_plus_button->set_disabled(zoom == zoom_max);
@@ -1590,15 +1842,42 @@ void GraphEdit::remove_valid_left_disconnect_type(int p_type) {
}
TypedArray<Dictionary> GraphEdit::_get_connection_list() const {
- List<Connection> conns;
- get_connection_list(&conns);
+ List<Ref<Connection>> conns = get_connection_list();
+
TypedArray<Dictionary> arr;
- for (const Connection &E : conns) {
+ for (const Ref<Connection> &conn : conns) {
Dictionary d;
- d["from_node"] = E.from_node;
- d["from_port"] = E.from_port;
- d["to_node"] = E.to_node;
- d["to_port"] = E.to_port;
+ d["from_node"] = conn->from_node;
+ d["from_port"] = conn->from_port;
+ d["to_node"] = conn->to_node;
+ d["to_port"] = conn->to_port;
+ arr.push_back(d);
+ }
+ return arr;
+}
+
+Dictionary GraphEdit::_get_closest_connection_at_point(const Vector2 &p_point, float p_max_distance) const {
+ Dictionary ret;
+ Ref<Connection> c = get_closest_connection_at_point(p_point, p_max_distance);
+ if (c.is_valid()) {
+ ret["from_node"] = c->from_node;
+ ret["from_port"] = c->from_port;
+ ret["to_node"] = c->to_node;
+ ret["to_port"] = c->to_port;
+ }
+ return ret;
+}
+
+TypedArray<Dictionary> GraphEdit::_get_connections_intersecting_with_rect(const Rect2 &p_rect) const {
+ List<Ref<Connection>> intersecting_connections = get_connections_intersecting_with_rect(p_rect);
+
+ TypedArray<Dictionary> arr;
+ for (const Ref<Connection> &conn : intersecting_connections) {
+ Dictionary d;
+ d["from_node"] = conn->from_node;
+ d["from_port"] = conn->from_port;
+ d["to_node"] = conn->to_node;
+ d["to_port"] = conn->to_port;
arr.push_back(d);
}
return arr;
@@ -1622,6 +1901,16 @@ void GraphEdit::_update_zoom_label() {
zoom_label->set_text(zoom_text);
}
+void GraphEdit::_invalidate_connection_line_cache() {
+ for (Ref<Connection> &c : connections) {
+ c->_cache.dirty = true;
+ }
+}
+
+float GraphEdit::_get_shader_line_width() {
+ return lines_thickness * theme_cache.base_scale + 4.0;
+}
+
void GraphEdit::add_valid_connection_type(int p_type, int p_with_type) {
ConnectionType ct(p_type, p_with_type);
valid_connection_types.insert(ct);
@@ -1806,6 +2095,15 @@ bool GraphEdit::is_showing_arrange_button() const {
return show_arrange_button;
}
+void GraphEdit::override_connections_shader(const Ref<Shader> &p_shader) {
+ connections_shader = p_shader;
+
+ _invalidate_connection_line_cache();
+ connections_layer->queue_redraw();
+ minimap->queue_redraw();
+ callable_mp(this, &GraphEdit::_update_top_connection_layer).call_deferred();
+}
+
void GraphEdit::_minimap_toggled() {
if (is_minimap_enabled()) {
minimap->set_visible(true);
@@ -1817,6 +2115,8 @@ void GraphEdit::_minimap_toggled() {
void GraphEdit::set_connection_lines_curvature(float p_curvature) {
lines_curvature = p_curvature;
+ _invalidate_connection_line_cache();
+ connections_layer->queue_redraw();
queue_redraw();
}
@@ -1825,10 +2125,13 @@ float GraphEdit::get_connection_lines_curvature() const {
}
void GraphEdit::set_connection_lines_thickness(float p_thickness) {
+ ERR_FAIL_COND_MSG(p_thickness < 0, "Connection lines thickness must be greater than or equal to 0.");
if (lines_thickness == p_thickness) {
return;
}
lines_thickness = p_thickness;
+ _invalidate_connection_line_cache();
+ connections_layer->queue_redraw();
queue_redraw();
}
@@ -1841,6 +2144,8 @@ void GraphEdit::set_connection_lines_antialiased(bool p_antialiased) {
return;
}
lines_antialiased = p_antialiased;
+ _invalidate_connection_line_cache();
+ connections_layer->queue_redraw();
queue_redraw();
}
@@ -1870,6 +2175,8 @@ void GraphEdit::_bind_methods() {
ClassDB::bind_method(D_METHOD("disconnect_node", "from_node", "from_port", "to_node", "to_port"), &GraphEdit::disconnect_node);
ClassDB::bind_method(D_METHOD("set_connection_activity", "from_node", "from_port", "to_node", "to_port", "amount"), &GraphEdit::set_connection_activity);
ClassDB::bind_method(D_METHOD("get_connection_list"), &GraphEdit::_get_connection_list);
+ ClassDB::bind_method(D_METHOD("get_closest_connection_at_point", "point", "max_distance"), &GraphEdit::_get_closest_connection_at_point, DEFVAL(4.0));
+ ClassDB::bind_method(D_METHOD("get_connections_intersecting_with_rect", "rect"), &GraphEdit::_get_connections_intersecting_with_rect);
ClassDB::bind_method(D_METHOD("clear_connections"), &GraphEdit::clear_connections);
ClassDB::bind_method(D_METHOD("force_connection_drag_end"), &GraphEdit::force_connection_drag_end);
ClassDB::bind_method(D_METHOD("get_scroll_offset"), &GraphEdit::get_scroll_offset);
@@ -1971,7 +2278,7 @@ void GraphEdit::_bind_methods() {
ADD_GROUP("Connection Lines", "connection_lines");
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "connection_lines_curvature"), "set_connection_lines_curvature", "get_connection_lines_curvature");
- ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "connection_lines_thickness", PROPERTY_HINT_NONE, "suffix:px"), "set_connection_lines_thickness", "get_connection_lines_thickness");
+ ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "connection_lines_thickness", PROPERTY_HINT_RANGE, "0,100,0.1,suffix:px"), "set_connection_lines_thickness", "get_connection_lines_thickness");
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "connection_lines_antialiased"), "set_connection_lines_antialiased", "is_connection_lines_antialiased");
ADD_GROUP("Zoom", "");
@@ -2025,6 +2332,9 @@ void GraphEdit::_bind_methods() {
BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, GraphEdit, grid_minor);
BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_COLOR, GraphEdit, activity_color, "activity");
+ BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, GraphEdit, connection_hover_tint_color);
+ BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, GraphEdit, connection_valid_target_tint_color);
+ BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, GraphEdit, connection_rim_color);
BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, GraphEdit, selection_fill);
BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, GraphEdit, selection_stroke);
@@ -2056,21 +2366,33 @@ GraphEdit::GraphEdit() {
panner.instantiate();
panner->set_callbacks(callable_mp(this, &GraphEdit::_pan_callback), callable_mp(this, &GraphEdit::_zoom_callback));
- top_layer = memnew(GraphEditFilter(this));
+ top_layer = memnew(Control);
add_child(top_layer, false, INTERNAL_MODE_BACK);
- top_layer->set_mouse_filter(MOUSE_FILTER_PASS);
+ top_layer->set_mouse_filter(MOUSE_FILTER_IGNORE);
top_layer->set_anchors_and_offsets_preset(Control::PRESET_FULL_RECT);
top_layer->connect("draw", callable_mp(this, &GraphEdit::_top_layer_draw));
- top_layer->connect("gui_input", callable_mp(this, &GraphEdit::_top_layer_input));
top_layer->connect("focus_exited", callable_mp(panner.ptr(), &ViewPanner::release_pan_key));
connections_layer = memnew(Control);
add_child(connections_layer, false, INTERNAL_MODE_FRONT);
- connections_layer->connect("draw", callable_mp(this, &GraphEdit::_connections_layer_draw));
+ connections_layer->connect("draw", callable_mp(this, &GraphEdit::_update_connections));
connections_layer->set_name("_connection_layer");
connections_layer->set_disable_visibility_clip(true); // Necessary, so it can draw freely and be offset.
connections_layer->set_mouse_filter(MOUSE_FILTER_IGNORE);
+ top_connection_layer = memnew(GraphEditFilter(this));
+ add_child(top_connection_layer, false, INTERNAL_MODE_BACK);
+
+ connections_shader = default_connections_shader;
+
+ top_connection_layer->set_mouse_filter(MOUSE_FILTER_PASS);
+ top_connection_layer->set_anchors_and_offsets_preset(Control::PRESET_FULL_RECT);
+ top_connection_layer->connect("gui_input", callable_mp(this, &GraphEdit::_top_connection_layer_input));
+
+ dragged_connection_line = memnew(Line2D);
+ dragged_connection_line->set_texture_mode(Line2D::LINE_TEXTURE_STRETCH);
+ top_connection_layer->add_child(dragged_connection_line);
+
h_scrollbar = memnew(HScrollBar);
h_scrollbar->set_name("_h_scroll");
top_layer->add_child(h_scrollbar);
diff --git a/scene/gui/graph_edit.h b/scene/gui/graph_edit.h
index 31cb495bf8..e24f039e84 100644
--- a/scene/gui/graph_edit.h
+++ b/scene/gui/graph_edit.h
@@ -39,6 +39,7 @@ class GraphEdit;
class GraphEditArranger;
class HScrollBar;
class Label;
+class Line2D;
class PanelContainer;
class SpinBox;
class ViewPanner;
@@ -112,12 +113,25 @@ class GraphEdit : public Control {
GDCLASS(GraphEdit, Control);
public:
- struct Connection {
+ struct Connection : RefCounted {
StringName from_node;
StringName to_node;
int from_port = 0;
int to_port = 0;
float activity = 0.0;
+
+ private:
+ struct Cache {
+ bool dirty = true;
+ Vector2 from_pos; // In graph space.
+ Vector2 to_pos; // In graph space.
+ Color from_color;
+ Color to_color;
+ Rect2 aabb; // In local screen space.
+ Line2D *line = nullptr; // In local screen space.
+ } _cache;
+
+ friend class GraphEdit;
};
// Should be in sync with ControlScheme in ViewPanner.
@@ -184,15 +198,15 @@ private:
GridPattern grid_pattern = GRID_PATTERN_LINES;
bool connecting = false;
- String connecting_from;
- bool connecting_out = false;
- int connecting_index = 0;
+ StringName connecting_from_node;
+ bool connecting_from_output = false;
int connecting_type = 0;
Color connecting_color;
- bool connecting_target = false;
- Vector2 connecting_to;
- StringName connecting_target_to;
- int connecting_target_index = 0;
+ Vector2 connecting_to_point; // In local screen space.
+ bool connecting_target_valid = false;
+ StringName connecting_target_node;
+ int connecting_from_port_index = 0;
+ int connecting_target_port_index = 0;
bool just_disconnected = false;
bool connecting_valid = false;
@@ -222,18 +236,28 @@ private:
bool right_disconnects = false;
bool updating = false;
bool awaiting_scroll_offset_update = false;
- List<Connection> connections;
- float lines_thickness = 2.0f;
+ List<Ref<Connection>> connections;
+ HashMap<StringName, List<Ref<Connection>>> connection_map;
+ Ref<Connection> hovered_connection;
+
+ float lines_thickness = 4.0f;
float lines_curvature = 0.5f;
bool lines_antialiased = true;
PanelContainer *menu_panel = nullptr;
HBoxContainer *menu_hbox = nullptr;
Control *connections_layer = nullptr;
- GraphEditFilter *top_layer = nullptr;
+
+ GraphEditFilter *top_connection_layer = nullptr; // Draws a dragged connection. Necessary since the connection line shader can't be applied to the whole top layer.
+ Line2D *dragged_connection_line = nullptr;
+ Control *top_layer = nullptr; // Used for drawing the box selection rect. Contains the minimap, menu panel and the scrollbars.
+
GraphEditMinimap *minimap = nullptr;
+ static Ref<Shader> default_connections_shader;
+ Ref<Shader> connections_shader;
+
Ref<GraphEditArranger> arranger;
HashSet<ConnectionType, ConnectionType> valid_connection_types;
@@ -248,6 +272,10 @@ private:
Color grid_minor;
Color activity_color;
+ Color connection_hover_tint_color;
+ Color connection_valid_target_tint_color;
+ Color connection_rim_color;
+
Color selection_fill;
Color selection_stroke;
@@ -274,30 +302,35 @@ private:
void _zoom_plus();
void _update_zoom_label();
- void _draw_connection_line(CanvasItem *p_where, const Vector2 &p_from, const Vector2 &p_to, const Color &p_color, const Color &p_to_color, float p_width, float p_zoom);
-
void _graph_element_selected(Node *p_node);
void _graph_element_deselected(Node *p_node);
void _graph_element_moved_to_front(Node *p_node);
void _graph_element_resized(Vector2 p_new_minsize, Node *p_node);
void _graph_element_moved(Node *p_node);
void _graph_node_slot_updated(int p_index, Node *p_node);
+ void _graph_node_rect_changed(GraphNode *p_node);
void _update_scroll();
void _update_scroll_offset();
void _scroll_moved(double);
virtual void gui_input(const Ref<InputEvent> &p_ev) override;
- void _top_layer_input(const Ref<InputEvent> &p_ev);
+ void _top_connection_layer_input(const Ref<InputEvent> &p_ev);
- bool is_in_port_hotzone(const Vector2 &p_pos, const Vector2 &p_mouse_pos, const Vector2i &p_port_size, bool p_left);
+ float _get_shader_line_width();
+ void _draw_minimap_connection_line(CanvasItem *p_where, const Vector2 &p_from, const Vector2 &p_to, const Color &p_color, const Color &p_to_color);
+ void _invalidate_connection_line_cache();
+ void _update_top_connection_layer();
+ void _update_connections();
void _top_layer_draw();
- void _connections_layer_draw();
void _minimap_draw();
-
void _draw_grid();
+ bool is_in_port_hotzone(const Vector2 &p_pos, const Vector2 &p_mouse_pos, const Vector2i &p_port_size, bool p_left);
+
TypedArray<Dictionary> _get_connection_list() const;
+ Dictionary _get_closest_connection_at_point(const Vector2 &p_point, float p_max_distance = 4.0) const;
+ TypedArray<Dictionary> _get_connections_intersecting_with_rect(const Rect2 &p_rect) const;
friend class GraphEditFilter;
bool _filter_input(const Point2 &p_point);
@@ -313,6 +346,7 @@ private:
#ifndef DISABLE_DEPRECATED
bool _is_arrange_nodes_button_hidden_bind_compat_81582() const;
void _set_arrange_nodes_button_hidden_bind_compat_81582(bool p_enable);
+ PackedVector2Array _get_connection_line_bind_compat_86158(const Vector2 &p_from, const Vector2 &p_to);
#endif
protected:
@@ -336,6 +370,9 @@ protected:
GDVIRTUAL4R(bool, _is_node_hover_valid, StringName, int, StringName, int);
public:
+ static void init_shaders();
+ static void finish_shaders();
+
virtual CursorShape get_cursor_shape(const Point2 &p_pos = Point2i()) const override;
PackedStringArray get_configuration_warnings() const override;
@@ -344,12 +381,17 @@ public:
bool is_node_connected(const StringName &p_from, int p_from_port, const StringName &p_to, int p_to_port);
void disconnect_node(const StringName &p_from, int p_from_port, const StringName &p_to, int p_to_port);
void clear_connections();
+
void force_connection_drag_end();
+ const List<Ref<Connection>> &get_connection_list() const;
+ virtual PackedVector2Array get_connection_line(const Vector2 &p_from, const Vector2 &p_to) const;
+ Ref<Connection> get_closest_connection_at_point(const Vector2 &p_point, float p_max_distance = 4.0) const;
+ List<Ref<Connection>> get_connections_intersecting_with_rect(const Rect2 &p_rect) const;
- virtual PackedVector2Array get_connection_line(const Vector2 &p_from, const Vector2 &p_to);
virtual bool is_node_hover_valid(const StringName &p_from, int p_from_port, const StringName &p_to, int p_to_port);
void set_connection_activity(const StringName &p_from, int p_from_port, const StringName &p_to, int p_to_port, float p_activity);
+ void reset_all_connection_activity();
void add_valid_connection_type(int p_type, int p_with_type);
void remove_valid_connection_type(int p_type, int p_with_type);
@@ -392,10 +434,10 @@ public:
void set_show_arrange_button(bool p_hidden);
bool is_showing_arrange_button() const;
- GraphEditFilter *get_top_layer() const { return top_layer; }
+ Control *get_top_layer() const { return top_layer; }
GraphEditMinimap *get_minimap() const { return minimap; }
- void get_connection_list(List<Connection> *r_connections) const;
+ void override_connections_shader(const Ref<Shader> &p_shader);
void set_right_disconnects(bool p_enable);
bool is_right_disconnects_enabled() const;
diff --git a/scene/gui/graph_edit_arranger.cpp b/scene/gui/graph_edit_arranger.cpp
index 29c3056b3b..49998beb42 100644
--- a/scene/gui/graph_edit_arranger.cpp
+++ b/scene/gui/graph_edit_arranger.cpp
@@ -65,8 +65,7 @@ void GraphEditArranger::arrange_nodes() {
float gap_v = 100.0f;
float gap_h = 100.0f;
- List<GraphEdit::Connection> connection_list;
- graph_edit->get_connection_list(&connection_list);
+ List<Ref<GraphEdit::Connection>> connection_list = graph_edit->get_connection_list();
for (int i = graph_edit->get_child_count() - 1; i >= 0; i--) {
GraphNode *graph_element = Object::cast_to<GraphNode>(graph_edit->get_child(i));
@@ -77,15 +76,16 @@ void GraphEditArranger::arrange_nodes() {
if (graph_element->is_selected() || arrange_entire_graph) {
selected_nodes.insert(graph_element->get_name());
HashSet<StringName> s;
- for (List<GraphEdit::Connection>::Element *E = connection_list.front(); E; E = E->next()) {
- GraphNode *p_from = Object::cast_to<GraphNode>(node_names[E->get().from_node]);
- if (E->get().to_node == graph_element->get_name() && (p_from->is_selected() || arrange_entire_graph) && E->get().to_node != E->get().from_node) {
+
+ for (const Ref<GraphEdit::Connection> &connection : connection_list) {
+ GraphNode *p_from = Object::cast_to<GraphNode>(node_names[connection->from_node]);
+ if (connection->to_node == graph_element->get_name() && (p_from->is_selected() || arrange_entire_graph) && connection->to_node != connection->from_node) {
if (!s.has(p_from->get_name())) {
s.insert(p_from->get_name());
}
- String s_connection = String(p_from->get_name()) + " " + String(E->get().to_node);
+ String s_connection = String(p_from->get_name()) + " " + String(connection->to_node);
StringName _connection(s_connection);
- Pair<int, int> ports(E->get().from_port, E->get().to_port);
+ Pair<int, int> ports(connection->from_port, connection->to_port);
port_info.insert(_connection, ports);
}
}
@@ -437,31 +437,30 @@ float GraphEditArranger::_calculate_threshold(const StringName &p_v, const Strin
float threshold = p_current_threshold;
if (p_v == p_w) {
int min_order = MAX_ORDER;
- GraphEdit::Connection incoming;
- List<GraphEdit::Connection> connection_list;
- graph_edit->get_connection_list(&connection_list);
- for (List<GraphEdit::Connection>::Element *E = connection_list.front(); E; E = E->next()) {
- if (E->get().to_node == p_w) {
- ORDER(E->get().from_node, r_layers);
+ Ref<GraphEdit::Connection> incoming;
+ List<Ref<GraphEdit::Connection>> connection_list = graph_edit->get_connection_list();
+ for (const Ref<GraphEdit::Connection> &connection : connection_list) {
+ if (connection->to_node == p_w) {
+ ORDER(connection->from_node, r_layers);
if (min_order > order) {
min_order = order;
- incoming = E->get();
+ incoming = connection;
}
}
}
- if (incoming.from_node != StringName()) {
- GraphNode *gnode_from = Object::cast_to<GraphNode>(r_node_names[incoming.from_node]);
+ if (incoming.is_valid()) {
+ GraphNode *gnode_from = Object::cast_to<GraphNode>(r_node_names[incoming->from_node]);
GraphNode *gnode_to = Object::cast_to<GraphNode>(r_node_names[p_w]);
- Vector2 pos_from = gnode_from->get_output_port_position(incoming.from_port) * graph_edit->get_zoom();
- Vector2 pos_to = gnode_to->get_input_port_position(incoming.to_port) * graph_edit->get_zoom();
+ Vector2 pos_from = gnode_from->get_output_port_position(incoming->from_port) * graph_edit->get_zoom();
+ Vector2 pos_to = gnode_to->get_input_port_position(incoming->to_port) * graph_edit->get_zoom();
// If connected block node is selected, calculate thershold or add current block to list.
if (gnode_from->is_selected()) {
- Vector2 connected_block_pos = r_node_positions[r_root[incoming.from_node]];
+ Vector2 connected_block_pos = r_node_positions[r_root[incoming->from_node]];
if (connected_block_pos.y != FLT_MAX) {
//Connected block is placed, calculate threshold.
- threshold = connected_block_pos.y + (real_t)r_inner_shift[incoming.from_node] - (real_t)r_inner_shift[p_w] + pos_from.y - pos_to.y;
+ threshold = connected_block_pos.y + (real_t)r_inner_shift[incoming->from_node] - (real_t)r_inner_shift[p_w] + pos_from.y - pos_to.y;
}
}
}
@@ -469,31 +468,30 @@ float GraphEditArranger::_calculate_threshold(const StringName &p_v, const Strin
if (threshold == FLT_MIN && (StringName)r_align[p_w] == p_v) {
// This time, pick an outgoing edge and repeat as above!
int min_order = MAX_ORDER;
- GraphEdit::Connection outgoing;
- List<GraphEdit::Connection> connection_list;
- graph_edit->get_connection_list(&connection_list);
- for (List<GraphEdit::Connection>::Element *E = connection_list.front(); E; E = E->next()) {
- if (E->get().from_node == p_w) {
- ORDER(E->get().to_node, r_layers);
+ Ref<GraphEdit::Connection> outgoing;
+ List<Ref<GraphEdit::Connection>> connection_list = graph_edit->get_connection_list();
+ for (const Ref<GraphEdit::Connection> &connection : connection_list) {
+ if (connection->from_node == p_w) {
+ ORDER(connection->to_node, r_layers);
if (min_order > order) {
min_order = order;
- outgoing = E->get();
+ outgoing = connection;
}
}
}
- if (outgoing.to_node != StringName()) {
+ if (outgoing.is_valid()) {
GraphNode *gnode_from = Object::cast_to<GraphNode>(r_node_names[p_w]);
- GraphNode *gnode_to = Object::cast_to<GraphNode>(r_node_names[outgoing.to_node]);
- Vector2 pos_from = gnode_from->get_output_port_position(outgoing.from_port) * graph_edit->get_zoom();
- Vector2 pos_to = gnode_to->get_input_port_position(outgoing.to_port) * graph_edit->get_zoom();
+ GraphNode *gnode_to = Object::cast_to<GraphNode>(r_node_names[outgoing->to_node]);
+ Vector2 pos_from = gnode_from->get_output_port_position(outgoing->from_port) * graph_edit->get_zoom();
+ Vector2 pos_to = gnode_to->get_input_port_position(outgoing->to_port) * graph_edit->get_zoom();
// If connected block node is selected, calculate thershold or add current block to list.
if (gnode_to->is_selected()) {
- Vector2 connected_block_pos = r_node_positions[r_root[outgoing.to_node]];
+ Vector2 connected_block_pos = r_node_positions[r_root[outgoing->to_node]];
if (connected_block_pos.y != FLT_MAX) {
//Connected block is placed. Calculate threshold
- threshold = connected_block_pos.y + (real_t)r_inner_shift[outgoing.to_node] - (real_t)r_inner_shift[p_w] + pos_from.y - pos_to.y;
+ threshold = connected_block_pos.y + (real_t)r_inner_shift[outgoing->to_node] - (real_t)r_inner_shift[p_w] + pos_from.y - pos_to.y;
}
}
}
diff --git a/scene/gui/option_button.cpp b/scene/gui/option_button.cpp
index 6386bb20c3..45cc9623be 100644
--- a/scene/gui/option_button.cpp
+++ b/scene/gui/option_button.cpp
@@ -60,7 +60,7 @@ Size2 OptionButton::get_minimum_size() const {
}
if (has_theme_icon(SNAME("arrow"))) {
- const Size2 padding = theme_cache.normal->get_minimum_size();
+ const Size2 padding = _get_current_stylebox()->get_minimum_size();
const Size2 arrow_size = theme_cache.arrow_icon->get_size();
Size2 content_size = minsize - padding;
@@ -605,6 +605,8 @@ void OptionButton::set_disable_shortcuts(bool p_disabled) {
OptionButton::OptionButton(const String &p_text) :
Button(p_text) {
+ _set_h_separation_is_valid_when_no_text(true);
+
set_toggle_mode(true);
set_process_shortcut_input(true);
set_text_alignment(HORIZONTAL_ALIGNMENT_LEFT);
diff --git a/scene/register_scene_types.cpp b/scene/register_scene_types.cpp
index 111d6447a0..64a1c72f9d 100644
--- a/scene/register_scene_types.cpp
+++ b/scene/register_scene_types.cpp
@@ -1184,7 +1184,9 @@ void register_scene_types() {
}
if (RenderingServer::get_singleton()) {
- ColorPicker::init_shaders(); // RenderingServer needs to exist for this to succeed.
+ // RenderingServer needs to exist for this to succeed.
+ ColorPicker::init_shaders();
+ GraphEdit::init_shaders();
}
SceneDebugger::initialize();
@@ -1236,6 +1238,7 @@ void unregister_scene_types() {
ParticleProcessMaterial::finish_shaders();
CanvasItemMaterial::finish_shaders();
ColorPicker::finish_shaders();
+ GraphEdit::finish_shaders();
SceneStringNames::free();
OS::get_singleton()->benchmark_end_measure("Scene", "Unregister Types");
diff --git a/scene/theme/default_theme.cpp b/scene/theme/default_theme.cpp
index 005a88d391..fa83e06315 100644
--- a/scene/theme/default_theme.cpp
+++ b/scene/theme/default_theme.cpp
@@ -225,20 +225,20 @@ void fill_default_theme(Ref<Theme> &theme, const Ref<Font> &default_font, const
// OptionButton
theme->set_stylebox("focus", "OptionButton", focus);
- Ref<StyleBox> sb_optbutton_normal = make_flat_stylebox(style_normal_color, 2 * default_margin, default_margin, 21, default_margin);
- Ref<StyleBox> sb_optbutton_hover = make_flat_stylebox(style_hover_color, 2 * default_margin, default_margin, 21, default_margin);
- Ref<StyleBox> sb_optbutton_pressed = make_flat_stylebox(style_pressed_color, 2 * default_margin, default_margin, 21, default_margin);
- Ref<StyleBox> sb_optbutton_disabled = make_flat_stylebox(style_disabled_color, 2 * default_margin, default_margin, 21, default_margin);
+ Ref<StyleBox> sb_optbutton_normal = make_flat_stylebox(style_normal_color, 2 * default_margin, default_margin, 2 * default_margin, default_margin);
+ Ref<StyleBox> sb_optbutton_hover = make_flat_stylebox(style_hover_color, 2 * default_margin, default_margin, 2 * default_margin, default_margin);
+ Ref<StyleBox> sb_optbutton_pressed = make_flat_stylebox(style_pressed_color, 2 * default_margin, default_margin, 2 * default_margin, default_margin);
+ Ref<StyleBox> sb_optbutton_disabled = make_flat_stylebox(style_disabled_color, 2 * default_margin, default_margin, 2 * default_margin, default_margin);
theme->set_stylebox("normal", "OptionButton", sb_optbutton_normal);
theme->set_stylebox("hover", "OptionButton", sb_optbutton_hover);
theme->set_stylebox("pressed", "OptionButton", sb_optbutton_pressed);
theme->set_stylebox("disabled", "OptionButton", sb_optbutton_disabled);
- Ref<StyleBox> sb_optbutton_normal_mirrored = make_flat_stylebox(style_normal_color, 21, default_margin, 2 * default_margin, default_margin);
- Ref<StyleBox> sb_optbutton_hover_mirrored = make_flat_stylebox(style_hover_color, 21, default_margin, 2 * default_margin, default_margin);
- Ref<StyleBox> sb_optbutton_pressed_mirrored = make_flat_stylebox(style_pressed_color, 21, default_margin, 2 * default_margin, default_margin);
- Ref<StyleBox> sb_optbutton_disabled_mirrored = make_flat_stylebox(style_disabled_color, 21, default_margin, 2 * default_margin, default_margin);
+ Ref<StyleBox> sb_optbutton_normal_mirrored = make_flat_stylebox(style_normal_color, 2 * default_margin, default_margin, 2 * default_margin, default_margin);
+ Ref<StyleBox> sb_optbutton_hover_mirrored = make_flat_stylebox(style_hover_color, 2 * default_margin, default_margin, 2 * default_margin, default_margin);
+ Ref<StyleBox> sb_optbutton_pressed_mirrored = make_flat_stylebox(style_pressed_color, 2 * default_margin, default_margin, 2 * default_margin, default_margin);
+ Ref<StyleBox> sb_optbutton_disabled_mirrored = make_flat_stylebox(style_disabled_color, 2 * default_margin, default_margin, 2 * default_margin, default_margin);
theme->set_stylebox("normal_mirrored", "OptionButton", sb_optbutton_normal_mirrored);
theme->set_stylebox("hover_mirrored", "OptionButton", sb_optbutton_hover_mirrored);
@@ -1161,6 +1161,9 @@ void fill_default_theme(Ref<Theme> &theme, const Ref<Font> &default_font, const
theme->set_color("selection_fill", "GraphEdit", Color(1, 1, 1, 0.3));
theme->set_color("selection_stroke", "GraphEdit", Color(1, 1, 1, 0.8));
theme->set_color("activity", "GraphEdit", Color(1, 1, 1));
+ theme->set_color("connection_hover_tint_color", "GraphEdit", Color(0, 0, 0, 0.3));
+ theme->set_color("connection_valid_target_tint_color", "GraphEdit", Color(1, 1, 1, 0.4));
+ theme->set_color("connection_rim_color", "GraphEdit", style_normal_color);
// Visual Node Ports