Vyuh Node Flow

Port Widget

Customizing port rendering and interaction

Port Widget

Image

Port Widget States

Diagram showing a port in different states: idle (default color), connected (connected color), highlighted (glow effect during connection drag), and hovered (slightly larger with border). Each labeled with state name.

PROTOTYPE PREVIEW

The PortWidget renders connection points on nodes. Ports are where connections attach, enabling the flow relationships between nodes.

Default Rendering

By default, ports are rendered automatically based on the PortTheme in your NodeFlowTheme:

NodeFlowEditor<MyData>(
  controller: controller,
  theme: NodeFlowTheme(
    portTheme: PortTheme(
      size: Size(12, 12),
      color: Colors.blue,
      connectedColor: Colors.green,
      highlightColor: Colors.yellow,
      borderColor: Colors.white,
      borderWidth: 2.0,
    ),
  ),
  nodeBuilder: (context, node) => MyNodeWidget(node: node),
  // Ports are rendered automatically
)

Custom Port Builder

For complete control over port appearance, provide a portBuilder:

NodeFlowEditor<MyData>(
  controller: controller,
  theme: NodeFlowTheme.light,
  nodeBuilder: (context, node) => MyNodeWidget(node: node),
  portBuilder: (context, controller, node, port, isOutput, isConnected, nodeBounds) {
    // Custom port appearance based on port data
    final color = _getColorForPortType(port.name);

    return PortWidget(
      port: port,
      theme: controller.theme?.portTheme ?? PortTheme.light,
      controller: controller,
      nodeId: node.id,
      isOutput: isOutput,
      nodeBounds: nodeBounds,
      isConnected: isConnected,
      color: color,
    );
  },
)

Color _getColorForPortType(String portName) {
  if (portName.contains('error')) return Colors.red;
  if (portName.contains('data')) return Colors.blue;
  if (portName.contains('trigger')) return Colors.orange;
  return Colors.grey;
}

PortWidget Properties

Required Properties

PropertyTypeDescription
portPortThe port model to render
themePortThemeTheme for styling
controllerNodeFlowController<T>Controller for connection handling
nodeIdStringID of the parent node
isOutputboolWhether this is an output port
nodeBoundsRectParent node bounds in graph coordinates

Optional Properties

PropertyTypeDefaultDescription
isConnectedboolfalseWhether port has connections
sizeSize?themeOverride port size
colorColor?themeOverride idle color
connectedColorColor?themeOverride connected color
highlightColorColor?themeOverride highlight color
borderColorColor?themeOverride border color
borderWidthdouble?themeOverride border width
snapDistancedouble8.0Hit area expansion

Callbacks

CallbackTypeDescription
onTapValueChanged<Port>?Called when port is tapped
onDoubleTapVoidCallback?Called when port is double-tapped
onContextMenuvoid Function(Offset)?Called for right-click
onHoverValueChanged<(Port, bool)>?Called on hover state change

Property Resolution

Port properties are resolved in this order (lowest to highest priority):

  1. Theme values (PortTheme) - Base styling
  2. Widget overrides (constructor parameters) - Per-widget customization
  3. Model values (Port properties) - Per-port customization
// Example: Port model overrides take precedence
final port = Port(
  id: 'special-port',
  name: 'Output',
  position: PortPosition.right,
  type: PortType.source,
  size: Size(16, 16),  // Overrides theme size
  color: Colors.purple, // Overrides theme color
);

// Theme size is 12x12, but this port will be 16x16 purple

Port States

Animation

Port State Transitions

Animation showing port transitioning between states: idle → hovered (mouse enter) → dragging (connection start) → highlighted (valid target during drag) → connected (connection complete).

PROTOTYPE PREVIEW

Ports automatically display different visual states:

Idle State

Default appearance when no interaction is happening.

PortTheme(
  color: Colors.grey,        // Idle color
  borderColor: Colors.white,
  borderWidth: 2.0,
)

Connected State

When the port has one or more connections attached.

PortTheme(
  connectedColor: Colors.green,  // Connected color
)

Highlighted State

During connection dragging, valid target ports are highlighted.

PortTheme(
  highlightColor: Colors.yellow,
  highlightBorderColor: Colors.orange,
)

The Port.highlighted observable is automatically managed by the controller during connection operations.

Port Shapes

Ports can have different shapes based on the theme:

PortTheme(
  shape: PortShape.circle,  // Default circular port
)

// Or custom shapes via PortShapeWidget

Built-in Shapes

ShapeDescription
PortShape.circleCircular port (default)
PortShape.squareSquare port
PortShape.diamondDiamond/rotated square
PortShape.capsuleRounded rectangle

Custom Port Shape

For completely custom shapes, create a PortShapeWidget:

class TrianglePortShape extends StatelessWidget {
  final double size;
  final Color color;
  final bool isOutput;

  const TrianglePortShape({
    required this.size,
    required this.color,
    required this.isOutput,
  });

