diff options
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 |