Port Widget
Customizing port rendering and interaction
Port Widget
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.
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
| Property | Type | Description |
|---|---|---|
port | Port | The port model to render |
theme | PortTheme | Theme for styling |
controller | NodeFlowController<T> | Controller for connection handling |
nodeId | String | ID of the parent node |
isOutput | bool | Whether this is an output port |
nodeBounds | Rect | Parent node bounds in graph coordinates |
Optional Properties
| Property | Type | Default | Description |
|---|---|---|---|
isConnected | bool | false | Whether port has connections |
size | Size? | theme | Override port size |
color | Color? | theme | Override idle color |
connectedColor | Color? | theme | Override connected color |
highlightColor | Color? | theme | Override highlight color |
borderColor | Color? | theme | Override border color |
borderWidth | double? | theme | Override border width |
snapDistance | double | 8.0 | Hit area expansion |
Callbacks
| Callback | Type | Description |
|---|---|---|
onTap | ValueChanged<Port>? | Called when port is tapped |
onDoubleTap | VoidCallback? | Called when port is double-tapped |
onContextMenu | void Function(Offset)? | Called for right-click |
onHover | ValueChanged<(Port, bool)>? | Called on hover state change |
Property Resolution
Port properties are resolved in this order (lowest to highest priority):
- Theme values (
PortTheme) - Base styling - Widget overrides (constructor parameters) - Per-widget customization
- Model values (
Portproperties) - 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 purplePort States
Port State Transitions
Animation showing port transitioning between states: idle → hovered (mouse enter) → dragging (connection start) → highlighted (valid target during drag) → connected (connection complete).
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 PortShapeWidgetBuilt-in Shapes
| Shape | Description |
|---|---|
PortShape.circle | Circular port (default) |
PortShape.square | Square port |
PortShape.diamond | Diamond/rotated square |
PortShape.capsule | Rounded 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
)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.
Connection Handling
The PortWidget automatically handles connection creation:
- Drag Start: User drags from a port
- Temporary Connection: Dashed line follows cursor
- Port Highlighting: Valid target ports glow
- Validation:
onBeforeCompletecallback is invoked - 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
- Consistent Sizing: Keep port sizes consistent across your application
- Color Coding: Use colors to indicate data types or connection categories
- Hit Area: Use adequate
snapDistancefor touch-friendly interfaces - Visual Feedback: Ensure clear distinction between states (idle, connected, highlighted)
- Port Labels: Use meaningful names that appear as tooltips or labels
See Also
- Ports (Core Concepts) - Port model and configuration
- Connections - Connection handling
- Theming - Port theme customization