  @override
  Widget build(BuildContext context) {
    return CustomPaint(
      size: Size(size, size),
      painter: TrianglePainter(
        color: color,
        pointsRight: isOutput,
      ),
    );
  }
}

Port Positioning

Ports are positioned relative to their parent node based on PortPosition and optional offset:

// Port on left edge, centered vertically
Port(
  id: 'input',
  name: 'Input',
  position: PortPosition.left,
  type: PortType.target,
)

// Port on right edge, offset up by 20 pixels
Port(
  id: 'output-1',
  name: 'True',
  position: PortPosition.right,
  type: PortType.source,
  offset: Offset(0, -20),  // 20px above center
)

// Port on right edge, offset down by 20 pixels
Port(
  id: 'output-2',
  name: 'False',
  position: PortPosition.right,
  type: PortType.source,
  offset: Offset(0, 20),   // 20px below center
)
Image

Port Positioning

Node diagram showing ports at all four positions (left, right, top, bottom) with offset examples. Arrows indicate offset directions from the default centered position.

PROTOTYPE PREVIEW

Connection Handling

The PortWidget automatically handles connection creation:

  1. Drag Start: User drags from a port
  2. Temporary Connection: Dashed line follows cursor
  3. Port Highlighting: Valid target ports glow
  4. Validation: onBeforeComplete callback is invoked
  5. Connection Created: If valid, connection is added
events: NodeFlowEvents(
  connection: ConnectionEvents(
    onBeforeStart: (context) {
      // Validate if connection can start from this port
      if (context.sourcePort.name == 'disabled') {
        return ConnectionValidationResult(
          allowed: false,
          reason: 'Cannot connect from disabled port',
        );
      }
      return ConnectionValidationResult(allowed: true);
    },
    onBeforeComplete: (context) {
      // Validate if connection can complete to this port
      if (context.sourcePort.name == context.targetPort.name) {
        return ConnectionValidationResult(
          allowed: false,
          reason: 'Cannot connect same port types',
        );
      }
      return ConnectionValidationResult(allowed: true);
    },
  ),
)

Hit Testing

Ports have an expanded hit area for easier targeting:

PortWidget(
  port: port,
  theme: theme,
  controller: controller,
  nodeId: nodeId,
  isOutput: true,
  nodeBounds: bounds,
  snapDistance: 12.0,  // Hit area extends 12px beyond visual
)

The snapDistance creates an invisible buffer around the port, making it easier to start connections.

Complete Example

class CustomPortNode extends StatelessWidget {
  final Node<MyData> node;
  final NodeFlowController<MyData> controller;
  final Rect nodeBounds;

  const CustomPortNode({
    required this.node,
    required this.controller,
    required this.nodeBounds,
  });

  @override
  Widget build(BuildContext context) {
    final theme = controller.theme ?? NodeFlowTheme.light;

    return Stack(
      clipBehavior: Clip.none,
      children: [
        // Node content
        Container(
          width: node.size.width,
          height: node.size.height,
          decoration: BoxDecoration(
            color: Colors.white,
            borderRadius: BorderRadius.circular(8),
            border: Border.all(color: Colors.blue),
          ),
          child: Center(child: Text(node.data.label)),
        ),

        // Input ports on left
        for (var i = 0; i < node.inputPorts.length; i++)
          Positioned(
            left: -6,  // Half port size outside node
            top: _calculatePortOffset(i, node.inputPorts.length, node.size.height),
            child: PortWidget(
              port: node.inputPorts[i],
              theme: theme.portTheme,
              controller: controller,
              nodeId: node.id,
              isOutput: false,
              nodeBounds: nodeBounds,
              isConnected: _isPortConnected(node.inputPorts[i].id),
            ),
          ),

        // Output ports on right
        for (var i = 0; i < node.outputPorts.length; i++)
          Positioned(
            right: -6,  // Half port size outside node
            top: _calculatePortOffset(i, node.outputPorts.length, node.size.height),
            child: PortWidget(
              port: node.outputPorts[i],
              theme: theme.portTheme,
              controller: controller,
              nodeId: node.id,
              isOutput: true,
              nodeBounds: nodeBounds,
              isConnected: _isPortConnected(node.outputPorts[i].id),
              color: Colors.green,  // Custom color for outputs
            ),
          ),
      ],
    );
  }

  double _calculatePortOffset(int index, int total, double nodeHeight) {
    final spacing = nodeHeight / (total + 1);
    return spacing * (index + 1) - 6;  // Center port on position
  }

  bool _isPortConnected(String portId) {
    return controller.connections.any(
      (c) => c.sourcePortId == portId || c.targetPortId == portId,
    );
  }
}

Best Practices

  1. Consistent Sizing: Keep port sizes consistent across your application
  2. Color Coding: Use colors to indicate data types or connection categories
  3. Hit Area: Use adequate snapDistance for touch-friendly interfaces
  4. Visual Feedback: Ensure clear distinction between states (idle, connected, highlighted)
  5. Port Labels: Use meaningful names that appear as tooltips or labels

See Also

On this page