diff --git a/packages/vyuh_node_flow/CHANGELOG.md b/packages/vyuh_node_flow/CHANGELOG.md index 4197652..59f7654 100644 --- a/packages/vyuh_node_flow/CHANGELOG.md +++ b/packages/vyuh_node_flow/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.27.3+2 + + - **FIX**: wire default port hover events. + ## 0.27.3+1 - **FIX**(ports): render a `PortType.both` port once instead of twice, which raised a duplicate `ValueKey` assertion. ([#36](https://github.com/vyuh-tech/vyuh_node_flow/pull/36)) diff --git a/packages/vyuh_node_flow/lib/src/editor/layers/nodes_layer.dart b/packages/vyuh_node_flow/lib/src/editor/layers/nodes_layer.dart index 81dd09f..3337fc6 100644 --- a/packages/vyuh_node_flow/lib/src/editor/layers/nodes_layer.dart +++ b/packages/vyuh_node_flow/lib/src/editor/layers/nodes_layer.dart @@ -43,6 +43,7 @@ class NodesLayer extends StatelessWidget { this.onNodeContextMenu, this.onNodeMouseEnter, this.onNodeMouseLeave, + this.onPortHover, this.onPortContextMenu, this.portSnapDistance = 8.0, this.thumbnailBuilder, @@ -63,6 +64,7 @@ class NodesLayer extends StatelessWidget { onNodeContextMenu, void Function(Node node)? onNodeMouseEnter, void Function(Node node)? onNodeMouseLeave, + void Function(String nodeId, String portId, bool isHover)? onPortHover, void Function(String nodeId, String portId, ScreenPosition screenPosition)? onPortContextMenu, double portSnapDistance = 8.0, @@ -78,6 +80,7 @@ class NodesLayer extends StatelessWidget { onNodeContextMenu: onNodeContextMenu, onNodeMouseEnter: onNodeMouseEnter, onNodeMouseLeave: onNodeMouseLeave, + onPortHover: onPortHover, onPortContextMenu: onPortContextMenu, portSnapDistance: portSnapDistance, ); @@ -98,6 +101,7 @@ class NodesLayer extends StatelessWidget { onNodeContextMenu, void Function(Node node)? onNodeMouseEnter, void Function(Node node)? onNodeMouseLeave, + void Function(String nodeId, String portId, bool isHover)? onPortHover, void Function(String nodeId, String portId, ScreenPosition screenPosition)? onPortContextMenu, double portSnapDistance = 8.0, @@ -113,6 +117,7 @@ class NodesLayer extends StatelessWidget { onNodeContextMenu: onNodeContextMenu, onNodeMouseEnter: onNodeMouseEnter, onNodeMouseLeave: onNodeMouseLeave, + onPortHover: onPortHover, onPortContextMenu: onPortContextMenu, portSnapDistance: portSnapDistance, ); @@ -133,6 +138,7 @@ class NodesLayer extends StatelessWidget { onNodeContextMenu, void Function(Node node)? onNodeMouseEnter, void Function(Node node)? onNodeMouseLeave, + void Function(String nodeId, String portId, bool isHover)? onPortHover, void Function(String nodeId, String portId, ScreenPosition screenPosition)? onPortContextMenu, double portSnapDistance = 8.0, @@ -148,6 +154,7 @@ class NodesLayer extends StatelessWidget { onNodeContextMenu: onNodeContextMenu, onNodeMouseEnter: onNodeMouseEnter, onNodeMouseLeave: onNodeMouseLeave, + onPortHover: onPortHover, onPortContextMenu: onPortContextMenu, portSnapDistance: portSnapDistance, ); @@ -186,6 +193,9 @@ class NodesLayer extends StatelessWidget { /// Callback invoked when mouse leaves a node. final void Function(Node node)? onNodeMouseLeave; + /// Callback invoked when a port hover state changes. + final void Function(String nodeId, String portId, bool isHover)? onPortHover; + /// Callback invoked when a port is right-clicked (context menu). /// The [screenPosition] is in screen/global coordinates for menu positioning. final void Function( @@ -291,6 +301,7 @@ class NodesLayer extends StatelessWidget { onMouseLeave: onNodeMouseLeave != null ? () => onNodeMouseLeave!(node) : null, + onPortHover: onPortHover, onPortContextMenu: onPortContextMenu, portSnapDistance: portSnapDistance, child: NodeWidget( diff --git a/packages/vyuh_node_flow/lib/src/editor/node_flow_editor.dart b/packages/vyuh_node_flow/lib/src/editor/node_flow_editor.dart index bd39ab0..67a3f72 100644 --- a/packages/vyuh_node_flow/lib/src/editor/node_flow_editor.dart +++ b/packages/vyuh_node_flow/lib/src/editor/node_flow_editor.dart @@ -637,6 +637,7 @@ class _NodeFlowEditorState extends State> onNodeContextMenu: _handleNodeContextMenu, onNodeMouseEnter: _handleNodeMouseEnter, onNodeMouseLeave: _handleNodeMouseLeave, + onPortHover: _handlePortHover, onPortContextMenu: _handlePortContextMenu, portSnapDistance: widget .controller @@ -706,6 +707,7 @@ class _NodeFlowEditorState extends State> onNodeContextMenu: _handleNodeContextMenu, onNodeMouseEnter: _handleNodeMouseEnter, onNodeMouseLeave: _handleNodeMouseLeave, + onPortHover: _handlePortHover, onPortContextMenu: _handlePortContextMenu, portSnapDistance: widget .controller @@ -737,6 +739,7 @@ class _NodeFlowEditorState extends State> onNodeContextMenu: _handleNodeContextMenu, onNodeMouseEnter: _handleNodeMouseEnter, onNodeMouseLeave: _handleNodeMouseLeave, + onPortHover: _handlePortHover, onPortContextMenu: _handlePortContextMenu, portSnapDistance: widget .controller diff --git a/packages/vyuh_node_flow/lib/src/editor/node_flow_editor_widget_handlers.dart b/packages/vyuh_node_flow/lib/src/editor/node_flow_editor_widget_handlers.dart index 38f6b91..c923856 100644 --- a/packages/vyuh_node_flow/lib/src/editor/node_flow_editor_widget_handlers.dart +++ b/packages/vyuh_node_flow/lib/src/editor/node_flow_editor_widget_handlers.dart @@ -71,6 +71,21 @@ extension _WidgetGestureHandlers on _NodeFlowEditorState { // Port Gesture Handlers // ============================================================ + /// Handles port hover changes. + void _handlePortHover(String nodeId, String portId, bool isHover) { + final node = widget.controller.getNode(nodeId); + if (node == null) return; + + final port = node.ports.where((p) => p.id == portId).firstOrNull; + if (port == null) return; + + if (isHover) { + widget.controller.events.port?.onMouseEnter?.call(node, port); + } else { + widget.controller.events.port?.onMouseLeave?.call(node, port); + } + } + /// Handles port context menu (right-click). /// /// The [screenPosition] is in screen/global coordinates, passed directly diff --git a/packages/vyuh_node_flow/pubspec.yaml b/packages/vyuh_node_flow/pubspec.yaml index d2c9cac..3d8e30c 100644 --- a/packages/vyuh_node_flow/pubspec.yaml +++ b/packages/vyuh_node_flow/pubspec.yaml @@ -1,6 +1,6 @@ name: vyuh_node_flow description: A flexible, high-performance node-based flow editor for Flutter. Build visual programming interfaces, workflow editors, diagrams, and data pipelines. -version: 0.27.3+1 +version: 0.27.3+2 homepage: https://flow.vyuh.tech repository: https://github.com/vyuh-tech/vyuh_node_flow diff --git a/packages/vyuh_node_flow/test/widget/node_flow_editor_test.dart b/packages/vyuh_node_flow/test/widget/node_flow_editor_test.dart index 901cfd9..d3240f3 100644 --- a/packages/vyuh_node_flow/test/widget/node_flow_editor_test.dart +++ b/packages/vyuh_node_flow/test/widget/node_flow_editor_test.dart @@ -1,6 +1,7 @@ @Tags(['widget']) library; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:vyuh_node_flow/vyuh_node_flow.dart'; @@ -339,6 +340,67 @@ void main() { expect(initCalled, isTrue); }); + + testWidgets('default ports fire mouse enter and leave callbacks', ( + tester, + ) async { + final node = createTestNodeWithOutputPort( + id: 'node-1', + portId: 'output-1', + ); + controller.addNode(node); + + Node? enteredNode; + Port? enteredPort; + Node? leftNode; + Port? leftPort; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: SizedBox( + width: 800, + height: 600, + child: NodeFlowEditor( + controller: controller, + nodeBuilder: (context, node) => const SizedBox.expand(), + theme: NodeFlowTheme.light, + events: NodeFlowEvents( + port: PortEvents( + onMouseEnter: (node, port) { + enteredNode = node; + enteredPort = port; + }, + onMouseLeave: (node, port) { + leftNode = node; + leftPort = port; + }, + ), + ), + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); + + final portFinder = find.byType(PortWidget); + expect(portFinder, findsOneWidget); + + final mouse = await tester.createGesture(kind: PointerDeviceKind.mouse); + await mouse.addPointer(location: const Offset(700, 500)); + await mouse.moveTo(tester.getCenter(portFinder)); + await tester.pump(); + + expect(enteredNode, same(node)); + expect(enteredPort, same(node.ports.single)); + + await mouse.moveTo(const Offset(700, 500)); + await tester.pump(); + + expect(leftNode, same(node)); + expect(leftPort, same(node.ports.single)); + }); }); group('NodeFlowEditor - Layout and Sizing', () {